clang
LLVM
WebAssembly

lldでwasmをリンクするまで

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-rtlibcxxもダウンロードしておきます。

まず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のコマンド名です。

使い方の基本は、

  1. 出力するWASMファイルの名前を-oオプションで指定する
  2. そのWASMファイルに出力したい.oファイルを列挙する

です。例えばadd.osub.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から呼べます。

  1. 呼びたい関数を宣言する
  2. C側で定義しないままその関数を呼ぶ
  3. --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で書く、といった用途にはこちらの方が直接的に作業できるのではないでしょうか。