/emscripten-example

emscripten wasm example in node.js

Primary LanguageJavaScript

emscripten example

Usage

$ npm run
# show commands
Scripts available in emscripten-example via `npm run-script`:
hello:build
    emcc src/hello.c -o out/hello.js
hello:run
    node out/hello.js
hello-func:build
    emcc src/hello-func.c -o out/hello-func.js -s EXPORTED_FUNCTIONS="['_hello']"
hello-func:run
    node call-hello.js
hello-wasm-module:build
    emcc src/hello-func.c -o out/hello-module.js -s WASM=1 -Wall -s MODULARIZE=1 -s EXPORTED_FUNCTIONS="['_hello']"
hello-wasm-module:run
    node call-module.js
hello-wasm-direct:build
    emcc ./src/count-only-my-code.c -o ./out/count-only-my-code.wasm -Wall -s WASM=1 -s SIDE_MODULE=1 -s ONLY_MY_CODE=1 -s EXPORTED_FUNCTIONS="['_add']"
hello-wasm-direct:run
    node call-wasm.js

インストール

Emscripten SDKをインストール

brew install llvm
brew install emscripten --with-closure-compiler

ホントはバージョン管理できるEmscripten SDKを使うほうがよさそう。

.emscriptenの設定

一度、emccを実行するとパスが上手く通ってない的なエラーがでる。

✈ emcc -v
WARNING:root:LLVM version appears incorrect (seeing "9.0", expected "4.0")
CRITICAL:root:fastcomp in use, but LLVM has not been built with the JavaScript backend as a target, llc reports:
===========================================================================
(no targets could be identified: [Errno 2] No such file or directory)
===========================================================================
CRITICAL:root:you can fall back to the older (pre-fastcomp) compiler core, although that is not recommended, see http://kripken.github.io/emscripten-site/docs/building_from_source/LLVM-Backend.html
INFO:root:(Emscripten: Running sanity checks)
CRITICAL:root:Cannot find /usr/bin/llvm-link, check the paths in ~/.emscripten

Emscriptenの環境設定を参考に~/.emscriptenの設定ファイルを変更した。

次の二箇所を変更したら動いた。Node.jsとかはnodebrewとかが勝手にパス入ってたので問題なかった。

EMSCRIPTEN_ROOT = os.path.expanduser(
    os.getenv('EMSCRIPTEN') or 
    '/usr/local/opt/emscripten/libexec')
LLVM_ROOT = os.path.expanduser(
    os.getenv('LLVM') or 
    '/usr/local/opt/emscripten/libexec/llvm/bin')

再度 emcc -v で確認して OK。

✈ emcc -v
INFO:root:generating system asset: is_vanilla.txt... (this will be cached in "/Users/azu/.emscripten_cache/is_vanilla.txt" for subsequent builds)
INFO:root: - ok
INFO:root:(Emscripten: Running sanity checks)
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.37.22
clang version 4.0.0  (emscripten 1.37.22 : 1.37.22)
Target: x86_64-apple-darwin16.7.0
Thread model: posix
InstalledDir: /usr/local/opt/emscripten/libexec/llvm/bin
INFO:root:(Emscripten: Running sanity checks)

チュートリアル

Emscripten Tutorial — Emscripten 1.37.22 documentationを参照

Hello World on Node.js

CのHello Worldを作る。

#include <stdio.h>

int main() {
  printf("hello, world!\n");
  return 0;
}

これをemscriptenでjsへトランスパイルする。

emcc src/hello.c -o out/hello.js
# 先にout/ディレクトリ作ってないとエラーだった

Node.jsで実行する

✈ node out/hello.js
Hello, world!

Notes ファイルサイズ

何も最適化しないデフォルトは300kb

✈ ll out
total 776
-rw-r--r--  1 azu  staff   100K 11  4 22:51 hello.html
-rw-r--r--  1 azu  wheel   283K 11  4 22:52 hello.js

Optimizing codeができるので、-O1してみると半分ぐらい。

✈ emcc -O1 src/hello.c -o out/hello.js

~/.ghq/github.com/azu/emscripten-example master*
✈ ll out
total 544
-rw-r--r--  1 azu  staff   100K 11  4 22:51 hello.html
-rw-r--r--  1 azu  wheel   168K 11  4 22:54 hello.js

-O2すると何かmemファイルと一緒に生成される。

✈ ll out
total 336
-rw-r--r--  1 azu  staff   100K 11  4 22:51 hello.html
-rw-r--r--  1 azu  wheel    57K 11  4 22:55 hello.js
-rw-r--r--  1 azu  staff   389B 11  4 22:55 hello.js.mem

If --memory-init-file is used, a .mem file will be created in addition to the generated .js and/or .html file.

同じディレクトリにあるなら読み込んで実行してくれる。

hello function on Node.js

先ほどのhello, worldをJavaScriptから呼べるhello関数にする。 まずは先ほどのコードにhello関数を作る。

#include <stdio.h>

void hello(){
  printf("Hello, world!\n");
}

int main(int argc, char** argv){
  hello();  
  return 0;
}

JavaScriptからC(C++)のコードを呼ぶ場合は、何個かあるらしい。

  • コンパイルオプションのEXPORTED_FUNCTIONSを使う
  • コード上でextern "C"してexternしておく方法

今回はEXPORTED_FUNCTIONSを使う。

オプションで_付きにして関数名を指定すると、吐き出したファイルにその関数がexportされる。

Note that you need _ at the beginning of the function names in the EXPORTED_FUNCTIONS list.

