吃得饱系列-macOS 平台使用 Rust 调 ObjC 探索(上)
目标
完全使用 Rust 调 macOS 平台现成的库,譬如 Foundation 相关数据结构跟方法(只实现部分目前)。
创建项目
事已至此,先吃饭吧!把项目创建出来,项目名就叫 chart。其实这只是某个小想法的一部分,所以用这个名字不要觉得奇怪。
cargo new chart
我们即将做得工程初期目录长这样
tree -L 3
.
├── Cargo.lock
├── Cargo.toml
├── chart
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── foundation
│ ├── Cargo.toml
│ ├── build.rs
│ └── src
│ └── lib.rs
└── rustfmt.toml
包装 Foundation
众所周知,开发 macOS/iOS App 过程中,会用到各种数据结构,譬如 NSString
, NSArray
等等,先思考一下如何用 Rust 包装一个 Foundation 的对象。
接下来创建 foundation crate,不过在此之前,修改最外层的 Cargo.toml 文件内容
[workspace]
members = [
"chart",
"foundation",
]
resolver = "2"
[workspace.package]
edition = "2021"
[profile.release]
lto = true
debug = true
同时可以修改最外层的 rustfmt.toml
内容,可以根据自己的代码风格修改对应的格式配置
max_width = 120
fn_call_width = 100
tab_spaces = 2
接着把 foundation 的 build.rs 文件修改一下,这一步是告诉 rustc 链接 Apple 平台的 framework
fn main() {
let target = std::env::var("TARGET").unwrap();
if target.contains("-apple-") {
println!("cargo:rustc-link-lib=framework=Foundation");
}
}
还有 foundation Cargo.toml 的内容,dependencies 一栏下面的 objc,block 是两个要用到的第三方包,它帮我们提供了基础的 ObjC 层包装
[package]
name = "foundation"
version = "0.1.0"
edition = "2021"
[dependencies]
objc = "0.2.7"
block = "0.1.6"
现在来正式改写 foundation crate 下 lib.rs 的内容。
目标 -> NSString
我们来完成第一个小目标,使用 Rust 包装 NSString,这个在各种项目肯定会使用到,所以非常有必要过一遍。
在开始之前,我们确定一个逻辑,就是裸写 Rust 怎么包装 NSString,这里用到一个 Haskell 很常见的 newtype
的方式,只不过在 Rust 中是以命名 Tuple
形式存在(由于我们加上了 #[repr(transparent)]
让该结构体透明化,这样它的实例在内存中就等同于 id
类型对应的值)。我们预先知道一个事,那就是 ObjC 对象是一个带 isa
指针的结构体,所以
#[allow(non_camel_case_types)]
pub type id = *mut objc::runtime::Object;
#[repr(transparent)]
#[derive(Clone)]
pub struct NSString(pub id);
impl std::ops::Deref for NSString {
type Target = objc::runtime::Object;
fn deref(&self) -> &Self::Target {
unsafe { &*self.0 }
}
}
我们顺大便实现了 Deref trait,这样的话,当 Rust 帮我们自动解引用时,就能拿到 objc 的对象啦。#[allow(non_camel_case_types)]
这一句是禁用类型必须是首字母大写的警告,因为我们要模仿 ObjC 所以 id
很合理,不然就要写成 Id
。
接着实现一下 Message
trait,这里是为了让该结构体可以发消息(毕竟 ObjC 的面向对象是 Smalltalk 风格)
/// https://github.com/SSheldon/rust-objc/blob/master/src/message/mod.rs#L5
unsafe impl objc::Message for NSString {}
derive 实现 Message(可选)
#[repr(transparent)]
#[derive(Clone, Message)]
pub struct NSString(pub id);
实现一些 NSString
的方法
既然能发消息,那我们就可以实现 alloc
啦
use objc::msg_send;
impl NSString {
pub fn alloc() -> NSString {
NSString(unsafe { msg_send![objc::class!(NSString), alloc] })
}
}
写了上面这段代码后,你的 Editor/IDE 就会提示你没有 sel
/sel_impl
,所以我们还要 use sel
跟 sel_impl
use objc::{msg_send, sel, sel_impl};
在 objc 包的加持下,我们轻松实现了 NSString 的 alloc
。现在先来实现一个非常简单的 NSString 方法,那就是 UTF8String
,不过在写这个方法之前,还得实现一个最基本的操作,那就是
pub trait ObjCObject {
fn objc_object(&self) -> id;
}
impl ObjCObject for NSString {
fn objc_object(&self) -> id {
self.0
}
}
其实就是取出 ObjC 对象对应的指针,因为后面会用到(当然也可以写个 derive 宏做做这事)。 然后我们就可以
impl NSString {
pub fn utf8_string(&self) -> *const c_char {
use objc::{
class,
declare::ClassDecl,
msg_send,
sel, sel_impl,
};
unsafe {
let target = self.objc_object();
let ret: *const c_char = msg_send![target, UTF8String];
ret
}
}
}
由于我们写 Rust 时写字符串要么是 String
要么是 slice
,所以我们接下来还要实现从 C 语言指针构造 NSString 实例的方法,那从这个方法下手 initWithBytes:length:encoding:
。
pub type NSUInteger = u64;
pub type NSStringEncoding = NSUInteger;
pub fn init_with_bytes_length_encoding(&self, bytes: *const c_void, length: usize, encoding: NSStringEncoding) -> NSString {
unsafe {
let target = self.objc_object();
let ret: NSString = msg_send![target, initWithBytes:bytes length:length encoding:encoding];
ret
}
}
现在算是完成初步的 Rust 调用 ObjC 啦,不过体验其实很不好,因为有大量模板代码,所以我打算花点时间写个过程宏处理这些模板代码。 总之 Rust 真好玩。
转载自:https://juejin.cn/post/7247740446280269885