在开发较大的 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% 的性能提升。
1
povsister 113 天前
看到一半我就猜 op 是不是字节的。
点开博客,果然。 有无关键指标透露下,比如需求吞吐量和 go 转 rust 的研发消耗情况如何? |
2
ihciah OP @povsister 性能数据是因人而异的,要看参数和响应的类型与大小,也和使用异步或同步接口有关,以及 go 版本。考虑到这些 diff ,我这里目前没有非常官方的数据,你可以在你的场景下测下(简单写一下 benchmark),对比 rpc 等方式应当有较大提升。
|
3
fgwmlhdkkkw 112 天前
,,,
|
4
ihciah OP @fgwmlhdkkkw 没看懂,麻烦直白一点?如果你是指我的前一条回复,那么我确实可以 post 一些数据,但参考意义并不大,例如:
在基于 CGO 的版本中,走异步调用,单核 1000 并发请求模拟 10ms 延迟的 go(GOMAXPROCS=2),QPS 87000 左右(可以以此估算延迟),go1.22.4 下 cpu 占用率 44.18%,go1.18.10 下 cpu 占用 65.35%(两个 go 版本的 QPS 接近)。测试使用的 Request 是例子 DemoComplicatedRequest 。内核版本 6.7.3 ,cpu 是 intel platinum [email protected] 。 如果要涉及方案对比,这个数据对比更无法得出的可被公认的数字,因为对比方案的实现和序列化方式都是因人而异的。 本文和此次分享侧重技术方案本身,向大家介绍一个全新的问题,以及我解决该问题的设计与思考,纯技术分享性质,希望对这个问题或技术本身感兴趣的人多多 comment ! |
5
fzdwx 112 天前
半年前就关注过,大佬牛逼!现在能支持 windows 平台吗?然后期待 rust 调用 go
|
7
fgwmlhdkkkw 112 天前
@ihciah #4 额,,,我是说挺厉害的,感觉就像花式抛球呀。不像吗?
|
8
yb2313 112 天前
字节要开始转 rust 了吗, rrrrrust, 启动
|
9
nomagick 112 天前 via Android
需要 ffi 调用的公共库代码麻烦用 C 重写谢谢,不要把 go 再传播到其他语言了
|
10
linrongbin 112 天前
@yb2313 之前看到 rust 内部有宣传写了个 api 框架,这样就可以直接 rust 写业务代码了
|
11
linrongbin 112 天前
牛逼,为啥 V2EX 不能给帖子点赞呢。。。
|
12
0o0O0o0O0o 112 天前 via iPhone
OP 快把 mem-ring 的 How it Works 端上来,还有 bench 代码,感兴趣
|
13
zizon 112 天前
感觉你真要用 rust 重写的话也应该是从 go 调 rust 开始,逐步把实现迁移成 rust.
毕竟 go 代码带 runtime 属性,本质是属于除了你业务代码之外 runtime 特性你也隐性依赖,脱离不了. |
14
xieren58 112 天前
都用 rust 了. 还调 go... 有点无语...
|
15
ihciah OP > 不要把 go 再传播到其他语言了 / 都用 rust 了. 还调 go
这个实际存在的需求:不是所有人都会写 rust ,并且也不是所有组件都能被快速重写。作为一个帮助 go 转 rust 的工具,它对于解决实际需求和促使大家重写都有积极意义。正如 zig 与 rust 的关系一样(如果你已经能用 rust 了那么就不需要 zig ),理想情况是大家都不需要依赖本项目,但达成理想的过程可能需要。 > 真要用 rust 重写的话也应该是从 go 调 rust 开始,逐步把实现迁移成 rust 主逻辑应当被优先重写。如果只是优化 go lib 的性能,除了 rust 外还有很多手段可以用,最终业务开发者还是只写 go 。go runtime 确实无法避免,但这个是技术细节了。 > 这样就可以直接 rust 写业务代码了 在我当前公司内部有一个网关就这么开发:使用我们提供的 rust 网关框架,业务开发只写 go 。 > go 调用 rust 在规划中啦! go 调用 rust 一般是同步 call ,直接 CGO 即可。异步的话需要一点额外的工作。 > 现在能支持 windows 平台吗 支持的。现在 rust2go 支持 tokio/monoio 等 runtime 。 > mem-ring 的 How it Works 有空我写下~这部分在那篇介绍 blog 里也有比较简短的介绍。 > 字节要开始转 rust 了吗 很久以前就开始了,我当前公司内网的一堆基础 sdk 都是我从零搓的。 |
16
aw2350 110 天前
grpc 不好吗
|
17
VVVYGD 109 天前
虽然有点倒反天罡,但是还是比较行的,讲解了动静态链 FFI 原理。用了 rust 之后,只觉得 C++在语法主义层面比 rust 好,不过开发工具 c++没有 rust 好,所以一直坚持用 rust 实现各种 api 业务。 快速的项目用 python 组合 Nextjs+ AI 写真的爽
|