« Android版のサウンドの制限 | メイン | wasm に clang を使った場合に C 内で関数呼び出しする場合に出るエラー »

2017年02月12日

WebAssembly:: C/C++ を wasm に clang を使用してコンパイル、JavaScript から呼び出す

    

WebAssembly は、2017年第一四半期のどこかで一区切りつけて破壊的変更が行われる予定ということなので、それ以後またツールチェーンの更新が必要になる。
以下、clang を使う方法で書いているが、Emscripten を使う方が簡単な様子。

環境構築
WindowsでWebAssemblyの環境を整える で書かれているのと大体同じだけど、「sexpr-wasm-prototype」は「WABT」に代わっている。
コンパイルエラーは最新版 (2017/2/11 に clone) では発生しなかった。
後、ALL_BUILD.vcxproj を開くと書かれているが、開くのはソリューションファイル LLVM.sln/binaryen.sln/WABT.sln が本筋。
コンパイル手順として、clang→llc→s2asm→sexpr-wasm と書かれているが、これも clang→llc→s2asm→wast2wasm となる。
コマンドとしては以下。

clang -S -emit-llvm --target=wasm32 sample.c
llc sample.ll -march=wasm32
s2wasm sample.s -o sample.wast
wast2wasm sample.wast -o sample.wasm


実行環境
Firefox 51 だと wasm 有効にしても「failed to match binary version」とエラーが出て動かない。少し古い binaryen だと動くかもしれないが今回は無視。Chrome で試す。
Chrome 56 は動かせた。ただローカルに置いた html ファイルから wasm ファイルを XMLHttpRequest で読み込ませようとしたらセキュリティーエラーが出るのでそれを回避するオプションをつけて起動する。
Chrome のショートカットを作って、「--user-data-dir="%UserProfile%\AppData\Local\Google\Chrome\UserDataLocal" --disable-web-security」などの引数を追加して起動する。
--user-data-dir= でユーザーデータフォルダを指定しているが、デフォルトの User Data のままではセキュリティ無効が効かなかったので、別途フォルダ作ってそちらを使うようにした。
後、wasm をアドレスバーに chrome://flags/#enable-webassembly と入力して有効に変更する必要がある。
ユーザーデータフォルダを切り替えたら wasm の指定はデフォルトになっているので注意。


C で書いた関数の呼び出し
Understanding the JS API に大体書かれている。
fetch で wasm ファイルを読むように書かれているが、ローカル file:// だと動かないので、XMLHttpRequest を使う必要がある。
サーバーに置くのならこの辺り関係ないが、まずはローカルで色々と実験するだろうから注意が必要。
wasm ファイルが読み込めたら WebAssembly.compile でコンパイルして、new WebAssembly.Instance でインスタンス化。
インスタンス化されたら instance.exports.func(); などとして呼び出せる。

function instantiate(bytes, imports) {
  return WebAssembly.compile(bytes).then(m => new WebAssembly.Instance(m, imports));
}
var importObject = { env: { print : arg => console.log(arg) } };
var instance;
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 );
  instantiate( binarray, importObject ).then( inst => instance = inst );
  document.getElementById( 'countup' ).addEventListener( 'click', function (){
    putString();
    this.value = instance.exports.count();
  }, false);
};
xhr.send(null);

このサンプルスクリプトでは id が countup のボタンが html に必要。
then と書かれているのは非同期処理の Promise と言う仕組みのため。
WebAssembly.compile は非同期処理で Promise を返すため、then で非同期処理が終わった後の処理を記述する。
arg => console.log(arg) などは JavaScript のラムダ式。


JavaScript の関数を C 側から呼び出す
wasm 側で import する関数は、WebAssembly.Instance の第二引数に namespace + 関数名 のオブジェクトを渡せばよいようだ。
{ "namespace" : { "function" : function } }; のようなオブジェクト。
S 式ではリンクのように import で関数に割り当てればいいようだが、C 言語側からの場合は単純に extern で宣言して呼び出せば env.function_name のような形で import する宣言が追加されていたので、{ "env" : { "function_name" : function } } と言う形でオブジェクトを作れば、その関数をすんなり呼び出せる。
関数の形式は wast (wasm のテキスト形式の S 式) ファイルの import 部分を見ればわかるから、その形で JavaScript で関数を書けばよい。

extern void print( int );
int c=0;
int count(){
  print( c );
  return c++;
}


整数引数で JavaScript の関数を呼び出すのは問題ないが、文字列(バイト列)を受け渡しするのはひと手間必要になる。


投稿者 Takenori : 2017年02月12日 18:01




comments powered by Disqus
Total : Today : Yesterday : なかのひと