方针

彻底使用 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(可选)

这一末节不是必选行为,能够作为一个小作业,因为就一行代码算了,只不过为了好玩。具体完成能够参考 吃得饱系列-写个 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 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 真好玩。