emcc src/hello-func.c -o out/hello-func.js -s EXPORTED_FUNCTIONS="['_hello']"

これを使う側のNode.jsのコードは次のような感じ。 普通にrequireしてEXPORTED_FUNCTIONSした名前を呼ぶだけ。

const func = require("./out/hello-func");
func._hello();

参考:

WASM in Node.js

.wasm module

-s MODULARIZE=1を使うとemscriptenがwasmをラップしたjsを作ってくれて、なんやかんやしてくれる。

emcc src/hello-func.c -o out/hello-module.js -s WASM=1 -Wall -s MODULARIZE=1 -s EXPORTED_FUNCTIONS="['_hello']"

MODULARIZE=1を付けたコマンドを実行すると.js.wasmが生成される。 これは一緒に生成された.js(emscriptenのラッパー)を読み込んで実行できる。

// emcc src/hello-func.c -o out/hello-module.js -s WASM=1 -Wall -s MODULARIZE=1 -s EXPORTED_FUNCTIONS="['_hello']" -O3
// emscripten module code
const path = require("path");
// to resolve wasm file
process.chdir(path.join(__dirname, "out"));
const Module = require("./out/hello-module");
Module().then(function(instance) {
    instance._hello();
});

直接実行より遥かに簡単。

.wasm direct

.wasmファイルを作ってNode.jsで実行する

-s WASM=1 -o out.htmlで実行すると、wasmファイルとそれを実行するhtmlが生成できるらしい。 emccで.wasmだけを作成する方法はよくわからなかった。

-s SIDE_MODULE=1 -s ONLY_MY_CODE=1がセットじゃないと.wasmだけを生成は出来ないっぽいふんいき。 (そのうちかわりそう)

ONLY_MY_CODE=1だとCでincludeできないんので別のサンプルコードにする。

int add(int x, int y){
  return x + y;
}

count-only-my-code.cというコードを作ってこれをwasmにする。

✈ emcc ./src/count-only-my-code.c -o ./out/count-only-my-code.wasm -Wall -s WASM=1 -s SIDE_MODULE=1 -s ONLY_MY_CODE=1 -s EXPORTED_FUNCTIONS="['_add']"

出力内容。いったんasm.jsで吐いて、それをasm2wasmで変換してくれるらしい。

✈ ll out
total 368
-rw-r--r--  1 azu  staff   837B 11  5 09:04 count-only-my-code.asm.js
-rw-r--r--  1 azu  wheel   172K 11  5 08:49 count-only-my-code.js
-rw-r--r--  1 azu  wheel   280B 11  5 09:04 count-only-my-code.wasm

これを実行するにはWebAssemblyで実行環境を定義して、wasmを読んでコンパイルして使う感じっぽい。

(Learn the Hard Way)nodejs-8でのWebAssembly自体を調べてみた - Qiitaを参考にして読み込んで実行するのを書いた。

const { promisify } = require("util");
const fs = require("fs");
const path = require("path");
const readFile = promisify(fs.readFile);

readFile(path.join(__dirname, "./out/count-only-my-code.wasm"))
    .then(WebAssembly.compile)
    .then(waModule => new WebAssembly.Instance(waModule, {
        env: {
            STACKTOP: 0,
            STACK_MAX: 256,
            abortStackOverflow: function (i32) { console.log("stack oveflow"); },
            memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),
            table: new WebAssembly.Table({
                initial: 0, 
                maximum: 0, 
                element: "anyfunc"
            }),
            memoryBase: 0,
            tableBase: 0,
        },
    }))
    .then(instance => {
        // functions exposed in "exports"
        console.log(instance.exports._add(1, 2));
    });

stack oveflowするけど実行はできた。

✈ node call-wasm.js
stack oveflow
3

FAQ

-s SIDE_MODULE=1 -s ONLY_MY_CODE=1 だとエラーがでる。


Traceback (most recent call last):
  File "/usr/local/bin/emcc", line 13, in <module>
    emcc.run()
  File "/usr/local/Cellar/emscripten/1.37.22/libexec/emcc.py", line 1812, in run
    wasm_text_target, misc_temp_files, optimizer)
  File "/usr/local/Cellar/emscripten/1.37.22/libexec/emcc.py", line 2279, in do_binaryen
    subprocess.check_call(cmd)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 535, in check_call
    retcode = call(*popenargs, **kwargs)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 522, in call
    return Popen(*popenargs, **kwargs).wait()
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 710, in __init__
    errread, errwrite)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 1335, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory

となるのはEMCC_DEBUG=1を付けて実行すると何が原因か分かる。

DEBUG:root:asm2wasm (asm.js => WebAssembly): bin/asm2wasm ./out/count-only-my-code.asm.js --total-memory=16777216 --trap-mode=allow --table-max=-1 --mem-max=-1 -o ./out/count-only-my-code.wasm

でオチていたので、binaryenのパスが通ってなかった。

ファイルサイズ

✈ ll out
total 1800
-rw-r--r--  1 azu  staff   837B 11  5 09:04 count-only-my-code.asm.js
-rw-r--r--  1 azu  wheel   172K 11  5 08:49 count-only-my-code.js
-rw-r--r--  1 azu  wheel   280B 11  5 09:04 count-only-my-code.wasm
-rw-r--r--  1 azu  wheel   283K 11  5 09:21 hello-func.js
-rw-r--r--  1 azu  wheel   101K 11  5 09:25 hello-module.js
-rw-r--r--  1 azu  staff    43K 11  5 09:25 hello-module.wasm
-rw-r--r--  1 azu  wheel   283K 11  5 09:21 hello.js