Calmery.me

みっかぼうずにならないようがんばる

WebAssemblyを試してみた

メモ.WebAssembly については WebAssembly のコンセプト - WebAssembly | MDN を参考にするとよさそう.

さらに追記

$ nvm install 8.0.0
$ nvm use 8.0.0
$ node -v
v8.0.0
$ node
> WebAssembly
{}

新しくリリースされた v8.0.0 の Node.js ではデフォルトで使えるようになってるみたい.
追記のところで main 関数がとか言っていたけど Emscripten を使えばその辺りもいい感じしてもらえるっぽい.

見直してみると,色々と勘違いしているところもあるけれど記録としてそのままに.

追記

main 関数があっても動かせた.と言うか足りない関数は補ってねという感じなのかな.
楓 software: wasm に clang を使った場合に C 内で関数呼び出しする場合に出るエラー

(index):25 Uncaught (in promise) LinkError: WebAssembly.Instance(): Import #0 module="env" function="printf" error: function import requires a callable
    at fetch.then.then.then.module (http://localhost/:25:24)
    at <anonymous>
$ s2wasm main.s -o main.wast --allocate-stack 1024
const loadWebAssembly = ( filename, imports ) => {
    return fetch( filename )
        .then( response => response.arrayBuffer() )
        .then( buffer => WebAssembly.compile( buffer ) )
        .then( module => {
            imports = imports || {}
            imports.env = imports.env || {}

            imports.env.printf = arg => console.log( arg )

            if( !imports.env.memory )
                imports.env.memory = new WebAssembly.Memory( {
                    initial: 256
                } )

            if( !imports.env.table )
                imports.env.table = new WebAssembly.Table( {
                    initial: 18,
                    element: 'anyfunc'
                } )

            return new WebAssembly.Instance( module, imports )
        } )
}

loadWebAssembly( 'main.wasm' ).then( instance => {
    const exports = instance.exports
    exports.main()
} )

Google Chrome Canary や Node.js で WebAssembly を読み込むための関数を作った(よくわかっていないのでものによっては動かないかも)
WebAssembly/loadWebAssembly.js at master · calmery/WebAssembly · GitHub

環境構築

Mac OS X 10.11.6
Google Chrome Canary 59.0.3030.0 canary (64-bit)
Firefox Nightly 54.0a1 (2017-03-05) (64 bit)
Node.js v8.0.0-pre

おまじない.

$ pyenv global 2.7.12

The LLVM Compiler Infrastructure Project

$ cd
$ mkdir workspace
$ cd workspace
$ git clone http://llvm.org/git/llvm.git
$ cd llvm/tools
$ git clone http://llvm.org/git/clang.git
$ cd ../projects
$ git clone http://llvm.org/git/compiler-rt
$ cd ~/workspace
$ mkdir llvm_build
$ cd llvm_build
$ cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX=/usr/local -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly ../llvm
$ make -j 8
$ sudo make install

GitHub - WebAssembly/binaryen: Compiler infrastructure and toolchain library for WebAssembly, in C++

$ cd ~/workspace
$ git clone https://github.com/WebAssembly/binaryen.git
$ cd binaryen
$ cmake . && make
$ sudo make install

GitHub - WebAssembly/wabt: The WebAssembly Binary Toolkit

$ cd /usr/local/src
$ sudo git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ sudo make gcc-debug-no-tests
$ cd
$ echo "export PATH=$PATH:/usr/local/src/wabt/out/gcc/Debug/no-tests/" >> .bash_profile

Node.js は v8.0.0-pre でないと Google Chrome Canary や Firefox Nightly で動く WebAssembly が動かない.そのため v8/nodenodejs/node を使う.Nightly builds の v8.0.0-nightly20170307efaab8fccf も試したが動かなかった.
GitHub - nodejs/node: Node.js JavaScript runtime

$ git clone https://github.com/nodejs/node.git
$ cd node
$ git checkout -b canary origin/canary
$ ./configure
$ make -j4
 if [ ! -r node -o ! -L node ]; then ln -fs out/Release/node node; fi
$ ./out/Release/node -v
 v8.0.0-pre
$ ./out/Release/node --expose-wasm
> WebAssembly
{}

GitHub - v8/node: Node.js JavaScript runtime

$ git clone https://github.com/v8/node.git
$ cd node
$ git checkout -b vee-eight-lkgr origin/vee-eight-lkgr
$ ./configure
$ make -j4
 if [ ! -r node -o ! -L node ]; then ln -fs out/Release/node node; fi
$ ./out/Release/node -v
 v8.0.0-pre
$ ./out/Release/node --expose-wasm
> WebAssembly
{}

WebAssembly に変換する

C や C++ で書かれたコードを WebAssembly に変換する.

extern "C" {
    int fib( int n ){
        switch( n ){
            case 0: 
            	return 0;
            case 1: 
            	return 1;
            default: 
            	return fib( n - 2 ) + fib( n - 1 );
        }
    }
}

C や C++ のコードをまず LLVM bitecode に変換し,次にアセンブリに変換する.その後 S 式に変換しバイナリに変換する.

$ clang++ -v
clang version 5.0.0 (http://llvm.org/git/clang.git fc2d9054c86c9b8acbc98b06f1b9d8aa0e5d40f9) (http://llvm.org/git/llvm.git 58580c59aefa59fb793a72dd5abeb6a60340a317)
$ clang++ main.cpp -emit-llvm --target=wasm32 -Oz -c -o main.bc
$ llc -version
LLVM (http://llvm.org/):
  LLVM version 5.0.0svn
...
$ llc main.bc -march=wasm32 -filetype=asm -o main.s
$ s2wasm main.s > main.wast
$ sexpr-wasm main.wast -o main.wasm

追記
sexpr-wasm ではなく wast2wasm を使うようにする.

$ wast2wasm main.wast -o main.wasm

動かしてみる

GitHub - 1984weed/webassembly-sample を参考にした.

const loadWebAssembly = ( filename, imports ) => {
    return fetch(filename)
        .then( response => response.arrayBuffer() )
        .then( buffer => WebAssembly.compile( buffer ) )
        .then( module => {
            imports = imports || {}
            imports.env = imports.env || {}
            imports.env.memoryBase = imports.env.memoryBase || 0
            imports.env.tableBase = imports.env.tableBase || 0
            
            if( !imports.env.memory )
                imports.env.memory = new WebAssembly.Memory( {
                    initial: 256
                } )

            if( !imports.env.table )
                imports.env.table = new WebAssembly.Table( {
                    initial: 0,
                    element: 'anyfunc'
                } )

            return new WebAssembly.Instance( module, imports )
        } )
}

loadWebAssembly( 'main.wasm' ).then( instance => {
    const exports = instance.exports
    console.log( exports ) // => Object {memory: W…y.Memory, fib: function}
} )

Node.js はオプションで --expose-wasm をつける必要がある.

// Node.js v8.0.0-pre
// node --expose-wasm
const fs = require( 'fs' )
const buffer = fs.readFileSync( './main.wasm' )

const arrayBuffer = new Uint8Array( buffer ).buffer

WebAssembly.compile( arrayBuffer ).then( module => {
    let imports = {}
    imports.env = {}
    imports.env.memoryBase = 0
    imports.env.tableBase = 0

    if( !imports.env.memory )
        imports.env.memory = new WebAssembly.Memory( {
            initial: 256
        } )

    if( !imports.env.table )
        imports.env.table = new WebAssembly.Table( {
            initial: 0,
            element: 'anyfunc'
        } )

    const instance = new WebAssembly.Instance( module, imports )
    console.log( instance.exports )
} )
// Node.js v7.7.1
Promise {
  <rejected> Error: WebAssembly.compile(): Wasm decoding failedResult = expected version 0c 00 00 00, found 01 00 00 00 @+4

ちなみに C++ では名前に修飾がついてしまうので extern "C" で囲う必要がある.

console.log( instance.exports ) // => Object {memory: W…y.Memory, _Z3fibi: function}
> WebAssembly.Instance( module ).exports
{ _Z3fibi: [Function: 0], memory: Memory {} }

サンプルなどを見ていると WebAssembly ではなく Wasm を使っているものがある.だが Google Chrome Canary や Firefox Nightly では Wasm がない.

// Replace
Wasm.instantiateModule() // => ReferenceError: WASM is not defined
WebAssembly.Instance()

Node.js v7.2.1

Getting Started With WebAssembly in Node.js | www.thecodebarbarian.com
この方法は v.7.7.1 では動かなかった.sexpr-wasm demo も s2wasm で作られた wast がそのままでは使えない.書き換えれば使えはしたが v7.7.1 で動かなかった.

Emscripten で変換する

Developer’s Guide - WebAssembly を参考にして導入する.
Main — Emscripten 1.37.22 documentation
latest ではなく incoming を使う.

$ git clone https://github.com/juj/emsdk.git
$ cd emsdk
$ ./emsdk install sdk-incoming-64bit binaryen-master-64bit
$ ./emsdk activate sdk-incoming-64bit binaryen-master-64bit
$ source ./emsdk_env.sh
$ emcc
INFO:root:(Emscripten: Running sanity checks)
WARNING:root:no input files
$ mkdir hello
$ cd hello
$ echo '#include <stdio.h>' > hello.c
$ echo 'int main(int argc, char ** argv) {' >> hello.c
$ echo 'printf("Hello, world!\n");' >> hello.c
$ echo '}' >> hello.c
$ emcc hello.c -s WASM=1 -o hello.html
$ ls
hello.c		hello.html	hello.js	hello.wasm
$ node -v
 v8.0.0-pre
$ node --expose-wasm hello.js
trying binaryen method: native-wasm
asynchronously preparing wasm
binaryen method succeeded.
run() called, but dependencies remain, so not running
Hello, world!

ここで生成された hello.wasm は動かしてみるのように読み込むことはできなかった.

> WebAssembly.compile( arrayBuffer ).then( ...
Promise { <pending> }
(node:92075) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): LinkError: WebAssembly.Instance(): Import #0 module="env" function="DYNAMICTOP_PTR" error: global import must be a number
(node:92075) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

どうやら main 関数があると動かないみたい.出力されたファイルをそのまま実行すると動きはする.Google Chrome Canary や Firefox Nightly も出力された hello.html では動くのだが動かしてみるのように読み込むと動かない.以下のようにオプションを指定すると動いた.だがこちらも main 関数があると動かない.この辺りはもうちょっと調べたい.(追記 未確認だけど main 関数というよりは存在しない関数を呼び出しているからっぽい.ここでは main 関数内で呼び出している printf 関数のせいかも)

$ emcc hello.c -Os -s WASM=1 -s SIDE_MODULE=1 -s 'BINARYEN_METHOD="native-wasm"' -o hello.wasm
$ ls
main.cpp main.wasm
$ node --expose-wasm
> const fs = require( 'fs' )
> const buffer = fs.readFileSync( 'hello.wasm' )
> const arrayBuffer = new Uint8Array( buffer ).buffer
...

動かしてみる のコードを使用して動かした結果はこうなった.

emcc hello.c -s WASM=1 -o hello.html

main 関数があってもなくても読み込めない.

emcc hello.c -Os -s WASM=1 -s SIDE_MODULE=1 -s 'BINARYEN_METHOD="native-wasm"' -o hello.wasm

main 関数がなければ Google Chrome Canary,Firefox Nightly,Node.js v8.0.0-pre で動かせた.
最適化の -Os を抜くと読み込めなかったりとよくわからない.

Rust から変換する

Compiling Rust to WebAssembly Guide – Hacker Noon
結局は Emscripten と同じみたい.

fn main(){
    println!( "Hello World" );
}
$ rustc --target=wasm32-unknown-emscripten hello.rs -o hello.html
$ node -v
 v8.0.0-pre
$ node --expose-wasm hello.js
trying binaryen method: native-wasm
asynchronously preparing wasm
binaryen method succeeded.
run() called, but dependencies remain, so not running
Hello World

まとめ

Emscripten を使えば良さそうだけどよくわからなかった.調べる.