13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NimAdvent Calendar 2021

Day 2

NimでWebAssemblyをやる【2021年版】

Last updated at Posted at 2021-12-01

最近のところ、WebAssemblyがアツいですね。NimでもWebAssemblyを試してみたという内容の記事がいくつか見つかります。
というわけで、$n$番煎じ($n \geq 2$)どころかもはや出涸らし気味ですが、Nimでもお手軽にWebAssemblyできるよということを知ってもらうために、この場を借りて私もやってみたいと思います。

必要なもの

  • Nim
  • emscripten

以上です。

やり方

1. Nimのプロジェクトを作る

まずは普通に作ります。プロジェクト名は nim_emscripten として話を進めます。

$ nimble init

できたnimbleファイルの設定のうち、次の2つを以下のように書き換えます。

# 拡張子まで書く必要あり。
bin           = @["nim_emscripten.html"]
# c以外はダメ(ではないが、諸々面倒なのでcだけにする)
backend       = "c"

注意点として、binに指定する出力ファイル名には拡張子まで含める必要があるというものがあります。.html であればemscriptenが出力するデフォルトのHTMLが出力され、そのままプレビューできますし、.js であれば、wasmを読み込むjsのコードが出力されます。拡張子を指定しなかった場合、たとえばWindowsであれば拡張子が .exe なのに中身がjavascriptという謎ファイルが出力されますし、Linux/MacOSでも同様に拡張子なしのjsファイルが出力されます。

2. コンパイラオプションを書く

emscriptenでは、gccのかわりにemccを使います。Nimが吐いたCのソースコードをemccでコンパイルするために、コンパイラオプションを設定する必要があります。そこで、nimbleファイルと同じディレクトリに以下のような nim.cfg ファイルを作ります。

nim.cfg
cc = gcc
# windowsの場合は、"emcc.bat"にする
gcc.exe = "emcc"
# windowsの場合は、"emcc.bat"にする
gcc.linkerexe = "emcc"
gcc.options.linker = ""
# WebAssemblyのVMは32bitアーキテクチャです。
# なので、i386(32bit architecture)をターゲットにします。
cpu = "i386"
# windows.hなどのOS固有のヘッダは利用できないため、常にlinuxをターゲットとしてビルドします。
os = "linux"
# コンパイラに渡すオプションです。
# WASM=1 を指定しなかった場合、emccはデフォルトでasm.jsを出力します。
# EXIT_RUNTIMEが1 (またはNO_EXIT_RUNTIMEが0)の場合、main関数の実行が完了した後にjsから
# Nimで書いたコードが呼べなくなります。
# EXPORTED_RUNTIME_METHODSには、ccall または cwrap、もしくはその両方を指定します。
# 両方を指定する場合は、EXPORTED_RUNTIME_METHODS=ccall,cwarp のようにコンマで区切ります。
passC = "-s WASM=1 -s NO_EXIT_RUNTIME=1 -s EXPORTED_RUNTIME_METHODS=ccall -O3"
# リンカに渡すオプションは、コンパイラに渡すものと揃えます。
passL = "-s WASM=1 -s NO_EXIT_RUNTIME=1 -s EXPORTED_RUNTIME_METHODS=ccall -O3"

これでプロジェクトのセットアップは完了です。あとは好きにコードを書くことができます。

3. コードを書いてみる

例えばこんなコードを書いてみます。

src/nim_emscripten.nim
var memo = newSeq[int]()

# void __attribute__((used)) hello();
proc hello() {.exportc, codegenDecl: "$# __attribute__((used)) $#$#".} =
  echo "Hello, World!"

# int __attribute__((used)) fibonacci(int n);
proc fibonacci(n: int): int {.exportc, codegenDecl: "$# __attribute__((used)) $#$#".} =
  if (n < 2):
    return n
  else:
    if memo.len < (n-1):
      memo.add(fibonacci(n - 1) + fibonacci(n - 2))
    return memo[n-2]

hello()
echo fibonacci(10)

javascriptから呼びたいコードには、exportc プラグマと、 codegenDecl プラグマをつける必要があります。codegenDecl プラグマで変なコード片を生成する必要があるのは、emccがデッドコードを削除してしまったり、インライン関数化する最適化を施してしまったりするからです。
コンパイラ対して、コンパイラの把握していない場所で関数が使用されていることを明示的に通知しなかった場合、吐かれたWebAssemblyのバイナリにはその関数が含まれないことがあります。
ちなみに、emscriptenのツールキットにおいては、この__attribute__((used))EMSCRIPTEN_KEEPALIVEマクロとして提供されています。

このコードを保存したら、普段通りに nimble build します。
生成された nim_emscripten.html を開き、こんな画面が表示されたら成功です。

emscripten output

javascriptからfibonacciを呼び出すには、以下のようにします。

fibonacciを呼び出す
console.log(ccall('fibonacci', Int32Array, Int32Array, new Int32Array([10])));
// -> 55

最後に

WebAssemblyのツールキットが随分と充実してきているように感じます。昔はもっとしんどかった。
NimがCのコードを吐いてくれるので、かなりお手軽にWebAssemblyを試すことができました。

13
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?