方针
彻底使用 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(可选)
这一末节不是必选行为,能够作为一个小作业,因为就一行代码算了,只不过为了好玩。具体完成能够参考 吃得饱系列-写个 Rust 进程宏(derive)
假设完成这个派生宏后,咱们就能够把上面那段代码删掉,把 NSString
结构的界说替换成
#[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 真好玩。