🐡

Unityで使うマルチプラットフォーム対応のPluginをRustで作る

2023/07/16に公開
1

これは何

2023年現在において、UnityからRust製のライブラリクレートを呼び出して使う、という事は広く行なわれています(本当に??)

特に最近は csbindgen が実用的なサンプルを提示していることもあり、C# (僕の場合はUnity)からRustのコードを呼び出せる環境のメモを残しておこうと思って書いた物です。

僕自身はRust歴半日くらいで、作法についても理解し切れていないので、明らかにおかしな点や、もっと良い手法があれば是非教えて下さい。

今回検証に使ったプロジェクトは以下にあるので、説明が不足している箇所などはコードを見て確認して貰えたら、と思います。

https://github.com/neon-izm/rust-sandbox

副読教材としてこちらの動画もすごく参考になったので、この記事に興味を持った人にはあわせておすすめします。

UnityだけどRust言語を使ってみたい! - Unityステーション
https://www.youtube.com/watch?v=2e9HgBns92c

↑の動画で言及があるInterOperabilityのためのRustとC#のサンプル集が実用なら便利です
https://github.com/keijiro/UnityRustTestbed

開発環境と目標

開発環境

  • Windows 11Pro(x64)
  • VisualStudio 2022(最近はVSのtoolchainでデバッグできるようです!)
  • rustc 1.71.0
  • JetBrains CLion 2023.1.4
  • Android Studio 2022.1.1
  • Unity 2021.3.25f1

目標

最小限のRustのライブラリを作成し、csbindgenでUnity向けのグルーコードを生成して、WindowsとAndroid向けにビルドしたUnityの実行ファイルでRustのライブラリ内関数を呼び出す。

現在カバーしていない範囲

これらについては特にカバーできていないので補足コメントを歓迎します。
Github issueへのコメントも歓迎です https://github.com/neon-izm/rust-sandbox/issues

  • iOS,MacOSでの動作検証 書きました→ https://zenn.dev/izm/articles/5ea081e916156b
  • Rust側のcrateの構成が複雑になった場合の挙動
  • Rust側のライブラリのリリースビルド最適化
  • VSCodeでの追試(すみません、JetBrainsに魂を売ってしまったのでVSCode全く分らないです)

開発環境のセットアップ

僕の理解を併せて書いておくので、誤りがあれば教えてください。

  1. CLion上でRustの開発環境をセットアップする
  • https://plugins.jetbrains.com/plugin/8182-rust/docs これを参考にしました
  • Rust AnalyzerがCLion向けに存在したようですが、最近はRust plugin for the IntelliJ Platformを使う、という事になっているようです
  • Clippy の適用 をしておくことで、誤りに気付きやすい
  • rustfmtの適用 やっておくと見やすい。(CLionではctrl+alt+lでコードフォーマットが掛かります)
  • (可能なら)Github copilotのCLion向けセットアップを済ませておく
    • 新規で触る言語なら、Copilotを入れて //if not empty print values みたいな無駄な(!?)コメントを書くだけで基礎的な文法のチートシートになるので便利
  1. Android用のNDKをダウンロードしておく
  • 後述するcargo-ndkの関係上、Android NDKはr23以降をダウンロードしておく必要があることに注意!(23.0.7599858で試しました)
    • (Unityは長らくr21を使っているので混ぜて大丈夫なのかな…)
  • Windowsの環境変数でANDROID_SDK_ROOTANDROID_NDK_ROOTがセット済みなことを確認する
  • cmd.exeを起動してenvと打って実行すれば一覧が出ます
  • 典型的なAndroidStudioのセットアップでは
    • ANDROID_SDK_ROOTはC:\Users\○○\AppData\Local\Android\Sdk
    • ANDROID_NDK_ROOTはC:\Users\○○\AppData\Local\Android\Sdk\ndk\23.0.7599858などになります。(無ければ追加しましょう)
  1. cargo-ndkを入れる cargo install cargo-ndk。これはRust製のライブラリをAndroidから呼び出すとき、jniのグルーコードが必要という難点を解消するライブラリで、特にcsbindgenとの相性も良いのでおすすめです。
  2. rustup target で適切な対象プラットフォームを追加します。
