iOS 编译和部署 Rust Library

4,700 阅读6分钟

这是一篇译文,原文链接为 Building and Deploying a Rust library on iOS

首先,我们需要安装 Xcode,然后设置 Xcode 编译工具。如果你已经安装了 Xcode 编译工具并且已经将其更新到最新了,你可以跳过这一步。

xcode-select --install

接下来,我们需要确保安装了 Rust 环境来编译 iOS 架构产物。这一步我们需要安装 rustup。同样的,如果你已经安装了,就可以跳过这一步。Rustup 安装工具将安装 Rust 官方渠道的 release 包并且方便你切换不同的 release 版本。这样有益于你将来的 Rust 开发,而不止于是这篇文章所讨论的。

curl https://sh.rustup.rs -sSf | sh

然后添加 iOS 架构到 rustup,这样我们就能编译跨平台产物了。

rustup target add aarch64-apple-ios armv7-apple-ios armv7s-apple-ios x86_64-apple-ios i386-apple-ios

当你安装 Rust 也会同时安装 cargo(类似 cocoapods、pip、gems 的 Rust 的包管理工具)。现在我们使用 cargo 来安装 cargo-lipo。这是一个 cargo 自动创建 iOS library 的跨平台子命令。没有这个 crate,跨平台编译 Rust 成 iOS 产物将难于上青天。

cargo install cargo-lipo

现在我们已经完成环境配置准备开始,我们先来创建项目路径。

mkdir greetings
cd greetings
cargo new cargo
mkdir ios

cargo new cargo 初始化了一个以 cargo 命名的带有默认文件和路径的全新的 Rust 项目,cargo 路径下有个文件叫做 Cargo.toml,这个文件是包管理描述文件(类似 cocoapods 的 podfile),并且这里有一个子路径叫做 src,这个路径下有一个 lib.rs 文件(如果没有就 touch lib.rs 新建一个),我们写的 Rust 代码都放在这个文件夹下。

我们的 Rust 项目将非常简单,就是一个 Hello World 项目。它包含一个函数叫做 rust_greeting,这个函数接受一个 string 作为入参并返回一个 string。因此,假如入参是 “world”,返回值是 “Hello world”。

打开 cargo/scr/lib.rs 复制如下代码。

use std::os::raw::{c_char};
use std::ffi::{CString, CStr};

#[no_mangle]
pub extern fn rust_greeting(to: *const c_char) -> *mut c_char {
    let c_str = unsafe { CStr::from_ptr(to) };
    let recipient = match c_str.to_str() {
        Err(_) => "there",
        Ok(string) => string,
    };

    CString::new("Hello ".to_owned() + recipient).unwrap().into_raw()
}

#[no_mangle]
pub extern fn rust_greeting_free(s: *mut c_char) {
    unsafe {
        if s.is_null() { return }
        CString::from_raw(s)
    };
}

我们一起来看看上面都写了啥。

我们不可能用 Rust 来调用这个 library,所以我们用 C 桥接。编译默认会在编译时破坏函数名,这里使用 #[no_mangle] 来告诉编译器不要破坏函数名,确保我们的函数名称被导入到 C 文件。

extern 告诉 Rust 编译器这个方法将要在 Rust 以外的地方调用,因此要确保其按照 C 的调用规则编译。

rust_greeting 方法接受一个 C 类型的字符串数组指针,我们需要把这个 C 字符串转成 Rust 的 str 类型。首先,我们用指针创建一个 CStr 对象。然后我们把这对象转成 Rust 的 str 类,然后检查转换是否成功,假如有错误发生,那我们以 there 代替入参,否则我们使用入参。然后在入前面拼接一个 Hello,然后返回,返回的 string 我们需要转成 CString 然后返回给 C 代码。

使用 CString 并且返回原始值能保证字符在方法返回以后仍然没有被释放。如果字符串被释放了,指针将会被指向空内存或者完全是其他位置。但是为了确保函数返回以后字符串仍然存在,我们需要开辟一块内存,并不再对这块内存做任何操作。这是造成内存泄漏的原因,所以提供第二个函数 rust_greeting_free,函数入参是一个 CString,并负责管理他的内存。我们需要记得调用 rust_greeting_free 来确保我们不会在 iOS 平台碰到问题。

