likes
comments
collection
share

吃得饱系列-macOS 平台使用 Rust 调 ObjC 探索(上)

作者站长头像
站长
· 阅读数 33

目标

完全使用 RustmacOS 平台现成的库,譬如 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 selsel_impl

use objc::{msg_send, sel, sel_impl};

在 objc 包的加持下,我们轻松实现了 NSStringalloc。现在先来实现一个非常简单的 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 真好玩。