Emscriptenを使って、C/C++に依存しているRustのコンソールアプリをReact SPA化した時のメモ。
環境構築
emcc
emcc
というEmscriptenコンパイラのフロントエンドが必要で、そのためにemsdkのインストールが必要。
emsdkのインストールと有効化
https://emscripten.org/docs/getting_started/downloads.html
に従って、emsdkのインストールとアクティベイトをした上で、ビルドを実行するshell上で、emsdk_envを実行して、emccを使えるようにする必要がある。
# shellを開くたびに実行が必要
$ . ./emsdk/emsdk_env.sh
ビルド
まず、emscriptenターゲットを追加する。
rustup target add wasm32-unknown-emscripten
次に、targetにwasm32-unknown-emscripten
を指定して、cargoでビルド。CARGO_BUILD_RUSTFLAGS
を使って、emccのオプションを指定。
$ CARGO_BUILD_RUSTFLAGS="内容は後述" cargo build --target=wasm32-unknown-emscripten --release
ビルドが成功すると、Cargo.tomlで指定したパッケージ名で、2つのファイルが作られる
ファイル名 | 説明 |
---|---|
[パッケージ名].js | JavaScriptからWebAssemblyを使うためのグルーコード |
[バッケージ名].wasm | WebAssembly本体 |
emccオプションの指定方法
-C link-arg=...
の形で、CARGO_BUILD_RUSTFLAGS
の中で指定する。空白がある場合にはバラして別々にする必要があるので注意が必要。
例えば、-s ASYNCIFY
を指定したい場合は、-C link-arg=-s -C link-arg=ASYNCIFY
となる。
C/C++部分のビルド
C/C++部分については事前に、emconfigure
とemmake
で、configure
とmake
をラップして実行して、別途ソースからLLVMのbitcodeにビルドしておく必要がある。em...
は元のスクリプトの環境変数を書き換え等を行ってbitcodeを出力するスクリプトに変換するためのツール。.libs
ディレクトリにbitcodeを含んだファイルが出力される。
下の様に実行する。詳細はこちら、https://emscripten.org/docs/compiling/Building-Projects.html#building-projects
$ ./emconfigure ./configure
$ ./emmake make
更に、そのbitcodeをリンクできるように、bitcodeがあるディレクトリへのパスをemccオプションで指定する。
-L native=[レポジトリへのパス]/.libs
うまく行かない場合
結構な割合で依存ライブラリのビルドに失敗したので、エラーメッセージを見でケースバイケースで対処した。大きく分けて下の2つの方法で問題が解決できた。
configureスクリプトのパラメーターの調整
configure
スクリプトに然るべきパラメーターを与えることでうまく行く場合があった。例えば以下のようなもの。
- assemblyはサポートされないので、使わないようにする。
- アーキテクチャーはgenericなものを選択する
- c++のサポートをオンにする
ソースコードの修正(C/C++もRustも)
ソースコードの一部がなんらかの理由でWebAssemblyにできない場合があったので、以下のような形で解消した。
- 使われていないコードを消す
- スレッドを使っているコードを使わないように書き直す。C/C++の場合は、
USE_PTHREADS=1
というオプションがあるので、これで動かせる場合もありそうだが、自分のケースではうまく行かなかった。まだ実装が不十分なので。。という内容の開発者の書き込みもあった。詳細はこちら、https://emscripten.org/docs/porting/pthreads.html - JavaScriptでコードを置き換えた
- 戻り値が省略されている等、一致しないシグネチャーを合わせた
などなど
修正した依存ライブラリの組み込み
直接依存していないものも含めて、修正済みの依存ライブラリをパッケージに組み込むには、Cargo.tomlで以下のような形で、patch指定をする必要が有る。
creates-ioから取得しているライブラリ
[patch.crates-io]
some-library = { path = "./dependency/some-library" }
github等から取得しているライブラリ
[patch."https://github.com/foo/some-library"]
some-library = { version = "0.5.0", path = "./dependency/some-library" }
その他
- Cargo.tomlに
edition = "2018"
がないとビルドできなかった。 Rust 2018というC++11みたいなものを使えという意味らしい。
JavaScriptとRustの相互呼び出し
RustからJavaScriptの関数を呼び出す
グローバルな名前空間で定義したJavaScriptの関数を、Rust側から呼び出せる。
Rustからは、emscripten_run_script*
にJavaScriptを書いた文字列を渡すと、JavaScript上でevalしてくれる。結果を返すタイプの関数であれば、その結果も返してくれる。
関数 | 戻り値 |
---|---|
emscripten_run_script_string | *const std::os::raw::c_char |
emscripten_run_script_int | std::os::raw::c_int |
emscripten_run_script | なし |
他に試していない関数がいろいろとここに、https://emscripten.org/docs/api_reference/emscripten.h.html
以下は例。
JavaScript
function foo(s: string) {
console.log(s)
}
window.foo = foo // global関数に
Rust
use std::ffi::CString;
extern "C" {
pub fn emscripten_run_script(s: *const std::os::raw::c_char);
}
pub fn set_status(s: &str) {
unsafe {
let script = CString::new(format!("foo({:?})", s)).unwrap();
emscripten_run_script(script.as_ptr());
}
}
JavaScriptからRustの関数を呼び出す
emccのオプションに、-s EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap']
の追加して、
グルーコードが提供するcwrap
という関数を使って、関数をJavaScript側で定義する。
cwrapの最後の引数に{ async: true }
を指定するとPromise<戻り値の型>
を返すようになる。省略すると戻り値の型を返す。Promise化のためには、emccオプションに-s ASYNCIFY
の追加が必要。戻り値の型をvoid
にしたい場合はnull
を指定する。
更に、emccのオプションのEXPORTED_FUNCTIONS
のリストに、-s EXPORTED_FUNCTIONS=['_foo']
のように、関数名の先頭にアンダースコアーをつけたものを指定する必要が有る。これがないと最適化で呼び出す関数が消されるらしい。
JavaScript側
const foo = Module.cwrap("foo" /* 関数名*/, "string" /* 戻り値の型 */, ["string", "number"] /* 引数の型 */, { async: true })`
Rust側
use std::os::raw::{c_char, c_int};
#[no_mangle]
pub extern fn foo(name: *const c_char, age: c_int) -> *const c_char {
...
}
c_charポインタからStringへの変換
例
let s = CStr::from_ptr(some_c_char_ptr).to_str().unwrap();
ポインター型を返す場合
文字列型であれば、into_raw()
とすると、ライフサイクルの監視から外れて、関数の終わりでライフサイクルが終わったとしも、値がJavaScript側に渡る。例えば、
CString::new("some string").unwrap().into_raw()
のようにする。
into_raw()
で取得したポインタは、どこかでfrom_raw()
を使ってRustの世界に戻して、割当てられたメモリを開放する必要がある。
このような形でJavaScriptに戻すと、モジュールのリニアメモリに文字列が書き出されて、その先頭へのオフセットが戻ってくるらしい。後は文字列の長さがわかれば、リニアメモリから直接文字を読み出せる。説明はここ。https://stackoverflow.com/questions/47529643/how-to-return-a-string-or-similar-from-rust-in-webassembly
cwrapを使う場合、おそらくその辺りをcwrapがやってくれている。
JavaScript側でEmscriptenのheapにメモリを割り当てて、それを使う方法もあるらしい。ここにその話が書いてある。https://stackoverflow.com/questions/17883799/how-to-handle-passing-returning-array-pointers-to-emscripten-compiled-code
相互に渡せる型
JavaScript | Rust |
---|---|
number | c_int |
string | *const c_char |
byte array | *const c_int (メモにないので未確認。 バイト列へのポインタだったと記憶) |
React SPAへのwasmの組み込み
以下、webpackを使っている想定。必要なファイルは、wasm本体とグルーJSのみ。
必要なemccオプション
以下のビルドオプションを追加。
オプション | 説明 |
---|---|
-s MODULARIZE=1 |
モジュールの形でグルーJSを出力 |
-s ENVIRONMENT=web |
ブラウザがサポートしないコードを出力しない |
wasmのロード
タイミング
ルートコンポーネントのcomponetDidMount
あたり。
方法
グルーJSでdefault exportされている関数を呼び出す。その戻り値を使うと、wasmとやり取りすることが出来るようになる。
仕組み
グルーJSは自身でwasmをロードする。デフォルトでは、グルーJSはローカルファイルシステムにwasmがある前提で動くので、locateFile
フックを使って、wasmファイルへのパスをURLに変換する。その上で、webpackのfile-loaderでwasmファイルを内容変えずにファイルとして出力して、そのURLがwasmファイルを返すようにすると、グルーJSはwasmをネットワーク経由でロードする。
Webpackの設定
module: {
rules: [
...
{
test: /\.wasm$/,
type: "javascript/auto", // これがないと必要なヘッダがないとエラーになる
loader: "file-loader",
options: {
name: '[name]-[hash].[ext]' // ブラウザにキャッシュされないようにhashを含める
}
}
]
wasmをロードするコード
const glue = require("[グルーJSへの相対パス]/foo.js")
const wasm = require("[wasm本体への相対パス]/foo.wasm")
...
componentDidMount() {
const module = glue({
locateFile(path: string) { // convert from source wasm file name to asset url
if (path === "foo.wasm") {
return `${ファイル名部分を除いたwasmファイルのURL}/${wasm.default}` // wasm.defaultにはhashが含まれた実際にwebpackが出力したファイル名が入る
}
return path
}
})
module.onRuntimeInitialized = () => {
// wasmが使用可能になると、この関数が呼び出される。
}
}
アプリのレスポンスの改善
wasm側のコードが動いている間は、ブラウザ側のイベントループが止まってしまうので、実行に時間がかかるコードを動かす場合は、画面が固まってしまうが、
extern "C" {
pub fn emscripten_sleep(i: std::os::raw::c_int);
}
で、emscripten_sleepへのbindingを作って、
emscripten_sleep(1);
をRust側で呼び出すと、その時点で、一時的にブラウザ側に処理を戻すことができる。
なお、emccオプションに-s ASYNCIFY
の追加が必要。
非同期イベントループ
wasm側で非同期のイベントループを作って、1ループごとに処理をブラウザに戻すこともできる。
Rustのコードを1から作るのなら、この形で作るべき。以下の様に1ループ分の処理を実行する関数を定義して、emscripten_set_main_loop
に渡せば良い。
fn f() {
// 1ループ分の処理
}
fn main() {
...
emscripten_set_main_loop(f, 60 /* fps */, 1 /* 1 for infinite loop */);
...
}
細かい説明はここに、
https://emscripten.org/docs/porting/emscripten-runtime-environment.html