やったこと
GBAのエミュレータ(visualboyadvance-m)上の太陽センサー*1の値をエミュレータ内threadに立てたHTTPサーバによって操作できるようにした。
今回はRust FFIを利用してできるだけ元のコードを汚さずに、かつHTTPサーバの処理等はRustの世界で閉じたものにする、という方向で実装していた。
ちなみにFFIを触っているが自分はRustはまあともかくC/C++は多少読めるけどちゃんと書くのは無理、ぐらいのC/C++の習熟度です。
Rust FFIを実装する上で必要な作業
正直、割と曖昧なまま触っている部分も多いが概ね以下の作業が必要。Ubuntu(apt)のmxe repoが古く、C++17サポートが不完全であるためWindows target向けのクロスコンパイルが壊れているので、それらをなんとかする作業も必要だったが、本筋ではないので割愛*2。
- C/C++側
- Rust側で触る想定のリソース(変数・関数)をRust側から見つけられるようにexposeする
- Rustからexposeされている関数等のリソースをextern宣言し、呼び出したい箇所で使用するように実装する。
- Rust 側
- ビルドツールたち
- CMakeLists.txt を頑張って編集して、C/C++側のオブジェクトファイルとRust側のstaticlibをリンクする
C/C++側: Rust側で触る想定のリソース(変数・関数)をRust側から見つけられるようにexposeする
今回、Rust側では以下のリソースを触る想定となっている。
- 太陽センサーの値の取得する関数
- 太陽センサーの値を更新する関数
そのため、以下のように extern "C"
を利用してCのABIで*3 関数をexposeしてあげる必要がある
extern "C" uint8_t systemGetSensorDarkness() { return sensorDarkness; } // (snip) extern "C" int level = 0; extern "C" void systemUpdateSolarSensor() { uint8_t sun = 0x0; //sun = 0xE8 - 0xE8 (case 0 and default) // このlevelはRust側にexposeして触りたい // int level = sunBars / 10;
systemUpdateSolarSensor
関数のローカル変数を extern
してexposeしているのは、 systemUpdateSolarSensor
関数のシグネチャを変更する場合他の関数の呼び出し方を変更する必要があり、シグネチャを変更せずに済むようにRust側で level
を編集してから呼び出すといった形で実装することにしたため。
その後、今回 extern
宣言に変更したリソースを使っている他の箇所で同様に extern "C"
する形に変更していく必要がある。(static
と extern
ではメモリの確保のされ方(主にmangling周り)が変わるので別のアドレスが確保される可能性がありそうなので)
余談だが、最初は sensorDarkness
という systemUpdateSolarSensor
で更新している値を直接変更していたが、なぜか元の値に戻ってしまうのでこういった形で触るようにした。たぶん触ろうとしている関数が適切じゃないんだと思う。
C/C++側: Rustからexposeされている関数等のリソースをextern宣言し、呼び出したい箇所で使用するように実装する。
Rust側でexposeする関数は void start_server(void)
なので以下のように宣言しておく。
// src/wx/wxvbam.cpp extern "C" { void start_server(void); }
そしてたぶん一回だけ呼ばれるであろう関数で呼び出すようにしておく。このあたりは適当なので本当はちゃんと考えたほうがいい。*4
// src/wx/wxvbam.cpp bool wxvbamApp::OnInit() { start_server();
Rust側: C/C++側のリソースの宣言
Rustonomiconとか参考にしましょう。
今回の場合は以下みたいな感じ、特に説明することはないですが、リンク時に undefined reference
とか言われる場合は extern
宣言をミスってるとかそういうことだと思います。C++ ABIで単に extern
するとおそらく普通にmanglingされるのでRust側から見つけられないです(たぶん)。
リンク時にミスってたらひたすらオブジェクトファイルをnm
とかしてシンボル一覧と睨めっこしましょう。俺はした。
extern "C" { static mut level: i8; fn systemUpdateSolarSensor(); fn systemGetSensorDarkness() -> u8; }
Rust側: C/C++側で使ってもらう関数の実装、expose
単純に extern "C"
で関数を宣言しましょう。 #[no_mangle]
をつけてこちらもmanglingされないように気をつけて。
#[no_mangle] extern "C" fn start_server() {
また、今回サーバ実装部分をaxumで実装しているので、axumのサーバ実行時にFutureをExecutorで実行してあげる必要があります。(単に実行するだけではFuture内の処理は実行されない)(と思う)
std::thread::Builder::new() .name("solar_server".into()) .spawn(|| { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { axum::Server::bind(&"0.0.0.0:8000".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); }); });
単に tokio::runtime::Runtime::block_on
を実行するとそこでthreadがブロックされるので、適当に std::thread::Builder::spawn
してあげましょう。これで別Threadでサーバが動いてくれるはず。
CMakeLists.txt を頑張って編集して、C/C++側のオブジェクトファイルとRust側のstaticlibをリンクする
ここが一番分かっていないけどなんとかしました。適当に以下でいけたぜ、という程適当にはやっていないがあんま詳しくないぜ。
# src/wx/CmakeList.txt target_link_libraries(visualboyadvance-m ${CMAKE_SOURCE_DIR}/solar-server/target/x86_64-pc-windows-gnu/release/libsolar_server.a userenv ntdll bcrypt)
やっていることとしては wx
内にある CMakeLists.txt
で その中のプロジェクト(?)である visualboyadvance-m
のオブジェクト達を今回作ったRustのstaticlibとリンクしてあげるという作業です。
userenv ntdll bcrypt
あたりはRustのstaticlibが解決できなかったシンボルを解決するためにリンクしています。 GetUserProfileW
みたいなWindows APIっぽい関数が見つけられていなかったので、そのあたりで必要そうなライブラリを探してきてリンクしたという形になります。
ビルド
export PATH="${PATH}:/PATH/TO/MXE/usr/bin" mkdir build && cd build /PATH/TO/MXE/usr/bin/x86_64-w64-mingw32.static-cmake .. -DCMAKE_RELEASE_TYPE=Release -G Ninja ninja
やるだけ、リンクに失敗したらシンボルを見てコードを修正していくしかないので頑張りました。