Nimをemscriptenを用いてWebAssemblyに変換し,ccallでブラウザから呼び出しました.

対象環境

MacOSX,Linux

参考にした資料

https://qiita.com/snowlt23/items/5f05da45cbd1df4abb07

この記事で使うファイル

https://github.com/m77so/nim-wasm-sample にあります.

実行環境の用意

emscriptenをインストールしたいです.http://kripken.github.io/emscripten-site/ に正攻法の手順は書いてありますが,ビルドに2~3時間かかって辛いのでDockerを使います.

Dockerのインストール手順は省略します.

以上を行うDockerfileがこちらになります.

Dockerfile

FROM trzeci/emscripten
MAINTAINER m77so
USER emscripten
RUN curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y 
RUN mkdir -p /home/emscripten/.config/

ENV PATH=/home/emscripten/.nimble/bin:${PATH}
COPY nim.cfg /home/emscripten/.config/
RUN echo 'import strutils;"test".echo' > /tmp/test.nim \
    && nim c -d:release -d:emscripten -o:/tmp/test.js /tmp/test.nim 

nim.cfg

cc = clang
@if emscripten:
   clang.exe = "emcc"
   clang.linkerexe = "emcc"
   clang.options.linker = ""
   cpu = "i386"
@end

以下を実行するとイメージが作れます.nim.cfgとDockerfileは同じ階層に置きます.

$ docker build -t nimwasm:0.01 .

プログラムの作成

サンプルプログラムは,パスカルの三角形のn段m列目の値を求めるプログラムです.

pascal.nim

proc pascal*(n,m: int): int64 {.exportc: "pascal", varargs.} =
  if m == 0 or m == n : 1'i64
  else: pascal(n-1,m-1) + pascal(n-1,m)

ポイントはexportcプラグマを函数につける点です.これにより,Cにトランスパイルされても函数名がpascalのままになります.

またどの函数を呼び出し可能にするかをemscriptenに教えてあげるファイルを用意します.ファイル名は実行ファイル名と同名の.nim.cfgとします.

pascal.nim.cfg

passC = "-s WASM=1 -s 'BINARYEN_METHOD=\"native-wasm\"'  -s -Iemscripten"
passL = "-O3 -s WASM=1 -s 'BINARYEN_METHOD=\"native-wasm\"' -s EXPORTED_FUNCTIONS=\'[\"_pascal\"]\' -s EXTRA_EXPORTED_RUNTIME_METHODS=\'[\"ccall\", \"cwrap\"]\' -Lemscripten"

正直「これで動いた」というだけなので,これが最適ではないと思います…….あんまりよくわかってません.

以上が必要です.

ビルド

$ docker run --rm -v $(pwd):/src -u emscripten nimwasm:0.01 nim c -d:release -d:emscripten -o:pascal.js pascal.nim

pascal.jsとpascal.wasmというファイルが出力されます.pascal.wasmがWebAssemblyバイナリ本体で,wasmのみでは辛いのでいろいろラップしてくれているのがpascal.js です.

ブラウザから実行

次のようなHTMLを用意しました.

いい感じにワンライナーのサーバを立ててアクセスします.

即座に実行したらModuleがまだちゃんと読まれていませんでした.ちゃんとloadを待つ方法がよくわからなかったのでsetTimeoutで誤魔化しています.誰か教えてください. Module.onRuntimeInitializedを使います.

ccallやcwrapの使い方は https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-ccall-cwrap とか見てください.

<script src="pascal.js"></script>
<script>
Module.onRuntimeInitialized = ()=>{
  var result = Module.ccall('pascal', // name of C function
  'number', // return type
  ['number','number'], // argument types
  [10,5]);
  console.log(result);
}
</script>

Node.JSで実行

var Module = require('./pascal.js')
Module.onRuntimeInitialized = ()=>{
  var result = Module.ccall('pascal', // name of C function
  'number', // return type
  ['number','number'], // argument types
  [10,5]);
  console.log(result);
}

おわり