Unreal Engine で Rust を (上手に) 使いたい
はじめに
Unreal Engine で Rust を使いたい。
使うだけなら DLL をビルドして UE のプロジェクト内にコピーすればいいだけですが、 UE プロジェクト外での管理では Rust コードの更新を UE 側に反映するのが面倒だったり、そもそも当該 UE プロジェクト専用の Rust コードである場合はまとめて管理したくもなります。
という事で、その辺を解決する方法を調べてみました。
実証コードはこちらです (UE 5.5.4) 。 UE プロジェクトになっていますが C++ と Rust のみで Contents は本題ではないので削除しています。
ターゲットは Windows です。他プラットフォームに対する場合は Build.cs の内容などで調整が必要になると思います。
モチベーション
そもそもなぜ UE で Rust を使いたいか、なのですが。
UE は標準の開発言語が C++ であるため、言語として Rust を使うメリットはほとんどないように思います。 Unity は C# が標準で、他言語はネイティブプラグインとして分離されているので、 C++ も Rust もある意味等価と言えます。
UE で Rust を使いたい最大の理由は、crates.io で提供されている様々なパッケージを使いたいからです。例えば UE で http リクエストを行いたい場合、まずは FHttpModule を使う事になると思いますが、 FHttpModule では要件を満たさない場合、 (外部アセットを使うとかでなければ) 途端に敷居が上がります (現代の http は難しいプロトコルですし、そうでなくても SSL の対応はまあまあ大変です) 。これを Rust の hyper やそれをより簡単に使えるようにした reqwest を使うと一気に解決します。
Rust は C++ と異なりライブラリパッケージが整備されていて、多くの場合簡単に使用することができ、数も充実しています。これらを使えるようにすることは大きなメリットになると思います。
プロジェクト準備
Windows では Rust の Toolchain は "stable-x86_64-pc-windows-msvc" を使用します。
また、ライブラリ化の方法として "静的ライブラリ (.lib / static link library)" と "動的ライブラリ (.dll / dynamic link library)" の 2 通りの方法がありますが、両方とも解説します。
共通
まず対象となる UE のプロジェクトを用意します。
次に Rust プロジェクトを作成します。 UE の場合、外部ライブラリは Source/ThirdParty 以下に配置するのが作法らしいので、それに則ります (アプリ本体でもプラグインでも同様) 。
cargo new --lib --vcs none --name rust_lib RustLib
ライブラリプロジェクトなので --lib 、 VCS は UE 側で設定されているはずなので none とします。
cargo new ではパッケージ名は通常省略 (ディレクトリ名と同じ) しますが、パッケージ名は Rust に合わせて snake case に、ディレクトリ名は UE に合わせて Upper Camel case にするように個別指定しています。
cbindgen (C ヘッダーファイルの作成) の準備
作成した Rust ライブラリは UE の C++ 側からアクセスできるように C/C++ のヘッダーファイルが必要になります。ヘッダーファイルの作成は cbindgen を用いる事で自動化することができます。
まず cargo.toml に cbindgen を追加します。
[build-dependencies]
cbindgen = "0.28.0"
次に cargo でのカスタムビルドスクリプトを記述する build.rs を下記のコードで追加します。
このようにすると cargo build 時にヘッダーファイルも生成されます。ヘッダーファイルは Rust から生成されるのでリポジトリ管理対象外にしましょう。
静的ライブラリとして作成する
cargo.toml でシステム互換の静的ライブラリを作成する指定をします。
[lib]
crate-type = ["staticlib"]
Rust プロジェクトのディレクトリ下に UE のビルド定義ファイルである Build.cs を作成します。
- ライブラリのビルドは UE と独立しているので Type に ModuleType.External を指定
- Shipping 時は release ビルド、それ以外は debug (dev) ビルドをリンクするように設定
- PublicAdditionalLibraries にリンクする Rust ライブラリを追加
- UE でのビルド時、システム DLL のインポート解決ができなかったので、リンクエラーになったライブラリも追加
とりあえずこのような配置になりました。
動的ライブラリとして作成する
cargo.toml で動的ライブラリを作成する指定をします。
[lib]
crate-type = ["cdylib"]
静的ライブラリと同様に Rust プロジェクトのディレクトリ下に UE のビルド定義ファイルである Build.cs を作成します。
内容としては静的ライブラリのものと基本的には同じですが、
- RuntimeDependencies に .dll とデバッグ用シンボルファイル (.pdb) を追加
- これにより Editor 上での実行時の参照先に追加され、パッケージ作成時には当該ファイルがパッケージ内容に含まれるようになります
- PublicAdditionalLibraries にインポートライブラリの .lib を指定
DLL は静的ライブラリと異なり、ランタイムライブラリの混在などを考える必要はありません (DLL のビルドに成功していれば OK) 。
ビルド
Rust ライブラリは UE での作業前に個別にビルドします。 UE の BuildTool でビルドできるとよかったのですが、少なくとも開発中においては UE の作業前に Rust 側のビルドをしていないとヘッダーファイルもない状態なので、これはこれでよいかなと思います。 CI を利用している場合は UE の前に Rust のビルドタスクの追加をしてください。
上記の Build.cs に則った場合、 Shipping 以外では debug を、 Shipping では release を使用するのでビルドターゲットに注意してください。
Rust ライブラリのビルド後は UE 側で通常の開発作業を行ってください。
デバッグ
静的、動的どちらもシンボル情報ありなので、デバッガでブレイクしてその時点の状態を観察することができます。
UE のプロジェクトを開き、 Unreal Editor に対してプロセスアタッチして確認しています。変数の中身の確認ができています。
ただ Rider の場合は .rs にブレイクポイントがおけなかったので、 C++ の呼び出し側でブレイクポイントをセットしておいて、ステップインで Rust コードに入ることで確認をしています。
一方で Visual Studio の場合は .rs でもブレイクポイントがおけます。
私は基本的に Rider で作業をしていますが、デバッガに関しては Visual Studio の方が (これに限らず) 強力なので、どうしてもという時は Visual Studio を使ったりして使い分けをしています。
おわりに
という事で現状でまずやりたい事は達成できたので満足はしています。
静的ライブラリ (.lib) にするか動的ライブラリ (.dll) にするか、はほとんどの場合動的の方がトラブルが出る可能性も低いので動的にしておいた方が無難かなあとは思いますが、静的の場合は関数のエントリーポイントの名前を見えないようにできるというメリットもあります (DLL は dumpbin などで簡単に見れます) ので、秘匿性を上げたい場合は静的ライブラリにするのがよいかなと思います。
これで Rust コードを UE に組み込む事はできるようになりましたが、実際に Rust を使っていくにはまだ課題があります。特に非同期ランタイムの扱いはなかなか難しくて、ここをシームレスに扱えると大分よくなるのですが・・・ これはどちらかという C++ と Rust 間の問題かもしれませんが、この辺りも模索していきたいです。
Discussion