专栏名称: 程序人生
十年漫漫程序人生,打过各种杂,也做过让我骄傲的软件;管理过数十人的团队,还带领一班兄弟姐妹创过业,目前在硅谷一家创业公司担任 VP。关注程序人生,了解程序猿,学做程序猿,做好程序猿,让我们的程序人生精彩满满。
目录
相关文章推荐
京东零售技术  ·  在京东 探索技术的无限可能 ·  2 天前  
程序员技术  ·  前美团员工求助:在美团背了个C绩效,慌得不行 ... ·  15 小时前  
伯乐在线  ·  HR ... ·  昨天  
伯乐在线  ·  HR ... ·  昨天  
51好读  ›  专栏  ›  程序人生

深度分析:前端中的后端-实现篇

程序人生  · 公众号  · 程序员  · 2021-02-01 08:29

正文

请到「今天看啥」查看全文



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 即可。其实没什么神秘的,只需要注意三点:

  • 使用 #[no_mangle] ,这样 Rust 编译器生成的 symbol 不会使用内部的混淆后的名字。

  • 使用 extern "C" 声明 C FFI。

  • 使用 C 认识的数据结构。如果是 struct,需要添加 #[repr(C)] 宏。

一个完整流程

我们看一个从 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 ),需要手工「释放」。释放的原则:







请到「今天看啥」查看全文