この記事は、Julia Advent Calendar 2016の17日目です。
Julia のコードを WebAssembly にしてみました。
TL;TR
Julia コードから無理矢理 WebAssemblyを作れたことは作れたけど実用レベルにはほど遠かった。
はじめに
動機
WebAssembly といえば LLVM1、Julia といえば LLVM、ならば Julia コードを WebAssembly 化できるのではないか、という単純な発想です。
WebAssemblyとは
Webブラウザのためのバイナリコードフォーマットです。
詳しくは、公式サイトを参照してください。とても変化が速いので、公式に勝る情報源はありません。
Qiita にも WebAssemblyタグ のついた記事がいくつかあります。
ただし、すでに内容が古くなっている記述も多いため、気をつけてください。
Julia とは
手軽さと高速な動作を両立した動的プログラミング言語です。
詳しくは、Julia Advent Calendar 2016 の1日目の記事でもある@bicycle1885さんの記事などを参照してください。
準備
ブラウザ
WebAssembly を動かすためには、以下のブラウザのいずれかが必要です。
- Chrome Canary chrome://flags/#enable-webassembly を有効にして再起動
-
Firefox Nightly
about:config で about:config#javascript.options.wasm を
true
に設定
通常版のブラウザでも対応していますが、対応している WebAssembly のバージョンが古く、最新のツールで作成した WebAssembly を動かせないことがあります。なので、Canary/Nightlyをお勧めします(Edgeの状況は未確認)。
ツール
WebAssembly を作るためには、以下の3つのツール群が必要です。
それぞれ、最新のソースをダウンロードしてビルドしてください。
LLVMではLLVM_EXPERIMENTAL_TARGETS_TO_BUILD
にWebAssembly
を設定する必要があります。
ビルドに必要な設定および手順は、以下の記事が参考になります。
WebAssemblyを使ってみる(C/C++をWebAssemblyに変換してChromeで実行)
WindowsでWebAssemblyの環境を整える
なお、sexpr-wasm-prototype
はWABT
に置き換わりました。
動作確認環境
OS: Windows 10
Julia: 0.5.0
LLVM: 3.9.0
ブラウザ: Chrome Canary 57.0.2954.1
Julia コードから作成した WebAssembly をブラウザで実行する
WebAssembly の作成
WebAssembly を作成するには、以下のような手順を踏みます。
- Julia (.jl) から LLVM IR (.ll) に変換する
- LLVM IR (.ll) からアセンブラ (.s) に変換する
- アセンブラ (.s) から WebAssemblyテキスト形式 (.wast) に変換する
- WebAssemblyテキスト形式 (.wast) から WebAssemblyバイナリ形式 (.wasm) に変換する
今回 WebAssembly 化する Julia コードは以下のような極めて単純なものです。
function add(a, b)
return a + b
end
code_llvm(add, (Int32, Int32))
肝になるのはcode_llvm()
関数です。これは、引数に与えられた関数の LLVM IR 出力を得る関数です。通常はデバッグやチューニングを目的として用います。
実は Julia コマンドには LLVM bitcode ファイル(LLVM IR のバイナリフォーマット)を出力するオプション--output-bc
があります。しかし、このオプションを使って bitcode ファイルを得るためには、Julia のランタイム全体を含んだひとつのファイルとして出力しなければならないのです。
Julia のランタイムには、高速化を目的として外部のC言語ライブラリを呼び出している箇所が含まれます。また WebAssembly は、LLVM IR の全てに対応しているわけではありません。代表的なところでは末尾呼び出し命令が未サポートです。
したがって、WebAssembly化可能な Julia コードとは、すべてが pure Julia で実装されていて、かつサポートされている範囲の LLVM IR にコンパイルされなければなりません。
そのような条件を満たす関数を実装し、かつそこだけの LLVM IR を得るために、code_llvm()
関数を使います。
さて、ではこのadd()
関数の LLVM IR を出力してみましょう。
$ julia sample.jl > sample.ll
すると、このような出力が得られました。
; Function Attrs: uwtable
define i32 @julia_add_71360(i32, i32) #0 {
top:
%2 = add i32 %1, %0
ret i32 %2
}
関数名がマングリングされているのがわかりますね。
残念ですがこれは仕方ありません。以降、add()
関数ではなくjulia_add_71360()
関数と呼ぶことにしましょう。2
次に、これをアセンブラに変換します。LLVM に含まれるllc
コマンドを使います。
$ llc -march=wasm32 -filetype=asm -o sample.s sample.ll
出力されたsample.s
を見てみましょう。
.text
.file "sample.ll"
.globl julia_add_71360
.type julia_add_71360,@function
julia_add_71360: # @julia_add_71360
.param i32, i32
.result i32
# BB#0: # %top
i32.add $push0=, $1, $0
# fallthrough-return: $pop0
.endfunc
.Lfunc_end0:
.size julia_add_71360, .Lfunc_end0-julia_add_71360
ふむふむ。では次。いよいよ WebAssembly の世界に足を踏み入れていきます。
Binaryen に含まれるs2wasm
コマンドを使い、まずは WebAssemblyのテキスト形式ファイルを作成します。
$ s2wasm -o sample.wast sample.s
(module
(table 0 anyfunc)
(memory $0 1)
(export "memory" (memory $0))
(export "julia_add_71360" (func $julia_add_71360))
(func $julia_add_71360 (param $0 i32) (param $1 i32) (result i32)
(i32.add
(get_local $1)
(get_local $0)
)
)
)
ご覧の通り、S式です。これなら手で書くのも難しくないですね😏
さて、では最後。本命のバイナリ形式ファイルを作ります。
WABT に含まれるwast2wasm
コマンドを使いましょう。
$ wast2wasm -o sample.wasm sample.wast
これでやっと WebAssembly のバイナリファイルを手に入れました。
ブラウザで実行する
さて、できあがった.wasm
を実行するためには、JavaScript のコードを書かなければなりません。というのも、現在の WebAssembly は JavaScript の代替として動くものではなく、ES6 のモジュール機構に統合されるものだからです。
また、.wasm
ファイルは、タグか何かで簡単に読み込めるものではなく、バイト配列としてソースコードに埋め込んだり、xhr/fetch で外部から取得したりする必要があります。そして、それをさらにコンパイルしてインスタンス化しないとモジュールとして使えないのです。面倒くさい! しかし、将来はもっと簡単に実行できるようになるそうなので、しばらく我慢しましょう。
ということで、モジュールを読み込んで実行するためのスクリプト、挙動を確認するためのロジックを含んだ HTML ファイルを作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes" />
</head>
<body>
<div>
<input id="a" type="number" value="1242">+<input id="b" type="number" value="372">=<span id="c"></span>
</div>
<button type="button" onclick="calc()">Add!</button>
<script>
function calc() {
const a = Number(document.getElementById('a').value);
const b = Number(document.getElementById('b').value);
fetch('sample.wasm')
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.compile(bytes))
.then(m => new WebAssembly.Instance(m))
.then(instance => document.getElementById('c').innerText = instance.exports.julia_add_71360(a, b));
}
</script>
</body>
</html>
大事なところはここです。
fetch('sample.wasm') // 1. 外部からファイルを取得
.then(response => response.arrayBuffer()) // 2. バイト列の取り出し
.then(bytes => WebAssembly.compile(bytes)) // 3. コンパイル
.then(m => new WebAssembly.Instance(m)) // 4. インスタンス化
.then(instance => document.getElementById('c').innerText = instance.exports.julia_add_71360(a, b)); // 5. 呼び出し
これでやっと WebAssembly を実行する準備が整いました。
.wasm
と HTML をどこかの Web サーバー上に配置するか、こんな感じのスクリプトを書いて Web サーバーをローカルで起動し、挙動を確認してみましょう。
const cnct = require('connect');
const static = require('serve-static')
cnct().use(static('./')).listen(3000);
$ node server.js
WebAssembly機能を有効にした Chrome Canary でページにアクセスすると、このように表示されます。
※ こちらに上のサンプルを置きました。Chrome Canary などでアクセスしてください。
もっと高度なスクリプトを WebAssembly 化したい!
と思って試行錯誤してみたのですが、いくつかの壁にぶつかってほとんど前に進めませんでした。
-
code_llvm()
関数で出力した LLVM IR は、Julia で定義した型のサイズが解決されていない。 - LLVM組み込み関数の呼び出しを変換できない。
- etc...
これらは Julia 本体のソースに手を入れたり、外部ツールで LLVM IR を書き換えてあげたりすればある程度の所までは解決するのではないかと思います。
また、WebAssembly には関数を export するだけでなく、import する機能やメモリを共有する機能もありますが、今回はそこまで調査できませんでした。
Julia コードの WebAssembly 化、ないしは何らかの形でブラウザ上で動かすことに興味を持っている人はいるようなので(https://github.com/JuliaLang/julia/issues/9430 や https://github.com/JuliaLang/julia/issues/2418 など)、その人たちのアクションに期待しつつ、私も引き続き挑戦をしていきたいです。