rustup target add \
    aarch64-linux-android \
    armv7-linux-androideabi \
    x86_64-linux-android \
    i686-linux-android

rustup set default-host x86_64-pc-windows-msvcはセットしておきましょう。

  1. ClionのtoolchainでVisualStudioを指定します。一番上が使われると思います。

Rustのライブラリプロジェクトを作る

  • Libraryプロジェクトを適当に作成します。今回は仮で「unity_rsgen_sample」と言う名前にします。
  • [build-dependencies] にcsbindgen = "1.7.3" を追加します
    lib.rsに
#[no_mangle]
pub extern "C" fn my_add(x: i32, y: i32) -> i32 {
    x + y
}
#[no_mangle]
pub extern "C" fn my_sub(x: i32, y: i32) -> i32 {
    x - y
}

を書いておきます。
以下、Cargo.toml内に

[lib]
crate-type = ["cdylib","lib"]

を記述しておきます。 "lib"はRustの別プロジェクト(unity_rsgen_sample_cli)から参照するために必要で、WindowsとAndroidは"cdylib"です。

( note: iOSも考慮するなら、場合分けをする必要があるかも)

また、csbindgenを動かすためにunity_rsgen_sampleのプロジェクト直下にbuild.rsを配置して、以下のように生成コードを書きます。

生成するcsのアクセシビリティは簡単のためpublicにしています。

fn main() {
    csbindgen::Builder::default()
        .input_extern_file("./src/lib.rs")
        .csharp_dll_name("unity_rsgen_sample")
        .csharp_class_name("NativeMethods") // optional, default: NativeMethods
        .csharp_namespace("CsBindgen") // optional, default: CsBindgen
        .csharp_class_accessibility("public") // optional, default: internal
        .csharp_entry_point_prefix("") // optional, default: ""
        .csharp_method_prefix("") // optional, default: ""
        .csharp_use_function_pointer(true) // optional, default: true
        .csharp_disable_emit_dll_name(false) // optional, default: false
        .csharp_dll_name_if("UNITY_IOS && !UNITY_EDITOR", "__Internal") // optional, default: ""
//Unityの開発イテレーションを早めたい、とかであれば直接Unityプロジェクトの場所にcsファイルを出力する 
//.generate_csharp_file("../../UnitySimple/Assets/Plugins/NativeMethods.g.cs")
        .generate_csharp_file("../dotnet/NativeMethods.g.cs")
        .unwrap();
}

Rustのライブラリを呼ぶcliプロジェクトを作る

Rust製のライブラリプロジェクトをそのままデバッグすることも可能ですが、一旦CLion内で完結するデバッグ環境を作る意味で、ライブラリプロジェクトをimportして使うRust製のcliアプリケーション(GUIがなく、コマンドラインで動くアプリケーション)を作ります。

同じフォルダ階層に新規Rustプロジェクトを作り(unity_rsgen_sample_cliとします)
Cargo.toml内に

[dependencies]
# local dependencies
unity_rsgen_sample = { path = "../unity_rsgen_sample" }

と書いてローカルのRustライブラリを参照します。

fn main() {

    let add = unity_rsgen_sample::my_add(1, 2);
    println!("add result: {}", add);
}

普通にCLion上のデバッグでブレークポイントをprintln!の行に貼って、変数のウォッチなどが動く事を確認します。

csbindgenでC#コードとdllを生成する

windows dll(と、csコード)はCLion内のterminalでunity_rsgen_sampleに移動してcargo build --releaseでリリースビルドが生成できます。

この時点でdllファイル(unity_rsgen_sample.dll)とcsファイル( NativeMethods.g.cs )がRustのプロジェクト内のどこかに出来ていれば正しくセットアップが出来ています。

また、後述するWindowsアプリ内でのデバッガのアタッチ時はreleaseフラグを付けずに
cargo build を使うと良いと思います。

cargo ndk でAndroid向けのsoファイルを生成する

