🧩

Unity:Web ビルド用 Wasm プラグインを Rust で作る

に公開

以前、Rust からコンパイルした WebAssembly モジュールを Unity (WebGL) から呼び出すという記事を書きました。

https://zenn.dev/ruccho/articles/261136f7bdb003

この記事では Rust からコンパイルした WebAssembly を独立したモジュールとして実行し、JavaScript を介して Unity (C#) 側とやり取りする方法をとっていました。

ところが、実は Unity の Web ビルドでは、静的ライブラリとしてコンパイルした WebAssembly を直接ネイティブプラグインとしてインポートし、静的にリンクして単一の Wasm モジュールにすることができることを知りました。

https://docs.unity3d.com/6000.3/Documentation/Manual/webgl-native-plugins-with-emscripten.html

こちらの方法なら C# とのやりとりに JavaScript を介する必要もないですし、メモリ空間も共有されているのでコピーとか減らせて効率が高まりそうです。

今回はこの方法を使って Rust で書いたネイティブプラグインを Web で動かします。

Rust を書く

通常のネイティブプラグインと同様に C FFI をやります。ここは以前の記事と同じです。

#[unsafe(no_mangle)]
pub unsafe extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

コンパイル

Emscripten の準備

Unity は C# を IL2CPP で C++ に変換し、Emscripten を使って WebAssembly にコンパイルします。WebAssembly 用のネイティブプラグインを作成する場合、プラグイン側も Emscripten を使ってコンパイルする必要があります。

Rust はピュアな WebAssembly を生成するための wasm32-unknown-unknown ターゲットのほかに Emscripten を使用する wasm32-unknown-emscirpten ターゲットが提供されており、今回はこちらを利用してコンパイルします。

さらに、wasm32-unknown-emscripten ターゲットは Emscripten 自体を含んでいないため、別途用意する必要があります。Unity のドキュメントによると、バイナリ互換性のために Unity が使用しているのと同じバージョンの Emscripten を使用する必要があるそうです。

To ensure LLVM binary format compatibility, the version of Emscripten that’s used to compile the plug-ins must match the version of Emscripten that Unity uses.

同じページに各 Unity のバージョンが使用している Emscripten のバージョンも記載されていますが、3.1.38-unity などとサフィックスがついておりなにやらカスタムされていそうです。今回は面倒なので Unity にバンドルされた Emscripten を直接使用することにします。

.cargo/config.toml に次のように記述します。

config.toml
[target.wasm32-unknown-emscripten]
rustflags = ["-C", "link-args=--no-entry"]
linker = "C:/Program Files/Unity/Hub/Editor/<Unity Version>/Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten/emscripten/emcc.bat"

[env]
EM_CONFIG = "C:/Program Files/Unity/Hub/Editor/<Unity Version>/Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten/.emscripten"

Rust ツールチェインの固定

Rust 1.87 以降だと Unity で使われている Emscripten のバージョンと互換性がないらしく、ビルドに失敗します。いろいろやり方はありますが、今回は rust-toolchain ファイルを使ってツールチェインのバージョンを固定してしまいます。

rust-toolchain
1.86

そのうえで wasm32-unknown-emscripten ターゲットをインストールします。

Cargo.toml
rustup target add wasm32-unknown-emscripten

ビルド

Cargo.toml に静的ライブラリを生成するための指定をしてコンパイルします。

Cargo.toml
[lib]
crate-type = ["staticlib"]
cargo build --target wasm32-unknown-emscripten

成果物として .a ファイルが吐き出されます。ガワはいわゆる GNU アーカイブファイルですが、中には WebAssembly のオブジェクトファイルに相当するものが入っています。

Unity

Unity に .a ファイルをインポートして、WebGL 向けに設定します。

C# から呼び出します。DLL 名には __Internal を指定します。

public static class NativeMethods
{
    [DllImport("__Internal")]
    public static extern int add(int a, int b);
}

このまま Web ビルドして完成です。

JavaScriptとの連携

wasm32-unknown-emscripten では残念ながら wasm-bindgen が使用できないようですが、Emscripten が提供する API を通じて JavaScript 側と通信可能です。Rust 向けに emscripten_sys というクレートも公開されています。

Rust から eval とかできます。

use std::ffi::{CStr, c_char};

unsafe extern "C" {
    fn emscripten_run_script(script: *const c_char);
}

pub fn run_script(script: &CStr) {
    unsafe {
        emscripten_run_script(script.as_ptr());
    }
}

fn a() {
    run_script(c"console.log(\"hoge\")");
}

以上です!

Discussion