在开发较大的 Rust 程序时,有时候需要调用一些 Go 实现的代码;特别是在将 Go 程序用 Rust 重写时,更需要 Rust 和 Go 混编的能力来渐进式重写,相信这对于很多公司来讲都是一个较强的需求。
我从零设计并实现了一个支持 Rust 异步调用 Golang 的框架,欢迎各位使用或一起让它变得更好!
项目开源于 https://github.com/ihciah/rust2go
我写了一篇 blog 详细介绍它的技术细节:Rust-Golang FFI 框架设计与实现
我也会在 2024 年 9 月 8 日下午的 RustConfChina2024 上介绍这个项目的设计与实现,欢迎大家关注!
定义调用需要的 struct 和 trait
按 Rust 写法写即可,放置于代码目录内直接使用; struct 支持嵌套自定义结构; trait 参数支持传递引用。
定义调用参数和返回值,并添加修饰宏 | 定义调用 trait 并添加修饰宏 |
---|---|
利用 rust2go-cli 生成 Go 代码,并实现生成的 interface
生成 Go 代码 | 实现生成的 Go interface |
---|---|
在项目中添加 build.rs
以自动化构建 Golang 并链接
添加 build.rs |
---|
开始调用
你现在可以直接使用已经定义的 struct 来调用生成的 trait 实现了!
使用生成的 TraitImpl |
---|
你不需要折腾复杂的编译过程,直接 cargo build
/ cargo run
即可!不出意外的话,可以预期下面的结果:
注:默认是静态链接,可以修改 build.rs 切换为动态链接
通常 Rust 调用其他语言( C/C++)只需要借助 C FFI 接口实现即可,有 bindgen
, cbindgen
, cpp!
等工具可以快速实现。
但这对 Golang 并不适用,这里的问题在于:
内存布局差异:Go 结构和 C 结构内存布局不同,无法互相理解。
异步系统差异:Go 代码运行在 go runtime 上,其很有可能是异步的,常规 FFI 会占用调用方线程等待,造成调用方 Runtime 卡住或线程池开销。
例如 Go 实现中包含一个 HTTP 请求,那么 Rust 线程会在这个请求完成前一直阻塞,造成性能问题。即便使用 spawn_blocking
等手段将其放到线程池中,也会造成极大的资源开销。
生命周期管理:考虑异步的情况下,需要妥善管理参数和返回值的生命周期;同时也需要妥善处理调用方取消调用时的内存安全问题。
例如调用参数传递引用,但在 Golang 执行完毕,调用方已经取消调用 drop Future 并 drop 调用参数,这时候 Go 端还在使用这个参数,就会造成内存安全问题。
另一个问题是,当 Go side 执行结束后,需要将结果返回给 Rust side 。此时该数据一定是 Rust side 负责管理的,那么如何完成变长数据的传递呢?
本文仅仅简单概述关键问题的解决思路,详细设计请移步 Rust-Golang FFI 框架设计与实现
内存布局问题
我设计了一套过程宏,用于自动生成某个结构体对应的 Ref
结构,这个结构是 repr(C)
的,用于直接传递其指针给对端。
同时,我也会在 go 代码生成时 parse 这个定义,并生成对应的 CGO 结构体,用于对端理解传递的指针。
当然,原始结构到 Ref 结构的转换也是基于过程宏自动实现的。为了性能,这里的实现较为复杂,区分了多种嵌套类型。例如,对于 String
只需要传递指针和长度,但如果要传递 Vec<String>
,则不得不生成一个中间结构,因为对端并不能理解 String
的内存布局(不知道数据的指针和长度要怎么从 String
这个结构中读到)。
异步支持
如果你对 Rust 异步不够了解,可以参考我的这篇介绍:Rust Runtime 设计与实现-科普篇
基于 CGO 调用,在 Golang 侧将任务 go 出去执行后立刻返回,本质上发起调用可以理解为一次 task dispatch 。
在 Go 函数执行结束后,它需要将结果返回给 Rust 。由于 Golang 函数已经执行完毕,数据的所有权一定是 Rust 侧在维护,但 Rust 侧无法预知 Go 侧返回的数据大小,因此这里使用了一个非常巧妙的设计:在调用时,Rust 侧传递一个 set_result
函数指针(该函数由 Rust 侧实现),在 go 执行完毕后,通过 CGO 调用该函数来拷贝返回结果并 wake Future 。
生命周期管理
我设计了一个 AtomicSlot 用于管理参数和返回值的生命周期,这个结构会被双边同时访问,借助原子操作保证并发安全。其管理的内存会在双边都退出后释放,这样保证了 Future drop 时的内存安全。
考虑到低版本 Golang 的 CGO 性能问题(go 1.21 开始 CGO 性能有较大提升),我还设计并实现了一个共享内存队列来替代 CGO 调用,这是一个无锁队列,一侧读一侧写(类似 virtio ring 的设计)。
这个共享内存队列实现在一个单独的包中,如果有这方面的需求,可以单独引入使用。
经 benchmark 共享内存版本在 Go 1.18 下相比 CGO 版本有最多 20% 的性能提升。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.