2016年04月15日

WebAssembly のバイトコードの仕様を眺める

BinaryEncoding

整数と実数の演算、メモリ読み書き、関数呼び出しがあって、文字列系の命令はない。
メモリ周りの命令と組み合わせると文字列処理は出来るからそれで対応ってことかな?
将来サポートするかもしれないっぽいようだけど。

制御構造はASTで保持する形のようなので、よくあるバイトコードに比べると抽象度が高い。

さらに詳しくは、binaryenの各種ツールやソースコード眺めて試してみるとわかるかな。

投稿者 Takenori : 22:08

2017年02月12日

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 : 18:01

wasm に clang を使った場合に C 内で関数呼び出しする場合に出るエラー

extern void print( const char*, int );
int stringLength( const char* s ) {
  int len = 0;
  while( s[len] ) { len++; }
  return len;
}
int c=0;
int count(){
  const char* text = "Hello world!";
  print( text, stringLength(text) );
  return c++;
}

このように JavaScript から count() を読んで、その中で stringLength() を呼ぶようなプログラムを書くと "Uncaught RuntimeError: memory access out of bounds" などと出てうまく動かない。
ソースコードに何か問題があるのか?と言うとそうではなく、コンパイル時にスタックの指定が必要とのこと。
具体的には――

s2wasm count.s -o sample.wast --allocate-stack 1024

のように --allocate-stack オプションが必要になる。

投稿者 Takenori : 18:28

WebAssembly がデフォルトで有効化されるのはいつ?

問題なく進めば2017年の第一四半期にバイナリフォーマットの初期バージョンが固まって、ブラウザで順次有効化されるらしい。
Firefox は 2017年3月のバージョン52 で有効化が予定されているとか。

2017年4月以降に本格的に取り組むと変更の対応が少なそう。


以下の記事に書かれている。
コミュニティにフィードバックを求めるためのWebAssemblyのブラウザプレビュー
JavaScriptを補完するウェブ用の新バイナリフォーマット「WebAssembly」--ブラウザプレビュー段階に

投稿者 Takenori : 19:31

wasm と JavaScript の間の文字列(バイト列)の受け渡し

extern void print( const char* ); などとしてコンパイルしたものの wast ファイルを見ると const char* は i32 となっている。以前見たバイトコードの仕様では文字列型はなかった。
Emscripten ではヒープの実態は ArrayBuffer となっていたが、WebAssembly でもメモリは ArrayBuffer で確保されているよう。
Understanding the JS API の Memory で書かれている。
メモリは、wasm で import / export 可能とあるが、C からコンパイルした wast を見ると (export "memory" (memory $0)) となっている。
このメモリに対して直接値を書き込む、もしくは読み込むことでバイト列を読み書きできると書かれている。
上述の print 関数の const char* は i32 として JavaScript に渡されるが、この値は何かというと、export された memory のインデックスを表していた。
JavaScript 側で文字列終端チェックを省くために C 側で長さを渡すように extern void print( const char*, int ); とした場合、JavaScript 側では以下のようにすることで文字列を受け取ることができる。

function print( offset, length ) {
  var buffer = new Uint8Array(instance.exports.memory.buffer, offset, length);
  console.log( String.fromCharCode.apply(null,buffer) );
}

const char* は、instance.exports.memory のインデックスとなっているので、そこから文字列長分を uint8 配列として切り出し、文字列化している ( ASCII 文字列以外ではうまくいかないかもしれない )。
これで C 側から文字列(バイト列)をコンソールに出力する関数が呼び出せる。


JavaScript から C 側へ文字列(バイト列)を渡すのはもう少し手間がかかる。
C 側で以下のように定義する。

static const int memSize = 1024;
char inputmemory[memSize];
char* getInputMemoryStart() { return inputmemory; }
int getInputMemoryLength() { return memSize; }

この領域に JavaScript から文字列を書き込んでもらい、そこを読み取ることで C 側で文字列を受け取れる。
JavaScript 側は以下のようになる(長さチェックはしていないし、ASCII 以外は考慮していないので注意)。

function putString( str ) {
  var offset = instance.exports.getInputMemoryStart();
  var length = instance.exports.getInputMemoryLength();
  var buffer = new Uint8Array(instance.exports.memory.buffer, offset, length);
  for( var i = 0; i < str.length; i++ ) {
    buffer[i] = str.charCodeAt(i);
  }
  buffer[str.length] = 0;
}

このスクリプトで文字列を書き込んでもらった後に――

const char* text = inputmemory;
print( text, stringLength(text) );

として C 言語側でアクセスできる。


以上でとりあえずは文字列の受け渡しが出来る。
ただ、実際には実用性に欠ける。
ある程度の規模で使うとなると malloc を自作するだろうから、そこで確保したメモリのポインターを JavaScript に渡せば、そのメモリの前に管理構造がついているような実装にすれば、その確保されたメモリの長さもわかるため、JavaScript でアクセスできる範囲も知ることができる。
そうすれば実用上も問題はなくなる。
malloc していないメモリを渡してしまうと壊れるが。

投稿者 Takenori : 22:59

 
Total : Today : Yesterday :