正文
service_init
Rust 侧的初始化。Swift 代码提供一个用于初始化的 protobuf 字节流的指针和长度,Rust 侧创建对应的运行时,然后返回给 Swift 一个句柄,供以后的请求使用。这个请求一般是 app 启动时调用。Swift 可以提供一些基本的服务器请求参数,比如设备 ID,平台,用户 ID,要请求的服务器域名(prod/staging/dev)等信息。Rust 代码会利用设备 ID 和用户 ID(如果存在)在本地存储里查找是否有之前储存的用户状态,如果有,就加载到 State 中;如果没有,就创建新的 State。
service_dispatch/service_dispatch_block
这两个函数一个用于异步请求,一个用于同步请求。同步请求会阻塞 Swift 代码所在的线程;而异步请求则在不同的线程执行,完成之后调用 Swift 侧提供的 callback,提交结果。
请求的时候会提供之前获取的句柄,来找到对应的 Rust 运行时及状态。此外,还要提供请求所包含的 protobuf 字节流的指针和长度。因为所有的请求都走这一个接口,所以它被封装成为 protobuf 的一个 oneof message,如下所示(有删减):
这种通过使用 oneof 来统一调用接口的方法,我是跟 Tendermint 的 ABCI 学的,非常好用。这样,我们在处理请求的时候,就可以根据其类型进行相应的 dispatch 了:
之所以提供一个同步和一个异步的接口,完全是为了客户端灵活而设置的。我自己没有做过生产环境的客户端,不知道哪种方式最适合客户端使用,所以干脆都提供了。好在对于 Tokio 来说,不过是
spawn
和
block_on
的区别而已。
我看了 Firefox sync 的部分代码,它只提供了同步调用的接口,所以整体上的设计比我这里所列的要简单。其实同步调用挺好的,不容易出错。
service_dispatch
接口具体在 Rust 中的实现并不困难。我们只需要了解如何做 Rust C FFI 即可。其实没什么神秘的,只需要注意三点:
一个完整流程
我们看一个从 Swift 到 Rust 的完整的 Ping/Pong 的代码,看看具体是怎么运作的。
首先在 Swift 侧,我们先初始化
service
结构。初始化的时候会调用 Rust 侧的初始化,生成上文我们所说的 runtime/state。
当我们在 Swift 里调用
service.ping
时,会先生成一个
AbiRequestPing
。这是我用 Apple 官方的 swift protobuf 库,基于我定义的 protobuf 生成的结构。由于 Swift import 一个库之后,所有的结构就无需 namespace 可以直接访问,所以我加了一个前缀(在 protobuf 定义:
option swift_prefix="Abi"
),一来好找,二来避免和其它数据结构冲突。
生成好
AbiRequestPing
后,需要将其进一步封装到
AbiNativeRequest
(见上文的 protobuf 定义),然后将其序列化成字节流。因为接下来要将这个字节流传给 Rust,所以我们需要将其转换成
UnsafeByte
。之后调用
service_dispatch_block
,同步返回结果 —— 为了简单起见,我们先不看异步的流程。这个结果是一个
ByteBuffer
结构。这是 Rust 传给 Swift 的指针,所以我们需要将其处理成一个
UnsafeRawBufferPointer
,封装成
Data
,再反序列化成
AbiResponsePong
。
这里面的核心是
rustCall
函数,它负责处理和内存安全相关的代码,我们先放下不表。
Rust 侧的
service_dispatch_block
,会把传入的指针转换成
Vec
,然后再反序列化成
NativeRequest
,就可以正常使用了。
内存管理
这时候,你可能会想到:数据在 Swift 和 Rust 间传来传去,究竟谁应该负责清理内存?
答案是:谁原本拥有的内存,谁负责释放。
Swift 侧是调用方,其传递给 Rust 的内存都在
withUnsafeBytes
闭包中,Rust 函数调用栈结束后,对该内存的引用消失,所以没有内存泄漏的危险,不需要手工处理。
Rust 是被调方,内存传递给 Swift 后,并不知道 Swift 会何时何地结束引用,所以 Rust 自己的所有权模型被略过(因为使用了
unsafe
),需要手工「释放」。释放的原则: