V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
ihciah
V2EX  ›  分享创造

给 Rust 来点 Go - 高性能异步 FFI 框架 Rust2Go

  •  7
     
  •   ihciah ·
    ihciah · 10 天前 · 2370 次点击

    背景

    在开发较大的 Rust 程序时,有时候需要调用一些 Go 实现的代码;特别是在将 Go 程序用 Rust 重写时,更需要 Rust 和 Go 混编的能力来渐进式重写,相信这对于很多公司来讲都是一个较强的需求。

    我从零设计并实现了一个支持 Rust 异步调用 Golang 的框架,欢迎各位使用或一起让它变得更好!

    项目开源于 https://github.com/ihciah/rust2go

    我写了一篇 blog 详细介绍它的技术细节:Rust-Golang FFI 框架设计与实现

    我也会在 2024 年 9 月 8 日下午的 RustConfChina2024 上介绍这个项目的设计与实现,欢迎大家关注!

    核心技术

    1. 异步支持:支持异步调用 Go 函数,避免阻塞 Rust 线程。
    2. 引用优先的内存布局转换:在可能时优先传递引用,避免内存拷贝;同时支持在传递深层递归结构时最小化内存拷贝。
    3. 用户友好的使用体验:借助 Rust 过程宏和代码生成工具,为用户带来简单方便的使用体验。
    4. 内存安全:框架内部支持管理参数所有权,避免内存泄漏和悬垂指针。

    使用姿势

    1. 定义调用需要的 struct 和 trait

      按 Rust 写法写即可,放置于代码目录内直接使用; struct 支持嵌套自定义结构; trait 参数支持传递引用。

      定义调用参数和返回值,并添加修饰宏 定义调用 trait 并添加修饰宏
      define structs with macro define traits with macro
    2. 利用 rust2go-cli 生成 Go 代码,并实现生成的 interface

      生成 Go 代码 实现生成的 Go interface
      generate go code implement go interface
    3. 在项目中添加 build.rs 以自动化构建 Golang 并链接

      添加 build.rs
      add build.rs
    4. 开始调用

      你现在可以直接使用已经定义的 struct 来调用生成的 trait 实现了!

      使用生成的 TraitImpl
      use generated impl

      你不需要折腾复杂的编译过程,直接 cargo build / cargo run 即可!不出意外的话,可以预期下面的结果:

      注:默认是静态链接,可以修改 build.rs 切换为动态链接

      demo result

    问题与难点

    通常 Rust 调用其他语言( C/C++)只需要借助 C FFI 接口实现即可,有 bindgen, cbindgen, cpp! 等工具可以快速实现。

    但这对 Golang 并不适用,这里的问题在于:

    1. 内存布局差异:Go 结构和 C 结构内存布局不同,无法互相理解。

    2. 异步系统差异:Go 代码运行在 go runtime 上,其很有可能是异步的,常规 FFI 会占用调用方线程等待,造成调用方 Runtime 卡住或线程池开销。

      例如 Go 实现中包含一个 HTTP 请求,那么 Rust 线程会在这个请求完成前一直阻塞,造成性能问题。即便使用 spawn_blocking 等手段将其放到线程池中,也会造成极大的资源开销。

    3. 生命周期管理:考虑异步的情况下,需要妥善管理参数和返回值的生命周期;同时也需要妥善处理调用方取消调用时的内存安全问题。

      例如调用参数传递引用,但在 Golang 执行完毕,调用方已经取消调用 drop Future 并 drop 调用参数,这时候 Go 端还在使用这个参数,就会造成内存安全问题。

      另一个问题是,当 Go side 执行结束后,需要将结果返回给 Rust side 。此时该数据一定是 Rust side 负责管理的,那么如何完成变长数据的传递呢?

    设计与实现

    本文仅仅简单概述关键问题的解决思路,详细设计请移步 Rust-Golang FFI 框架设计与实现

    1. 内存布局问题

      我设计了一套过程宏,用于自动生成某个结构体对应的 Ref 结构,这个结构是 repr(C) 的,用于直接传递其指针给对端。

      同时,我也会在 go 代码生成时 parse 这个定义,并生成对应的 CGO 结构体,用于对端理解传递的指针。

      当然,原始结构到 Ref 结构的转换也是基于过程宏自动实现的。为了性能,这里的实现较为复杂,区分了多种嵌套类型。例如,对于 String 只需要传递指针和长度,但如果要传递 Vec<String>,则不得不生成一个中间结构,因为对端并不能理解 String 的内存布局(不知道数据的指针和长度要怎么从 String 这个结构中读到)。

    2. 异步支持

      如果你对 Rust 异步不够了解,可以参考我的这篇介绍:Rust Runtime 设计与实现-科普篇

      基于 CGO 调用,在 Golang 侧将任务 go 出去执行后立刻返回,本质上发起调用可以理解为一次 task dispatch 。

      在 Go 函数执行结束后,它需要将结果返回给 Rust 。由于 Golang 函数已经执行完毕,数据的所有权一定是 Rust 侧在维护,但 Rust 侧无法预知 Go 侧返回的数据大小,因此这里使用了一个非常巧妙的设计:在调用时,Rust 侧传递一个 set_result 函数指针(该函数由 Rust 侧实现),在 go 执行完毕后,通过 CGO 调用该函数来拷贝返回结果并 wake Future 。

    3. 生命周期管理

      我设计了一个 AtomicSlot 用于管理参数和返回值的生命周期,这个结构会被双边同时访问,借助原子操作保证并发安全。其管理的内存会在双边都退出后释放,这样保证了 Future drop 时的内存安全。

    性能优化

    考虑到低版本 Golang 的 CGO 性能问题(go 1.21 开始 CGO 性能有较大提升),我还设计并实现了一个共享内存队列来替代 CGO 调用,这是一个无锁队列,一侧读一侧写(类似 virtio ring 的设计)。

    这个共享内存队列实现在一个单独的包中,如果有这方面的需求,可以单独引入使用。

    经 benchmark 共享内存版本在 Go 1.18 下相比 CGO 版本有最多 20% 的性能提升。

    未来规划

    1. 当前仅支持 Vec 、String 、u8 、usize 等基础类型及其组合,未来需要支持 HashMap 等多种常见类型。
    2. 当前请求结构体定义不支持泛型参数,未来需要支持泛型参数(包括 lifetime )。
    3. 当前模式下,如需 Go 调用 Rust ,需要手动传递指针并调用,未来需要支持 Go 调用 Rust 的自动生成。
    4. 期待各位的建议!
    17 条回复    2024-09-10 12:11:05 +08:00
    povsister
        1
    povsister  
       10 天前
    看到一半我就猜 op 是不是字节的。
    点开博客,果然。

    有无关键指标透露下,比如需求吞吐量和 go 转 rust 的研发消耗情况如何?
    ihciah
        2
    ihciah  
    OP
       10 天前 via iPhone
    @povsister 性能数据是因人而异的,要看参数和响应的类型与大小,也和使用异步或同步接口有关,以及 go 版本。考虑到这些 diff ,我这里目前没有非常官方的数据,你可以在你的场景下测下(简单写一下 benchmark),对比 rpc 等方式应当有较大提升。
    fgwmlhdkkkw
        3
    fgwmlhdkkkw  
       10 天前
    ,,,
    ihciah
        4
    ihciah  
    OP
       10 天前 via iPhone
    @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 !
    fzdwx
        5
    fzdwx  
       10 天前
    半年前就关注过,大佬牛逼!现在能支持 windows 平台吗?然后期待 rust 调用 go
    fzdwx
        6
    fzdwx  
       10 天前
    @fzdwx #5 go 调用 rust ..
    fgwmlhdkkkw
        7
    fgwmlhdkkkw  
       9 天前
    @ihciah #4 额,,,我是说挺厉害的,感觉就像花式抛球呀。不像吗?
    yb2313
        8
    yb2313  
       9 天前
    字节要开始转 rust 了吗, rrrrrust, 启动
    nomagick
        9
    nomagick  
       9 天前 via Android
    需要 ffi 调用的公共库代码麻烦用 C 重写谢谢,不要把 go 再传播到其他语言了
    linrongbin
        10
    linrongbin  
       9 天前
    @yb2313 之前看到 rust 内部有宣传写了个 api 框架,这样就可以直接 rust 写业务代码了
    linrongbin
        11
    linrongbin  
       9 天前
    牛逼,为啥 V2EX 不能给帖子点赞呢。。。
    0o0O0o0O0o
        12
    0o0O0o0O0o  
       9 天前 via iPhone
    OP 快把 mem-ring 的 How it Works 端上来,还有 bench 代码,感兴趣
    zizon
        13
    zizon  
       9 天前
    感觉你真要用 rust 重写的话也应该是从 go 调 rust 开始,逐步把实现迁移成 rust.
    毕竟 go 代码带 runtime 属性,本质是属于除了你业务代码之外 runtime 特性你也隐性依赖,脱离不了.
    xieren58
        14
    xieren58  
       9 天前
    都用 rust 了. 还调 go... 有点无语...
    ihciah
        15
    ihciah  
    OP
       9 天前
    > 不要把 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 都是我从零搓的。
    aw2350
        16
    aw2350  
       7 天前
    grpc 不好吗
    VVVYGD
        17
    VVVYGD  
       6 天前
    虽然有点倒反天罡,但是还是比较行的,讲解了动静态链 FFI 原理。用了 rust 之后,只觉得 C++在语法主义层面比 rust 好,不过开发工具 c++没有 rust 好,所以一直坚持用 rust 实现各种 api 业务。 快速的项目用 python 组合 Nextjs+ AI 写真的爽
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1007 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 19:11 · PVG 03:11 · LAX 12:11 · JFK 15:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.