Rust で iOS 実機・シミュレータ両対応の xcframework を作って Swift から呼ぶ

こんにちは。亀山です。先日リリースした 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 に翻訳されることがわかりました。 味わい深いですね。

おわりに

ここまでの内容を実施したリポジトリはこちらです。

github.com

いかがでしたでしょうか。Rust、楽しいです。やっぱり自分がいつも使っている端末で動くと一層おもしろいですね。

また私は別のプロジェクトとして、さらに進んで OpenGL のバインディングを使って、Rust 側で描画処理を行ったり、Swift 側で定義したカメラの処理のコードを呼んで、画像認識を行ったりするコードを書くことができました。これらもそのうち成果としてまとめたいですね。

それではまた!