Webassemblyをコンパイルしたかっただけ


Webasmをコンパイルしたかっただけなんや

巷で噂になっているwebでAssemblyが書ける言語、webassembly。
我々はその効果に引き憑かれ、アマゾンの奥地に足を踏み入れるのであった

webasmとは


Javascriptの遅さを克服した代わりに、可読性とかもろもろを失った言語。(一応中間言語は読めはする)
なぜかわからないけどブラウザ上でAssemblyが動くと思っておけば良い。
公式ページにはこんなことが書いてある(ちょっと意訳かもしれません)

  • 効率的で速い!!
    バイナリー形式なので、コードデータが少なく効率的
  • 安全!!
    JavaScipt仮想マシンのように、メモリー内部でSandbox化されているので安全
  • デバッグがしやすい!!
    デバッグを手動で行う際に、テキストできれいに表示が可能である。
  • オープンウェブプラットフォームの一部である!!
    Webasmはバージョンレス、後方互換性あり等、将来のWebにおいても使用可能です。

歴史とか色々なことについては参考文献の方が参考になるのでそちらでお願いします。

今回やりたかったこと

今回やりたかったことは、C言語をWebAsmにコンパイルして、自分のプログラムのモジュールとしてブラウザで動かしてみるということです。
私のやり方が悪かったのか結構時間がかかってしまったので、今回はそれの解決策を考えてみましょうということです。
というわけでやってみましょう。

環境

これを試した環境です。
- Windows10 WSL2 Ubuntu 18.04TLS
- Emscripten gcc/clang-like replacement + linker emulating GNU ld 1.39.18
- Google Chrome

C言語からWASMを自前のプログラムで使うまで

0.EmScriptenをインストールする

とりあえずこのサイトを見ながらやればできました。
https://emscripten.org/docs/getting_started/downloads.html
一応やったことを説明しておきます。

# emsdkのリポジトリをダウンロード
git clone https://github.com/emscripten-core/emsdk.git

# ディレクトリに入る
cd emsdk

# 最新のSDKToolをインストール
./emsdk install latest

# ツールをアクティブ化する
./emsdk activate latest

# 現状のターミナルに環境変数を読み込む
# ターミナルへの接続が失われるたびにこの行は実行しなければならないので、.bashrcか何かに入れておくのをお勧めする。
source ./emsdk_env.sh

# インストール確認
emcc -v

1.C言語でプログラムを書く。

これは普通ですね。いつも通り書けばいいです。
とりあえずC言語で足し算の結果を返すようなプログラムをとりあえず書いてみます。

test.c
#include <emscripten.h>

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

ちなみに関数の前に書いてあるEMSCRIPTEN_KEEPALIVE
関数をエクスポートするという宣言。

2.WebAsmにコンパイルする

先ほど0で環境を整えているはずなので、emccコマンドが打てるようになっているはずです。
なので、先ほど作成したプログラムをwasmにコンパイルしてみましょう。
emcc -O0 test.c -o wasm.wasm -s WASM=1
オプション(必要そうなものだけとりあえず紹介)
-O0:最適化オプション、数字で最適化の度合いが変わる。
-o <target>:コンパイル後に出力するオブジェクト指定。.htmlと.jsと.wasmでそれぞれ出力されるものが違う
.htmlだとjsとwasmが、jsだとwasmが一緒に出力される。要はグルーコードと呼ばれる奴が一緒に出る、
-s [option]:asmのbuildオプションを記述できる。詳しくは下のページ参照。
今回指定したWASM=1は、wasmを出力するというオプション(本来は必要ないが、明示的に指定)
https://github.com/emscripten-core/emscripten/blob/master/src/settings.js

これを行うと、cpp.wasmというオブジェクトができるはず。

3.読み込み用のhtml,jsを記述。

読み込みを行うため(自分で使うため)、HTMLとJSを記述する。

test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>mytestDocument</title>
</head>
<body>
    <input type="number" name="num1" id="num1">+<input type="number" name="num2" id="num2">
    <p id="print"></p>
    <input type="button" name="reset" id="reset" value="plus">
    <script>
        var importObject = {
            env: {
                memory: new WebAssembly.Memory({ initial: 1024 }),
                table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
                emscripten_resize_heap: arg => 0,
                __handle_stack_overflow : () => console.log("overflow")
            },
            imports: { imported_func: arg => console.log(arg) }
        };

        WebAssembly.compileStreaming(fetch('wasm.wasm'))
        .then(module => WebAssembly.instantiate(module, importObject))
        .then(instance => {
            document.getElementById("reset").addEventListener(
                'click',()=>{
                    let n1 = document.getElementById('num1').value
                    let n2 = document.getElementById('num2').value
                    let result = instance.exports.addnum(n1,n2);
                    var p = document.getElementById("print");
                    p.innerText = result;
                },false
            );
        });
    </script>
</body>
</html>

超ザックリと解説を行うと、WebAssemblyモジュールからコンパイルを行う(要は読み込み。)
そのあと、インスタンス化を行い、そこから関数を呼び出す。

4.公開する

WebAsmの仕様上?httpsで公開しないといけないらしく、githubとか自前のサーバーとかでアップロードして公開する。
私の場合はgithub Pageを利用して公開してみている。

5.いったん実行して情報を集める。

とりあえずページをひらくと、動作がしないはずである。
デベロッパーツールで見てみると何やらエラーが出ているはず。

で、読んでみると何やらモジュールが足りない等なんやら色々言われている。
実はSourceタブのページを見ると、WASMが読める形になっている(というか中間言語っぽくなっている?)
エラー分を読むと、ここの上の方にあるimport文の関数が無いみたいなエラーが出てきている。
それを補う形で、JavaScriptのimportObjectの内部に読み込み用の関数を書いてみる。

書く内容としては、一番上のfuncの部分を読んでみて、importの内部に書いてある内容が関数の名前で、右のparamが引数。そしてresultの右が返り値の内容である。
今回の場合は、wasi_snapshot_preview1というオブジェクトの中に、
proc_exitと、args_getと、args_sizes_getの3つを用意すればよいことになる。
返り値の数も分かっている。
というわけで、とりあえずimportObjectの中身を下のように改変してみる。
返り値の扱いはどうでもいいらしく(この辺り動かすことに注力していたのでよくわかっていません。)適当に出力してみることに。

test.js
var importObject = {
  wasi_snapshot_preview1:{
    proc_exit : arg => console.log(arg),
    args_get : (a,b) => {console.log("a:"+a+"\nb:"+b);return 0},
    args_sizes_get : (a,b) => {console.log("a:"+a+"\nb:"+b);return 0},
  },
  env: {
    memory: new WebAssembly.Memory({ initial: 1024 }),
    table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
    emscripten_resize_heap: arg => 0,
    __handle_stack_overflow : () => console.log("overflow")
  },
  imports: { imported_func: arg => console.log(arg) }
};

再びアップロード

そうして、再びアップロードを行うと、エラーが解消されて、無事に自分が組み込んだものが使えるようになっているはず。
というわけでこれで無事に終了する。
できたものはこちら

終わりに

コンパイルするだけで結構時間がかかってしまったので大変だった。
今後注目されていく技術の一つではあると思うので、スピードテストとかも行ってみたい。

参考文献