📲

RustでAndroid/iOSクロスプラットフォームしちゃう

2023/05/16に公開

その前

Rustプログラミング言語は結構気になりまして、近年ITエンジニアの中ですごく流行っていました。
C、C++、goと同じコンパイル言語で、速度はもちろんのこと、癖とか、メモリーの使い方とか、本当にエンジニア向けの感じします。

自分は去年からRustを勉強し始めもうすぐ一年半のところ、そろそろアプリ開発向けに何かできるのかを検証したいと思いました。

Rustの本にはProgramming a Guessing Gameのページあり、この流れで、スマホアプリを作りました。Rustでクロスプラットフォーム的なもので完成できたと思います。

サンプルとしてのソースコードは下記で共有しました、あなたのRust道に少し参考になれば幸いです。

https://github.com/zamberform/rust_x_demo

仕組み

強いて言えば、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シリーズ(シュミレーター)で試したいと思います。


  1. 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"

  1. ビルド実行。
    両プラットフォームのビルドコマンドちょっとだけ違うけど、個人的には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の場合

  1. iOSプロジェクト作成
  2. Generallibresolv.tbdlib[ライブラリー名].aを関連付け
  3. Headerファイルを作成
  4. Build SettingsLibrary Search Pathsを設定:[rustプロジェクト]/target/universal/release
  5. Build SettingsObjective-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の場合

  1. Androidプロジェクト作成
  2. appのbuild.gradle でjni周り設定:
sourceSets {
        main {
            jniLibs.srcDirs = ['src/main/jniLibs']
        }
    }
  1. 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ライブラリーを作りたいと思います。

参考

https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-06-rust-on-ios.html

https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-21-rust-on-android.html

Discussion