我们还需要创建 C 桥接文件。 在 cargo / src 中创建一个名为 greetings.h 的新文件。在这个文件中,我们来定义一下 C 接口。我们需要确保 iOS 调用的每个 Rust 函数都在这里有定义。

#include <stdint.h>

const char* rust_greeting(const char* to);
void rust_greeting_free(char *);

让我们构建我们的代码,以确保它能正常工作。 为了做到这一点,我们必须完成 Cargo.toml 文件。 这将告诉 cargo 为我们的代码创建一个静态库和 C 动态库。

[package]
name = "greetings"
version = "0.1.1"
authors = ["fluffyemily <fluffyemily@mozilla.com>"]
description = "Example static library project built for iOS"
publish = false

[lib]
name = "greetings"
crate-type = ["staticlib", "cdylib"]

我们需要使用 cargo-lipo 构建我们的 iOS 库。构建产物位置在 cargo/target/。通用 iOS 库的位置在 cargo/target/universal/release/libgreetings.a

cd cargo
cargo lipo --release

这就是我们第一个 Rust 库,现在我们把它集成到 iOS 项目中去。

打开 Xcode 并创建一个新项目。 转到 File\New\Project... 并选择 iOS Application Single View Application 模板。 这个模板是 iOS 的默认应用程序。 点击下一步。

我们以 Greetings 命名我们的项目,确保创建的是一个 swift 项目。点击 Next 选择项目路径。我们这里使用我们之前创建的 ios 路径,如果你选择了其他路径,你将不得不修改我们稍后设置的一些路径。 点击创建。

从项目导航器中选择 Greetings 项目,然后确保选择 Greetings target。 打开常规选项卡, 向下滚动到 Linked Frameworks and Libraries 部分。

导入你的 libgreetings.a 库,方法是从 Finder 中拖动它,或者点击列表底部的 + ,点击 Add other... 并导航到 cargo/target/universal/release/, 选择 libgreetings.a,然后点击 Open。

链接 libresolv.tbd。 点击 Linked Frameworks 列表底部的 + 并在搜索框中键入 libresolv。 选择 libresolv.tbd,然后“添加”。

需要一个 bridging header 来访问我们创建的 C 文件。 首先,让我们将 greetings.h 导入到 Xcode 项目中,这样我们就可以链接到它。 转到 File\Add files to“Greetings.h” ... 导航到 Greetings.h 并选择 Add。

要创建 bridging header,请转到 File\New\File..。 从提供的选项中选择 iOS Source Header File 并选择 Next。 将文件命名为 Greetings-Bridging-Header.h 并选择 Create。

打开 bridging header 并修改为如下所示:

#ifndef Greetings_Bridging_Header_h
#define Greetings_Bridging_Header_h

#import "greetings.h"

#endif

我们需要告诉 Xcode 怎么链接 bridging header。 从项目导航器中选择 Greetings 项目,然后确保选择 Greetings target 并打开 Build Settings 选项卡。 将 Objective-C Bridging Header设置为 $(PROJECT_DIR)/Greetings/Greetings-Bridging-Header.h

我们还需要告诉 Xcode Rust 库链接地址。在 Build SettingsLibrary Search Paths 添加 $(PROJECT_DIR)/../../cargo/target/universal/release

按下 Command + R,编译成功。

现在我们已经将 Rust 库导入到我们的 iOS 项目中,并成功链接。但是我们还没有调用 Rust 库。我们新建一个 swift 文件,命名为 RustGreetings

添加以下代码:

class RustGreetings {
    func sayHello(to: String) -> String {
        let result = rust_greeting(to)
        let swift_result = String(cString: result!)
        rust_greeting_free(UnsafeMutablePointer(mutating: result))
        return swift_result
    }
}

这里创建了一个调用 Rust 库的新类用作接口。这将为我们在 APP 的主逻辑使用提供抽象,不用关心 Rust 库调用的细节,这些细节包括从 C 字符串到 Swift 字符串的转换,和我们不用调用释放内存的函数也不会出现内存泄漏。

打开 ViewController.swift 中的 viewDidLoad 方法并添加以下代码。

let rustGreetings = RustGreetings()
print("\(rustGreetings.sayHello(to: "world"))")

现在构建您的项目并运行它。 模拟器将打开并开始运行你的应用程序。 当视图加载时, Xcode 的控制台将输出 “Hello world”。

你可以在 Github 上找到这个示例工程的代码。