小天管理 发表于 2024年9月6日 发表于 2024年9月6日 背景 在开发较大的 Rust 程序时,有时候需要调用一些 Go 实现的代码;特别是在将 Go 程序用 Rust 重写时,更需要 Rust 和 Go 混编的能力来渐进式重写,相信这对于很多公司来讲都是一个较强的需求。 我从零设计并实现了一个支持 Rust 异步调用 Golang 的框架,欢迎各位使用或一起让它变得更好! 项目开源于 https://github.com/ihciah/rust2go 我写了一篇 blog 详细介绍它的技术细节:Rust-Golang FFI 框架设计与实现 我也会在 2024 年 9 月 8 日下午的 RustConfChina2024 上介绍这个项目的设计与实现,欢迎大家关注! 核心技术 异步支持:支持异步调用 Go 函数,避免阻塞 Rust 线程。 引用优先的内存布局转换:在可能时优先传递引用,避免内存拷贝;同时支持在传递深层递归结构时最小化内存拷贝。 用户友好的使用体验:借助 Rust 过程宏和代码生成工具,为用户带来简单方便的使用体验。 内存安全:框架内部支持管理参数所有权,避免内存泄漏和悬垂指针。 使用姿势 定义调用需要的 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% 的性能提升。 未来规划 当前仅支持 Vec 、String 、u8 、usize 等基础类型及其组合,未来需要支持 HashMap 等多种常见类型。 当前请求结构体定义不支持泛型参数,未来需要支持泛型参数(包括 lifetime )。 当前模式下,如需 Go 调用 Rust ,需要手动传递指针并调用,未来需要支持 Go 调用 Rust 的自动生成。 期待各位的建议!
已推荐帖子