WebAssemblyが動くブラウザが来た
2016/03/16
とうとうWebAssemblyが動くブラウザが出てきました。
今回はChrome Canary版で、実際にCで書いたコードをWebAssemblyに変換して実行して見るところまでやってみます。
なお環境はChromebookでビルド諸々をして、Windows上のChromeで動作確認しました。
実行に関してはブラウザを入れれば良いとして、ビルドに関してはLinuxやMac系のOSでだいたい同じ手順でできるはずです。
準備
コンパイルの手順
次のような流れでコンパイルします。
- Cのコードをclangを用いてLLVM IRにコンパイル
- LLVM IRをアセンブリを経由してWebAssemblyのテキスト形式までコンパイル
- テキスト形式のWebAssemblyをバイナリにコンパイル
この3つでそれぞれ使うものを順番に導入していきます。
Windowsはこちらの WindowsでWebAssemblyの環境を整える でVisualStudioとCmakeを使ってビルドしてください。
LLVM+clang
とりあえず最新のLLVMとclangをソースからコンパイルします。
WORKDIRは適当に置き換えてください。しばらく使います。
LLVMのダウンロード
export WORKDIR=/home/chronos/user/workspace
mkdir -p $WORKDIR
cd $WORKDIR
git clone http://llvm.org/git/llvm.git
cd llvm
clangのダウンロード
cd $WORKDIR/llvm/tools/
git clone http://llvm.org/git/clang.git
compiler-rtのダウンロード
いらないかもだけどまぁ他のと違って小さめだしついでに入れておく。
cd $WORKDIR/llvm/projects/
git clone http://llvm.org/git/compiler-rt
ここまで準備できたらビルドです。長い時間がかかるので覚悟しておきましょう。
cd $WORKDIR
mkdir llvm_build
cd llvm_build
cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX=/usr/local -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly $WORKDIR/llvm
make -j 8
sudo make install
インストールに成功すると、今回利用するclang
やllc
が使えるようになります。
binaryenの導入
LLVMで出力したファイルをWebAssemblyに変換するツールをインストールします。
git clone https://github.com/WebAssembly/binaryen.git
cd binaryen
cmake . && make
sudo make install
インストールに成功すると、binaryen-shell
、wasm-as
、wasm-dis
、asm2wasm
、wasm2asm
、s2wasm
、wasm.js
などのコマンドが使えるようになります。
sexpr-wasmの導入
本来は上だけで良いばずなのですが、最後のバイトコードへの変換に使うwasm-as
が、ブラウザで読めないものになってしまうので、そこだけは別のWebAssembly周りのツール群を導入します。
cd $WORKDIR
git clone https://github.com/WebAssembly/sexpr-wasm-prototype.git
cd sexpr-wasm-prototype
make
sudo cp out/sexpr-wasm /usr/local/bin
とりあえず今回使うsexpr-wasm
だけは使えるようにしておきます。
WebAssemblyの生成
実際にCのコードをWebAssemblyに変換します。
どこか適当な場所にディレクトリを作って、そこで作業をしてください。
Cのコードは以下です。
int c=0;
int count(){return c++;}
今回は呼び出すたびに1ずつ加算してその結果を返す関数のみです。
これをsample.c
とした場合、次のようにコンパイルします。
clang -emit-llvm --target=wasm32 -S sample.c
llc sample.ll -march=wasm32
s2wasm sample.s > sample.wast
sexpr-wasm -o sample.wasm sample.wast
流れは以下のようになっています。
-
clang
でC/C++→LLVM IRに変換-
sample.c
==>sample.ll
-
- llcでLLVM IRをアセンブリコードに変換
-
sample.ll
==>sample.s
-
- s2wasmでアセンブリコードをWebAssemblyのテキストコードに変換(中身はS式)
-
sample.s
==>sample.wast
-
- sexpr-wasmでWebAssemblyのテキストコードをバイトコードに変換
-
sample.wast
==>sample.wasm
-
一応生成物で重要なものだけ軽く目を通しておきます。
wast(WebAssemblyのテキスト形式:S式)の中身
(module
(memory 1
(segment 8 "\00\00\00\00")
)
(export "memory" memory)
(export "count" $count)
(func $count (result i32)
(local $$0 i32)
(i32.store offset=8
(i32.const 0)
(i32.add
(set_local $$0
(i32.load offset=8
(i32.const 0)
)
)
(i32.const 1)
)
)
(return
(get_local $$0)
)
)
)
;; METADATA: { "asmConsts": {},"staticBump": 11 }
count関数とグローバル変数をexportしているのは頭の片隅に入れておきます。
wasm(WebAssemblyのバイナリ)
00000000 00 61 73 6D 0A 00 00 00 0E 0A 73 69 67 6E 61 74 75 72 65 73 .asm......signatures
00000014 01 00 01 16 13 66 75 6E 63 74 69 6F 6E 5F 73 69 67 6E 61 74 .....function_signat
00000028 75 72 65 73 01 00 0A 06 6D 65 6D 6F 72 79 01 01 01 15 0C 65 ures....memory.....e
0000003C 78 70 6F 72 74 5F 74 61 62 6C 65 01 00 05 63 6F 75 6E 74 27 xport_table...count'
00000050 0F 66 75 6E 63 74 69 6F 6E 5F 62 6F 64 69 65 73 01 15 01 01 .function_bodies....
00000064 01 33 02 08 0A 00 40 0F 00 2A 02 08 0A 00 0A 01 14 0E 00 15 .3....@..*..........
00000078 0D 64 61 74 61 5F 73 65 67 6D 65 6E 74 73 01 08 04 00 00 00 .data_segments......
0000008C 00 .
だいたいこんな感じのバイナリになっています。最初の4Byteで判別できるかと思います。
ブラウザの準備
Firefoxのナイトリービルド?でも実行できるようですが、今回はChromeで動かすので、Chrome Canaryをインストールします。
起動後、以下URLをアドレスバーに入力し、詳細設定画面にあるWebAssemblyを有効にします。
chrome://flags/#enable-webassembly
もう一度再起動すると、WebAssemblyが使える状態になります。
HTMLとJavaScriptの用意
WebAssemblyのバイナリコードがsample.wasmとした時、次のようなHTMLで読み込みと実行が可能です。
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>WebAssembly Test</title>
</head>
<body>
<input type="button" id="countup" value="CountUp?" />
<script type='text/javascript'>
var xhr = new XMLHttpRequest();
xhr.open('GET', 'sample.wasm', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
var binary = xhr.response;
var binarray = new Uint8Array( binary );
var module = Wasm.instantiateModule( binarray, {} );
console.log( module );
document.getElementById( 'countup' ).addEventListener( 'click', function (){
this.value = module.exports.count();
}, false);
};
xhr.send(null);
</script>
</body>
</html>
ここでいくつかポイントが。
- JavaScriptみたいに、scriptタグで読み込んでくれたり、みたいな情報は今のところないっぽい?
- 今JavaSciptでファイルをダウンロードしているが、面倒でもこれが必要。
- 基本処理は以下の流れ
- 入手したバイナリデータ(binary:ArrayBuffer)を配列(binarray:Uint8Array)に変換
- Wasm.instantiateModuleで解析
- moduleには読み込んだWebAssemblyのコードなどが入っている。
console.logでmoduleを書き出しているので、ざっと出力を見ると以下の様な感じになっています。
module: Object {
expots: Object {
count: function,
memory: ArrayBuffer,
}
}
wastでmodule配下にあったものがそのまま出てる感じですね。
とりあえず、これでページにあるボタンを押すと、0から順番に1ずつクリックするたびに数値が増えていきます。
引数周りはまた扱いが別っぽいので、そのままでは動きません。これはまた調査する予定です。
その他
コンパイルめんどい
LLVM IRから直接wasmを出力したいので今回はLLVMから入れましたが、clangからだとemscriptenを用いた例が海外でよく見られました。
C/C++との連携だけならこれを導入するのが良いようです。
コンパイルめんどいその2
一応c2wasmというコマンドを作りました。
手動でコンパイルできるように各種コマンドを用意していれば使えます。
以下のように導入できます。
wget https://raw.githubusercontent.com/HirokiMiyaoka/til/master/WebAssembly/c2wasm
chmod a+x c2wasm
sudo cp c2wasm /usr/local/bin
以下のようにして使います。
c2wasm sample.c
c2wasm sample.c -o output.wasm
手順なんとかしたい
無駄にsファイル吐いてたりするので、何とかしてLLVM IRから直接wasmにしたい。
最後に
とりあえずHello, world!は無理でしたが、簡単な数値計算して、JavaScriptの世界に持ってくるくらいのことをやってみました。
今後ブラウザだけでなく、Electronも対応すれば、ゲームやアプリ面でさらなる活躍が期待できる技術なので、早いうちからいろいろと試してみたいところです。
次は引数の受け渡しや文字列の扱い周りに挑戦したいですね。