soファイルはCLion内のterminalでunity_rsgen_sampleに移動してcargo ndk -t arm64-v8a -o ./jniLibs build --releaseでリリースビルドが生成できます。
もし32bit環境も考慮する必要があれば-t armeabi-v7a とターゲットオプションを追加しましょう。
./jniLibs 以下にlibunity_rsgen_sample.soと言うファイルが生成されていることを確認します。(libっていう接頭語が付いちゃってますが、今のところ名前解決で問題は起きていません。おそらくcargo ndkの挙動による物だと思います)

このときにビルドが失敗するなら、環境設定を見直して下さい。

Unityプロジェクトを作る

適当に作ります。
特にAndroidは前の項目で64bit armのみを作っている場合IL2CPPビルドにしてtarget architecturesをARM64にチェックを入れるのを忘れないようにしましょう。

このようなテスト関数を作ってキーで呼び出したり、ボタンで呼び出したりします。

    public void TestAddFunc()
    {
        var addRet = NativeMethods.my_add(1, 2);
        Debug.Log($"native method add(1,2) ={addRet}");
    }

    public void TestSubFunc()
    {
        var subRet = NativeMethods.my_sub(1, 2);
        Debug.Log($"native method sub(1,2) ={subRet}");
    }

以下のようにdll,so,csファイルを配置します。(Pluginsの階層はプロジェクト直下では無くても大丈夫です)

  • /Assets/Plugins/NativeMethods.g.cs
  • /Assets/Plugins/Android/arm64-v8a/libunity_rsgen_sample.so
  • /Assets/Plugins/x86_64/unity_rsgen_sample.dll

UnityEditorを実行して、TestAddFunc()などを試してRustのライブラリ内の関数が呼べることを確認します。

Windowsでビルドして、CLionのデバッガを使う

UnityEditorからWindows向けのexeを出力します。
出力したexeの中にPlugins/x86_64/unity_rsgen_sample.dllがあることを確認して、アプリを実行してプラグイン内の関数が動く事を確認します。

デバッグをする時

exeの中のPlugins/x86_64/内のunity_rsgen_sample.dllファイルを消して、Rustのライブラリプロジェクトから出力するdebugのdllにシンボリックリンクを貼っておきます。

PowerShellの管理者権限実行をして、以下のように実行すると、0KBのsymlinkが出来ます。

New-Item -Value "C:\Github\unity_rsgen_sample\target\debug\unity_rsgen_sample.dll" -Path "C:\Github\UnitySimple\Build\UnitySimple_Data\Plugins\x86_64" -Name "unity_rsgen_sample.dll" -ItemType SymbolicLink

このexeファイルを実行した後、CLion内のライブラリプロジェクトの中のコードにブレークポイントを貼ってから、CLionのRun→Attach Processで起動中のUnity製のexeファイルを指定します。

そしてUnityのexeファイル内でmy_addを呼び出すと、CLionがアクティブになり、変数のウォッチや各種デバッグが可能になります。

あらかじめUnity側でCLionのデバッグがしやすいように変数パターンや呼び出しタイミングを検討したテストシーンを作っておくと良いと思います。

Androidでビルドして、グルーコード無しで呼び出せていることを確認する

こちらも実機ビルドをして確認します。

原理的にはx86向けのAndroidエミュレータビルドをすることでWindows向けと同様のデバッグが可能な気がしますが、UnityのAndroidエミュレータでの動作についてあまり詳しくないので未検証です。

ここまでが出来ると何が出来るの?

たとえばソーシャルゲームにおいてアクセストークンの暗号化、復号処理をピュアRustで実装して、サーバサイド、Unityそれぞれ共通ロジックで実装することが出来そうです。

また、純粋な計算処理(例えば音声のリアルタイム処理やエンコード、デコードなど)をRustに任せるというのも良いかも知れません。

感想

csrustgenがグルーコード地獄を解決してくれたおかげでRustに入門するきっかけになりました。
開発環境としてもC++よりモダンなRustでマルチプラットフォームで動く(要出典)ライブラリエコシステムを自由に(要出典)Unityでも使える可能性があって、なかなか便利そうだなと思いました。

Discussion