Skip to main content

Rust 和 Swift 的互操实践

实际上 rust 和 swift 的交互就是先把 rust 通过 c 接口暴露出来, 然后让 swift 和 c 交互, 因此可以先看看 rust 和 c 相关的互操, 记录到这篇文章.

一个简单的 Rust 库例子(用于 iOS 端的)

准备工作

首先, 需要安装 Xcode 的 build tool, 并保证 rust 正确安装.

然后, 需要安装 rust 的 iOS 交叉编译工具链:

# 可能这个里面 armv7 的现在已经不支持了, 另外 i386 的也是
rustup target add aarch64-apple-ios armv7-apple-ios \
armv7s-apple-ios x86_64-apple-ios i386-apple-ios

# 使用下面这个安装: arm64 和 x86 的 64 两个 iOS 跨平台编译支持
rustup target add aarch64-apple-ios x86_64-apple-ios

安装后, 需要再安装 cargo-lipo, 这个工具可以生成 universal binary:

cargo install cargo-lipo

开始开发

需要新建一些文件夹和工程文件:

mkdir greetings
cd greetings
cargo new cargo
mkdir ios

Rust 侧代码

打开创建的 cargo lib, 在其中的 lib.rs 中写如下内容:

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

// #[no_mangle] 用于告知 rust 编译器不要对函数名字进行 mangle 处理,
// 这样可以保证函数名字被导出到 C 的时候就好像是真的用 C 来写的函数一样.
//
// extern 关键字用于告知 rust 编译器这个函数会被外界调用
// 因此来保证这个函数在编译时使用的是 C 的调用约定(calling conventions)
#[no_mangle]
pub extern fn rust_greeting(input: *const c_char) -> *mut c_char {
// 传入的参数 "input" 是一个 c 字符数组
// 注意这里用到了 unsafe 块将 c 字符数组转换为 Rust 的 CStr 类型字符串
let c_str = unsafe { CStr::from_ptr(input) };
// 将它转换为 Rust 的 &str 类型字符串, 并判断结果进行对应处理
let recipient = match c_str.to_str() {
Err(_) => "there",
Ok(string) => string,
};
// 将结果转换为 CString, 这样可以回传给 C 使用, 这里实际是分配了内存
// 但不会被 Rust 自动释放, 因此我们需要下面的那个 rust_greeting_free 函数进行释放
CString::new("Hello ".to_owned() + recipient).unwrap().into_raw()
}

/// 传入我们之前生成的 str(由外界在使用完成后传入), 再在里面进行释放
#[no_mangle]
pub extern fn rust_greeting_free(s: *mut c_char) {
unsafe {
if s.is_null() { return }
CString::from_raw(s)
};
}

Rust 侧的代码就编写完成了.

C 侧代码(用于沟通 swift 和 Rust)

在刚才创建的 cargo/src 文件夹中, 新建一个名为 greetings.h 的文件, 并将我们需要暴露给 swift 的函数在里面声明:

#include <stdint.h>

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

更新 Rust 工程配置文件(Cargo.toml)

为了能够编译为正确的产物, 需要更新配置文件并指定我们需要的产物类型:

[package]
name = "greetings"
version = "0.1.1"
authors = ["fluffyemily <[email protected]>"]
description = "Example static library project built for iOS"
publish = false

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

编译最终产物

通过 cargo lipo 编译最终所需的 iOS 库:

cd cargo
cargo lipo --release

产物在这个位置: cargo/target/universal/release/libgreetings.a

将生成的库导入到 iOS 工程

新建一个 iOS 工程后, 要进行如下操作:

  1. 在 Linked Frameworks and Libraries 中链接刚才生成的库
  2. 同时链接 libresolv.tbd
  3. 将 C 头文件引入到工程
  4. 创建桥接文件并引入刚才的那个 C 头文件
  5. 在构建配置中的 Library Search Paths 中添加刚才链接的库的具体路径, 比如 $(PROJECT_DIR)/../../cargo/target/universal/release
  6. 新建 swift 文件, 写如下内容, 主要是通过 C 调用 rust 函数获取 C 字符串, 字符串转换到 swift 字符串, 并将刚才的调用结果回传给 rust 释放.
    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
    }
    }

参考

  1. 构建并部署一个 iOS 的 Rust library
  2. 上面那篇文章的 github 源码, 还额外添加了 android 的支持
  3. swift-bridge 库