(WIP) Rust+uniFFIで、Android/iOS向けの共通モジュールを書く
概要
参考記事
Rustで書いたコードを、Android(Kotlin)で呼び出す
JNI(Java Native Interface)を用いて呼び出します。
詳細は下記
Rustで書いたコードを、iOS(Swift)で呼び出す
Cブリッジ(C言語インターフェース)を用いて呼び出します。
詳細は下記
KotlinからJNI経由でRustモジュールを利用する概要
JNIとは、JavaプログラムがCやC++で書かれたネイティブコードと相互作用するためのインターフェースで、
Java(Kotlin)アプリケーションからシステムレベルの機能や、パフォーマンスが重要な計算を行うために最適化されたコードを呼び出すことができます。
JNIの基本的な流れ
- ネイティブメソッドの定義: Javaクラスでネイティブメソッドを定義する。
object Sample {
init {
System.loadLibrary("sample_rs")
}
external fun nativeMethod(data: ByteArray): String?
}
- ネイティブライブラリの実装: C/C++でそのメソッドを実装する。
#include <jni.h>
#include "Sample.h"
JNIEXPORT void JNICALL Java_Sample_nativeMethod(JNIEnv *env, jobject obj) {
// ネイティブコードの実装
}
-
ライブラリのコンパイル: Rustコードをコンパイルし、共有ライブラリ(.soファイルなど)を生成します。
-
Javaから呼び出す: Javaからネイティブメソッドを呼び出すことができる。
注意点
-
メモリ管理: JNIを使用する場合、メモリ管理に注意!
例えば、ネイティブコードで確保したメモリは、Javaのメモリ管理の中で自動的に開放されないので、意識的に開放する仕組み・コードを作る必要があります。
SwiftからCブリッジ(C言語インターフェース)を利用してRustのモジュールを呼ぶ方法
Cブリッジ(C言語インターフェース)
iOSではJNIのような仕組みは不要で、直接C言語インターフェース経由でSwiftからRust関数を呼び出せます。
-
Cインターフェース:
- Rustで**静的ライブラリ(.a)**を、iOSの環境別にビルドして生成する
-
ブリッジヘッダーの準備:
- Cブリッジを利用してRustモジュールをSwiftプロジェクトに取り込む際、
ヘッダーファイルをブリッジヘッダー(Objective-Cブリッジヘッダー)として指定し、Swiftからそのヘッダーで定義されたRustの関数にアクセスできるようにします。
- Cブリッジを利用してRustモジュールをSwiftプロジェクトに取り込む際、
※Rustコードから自動生成するツールがあります。詳細は後ほど。
// SampleBridge.h
#ifndef SampleBridge_h
#define SampleBridge_h
void sampleFunction();
#endif /* SampleBridge_h */
-
Swiftから呼び出す:
- Swiftコードの中で、ブリッジヘッダーを通じてRust関数を呼び出すことができます。
import Foundation class Sample { func callNativeFunction() { sampleFunction() // C関数を呼び出す } }
注意
- エラーハンドリング: C/C++とSwiftではエラー管理の仕組みが異なるため、適切にエラーハンドリングを行う必要があります。
- メモリ管理: Kotlin同様に、Swiftと異なるメモリ管理の手法を使用するため、メモリリークを避けるために明示的な管理が必要です。
ビルド設定の種類
-
staticlib
静的ライブラリとしてビルドします。たとえば、CやC++のプロジェクトに組み込む場合、ライブラリをリンク時に結合するために使います。 -
cdylib
C互換の動的ライブラリとしてビルドします。これは、共有ライブラリ(.so、.dylib、.dllなど)として出力され、他の言語からFFI(Foreign Function Interface)を通じて利用できるように設計されています。Rust特有のメタデータが含まれないため、C言語やSwift、Javaなどと連携しやすくなります。
uniFFIを利用したRustモジュール構築
Rust→Kotlin、Rust→Swiftの各ブリッジを抽象化・共通化して
そこで発生しがちなメモリ管理や型定義などのリスクを吸収してくれるツールとして、
mozillaが開発しているオープンソースツールのuniFFIがあるので、
これを利用して実装してきます。
uniFFIのバインディング設定
UniFFIでは、バインディングのインターフェースを定義する方法として、
udlという、専用の**インターフェース定義言語(WebIDLを元にしたIDL)**を用いて、公開APIを宣言する方法と、proc macroを利用して、バインディングの生成をする2通りの方法があります。
▼WebIDLについてはこちら
▼UDLのTypesと、RustのTypesのマッピング表
▼サンプル集
UDLが、Rust開発者にとってフレンドリーではない(Rustの記法と異なる部分が多かったり、エラーメッセージから間違いが判断できない)ので、
UDL2を作るべきだとか、proc macroからUDLを生成する方法の提案などが上がっています。
確かにWebIDLに馴染みのない一般的なRust開発者なら、procmacroによる実装の方がDXが良いですね。
UDLには、インターフェースのAPIドキュメントとしての中間生成物の意味もあるので、
proc macroのように中間生成物が無いと、人間が仕様を把握しづらいという状況が生まれていて、
これらにどう対応するか、が今後の課題となっているようです。
方針
それなりに規模が大きいプロダクトになってくると、
UDLがRustと同期が取れていることを確証する方法がなく、エラーメッセージもわかりづらいことから、管理が破綻する可能性が高いので、
今後はproc_macroを使ったバインディングを行なって行こうと思います。
参考:UDLを使った事例
サンプルとして、文字列を受け取ってハッシュ化する、という関数を定義します。
[package]
name = "hashlib"
version = "0.1.0"
edition = "2024"
[lib]
#Android用のcdylib(C互換動的ライブラリ)、iOS用のstaticlib(静的ライブラリ)を定義
crate-type = ["staticlib", "cdylib"]
[dependencies]
hex = "0.4.3"
sha2 = "0.10.8"
uniffi = {version = "0.29.1", features = ["cli"]}
[build-dependencies]
#uniffiの、ビルド用のcrate。
uniffi = { version = "0.29.1", features = [ "build" ] }
[[bin]]
name = "hashlib"
path = "hashlib.rs"
pub enum HashAlgorithm {
SHA256,
MD5,
}
pub struct HashResult {
pub algorithm: HashAlgorithm,
pub hex: String,
}
pub fn hash_bytes(data: &[u8], algo: HashAlgorithm) -> HashResult {
// `data`を`algo`でハッシュ計算し、結果をhex文字列に
let hex_str = match algo {
HashAlgorithm::SHA256 => { /* SHA-256計算 */ todo!()},
HashAlgorithm::MD5 => { /* MD5計算 */ todo!()},
};
HashResult { algorithm: algo, hex: hex_str }
}
pub fn hash_string(data: &str, algo: HashAlgorithm) -> HashResult {
let bytes = data.as_bytes();
hash_bytes(bytes, algo)
}
// hashlib.udl
namespace hashlib {
enum HashAlgorithm { "SHA256", "MD5" }; // 列挙型の定義{index=4}
dictionary HashResult { // 構造体の定義
HashAlgorithm algorithm;
string hex;
};
HashResult hash_bytes([ByRef] bytes data, HashAlgorithm algo);
HashResult hash_string([ByRef] string text, HashAlgorithm algo);
};
上記UDLでは、enum
はバリアント名を文字列リテラルで列挙し(Rust側のHashAlgorithm::SHA256
⇔ UDL上は"SHA256"
、dictionary
を使って構造体に相当する型を定義しています。
また関数hash_bytes
とhash_string
を公開し、それぞれバイト列(bytes
型)または文字列(string
型)を入力に取り、選択したアルゴリズムでハッシュ計算してHashResult
を返すよう宣言しています。引数には[ByRef]
を付けることで、Rust関数側で&[u8]
や&str
として受け取れることも示しています。
UDLファイルに合わせて、Rust側でも同じ関数・型を実装します
(サンプルのようにUDLの列挙型や構造体の定義が、Rust側にも同名のenumやstructに該当します)
UniFFIはビルド時にUDLとRustコード双方を見て、自動的に対応するブリッジ(FFI)コードを生成します。
UniFFIによるRust側FFIコードの自動生成
実際に、UniFFIに**Rust側のFFI Scaffolding(足場コード)**を生成させます。
方法は2通りありますが、ここではBuild Scriptを使う方法を取ります。
// build.rs
fn main() {
uniffi::generate_scaffolding("./src/hashlib.udl").unwrap();
}
Rustのライブラリコード(src/lib.rs)の先頭で、生成されたScaffoldingをincludeします:
uniffi::include_scaffolding!("hashlib");
これにより、コンパイル時にUDLに基づいたextern "C"関数群や型変換ロジックが、
自動生成・組み込みされます。
Rust関数hash_bytesやhash_stringがUniFFI経由で外部公開され、構造体HashResultやenumHashAlgorithmについても、各言語バインディング用に必要な変換実装が生成されます。
Android用のビルド
必要なtargetを入れて
-
aarch64-linux-android:64ビットARM向け
-
armv7-linux-androideabi:32ビットARM向け
-
i686-linux-android:32ビットIntel向け
-
x86_64-linux-android:64ビットIntel向け
これらは、次のようなコマンドで追加できます:
# Android 64ビットARM
rustup target add aarch64-linux-android
# Android 32ビットARM
rustup target add armv7-linux-androideabi
# Android 64ビットIntel
rustup target add x86_64-linux-android
# Android 32ビットIntel
rustup target add i686-linux-android
ビルド
#!/bin/sh
set -e
cargo build --lib --release --target=aarch64-linux-android
cargo build --lib --release --target=x86_64-linux-android
cp target/aarch64-linux-android/release/libauth2.so ../android/app/src/main/jniLibs/arm64-v8a/
cp target/x86_64-linux-android/release/libauth2.so ../android/app/src/main/jniLibs/x86_64/
cargo run --features=uniffi-cli --bin uniffi-bindgen -- generate -l kotlin --library target/aarch64-linux-android/release/libauth2.so --out-dir ../android/app/src/main/java
cargo buildと、cpで指定の場所にコピーしてくれる部分を自動的にやってくれるのが
こちらのプログラムです。
ちなみに、記事中にも言及がありましたが、
複雑なcrateを利用したりしていると、NDKの最新を使った時に関数が見つからないなどの問題が生じることがあるとのことで、NDKのバージョンを指定する必要がある場合、バージョンを指定することもできます。
例:NDK 25.2.9519653 を使いたい場合
export ANDROID_NDK_HOME="$HOME/Android/Sdk/ndk/25.2.9519653"
ちなみに依存crateの関数がバインディングされない問題が起きることがあると
UniFFIの公式に記載があり、
その場合には uniffi_reexport_scaffolding!
を使うと良い、との記載があるので、
もしうまく連携できない時があれば、NDKのバージョンを戻す前に、こちらも試してみようと思います。
詳細
uniffi_reexport_scaffolding! と uniffi::setup_scaffolding! は、Rust の UniFFI ライブラリで異なる目的で使用されます。これらは同じクレート内で一緒に使用可能ですが、状況によってどちらか一方だけを使う場合もあります。uniffi::setup_scaffolding! は自身のインターフェースをプロックマクロで定義する場合に使い、uniffi_reexport_scaffolding! は依存するコンポーネントのスキャフォールディングを再エクスポートする場合に使います。
概要
これらのマクロは、Rust と他の言語間の FFI(Foreign Function Interface)を設定する際に役立ちます。uniffi::setup_scaffolding! は、クレートがプロックマクロのみを使用する場合に初期スキャフォールディングを設定します。一方、uniffi_reexport_scaffolding! は、クレートが他の UniFFI コンポーネントに依存し、そのスキャフォールディング関数を再エクスポートする必要がある場合に使用されます。
使用方法
もしあなたのクレートがproc macroで自身のインターフェースを定義し、かつ他のコンポーネントに依存している場合、両方を使うことができます。例えば、uniffi::setup_scaffolding! を自身のインターフェース用に使い、foo_component::uniffi_reexport_scaffolding!() を依存コンポーネント用に呼び出します。
ただし、同じクレート内で uniffi::setup_scaffolding! と uniffi::include_scaffolding! を一緒に使うのは避けるべきだとドキュメントで警告されています(UDL ファイルとプロックマクロの混在)。
uniffi_reexport_scaffolding! は実際には UniFFI クレート自体ではなく、依存する各コンポーネントクレート(例: foo_component)が提供するマクロです。
bindgen binaryの生成
uniFFIを使って、Android, iOS用のbindgen binaryを生成していきます。
準備。
[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"
fn main() {
uniffi::uniffi_bindgen_main()
}
cargo run コマンドから実行できます。
#cargo run --features=uniffi/cli --bin uniffi-bindgen [args]
$ cargo run --features=uniffi/cli --bin uniffi-bindgen help
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/uniffi-bindgen help`
Scaffolding and bindings generator for Rust
Usage: uniffi-bindgen <COMMAND>
Commands:
generate Generate foreign language bindings
scaffolding Generate Rust scaffolding code
print-repr Print a debug representation of the interface from a dynamic library
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
buildからの bindgen
#ビルドして、target/release/libmath.so ファイルを生成
cargo build --release
#生成されたlibmath.soをもとに、generate
#for Android
cargo run --features=uniffi/cli --bin uniffi-bindgen generate --library target/release/libmath.so --language kotlin --out-dir out
#for iOS
cargo run --features=uniffi/cli --bin uniffi-bindgen generate --library target/release/libmath.so --language swift --out-dir out
Kotlin用に生成されたファイル
// This file was autogenerated by some hot garbage in the `uniffi` crate.
// Trust me, you don't want to mess with it!
@file:Suppress("NAME_SHADOWING")
package uniffi.math
// Common helper code.
//
// Ideally this would live in a separate .kt file where it can be unittested etc
// in isolation, and perhaps even published as a re-useable package.
//
// However, it's important that the details of how this helper code works (e.g. the
// way that different builtin types are passed across the FFI) exactly match what's
// expected by the Rust code on the other side of the interface. In practice right
// now that means coming from the exact some version of `uniffi` that was used to
// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin
// helpers directly inline like we're doing here.
import com.sun.jna.Library
import com.sun.jna.IntegerType
import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.Structure
import com.sun.jna.Callback
import com.sun.jna.ptr.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.CharBuffer
import java.nio.charset.CodingErrorAction
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.ConcurrentHashMap
// This is a helper for safely working with byte buffers returned from the Rust code.
// A rust-owned buffer is represented by its capacity, its current length, and a
// pointer to the underlying data.
/**
* @suppress
*/
@Structure.FieldOrder("capacity", "len", "data")
open class RustBuffer : Structure() {
// Note: `capacity` and `len` are actually `ULong` values, but JVM only supports signed values.
// When dealing with these fields, make sure to call `toULong()`.
@JvmField var capacity: Long = 0
@JvmField var len: Long = 0
@JvmField var data: Pointer?
...
省略
...
swift用に生成されたファイル(こちらも中身のボリュームが大きいので省略
out/math.swift
out/mathFFI.h
out/mathFFI.modulemap
kotlinのプロジェクトに上記を置くと、
JNAの依存関係が存在しないと言われる。
UniFFI では、マルチプラットフォーム(iOS, Android, JavaScript, など)に共通した仕組みとして「C の FFI (Foreign Function Interface) を経由しつつ、各言語で同じようなコードを生成する」手法がとられています。
Android と iOS で別々に JNI や Swift のバインディングを書くより、JNA(Java 側)や Swift の FFI 機能と組み合わせたほうが、開発チームとしては保守しやすい、という意図があります。
ただし、Android Studio/Gradle でビルドするときは JNA ライブラリが含まれていない ため、import com.sun.jna 部分でコンパイルエラーやランタイムエラーが出ます。したがって Android で動かすには、下記のように 追加で JNA を依存関係に入れる必要 があります。
chatGPTに、なぜJNAを使うのかを確認
dependencies {
// ネット上で配布されている jna-x.x.x.jar を入手 or Maven Central から取得
implementation 'net.java.dev.jna:jna:5.9.0' // バージョンは例
}
1. なぜ UniFFI の Kotlin コードが JNA を使用するのか
UniFFI では、マルチプラットフォーム(iOS, Android, JavaScript, など)に共通した仕組みとして「C の FFI (Foreign Function Interface) を経由しつつ、各言語で同じようなコードを生成する」手法がとられています。
Android と iOS で別々に JNI や Swift のバインディングを書くより、JNA(Java 側)や Swift の FFI 機能と組み合わせたほうが、開発チームとしては保守しやすい、という意図があります。
ただし、Android Studio/Gradle でビルドするときは JNA ライブラリが含まれていない ため、import com.sun.jna
部分でコンパイルエラーやランタイムエラーが出ます。したがって Android で動かすには、下記のように 追加で JNA を依存関係に入れる必要 があります。
`dependencies { // ネット上で配布されている jna-x.x.x.jar を入手 or Maven Central から取得 implementation 'net.java.dev.jna:jna:5.9.0' // バージョンは例 }`
このように、「UniFFI が JNA に頼っているため、Android でそのまま動かすには明示的に JNA を追加しないといけない」 というのが実態です。ユニFFIの公式ドキュメントでも Kotlin 向け生成コードを使う場合、org.mozilla.uniffi:uniffi-android
(JNA入り) を使うように書かれていたりします。
2. JNA を使うのは本当におかしいのか?
-
「Android は通常 JNI を使う、JNA は一般的ではない」というのは事実 です。
ただし UniFFI はデスクトップ版 Firefox など他の JVM 環境(Java, Kotlin, etc.)でも動作させる都合があるため、JNA を使うことで「Java / Kotlin であれば同じ方法で Rust とやり取りする」仕組みを持ち込みやすくしています。 -
Mozilla などで実際にこの仕組みが使われている例もあり、「Android でのユースケースを想定している」という意味では嘘ではありません。
ただし、Android にプリインストールされているわけではないので、外部ライブラリとして JNA を追加しないといけない、という点を理解していないとエラーになります。 -
JNA はデスクトップ向けに作られた経緯が強いので、パフォーマンスの面などで JNI より劣る可能性もあり得ます。Rust との高速なやりとりが多いアプリでは、UniFFI + JNA の間にオーバーヘッドが発生 することもあるかもしれません。
3. ではどうやって Android で UniFFI を使うのか
(1) 素直に JNA を追加する
一番手っ取り早いのが「Gradle に JNA の依存関係を追加して、生成されたコードをそのまま使う」です。
この場合は、以下のような手順になります。
-
Rust 側の Cargo.toml で
uniffi = 0.29.1
など適切なバージョンを指定。 -
uniffi-bindgen generate --language kotlin ...
を実行し、*.kt
ファイルを生成。 -
Android Studio のプロジェクトで、以下のようにビルドスクリプトに JNA 依存を追加:
`dependencies { implementation 'net.java.dev.jna:jna:5.9.0' }`
-
生成された
.kt
を組み込み、val result = someFunctionFromRust(...)
など呼び出す。 -
.so
ファイルをapp/src/main/jniLibs/arm64-v8a/
等に配置し、実機やエミュレータでビルド。
これで JNA が参照でき、Rust の関数を Kotlin から呼び出せるようになります。
(2) どうしても JNA を使いたくないなら (JNI の手書き)
「JNA のオーバーヘッドが許容できない」「JNI を使いたい」「JNA がアプリサイズ増大の原因になるのが嫌だ」という場合は、UniFFI が自動生成した Kotlin バインディングを使わずに、手動で JNI バインディングを書く必要 があります。
しかしこの場合、UniFFI の恩恵(自動生成)が受けられなくなるため、“せっかく UniFFI を導入した意味が薄れる” というデメリットがあります。
今のところ UniFFI が公式に JNI を使った Kotlin バインディングをサポートする予定は明言されていないため、もし公式機能として待ちたい場合は Issue を立てるなどが考えられます。
4. 今後の展望と対策
UniFFI はまだバージョンが 1.0 に到達していないため、今後のアップデートで変わっていく可能性もあります。しかし現状としては、Android で UniFFI を活用したい場合、JNA を依存関係に加えて使う のが公式の想定ワークフローです。Mozilla 公式リポジトリでも「なぜ JNI ではなく JNA を使うか」「Android でどう対処するか」といった議論が行われていますが、
-
Rust × Kotlin × Android × iOS × JavaScript … という複数プラットフォームを一括管理するため、JNA が一番実装労力が少なかった
-
既存の Mozilla 製品(Firefox など)で JNA を使う下地がある
という理由から、当面はこの方針が維持される可能性が高いです。
したがって、「Android でも UniFFI が動く」という文言は嘘ではないのですが、実際には JNA という追加ライブラリを使う必要がある ので、それを知らずに「Android だけでビルドしたら何も用意しなくても動く」と思うと矛盾を感じてしまうわけです。
5. まとめ
-
UniFFI は Android (Kotlin) 向けにも利用可能だが、現在の Kotlin バインディングは JNA を利用しており、Android では JNA を手動で導入しないと
com.sun.jna
が見つからずコンパイルエラーになる。 -
これは UniFFI の仕様であり、「Android で公式サポート」と言いつつ JNA が含まれるのは矛盾に見えるが、Mozilla 的には「JNA で統一すればマルチプラットフォーム構成が楽」という設計方針。
-
対策としては Gradle に
net.java.dev.jna:jna
を追加する のが現実的。どうしても JNI にしたいなら手動バインディングを書くしかなく、UniFFI が自動生成する Kotlin コードはそのまま使えない。
したがって、「Android 用に作られた」と言いつつ「JNA 依存コードが出てくる」矛盾は、「UniFFI の設計上、Android 向けでも JNA を使う想定であり、Android では JNA が標準同梱されていないから」というのが理由となります。現在の UniFFI の実態としては「JNA を明示的に追加すれば Android でも動きますよ」という形で対応する方針です。
NDKのバージョンと、最新のRustとの関連性や安定性についての調査結果まとめ
最新NDKバージョンと Rust/UniFFI の互換性・安定性
2025年3月現在の最新NDKは r28(安定版) で、直近のLTS版は r27c です。
Rust公式も「Androidサポートは最新のLTS NDKを追随する」方針を掲げており、
実際 Rust 1.68(2023年初頭)で内部ターゲットNDKを従来のr17からr25(2022年LTS)へ更新しました。
これによりRustコンパイラ標準ライブラリが新しいNDKに対応し、NDK r25以降でのRustビルドが公式にサポートされています。UniFFI自体はRustのFFIをラップするツールなので、Rust側のNDK対応状況に従います。現在はNDK r27やr28でもRust/UniFFIによるライブラリビルドが可能であり、開発者たちのブログや記事でも最新NDKを用いた成功例が報告されています(後述の事例参照)。
過去の「NDK 22 が必要」問題と現在の改善状況
2021~2022年頃の状況: Android NDK r23でGNU libgcc
が除去されLLVMのlibunwind
に置き換わった影響で、Rust標準ライブラリ(当時NDK r17ベース)が**-lgcc
を参照してリンクエラーになる**問題が発生していました。
このため 「NDK 23ではビルドが通らないのでNDK 22にダウングレードすべき」 といった声が多く、実際に NDK r22 では正常に .so
を生成できる報告がありました。
開発者はRust nightlyで標準ライブラリを再ビルドしたり、NDK内のlibunwind.a
にINPUT(-lunwind)
を埋め込むハックで対応したケースもあります。
改善と解決: 2023年にRust本体がNDK r25対応となったことで、NDK r23以降を使う際の libunwind
周りのワークアラウンドが不要になりました。
Rust 1.68以降では古いlibgcc
への依存が解消され、NDK r23+でそのままビルド可能です。「NDK 22でないと動かなかった」問題はRustツールチェインの更新によって解決済みであり、現在はNDKを意図的に旧版に留める必要はありません。
むしろRust公式は「プロジェクトがNDK r22以下を使っているならr23以上へ更新が必要」とアナウンスしています。
Android主要ABIにおける安定動作実績
Rustは主要なAndroid ABIすべてに対応しており、最新NDKとの組み合わせでも概ね安定しています。実際、Rust公式ドキュメントにはサポート対象として armv7(armeabi-v7a
)、armv8 64-bit(arm64-v8a
)、x86、x86_64 などのターゲットが明記され、最新LTS NDKでこれらをビルドすることが推奨されています。
最近のUniFFI利用事例でも、arm64-v8a(現在のAndroid端末の主流)やarmeabi-v7a(32-bit ARM、レガシー端末向け)向けライブラリが問題なくビルド・動作しています。
また、x86_64やx86についても、エミュレータ環境向けにRustターゲットを追加してビルド可能です。
- arm64-v8a (ARM64) – 現行デバイスの大半を占める64ビットARM環境です。Rustでは aarch64-linux-android ターゲットとしてサポートされ安定しています。
NDK最新版でも特段の問題報告はなく、UniFFI経由のKotlin呼び出しでも高い安定性が確認されています。
- armeabi-v7a (ARM32) – 32ビットARM (armv7) 向けもサポートされています。
近年は64ビット移行が進み利用端末は減りましたが、Rust製ライブラリを32ビットビルドして既存のarmeabi-v7a端末で動作させる例もあります。
- x86_64 (Intel/AMD 64-bit) – 主にAndroidエミュレータや一部特殊端末向けのABIです。Rustの x86_64-linux-android ターゲットとして公式サポートされ、UniFFIによるKotlinバインディング生成も含め一通り動作します。
ごく一部で、他言語ライブラリとのリンク時に特殊な組み込み関数(例:__extenddftf2
)が未解決になるケースが報告されています。
(MozillaのNSSライブラリ使用時に発生)が、これはRust自体の問題ではなく依存ライブラリ側のビルド設定によるもので、通常のユースケースでは遭遇しません。全体としてx86_64環境でも安定してRustライブラリが動作しています。
- x86 (Intel/AMD 32-bit) – こちらもエミュレータ用途が中心ですが、Rustターゲット i686-linux-android としてサポートされます。
NDKが提供するツールチェーンで問題なくビルド可能で、UniFFI生成コードを用いた機能も正常に動きます。
-
その他ABI – 新興の RISC-V アーキテクチャ(
riscv64-linux-android
)もNDK r27以降で試験的サポートが始まり、Rust側でもTier3ターゲットとなっています。
現時点で実用段階ではありませんが、将来的な対応も見据えた動きです。
開発者たちの最近の体験談・報告
最新NDKとRust/UniFFIによるAndroid開発について、コミュニティでは成功事例が数多く共有されています。その中で代表的なものをいくつか挙げます。
- Rust 1.68 + NDK 25 への移行 (Mozilla): Mozillaのアプリケーションサービスチームでは、NDKをr22からr25へ上げた際にリンクエラーが発生しましたが、Rustを1.68にアップグレードし対応することで解決しました。
特にx86_64ビルドで一部シンボルが未定義になる問題にも直面しましたが、「rust-android-gradleプラグイン」を用いたビルド設定の調整により解消しています。
この事例から、新しいRustコンパイラとNDKの組み合わせで問題が解決しうることが確認できます。
-
ブログ記事によるチュートリアル (2023~2024年): Rust + UniFFI を用いたAndroid開発手順を解説したブログも増えています。たとえば、2023年3月の記事ではDocker環境やNDKセットアップ方法とともに、NDK r25環境下でRustライブラリをビルドする際の注意点(NDK23+では
libunwind
対応のためのスクリプト実行が必要)を紹介しています。
一方、2024年1月のブログでは最新ツールを使った手順が紹介されており、cargo-ndkで arm64-v8aやx86_64を含む全ABI向けにビルドしUniFFIでKotlinバインディング生成まで問題なく行えることが示されています。
これら成功例は「最新NDKでもRust+UniFFIで安定して動作する」ことを裏付けています。
- コミュニティQ&Aの声: 開発者同士のQ&Aサイトでも議論が見られます。あるStack Overflow質問では、UniFFI生成のRustライブラリがエミュレータでロードできない問題について質問がありました。
これはNDK自体の不具合ではなく、Rust側コードにAndroidエントリポイント関数(android_main
)が必要なケースだったため、該当部分を修正して解決しています。
NDKバージョンによるトラブルの報告は見当たらず、根本的な互換性問題は現時点でほぼ解消されたといえます。
-
Gradleプラグイン活用: RustコードをAndroidプロジェクトに組み込むための Mozilla製Gradleプラグイン (rust-android-gradle) も積極的に使われています。2024年現在最新版の v0.9.4 が普及しており、Gradleの
ndkVersion
指定で NDK r27/r28 を指し示すことでシームレスにRustライブラリをビルド・組み込みできます。
2024年12月のMedium記事では実際に NDK r28 (28.0.12433566
) を指定してRustとUniFFIをプロジェクトに導入する方法が紹介されており、特段の問題なく動作しています。
国内の記事でも「記事執筆時点ではNDK 27.1.12297006」を用いてマルチモジュール構成のAndroidアプリにRustを導入する手順が共有されています。
これらから、NDK r27以降を使うことが実務上支障なく受け入れられていることが分かります。
Android APIレベルと互換性の傾向
NDKのバージョン更新に伴いサポートされる最低APIレベルも引き上げられる傾向があります。Rust 1.68でNDK r25を採用した際は、RustのAndroidターゲットの最低APIレベルが15(ICS)から19(KitKat)に上がりました。
さらにNDK側では、r26(2023年LTS)でKitKatのサポートが終了し**最低APIレベルが21(Android 5.0 Lollipop)**に引き上げられています。
したがってNDK r26/r27以降を使うRustライブラリは、実質的にAndroid 5.0以上で動作する形になります。
もっとも、Android 5.0未満の端末シェアはごく僅少(<0.5%程度)であるため、
最新NDKの採用によって互換性上大きな問題が生じる可能性は低いでしょう。実際、コミュニティでも「NDKを最新LTSに追随しても5年以上前の古いOS以外には対応できている」としておおむね受け入れられています。
もしどうしても古いAndroid(例: KitKat以前)をサポートする必要がある場合は、古いNDKとそれに対応した過去のRustコンパイラを使ってビルドする方法も考えられますが、
Google Playのポリシーやデバイス市場を踏まえると現実的にはAPI21以上を対象に最新NDKで開発するのが推奨となっています。
まとめ: 現在RustとUniFFIを用いたAndroidモジュール開発では、最新または少なくともNDK r25以降の利用が一般的かつ推奨です。過去に指摘されていたNDKアップデートによるビルド不具合(NDK 23問題)はRust側の対応により解消済みで、arm64-v8aやx86_64を含む主要ABIで安定して動作する実績が積み重ねられています。Android APIレベルもLollipop以降をカバーしており、最新NDKを積極的に採用して問題ない環境が整っていると言えるでしょう。
参考資料・出典: Rust公式ブログ
、Rust開発者フォーラム・Redditでの議論
、Mozilla開発者の報告
、コミュニティブログ記事(Sal.dev
・Forgen.tech
・Qiita
・Medium
)など。各種報告より該当箇所を抜粋しています。
XCFramework は以下のような手順でプロジェクトに追加できます。
代表的には「手動配置+Xcode の設定で組み込む方法」と「Swift Package Manager (SPM) を使う方法」があります。
いずれも、単にプロジェクト直下に置くだけでは不十分で、Xcode側でリンク・ビルド設定を行う必要があります。
1. 手動で追加する場合
1-1. プロジェクトにファイルを追加する
- Xcode でプロジェクトを開く。
- Finder から
.xcframework
を Xcode の Project Navigator(ファイル一覧)へドラッグ&ドロップする。- ダイアログが出る場合は "Copy items if needed" にチェックを入れておくと、xcframework がプロジェクトにコピーされる。
- 「どのターゲットに追加するか」を確認して、対応するターゲットを選択(アプリのターゲットなど)。
プロジェクト直下にフォルダを作って管理するのは構いませんが、「Xcode プロジェクトにドラッグして登録」しないとビルド時に認識されないので注意してください。
1-2. ターゲットの設定を確認する
- Xcode の「Project Navigator」でアプリやフレームワークのターゲットを選択し、「General」タブを開く。
- 下の方にある「Frameworks, Libraries, and Embedded Content」または「Linked Frameworks and Libraries」に、追加した
.xcframework
が含まれているか確認する。- 含まれていない場合は、そこに「+」ボタンから追加する。
- もしダイナミックフレームワーク(動的リンクのもの)の場合は、「Embed」をどうするかを設定する必要がある("Embed & Sign" など)。
- Rustでビルドした
.xcframework
が「静的ライブラリ(.a)を含むだけ」という場合は Embed が不要なことも多いです。
- Rustでビルドした
2. Swift Package Manager (SPM) を使う場合
.xcframework
を バイナリフレームワークとして Swift Package Manager による配布やプロジェクト取り込みを行う方法です。こちらを使うと、Package.swift
内で .xcframework
を指定して、プロジェクトに簡単に追加できます。
2-1. Package.swift にバイナリターゲットを記述
たとえば、ローカルにある .xcframework
を指定する簡単な例は以下のようになります。
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "MyLibraryWrapper",
platforms: [
.iOS(.v13)
],
products: [
// このパッケージとして公開したいライブラリなどを定義
.library(
name: "MyLibraryWrapper",
targets: ["MyLibraryWrapper"]
)
],
targets: [
// バイナリターゲット (xcframework) を定義する
.binaryTarget(
name: "MyBinaryLib",
path: "./MyBinaryLib.xcframework" // 相対パス
),
// ここでは MyLibraryWrapper というターゲットが
// バイナリターゲットに依存する例
.target(
name: "MyLibraryWrapper",
dependencies: [
"MyBinaryLib"
]
)
]
)
2-2. Xcode から「Add Packages」する
- 上記の
Package.swift
を含むリポジトリを用意して、Git 管理する。 - Xcode の「File > Add Packages...」からリポジトリのURLを指定して追加する。
- ターゲットのリンク設定に、バイナリターゲットが自動で追加される。
この方法だと、複数プロジェクトで簡単に使いまわしができるというメリットがあります。
ローカルの.xcframework
を直接参照する場合、Xcode 14 以降では「ローカルパッケージとして追加」も可能です。
3. 注意点
- 静的ライブラリ (static .a) を含む XCFramework か、動的フレームワーク (.framework) を含む XCFramework かによって、Xcode の設定("Embed & Sign" など)に違いがあります。
- プロジェクト配置をドラッグ&ドロップで行っただけでは、ターゲットのビルド設定に入っていなければコンパイル/リンク時に認識されません。必ず
General
→Frameworks, Libraries, and Embedded Content
などでリンク対象になっているか確認してください。 - シミュレータ用 / 実機用 など複数アーキテクチャを 1 つの
.xcframework
にまとめていない場合は、「ビルドできない」「リンクエラー」などの問題が起きることがあります。 - Swift Package Manager で利用する場合は、
Package.swift
のbinaryTarget
で指定するのをお忘れなく。
まとめ
-
ただプロジェクト直下に
.xcframework
を置くだけではなく、Xcode プロジェクト内にドラッグ&ドロップまたは「Add Files to ...」で追加し、対象ターゲットの Build Setting / General 設定でリンクを行う必要がある。 - Apple が推奨している最新の方法は Swift Package Manager で配布/管理すること。
- 「単に手動で入れたいだけ」の場合は、ドラッグ&ドロップで
.xcframework
を追加して、ターゲットの**Frameworks, Libraries, and Embedded Content
**に登録・設定する手順が一般的。
このいずれかの方法でXcodeへ組み込めば、アプリや別のフレームワークからXCFrameworkを利用できるようになります。
注意点
Swift Package Managerでは、バイナリターゲット(binaryTarget)の名前が
module.modulemapで定義されている名前と一致する必要があります。
module [ここ]
module auth2FFI {
header "auth2FFI.h"
export *
}
Swift コンパイラは、このバイナリターゲット名をモジュール名として認識し、import <モジュール名> の形式でアクセスします。
つまり:
- モジュールマップで module auth2FFI { ... } と定義されている場合:
- .binaryTarget(name: "auth2FFI", ...) が必須です
- swiftからも、この名前でインポート(import auth2FFI)する必要があります
- 一方、Package の名前(name: "xxx")は任意ですが、わかりやすさのために通常はバイナリターゲットと同じ名前にします。