RustでAndroid/iOSクロスプラットフォームしちゃう
その前
Rustプログラミング言語は結構気になりまして、近年ITエンジニアの中ですごく流行っていました。
C、C++、goと同じコンパイル言語で、速度はもちろんのこと、癖とか、メモリーの使い方とか、本当にエンジニア向けの感じします。
自分は去年からRustを勉強し始めもうすぐ一年半のところ、そろそろアプリ開発向けに何かできるのかを検証したいと思いました。
Rustの本にはProgramming a Guessing Gameのページあり、この流れで、スマホアプリを作りました。Rustでクロスプラットフォーム的なもので完成できたと思います。
サンプルとしてのソースコードは下記で共有しました、あなたのRust道に少し参考になれば幸いです。
仕組み
強いて言えば、Rustは現代のC/C++だと思います。
アプリに導入するであれば、バイナリライブラリーの形で、今までのC/C++の仕組みを利用することになっています。
環境構築
まずRustの環境を整えましょう、rustup
でしょう。
公式サイトのおすすめ方法になります。
Macの場合下記になります。Windows系の場合、インストラーは存在しています。結構スムーズに構築できます。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
現時点で使用しているRustのバージョンは以下になります。
rustc 1.69.0 (84c898d65 2023-04-16)
cargo 1.69.0 (6e9a83356 2023-04-12)
開発手順
ワークフローは以下になります。
具体的に展開していきましょう。
ライブラリー作成
アプリのプロジェクトと関連づけるため、先にRustライブラリープロジェクトを作成いたします。
cargo new appdemo --lib
ここはVSCodeの出番です。
Rustを実装
ライブラリーなので、main関数は必要ないでした。
用意するのは二つの関数でいいかもしれない。
//Rustの外にエクスポートするRustの関数なので、マングルしないようにコンパイラに指示するのコツです。
// 乱数を生成する
#[no_mangle]
pub extern fn create_secret_number() {
...
}
// 入力チェック
#[no_mangle]
pub extern fn guess(input: i32) -> *mut c_char {
...
}
ちょっと一点注意する場合ありまして、Androidのアプリプロジェクトと関連付けの時は、jniの仕組みにより、下記のようにAndroidだけのrust関数を作る必要があります。
#[cfg(target_os="android")]
#[allow(non_snake_case)]
pub mod android {
extern crate jni;
.....
#[no_mangle]
pub unsafe extern fn Java_reinf0rce_androiddemo_RustGame_guess(env: JNIEnv, _: JClass, android_input: jint) -> jstring {
...
}
#[no_mangle]
pub unsafe extern fn Java_reinf0rce_androiddemo_RustGame_createRandomNumber(_: JNIEnv, _: JClass) {
...
}
}
ビルド関連
クロスプラットフォームのため、一番肝心な仕事はビルドだと思います。
順番的に踏みましょう。
1. ビルド環境。
まずはiOSとAndroidのRust関連ビルドツールを揃える必要があります。
iOSの場合:
rustup target add aarch64-apple-ios x86_64-apple-ios
Androidの場合。
rustup target add aarch64-linux-android x86_64-linux-android
ここはaarch64シリーズ(実機)とx86_64シリーズ(シュミレーター)で試したいと思います。
-
Cargo.toml
でビルド設定を行います。
iOSの場合、xxx.aのは普通で、Androidの場合、xxx.soは多いに対して、下記の設定で十分と思います。
...
[lib]
crate-type = ["staticlib", "dylib"]
- androidのビルド設定
[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.5", default-features = false }
- cargoのndk周り設定(
.cargo/config
)
[target.aarch64-linux-android]
linker = "NDK/arm64/bin/aarch64-linux-android-clang"
[target.x86_64-linux-android]
linker = "NDK/x86/bin/x86_64-linux-android-clang"
- ビルド実行。
両プラットフォームのビルドコマンドちょっとだけ違うけど、個人的にはiosにlipo
、 androidはtarget
指定でいいと思います。
iOSの場合:
// aarch64とx86_64一緒に作ります
cargo lipo --release
Androidの場合:
// 実機
cargo build --target aarch64-linux-android --release
もしくは
// シュミレーター
cargo build --target x86_64-linux-android --release
アプリプロジェクトの関連付け
iOSの場合:
- iOSプロジェクト作成
-
General
でlibresolv.tbd
とlib[ライブラリー名].a
を関連付け - Headerファイルを作成
-
Build Settings
でLibrary Search Paths
を設定:[rustプロジェクト]/target/universal/release -
Build Settings
でObjective-C Bridging Header
設定:3作ったファイル
Headerファイルサンプルです。
#ifndef iOSDemo_Bridging_Header_h
#define iOSDemo_Bridging_Header_h
#include <stdint.h>
const char* guess(int input);
void create_secret_number();
#endif /* iOSDemo_Bridging_Header_h */
Androidの場合:
- Androidプロジェクト作成
- appの
build.gradle
でjni周り設定:
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
- jniフォルダにバイナリをコピー:x86_64やaarch64に
lib[ライブラリー名].so
をコピペ
アプリで呼び出し
ここまで来たら、呼び出しするだけになります。
iOSの場合(Swift):
class RustGame {
func createRandomNumber() {
create_secret_number()
}
func guessNumber(input: Int32) -> String {
let result = guess(Int32(input))
let swift_result = String(cString: result!)
return swift_result
}
}
結果:
Androidの場合(Kotlin):
class RustGame {
companion object {
init {
System.loadLibrary("appdemo")
}
@JvmStatic
private external fun createRandomNumber()
@JvmStatic
private external fun guess(input: Int): String
}
fun randomNum() {
createRandomNumber()
}
fun guessNum(input: Int): String {
return guess(input)
}
}
結果:
まとめ
最後は点数を付けたいと思います。
iOSはわりと楽にできて、70点として、Androidはrustコードを追記する必要があるとabi周りの設定があって、60点かなぁと思います。
Google先生が積極的にRustを使っているので、2022年でRustの特性のおかけて、メモリー安全性関連の脆弱性は76%から35%に減少したとのことでした。
また、rustはクロスプログラミング向けのffiがありまして、Taruiみたいな優秀なフレームワーク出たので、これから色々楽しみでしょうと信じています。
個人的には、Flutterと組めると思いまして、またの機会でRustベースのFlutterライブラリーを作りたいと思います。
参考
Discussion