LoginSignup
5
2

More than 3 years have passed since last update.

C/C++を使っているRustのコンソールアプリのReact SPA化

Last updated at Posted at 2020-03-24

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++部分については事前に、emconfigureemmakeで、configuremakeをラップして実行して、別途ソースから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

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2