TL;DR; lldを使えばwasm32-wasm
向けにコンパイルされた.o
ファイルをリンクして、WASMを出力できます。
書いてある内容はLinux、もしくはmacOSでは、そのまま使えると思います。使用しているLLDは6.0.0です。
ClangでもWASMを作れます。
CからWASMを作成するにはEmscriptenを使う、というのが普通でした。というより、ほぼ唯一の手段だった時もありました。
WASMのMVPがリリースされて以降、多くのコミュニティでWASMに対応するための開発がなされました。その一つの結果として、LLVMを利用したC/C++コンパイラーのClangがWASMに対応をしました。その後、LLVM向けのリンカlldもWASMに対応するようになりました。
この結果、次のようなパスで.wasm
ファイルを作れるようになりました。
clang lld
.c -----> .o ------> .wasm
ビルドとインストール
LLVM / Clang / lldのWASM対応は、現状ではオプショナルです。自分でWASM対応を設定してビルドする必要があります。
楽にビルドしたい場合
https://github.com/yurydelendik/wasmception を使うと楽に環境が整います。
自分で手間暇かけてビルドしたい場合
Clangとlldをビルドします。gitでとってきた6.0.0を使います。
% git clone https://git.llvm.org/git/llvm.git
% git clone https://git.llvm.org/git/lld.git
% git clone https://git.llvm.org/git/clang.git
% git clone https://git.llvm.org/git/compiler-rt.git
% git clone https://git.llvm.org/git/libcxx.git
Clang以外にcompiler-rtとlibcxxもダウンロードしておきます。
まずCmakeを使ってビルド用のルールを生成し、そのルールを使ってビルドするという流れです。私はHomebrewでインストールしたCmakeを使っています。
WASMに対応したものをビルドするには-DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly
をつけてCmakeを走らせます。あと-DLLVM_ENABLE_PROJECT
でLLVMと一緒にビルドするプロジェクト(ここではclang, lld, libcxx, compiler-rt)を指定しておきます。
-G
で生成するルールも選べます。省略するとMakefile
作るので、特に好みがない人は省略してもいいでしょう。私はなぜかNinjaを使うことにしました。
% mkdir llvm.build
% cd llvm.build
% cmake -GNinja -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly -DLLVM_ENABLE_PROJECT="lld;clang;compiler-rt;libcxx"
これでビルド用のルールファイルが生成されたので、おもむろにビルドします。-j
オプションは自分のCPUに合わせて適切に設定します。
% ninja -j8
しばらく時間がかかります。お風呂に行ったりしてくると終わってると思うので、できたものをインストールします。
% ninja install
ClangからWASMを作るには
先ほど述べたように、.o
を経由して.wasm
をつくります。図にするとこのような感じです。
clang lld
.c -----> .o ------> .wasm
各ステップを、それぞれ説明します。
.c -> .o
: wasm32-wasm
がターゲット
.c
から.o
を作るには、-c
オプションをつけてclang
を実行します。
この時にターゲットアーキテクチャを忘れずに指定しておきます:
% clang -c -target wasm32-wasm foo.c
これで、foo.o
というファイルが作られます。
.o -> .wasm
: wasm-ld
を使って変換
複数の.o
ファイルを取りまとめて実行ファイルを作るプログラムを「リンカ」と呼びます。lldはその一種です。
wasm-ld
が、WASMに対応したlldのコマンド名です。
使い方の基本は、
- 出力するWASMファイルの名前を
-o
オプションで指定する - そのWASMファイルに出力したい
.o
ファイルを列挙する
です。例えばadd.o
とsub.o
の中身をindex.wasm
へ出力するなら、このように実行します:
% wasm-ld -o index.wasm add.o sub.o
ケーススタディ
これで基本は触れたので、具体的な例をみてゆきます。
自作の関数をWASMにする場合
一番簡単な場合をまず取り上げてみます。1つの.c
ファイルからWASMファイルを作成する場合です。これは単体のアプリではなく、JSから呼ばれるライブラリをCで実装したというシナリオです。
例えば次のようなCのファイル(add.c
とでもしておきます)があったとします。これを変換してadd.wasm
を作ります。プログラム自体は簡素ですが、めっちゃCPUを使うような処理を実装した関数だと思って読んでください。clang
はデフォルトではC側の関数をJS側から参照できるように出力しません。JSから呼びたい関数にだけ、__attribute__((visibility("default")))
をつけましょう。
__attribute__((visibility("default")))
int add(int a, int b){
return a + b;
}
まず.c
から.o
を作ります。-Oz
はよしなに最適化をしてくれるオプションです。
% clang -c -Oz -target wasm32-wasm -o add.o add.c
これでadd.o
というファイルができました。これをwasm-ld
で変換してadd.wasm
を作ります。
% wasm-ld -no-entry -o add.wasm add.o
--no-entry
は、作成するWASMファイルにはエントリーポイントがないことを示すフラグです。エントリーポイントとは、プログラムが実行を始める場所のことです。WASMにはStart sectionと呼ばれるセクションを定義できます。そこで指定されている関数がWASMがインスタンス化された際に自動的に実行されます。つまりstart sectionに指定された関数が、そのWASMのエントリーポイントとなります。
独立したプログラムをWASMに出力するならstart sectionを設定するでしょう。しかし、今回はライブラリ的に使うので、エントリーポイントがありません。
ところがlldは、もともと実行可能ファイルを作ることを想定しています。そのためデフォルトでエントリーポイントを指定しようとします。
エントリーポイントなる関数はターゲットの環境によって異なります。wasm-ld
では_start
という関数がエントリーポイントであるとしています。そのため_start
という関数が未定義だとエラーとなってしまいます。このエラーを避けるために-no-entry
を指定します。
2つ以上の.o
ファイルからWASMを作る場合
1つのファイルでプロジェクトが完結するなんてありません。大抵は2つ以上のファイルで実装されています。
このケースでは、2つ以上の.o
ファイルからライブラリ的に使うWASMを作ります。
例えば、add.c
に加えて、sub.c
というファイルの内容も追加したいとしましょう。この場合は基本的に先ほどのケースと一緒です。
% clang -c -o add.o -Oz -target wasm32-wasm add.c
% clang -c -o sub.o -Oz -target wasm32-wasm sub.c
% wasm-ld -no-entry -o add.wasm add.o sub.o
それぞれコンパイルした後、wasm-ld
で一括処理をして出力をします。
エントリーポイントをCで実装する場合
_start
がデフォルトのエントリーポイントです。その関数を自分で実装します。
extern int add(int, int);
extern int sub(int, int);
int _start(int a, int b){
// アプリを起動する何かを実装
return sub(add(a, b), b);
}
これがstart.c
に実装されているとします。あとは、次のように--no-entry
オプションをつけずにwasm-ld
を実行すればエントリーポイントを実装できます。
% wasm-ld start.o add.o sub.o -o index.wasm
関数名などのデバッグ情報を削りたい場合は、--strip-all
オプションをつけると良いでしょう。
% wasm-ld --strip-all -o index.wasm start.o add.o sub.o
これで作成されたWASMは次のようになります:
(module
(type (;0;) (func (param i32 i32) (result i32)))
(module
(type (;0;) (func (param i32 i32) (result i32)))
(type (;1;) (func (param f64 f64) (result f64)))
(func (;0;) (type 0) (param i32 i32) (result i32)
get_local 0
get_local 0
call 1
get_local 1
call 3)
(func (;1;) (type 0) (param i32 i32) (result i32)
get_local 1
get_local 0
i32.add)
(func (;2;) (type 1) (param f64 f64) (result f64)
get_local 0
get_local 1
f64.add)
(func (;3;) (type 0) (param i32 i32) (result i32)
get_local 0
get_local 1
i32.sub)
(func (;4;) (type 1) (param f64 f64) (result f64)
get_local 0
get_local 1
f64.sub)
(table (;0;) 1 1 anyfunc)
(memory (;0;) 2)
(global (;0;) (mut i32) (i32.const 66560))
(export "memory" (memory 0))
(export "_start" (func 0)))
WASMのcode sectionからシンボルが全部消えていて、export sectionには_start
とメモリしか記述されていないことがわかります。
JSの関数をCから呼ぶ場合
WASMで定義されていない機能を使いたい場合、例えばDOMを操作したり、WebGLの関数を読んだりしたい場合、WASMからJSの関数を呼び出すことになります。
EmscriptenではEM_ASM
のようにインラインでJSを埋め込むこともできましたし、JSファイルをリンクするということもできました。
clang
/ wasm-ld
の場合は、次のように処理をするとJSの関数をCから呼べます。
- 呼びたい関数を宣言する
- C側で定義しないままその関数を呼ぶ
-
--allow-undefined
オプションを指定してwasm-ld
を実行する
一般のCのプログラムと同じく、宣言だけして定義しないと、別のファイルで実装されている関数やグローバル変数を参照するようになります。定義のないシンボルを参照することを「外部参照」、外部参照されたものの実体を見つけることを「外部参照の解決」と呼びます。リンカの仕事の1つは、外部参照の解決です。
例えば次の例では、js_func
という関数を外部参照しています:
int js_func(int, int);
int call_js_func(int a, int b){
return js_func(a, b);
}
wasm-ld
も外部参照の解決を行います。そして解決されない外部参照が見つかったら、エラーを出すのがデフォルトの振る舞いです。そのため、次のように実行するとjs_func
が解決されないため、2行目のようなエラーが出力されます。
% wasm-ld start.o add.o sub.o js_func.o --strip-all -o index.wasm
wasm-ld: error: js_func.o: undefined symbol: js_func
--allow-undefined
オプションをつけると、未解決の外部参照があってもエラーが出力されなくなります。そして未解決のものは、WASMがインスタンス化された時に外部から与えられるものとされます。
% wasm-ld start.o add.o sub.o js_func.o --strip-all --allow-undefined -o index.wasm
WASMのインスタンス化を行う際に、次のようにJSの関数を与えると、その関数が呼ばれます。
WebAssembly.instantiateStreaming(fetch("index.wasm"), {
js_func: (a, b) => Math.pow(a, 2) + b
});
まとめ
lldがWASMをサポートしたことで、clangを使ったWASMの出力が随分と簡単になりました。Cの開発スタイルそのままでWASMを出力できるのは、とてもありがたいと思っています。
個人的に一番嬉しいのは、ランタイムをつけないWASM作成が簡単に行えるようになった点です。EmscriptenでももちろんランタイムなしのWASM作成ができますが、デフォルトでランタイムをつけないlldの方が楽という印象を持っています。重たいJSの処理の一部分をCで書く、といった用途にはこちらの方が直接的に作業できるのではないでしょうか。