こんにちは。亀山です。先日リリースした feather for Mastodon はもうダウンロードしていただけましたでしょうか?まだの方はぜひお試しください!
ところで私は近ごろ Rust を勉強中で、その一環としてかつて C++ で作っていた iOS で動作するカメラアプリの移植を行っています。そこで、本記事では Rust で書いたライブラリから xcframework を作り、iOS で動かす方法について説明します。
環境構築
まず次のコマンドで Rust のプロジェクト一式を生成します。例としてプロジェクト名は rustios とします。
cargo new rustios --lib
次に VSCode で生成された rustios ディレクトリを開きます。
cd rustios
code .
補完やエラーの表示のため、拡張機能 rust-analyzer をインストールしておきます。
必須ではありませんが、.vscode/settings.json
に次の設定を入れることで、保存時のフォーマットを有効にできて便利です。
{ "editor.formatOnSave": true, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" }, "rust-analyzer.showUnlinkedFileNotification": false, "rust-analyzer.cargo.loadOutDirsFromCheck": true, "rust-analyzer.cargo.target": "aarch64-apple-ios-sim", }
次の行を書いておくことにより、コード生成を行った場合に補完候補として表示させることができます。どうやらコード生成はアーキテクチャごとに行われるためこのようになっているようです。
"rust-analyzer.cargo.loadOutDirsFromCheck": true, "rust-analyzer.cargo.target": "aarch64-apple-ios-sim",
iOS 向けビルド環境構築
シミュレータ (M1 Mac) と実機用のアーキテクチャを追加します。プロジェクトではなくマシンにインストールされるので一度だけ行えばよいです。
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
そして Cargo.toml ファイルに下記を追記します。これによりライブラリの .a
ファイルが生成されます。
[lib] crate-type = ["staticlib"]
次に下記の内容で .cargo/config.toml を作成します。これにより cargo build
コマンドで2つのアーキテクチャ用のライブラリが生成されます。
[build] target = ["aarch64-apple-ios", "aarch64-apple-ios-sim"]
ここまでの作業で、cargo build
を実行すると target/aarch64-apple-ios/debug/
と target/aarch64-apple-ios-sim/debug/
のディレクトリにライブラリ librustios.a
が生成されるようになりました。リリースビルドは cargo build --release
によって生成することができます。
xcframework ビルドスクリプト準備
2つの .a
ファイルが生成されたので、これを一つの xcframework としてまとめます。
次の xcodebuild コマンドによって、複数のライブラリファイルから xcframework を生成することができます。
xcodebuild -create-xcframework \ -library target/aarch64-apple-ios/debug/librustios.a \ -library target/aarch64-apple-ios-sim/debug/librustios.a\ -output build/debug/Rustios.xcframework
これにて Rust で iOS 実機・シミュレータ両対応の xcframework を作ることができるようになりました。
便利にするために、次のようなスクリプトとして create_framework.sh
と名前をつけて保存しておきます。
#!/bin/bash -e framework_name="Rustios" library_name="librustios" input_type=$1 if [ -z "${input_type}" ]; then echo "Error: No input type specified. Please specify '--release' or '--dev'." exit 1 fi build_type="release" if [ "${input_type}" = "--dev" ]; then build_type="debug" elif [ "${input_type}" != "--release" ]; then echo "Error: Invalid input type specified. Please specify '--release' or '--dev'." exit 1 fi mkdir -p build rm -rf build/${build_type}/${framework_name}.xcframework xcodebuild -create-xcframework \ -library target/aarch64-apple-ios/${build_type}/${library_name}.a \ -library target/aarch64-apple-ios-sim/${build_type}/${library_name}.a\ -output build/${build_type}/${framework_name}.xcframework
./create_framework.sh --release
または ./create_framework.sh --dev
コマンドによってフレームワークを生成することができます。
Xcode プロジェクトに追加する
Frameworks, Libraries, and Embedded Content の + ボタンから先程の Rustios.xcframework ファイルを追加します。
Swift/ObjC から呼べるようにする
ここまでで iOS アプリへ Rust 製フレームワークの導入は完了しました。ここからは Rust で実装した関数を Swift から呼べるようにしていきます。
Swift や ObjC は直接 Rust の関数を呼ぶことはできないため、C の関数として定義を行います。
試しに、cargo new
コマンドですでに生成されている lib.rs
ファイルに定義された add 関数を次のように C 関数で呼べる形式に修正します。
#[no_mangle] pub extern "C" fn add(left: usize, right: usize) -> usize { left + right }
次に、Xcode で適当な ObjC ファイルを作成して、次のダイアログから Bridging header を生成させます。
(作成した ObjC ファイルは削除してよいです)
アプリ名-Briding-Header.h に下記のように Rust で定義した関数と同じ名前の関数を定義します。
int add(int a, int b);
これで Rust の add 関数を Swift から呼べるようになりました!早速やってみましょう。
import SwiftUI @main struct RustiosAppApp: App { var body: some Scene { WindowGroup { ContentView() Text("\(add(2, 3))") } } }
でました。5です。感動しますね。
ヘッダーファイルの自動生成
しかし毎回このように関数の定義をヘッダーファイルに書くことは非常に面倒です。cbindgen
crate を導入し、ヘッダーファイルを Rust から生成することにします。
cargo add cbindgen --build
次に cbindgen の設定ファイルを生成します。今回は C++ ではなく C にするため、cbindgen.toml
として次の設定値を保存します。
language = "c"
cbindgen はコマンドラインから実行することもできますが、cargo build
で実行されるようになると便利です。次の build.rs ファイルを作って、ビルド時に自動的に実行されるようにします。
extern crate cbindgen; use std::env; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); cbindgen::generate(crate_dir) .expect("Unable to generate bindings") .write_to_file("build/includes/librustios.h"); }
cargo build
すると build/includes/librustios.h
が生成されるようになりました。
こちらを見てみると、Rust に書かれた関数定義の C バージョンができています。
#include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> uintptr_t add(uintptr_t left, uintptr_t right);
さて、これをそのまま Xcode に追加することもできますが、グローバルスコープになってしまいますし、Swift から参照する際に import
を使えません。そこで xcframwork がモジュールを定義するように変更します。
モジュールの定義
まず、次のような modulemap ファイルを作成し、module.modulemap
として保存します。先程生成するようにしたヘッダーファイルを参照するようにします。
module Rustios { header "librustios.h" export * }
こちらの modulemap ファイルと先程の生成されたヘッダーファイルを xcframework に含めるように先程の create_framework.sh
スクリプトを修正します。
+cp module.modulemap build/includes xcodebuild -create-xcframework \ -library target/aarch64-apple-ios/${build_type}/${library_name}.a \ + -headers build/includes \ -library target/aarch64-apple-ios-sim/${build_type}/${library_name}.a\ + -headers build/includes \ -output build/${build_type}/${framework_name}.xcframework
さて、ここまでで cargo build
./create_framework.sh --dev
によって、モジュール対応の xcframwork ができました!
Xcode のプロジェクトで先程書いた Briding-header.h の中身は消してしまいましょう。いよいよ framework の import です。import Rustios
を追記しましょう。
import SwiftUI import Rustios @main struct RustiosAppApp: App { var body: some Scene { WindowGroup { ContentView() Text("\(Rustios.add(2, 3))") } } }
名前空間までもらって、なんと立派になったことでしょうか。add
関数は Rustios.add
でも、単に add
でもどちらでも呼び出すことができます。
ところで、Swift からは add 関数はどのように見えているのでしょうか。import Rustios
の部分を Cmd+クリックしてみましょう。
すると自動生成された Swift 版の定義が開かれます。
public func add(_ left: UInt, _ right: UInt) -> UInt
librustios.h と比較してみます。
uintptr_t add(uintptr_t left, uintptr_t right);
また、Rust のコードはこちらです。
fn add(left: usize, right: usize) -> usize
usize は C の uintptr_t に翻訳され、さらに Swift の UInt に翻訳されることがわかりました。 味わい深いですね。
おわりに
ここまでの内容を実施したリポジトリはこちらです。
いかがでしたでしょうか。Rust、楽しいです。やっぱり自分がいつも使っている端末で動くと一層おもしろいですね。
また私は別のプロジェクトとして、さらに進んで OpenGL のバインディングを使って、Rust 側で描画処理を行ったり、Swift 側で定義したカメラの処理のコードを呼んで、画像認識を行ったりするコードを書くことができました。これらもそのうち成果としてまとめたいですね。
それではまた!