使用Rust开发MacOS/IOS适用的静态库

之前我们探索过 使用rust开发安卓用的动态链接库 ,本文主题是IOS使用Rust库。其实C/C++库操作类似,本文前半部分我将描述怎么把Rust library编译为静态/动态连接库,后半部分是怎么使用这个库。

一、准备工作

首先,我们必须安装 Xcode,然后设置 Xcode 构建工具。如果您已经安装了构建工具并且它们是最新的,则可以跳过此步骤。

xcode-select --install

接下来,我们需要确保 Rust 已安装并且可以交叉编译到 iOS 架构。为此,我们将使用 rustup。如果您已经安装了 rustup,则可以跳过此步骤。 Rustup 从官方发布渠道安装 Rust,使您能够轻松地在不同发布版本之间切换。

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

将 iOS 架构添加到 rustup 中,以便我们可以在交叉编译期间使用它们。

rustup target add x86_64-apple-ios aarch64-apple-ios
// 其他工具链,可以根据需求安装
rustup target list

安装方工具

cargo install cargo-lipo
cargo install cbindgen

cargo-lipo 是一个cargo的子命令,可以自动创建一个用于iOS的通用库。如果没有这个crate,交叉编译Rust在iOS上工作会非常困难。
cbindgen 则是用来生成头文件的crate。

二、编写核心代码

接下来我们改造一下之前的安卓动态链接库项目。
首先,修改 Cargo.toml

[package]
name = "rust-libs-demo"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.21.1", default-features = false }

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

[features]
clib = []

[profile.release]
lto = true
opt-level = 'z'
panic = 'abort'
  1. jni改成编译平台是安卓时才生效。
  2. crate-type里增加 staticlib
  3. 新增一个clib feature以便我们后面单独编译ios相关静态库。

接下来我们吧之前的android相关代码移动到一个单独的文件里android.rs,然后新增一个ios.rs文件:

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

///
/// 简单hello
/// # Arguments
///
/// * `name`:  称呼
///
/// returns: 返回的字符串
///
#[no_mangle]
pub extern "C" fn say_hello(name: *const c_char,) -> *const c_char {
    let c_name = unsafe { CStr::from_ptr(name) };
    let recipient = c_name.to_str().unwrap_or_else(|_| "there");
    CString::new("Hello ".to_owned() + recipient).unwrap().into_raw()
}

///
///  释放由rust代码产生的字符串内存
/// # Arguments
///
/// * `str`:  由rust代码产生的字符串
///
/// returns: ()
///
#[no_mangle]
pub unsafe extern "C" fn free_str(str: *mut c_char) {
    unsafe {
        if str.is_null() { return }
        let _ = CString::from_raw(str);
    }
}

#[no_mangle] 控制编译器 不要破坏C函数名,确保外部能通过 create_secret 调用到这个函数。
extern 告诉Rust编译器这个函数将从Rust外部调用,因此确保它是使用C调用约定编译的。
say_hello 接受的字符串是一个指向C字符数组的指针。然后我们必须将字符串从C字符串转换为Rust str 。首先,我们从指针创建一个 CStr 对象。然后我们将其转换为 str 并检查结果。如果出现错误,则不提供参数,我们替换 there ,否则使用提供的字符串的值。然后,我们将提供的字符串追加到问候字符串的末尾,以创建返回字符串。然后,返回的字符串被转换为 CString 并传递回C代码。
使用 CString 并返回原始表示将字符串保留在内存中,并防止它在函数结束时被释放。如果要释放内存,则提供给调用方的指针并将其指向空内存。但是,通过确保字符串在函数执行完成后仍然存在,我们已经分配了内存,但不再有任何处理它的方法。这就是内存泄漏的原因,所以我们必须提供第二个函数 free_str ,它接受指向C字符串的指针并释放该内存。我们需要在使用完对应字符串时从iOS代码中调用 free_str ,来释放内存。

接着修改lib.rs :

#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android;

#[cfg(feature = "clib")]
pub mod ios;

#[cfg(target_os = “android”)] 条件编译参数,只在编译android平台时才代码生效。
#[cfg(feature = “clib”)] 条件编译参数,编译库时通过增加features参数确定是否编译为C/C++库。

三、编译静态库

当我们完成核心代码的开发后就需要把rust代码编译成静态库:
执行cargo lipo --all-features --release --targets x86_64-apple-ios aarch64-apple-ios来编译,可以根据实际情况来增加或者减少目标平台。
然后我们就能在target里找到编译好的库文件了。
build
当然你也可以使 cargo build来编译最后手动使用 lipo 命令来合并多个平台的库文件,比如需要优化体积时就需要我们手动合并文件:

  • 先执行编译命令cargo +nightly build -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --all-features --target aarch64-apple-ios --target x86_64-apple-ios --release
  • 然后执行合并命令lipo -create ./target/x86_64-apple-ios/release/librust_libs_demo.a ./target/aarch64-apple-ios/release/librust_libs_demo.a -output ./dist/ios/universal/librust_libs_demo.a

编译完成后我们需要给对应库生成头文件以方便ios使用。
运行 cbindgen ./src/ios.rs -l c --output dist/ios/librust.h就能看到生成了对应头文件:

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

const char *say_hello(const char *name);
void free_str(char *str);

当然你也可以手动编写。

四、IOS/Mac Project 使用C库

  1. 可以拖动刚才编译好的 librust_libs_demo.a 到项目,在 General->Frameworks,Libraries…->Add Other 添加。
    添加libresolv.tdb到项目,可以直接在 General->Frameworks,Libraries... 中搜索到。
    image-1709545045511

  2. 添加C的头文件到项目
    xcode Add Files To '{project}'选中我们之前生成好的头文件。

  3. 编写Objective-C Bridge Header
    xcode File -> New -> File..., 选择Header File并命名为 Demo-Bridging-Header.h
    打开桥接文件并将其修改为如下所示:

#ifndef Demo_Bridging_Header_h
#define Demo_Bridging_Header_h

#import "librust.h"

#endif
  1. 配置项目路径
    xcode TARGETS {项目名}, Build Setting

Objective-C Bridging Header增加 $(PROJECT_DIR)/{项目名}/Demo-Bridging-Header.h

Search Paths -> Header Search Paths 增加 $(PROJECT_DIR)/{项目名}
Search Paths -> Library Search Paths 增加 $(PROJECT_DIR)/{项目名}

请注意{项目名}需要修改自己的目录

  1. 编写swift调用类
    注意这里有个要点,Rust返回的字符串需要归还Rust释放内存,其他语言同理。
import Foundation

class RustDemo {

   func sayHello(name: String) -> String {
        let result = say_hello(name)
        let swift_result = String(cString: result!)
        free_str(UnsafeMutablePointer(mutating: result))
        return swift_result
    }
}

之后我们就可以在其他文件里使用了。

let rustDemo = RustDemo()
print("\(rustDemo.sayHello(name: "jack"))")

demo代码地址

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×