kolmas.tech note

雑記と思索、偏った技術の覚え書き

今更WebAssembly・続 ー Cの関数をJavaScriptから呼ぶ

前エントリの続き。 blog.kolmas.tech

ライブラリ的なものを作る

前エントリで触れた通り、私がWebAssemblyを知ったきっかけは、Cで実装されたライブラリをクライアントサイドJavaScriptから利用しているものを見た事である。即ち、Cで実装された関数をJavaScript側から任意のタイミングで呼び出せるようにしている。前エントリのHello worldは、単純にCのmain関数が一回実行されただけであった。ここでは、この任意のCの関数を任意のタイミングで呼ぶ、という点に着手したい。

何はともあれ、まずは呼び出される側のCの関数=ライブラリ的なものを作る。

int add(int a,int b){
    return a+b;
}

int sub(int a,int b){
    return a-b;
}

シンプルに、int型の二引数を取って、その和を返すadd関数と、差を返すsub関数を定義した。

前エントリでも参照したMDN Web Docsの記事によれば、JavaScript側から任意のタイミングで呼び出す関数には、EMSCRIPTEN_KEEPALIVEを付けるそうだ。これは<emscripten/emscripten.h>に定義されている。

#include <emscripten/emscripten.h>

EMSCRIPTEN_KEEPALIVE int add(int a,int b){
    return a+b;
}

EMSCRIPTEN_KEEPALIVE int sub(int a,int b){
    return a-b;
}

これを前回と同様にemccコマンドでコンパイルする。前エントリに引き続き%はプロンプトである。

% emcc samplelib.c -o samplelib.html

一方、以下のEmscriptenの公式ドキュメントには違う方法が説明されている。 emscripten.org Cのソースコードは元のまま1で、emccコマンドの-sEXPORTED_FUNCTIONSオプションで対象となる関数を指定する。元の関数名の先頭にアンダースコアを付けて指定する。カンマで区切って複数関数を指定できる。

% emcc samplelib.c -o samplelib.html -sEXPORTED_FUNCTIONS=_add,_sub

個人的にはこちらの方が、元のCのソースコードをそのまま使えているという点で好みである。以降の例ではこちらの方式を使うことにする。一応、本エントリ以降の記述については、どちらの方法でコンパイルしても同様に働く事は確認している。

実際に呼び出す・弄り倒す

コンパイルの結果、前エントリのHello worldの例と同様にsamplelib.wasmsamplelib.jssamplelib.htmlが出来ているので、例によってウェブサーバ経由でHTMLファイルにアクセスする。これも前エントリと同様にコンソール風画面が出てくるが、今回はmain関数がないので、そこには何も表示されない。もっとも、作成したaddsub両関数にしても、標準出力に何かを書き出したりしていないが。

emccコマンドで出力されるJavaScript、ここではsamplelib.jsファイルはグルーコードと呼ぶらしい。WebAssemblyとJavaScriptの間を繋ぐグルー=接着剤。グルーコードは手ずから作ることも当然出来るらしい2が、Emscriptenが出力したものをそのまま使うのが手軽であるようだ。これにはModuleというオブジェクトがあり、このオブジェクトがWebAssembly関連の諸々を担っている。先のHTMLにアクセスしているブラウザ3のコンソールで弄ってみよう。

何か出来てる。
早速、このModuleオブジェクトの中に_add_subなるメソッドが見つかる。冒頭にアンダースコアが付いている4が、先程作成したCの関数そのもののようである。呼び出してみよう。
想定通りの動作、ではある。
Module._add(3,2)の返り値が5、Module._sub(3,2)の返り値が1、どちらも想定通りの動作である。

こうなるとちょっと遊んでみたくなる。Cと比べたらラフな型システム5であるJavaScriptから、色々な引数を焚べてこのメソッド、ひいてはCの関数を呼び出してみようではないか。

成程面白い実行結果。
C側の関数は整数型の引数を取るものだが、そこに文字列を焚べてみたところ0扱いされているようである。明らかに型変換できない引数を焚べると別の値で代替されるのだろうか。この辺りをグルーコードが処理しているという事だろう。実数を焚べてみた例では整数に丸められている様子だが、これについてはC的にも普通の挙動であるから、グルーコード側とC=WebAssembly側のどちらで変換されているのかは特定できない。
数値型に変換できそうな文字列は変換してくれている。
Module._add("3","2")の返り値が5になるあたり、グルーコードは、出来る限り型変換しようとしてくれてはいるようである。Number関数かparseInt関数あたりでも使っているのだろうか。そしてNaNは0扱いである、と。

折角だから和と差だけでなく、四則演算を揃えてみよう。C側で以下のようにmul及びdiv関数を新しく定義し、再度コンパイル6する。

int add(int a,int b){
    return a+b;
}

int sub(int a,int b){
    return a-b;
}

int mul(int a,int b){
    return a*b;
}

int div(int a,int b){
    return a/b;
}

となれば、やはり次に試してみたいのは0除算である。引数の型変換は問題なく通過するだろうから、C=WebAssembly側でエラーになる筈である。

WebAssemblyファイルの特定位置でのエラー、となっている。
Chromeの開発者ツールでWebAssembly内でエラーが発生した箇所まで見る事が出来る。
WebAssemblyという名前そのものと言えばまさにそうであるが、如何にもアセンブリ言語らしさを感じる風合いである。これを読み解くのは一筋縄ではいくまい。


  1. 即ちEMSCRIPTEN_KEEPALIVEを付けず、ひいては<emscripten/emscripten.h>#includeすることもない。本当に冒頭に掲載したままのCのソースコードそのもの。
  2. 参考。qiita.com
  3. 検証環境はGoogle Chrome。
  4. コンパイル時の-sEXPORTED_FUNCTIONSオプションでもアンダースコアを付けている。そういう文化?なのか。
  5. プログラマ視点から見る型システムがラフであるという事は、処理系にしてみたらよっぽど高度な処理をしているという事なのであって、その点でラフと言ってしまうのは心苦しい節はある。
  6. 当然、コンパイル時の-sEXPORTED_FUNCTIONSオプションには、今回追加した二関数も追加する。