#cgo #go-rust #rust-go

nightly fcplug

Foreign-Clang-Plugin 解决方案,例如解决 Rust 和 Go 双向调用

34 个版本

0.4.5 2024年3月29日
0.4.4 2024年3月23日
0.4.3 2023年9月21日
0.3.21 2023年9月5日
0.1.0 2023年6月8日

#69 in FFI

Download history 1/week @ 2024-06-07 46/week @ 2024-06-28

每月下载量 1,236

Apache-2.0

30KB
644 代码行

fcplug

Foreign-Clang-Plugin 解决方案,例如解决 Rust 和 Go 双向调用。

Crates.io Apache-2.0 licensed API Docs

特性

⇊调用者 \ 被调用者⇉ Go Rust
Go -
Rust -
  • Protobuf IDL 编码器解决方案:支持!
  • Thrift IDL 编码器解决方案:开发中...
  • 无编码器解决方案:开发中...

方案图

Fcplug Schematic

准备

  • 安装 Rust nightly
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup default nightly
  • 安装 Go

下载 Go

版本 go≥1.18

设置环境变量: CGO_ENABLED=1

  • 安装 protoc

使用 protoc v23.2

使用 protoc-gen-go v1.5.3

go install github.com/golang/protobuf/[email protected]

使用示例

以 Protobuf IDL 序列化解决方案为例。

查看 echo_pb

步骤 1:创建/准备一个 crate

通常,Fcplug 在 Crate 的 build.sh 中执行,代码自动生成到当前 Crate。

  • 如果您没有 Crate,请执行以下命令创建它
cargo new --lib {crate_name}
  • 添加 staticlib crate-type 和一些依赖包,打开 build.rs 的调试日志,并在 Cargo.toml 中进行如下编辑
[lib]
crate-type = ["rlib", "staticlib"]

[profile.dev.build-override]
opt-level = 0
debug = true

[dependencies]
fcplug = "0.3"
pilota = "0.7.0"
serde = "1"
serde_json = "1"

[build-dependencies]
fcplug-build = "0.3"

步骤 2:编写定义 FFI 接口的 IDL 文件

以 ProtoBuf 格式编写 IDL 文件 {ffi_name} .proto,您可以将其放在 {crate_name} 的根目录下,内容示例如下

syntax = "proto3";

message Ping {
  string msg = 1;
}

message Pong {
  string msg = 1;
}

// go call rust
service RustFFI {
  rpc echo_rs (Ping) returns (Pong) {}
}

// rust call go
service GoFFI {
  rpc echo_go (Ping) returns (Pong) {}
}

步骤 3:编写自动生成的代码 build.rs

#![allow(unused_imports)]

use fcplug_build::{Config, generate_code, UnitLikeStructPath};

fn main() {
    generate_code(Config {
        idl_file: "./echo.proto".into(),
        // go command dir, default to find from $GOROOT > $PATH
        go_root_path: None,
        go_mod_parent: "github.com/andeya/fcplug/samples",
        target_crate_dir: None,
    });
}

步骤 4:初步代码生成

  • 在当前 Crate 下执行
cargo build
# `cargo test` and `cargo install` will also trigger the execution of build.rs to generate code
  • 将生成的 src/{ffi_name}_ffi mod 添加到 Crate,即在 lib.rs 文件中添加 mod {ffi_name}_ffi

步骤 5:实现 FFI 接口

  • 在 Rust 端,您需要在 src/{ffi_name}_ffi/mod.rs 的新初始化文件中实现 RustFfi 和 GoFfi 特定的 trait 方法。
    文件的完整示例代码如下
#![allow(unused_variables)]

pub use echo_pb_gen::*;
use fcplug::{GoFfiResult, TryIntoTBytes};
use fcplug::protobuf::PbMessage;

mod echo_pb_gen;

impl RustFfi for FfiImpl {
    fn echo_rs(mut req: ::fcplug::RustFfiArg<Ping>) -> ::fcplug::ABIResult<::fcplug::TBytes<Pong>> {
        let _req = req.try_to_object::<PbMessage<_>>();
        #[cfg(debug_assertions)]
        println!("rust receive req: {:?}", _req);
        Pong {
            msg: "this is pong from rust".to_string(),
        }
            .try_into_tbytes::<PbMessage<_>>()
    }
}

impl GoFfi for FfiImpl {
    #[allow(unused_mut)]
    unsafe fn echo_go_set_result(mut go_ret: ::fcplug::RustFfiArg<Pong>) -> ::fcplug::GoFfiResult {
        #[cfg(debug_assertions)]
        return GoFfiResult::from_ok(go_ret.try_to_object::<PbMessage<_>>()?);
        #[cfg(not(debug_assertions))]
        return GoFfiResult::from_ok(go_ret.bytes().to_owned());
    }
}
  • 在一次性生成的文件 ./cgobin/clib_goffi_impl.go 中实现 GoFfi 接口。
    此文件的完整示例代码如下
package main

import (
	"fmt"

	"github.com/andeya/fcplug/samples/echo_pb"
	"github.com/andeya/gust"
)

func init() {
	// TODO: Replace with your own implementation, then re-execute `cargo build`
	GlobalGoFfi = GoFfiImpl{}
}

type GoFfiImpl struct{}

func (g GoFfiImpl) EchoGo(req echo_pb.TBytes[echo_pb.Ping]) gust.EnumResult[echo_pb.TBytes[*echo_pb.Pong], ResultMsg] {
	_ = req.PbUnmarshalUnchecked()
	fmt.Printf("go receive req: %v\n", req.PbUnmarshalUnchecked())
	return gust.EnumOk[echo_pb.TBytes[*echo_pb.Pong], ResultMsg](echo_pb.TBytesFromPbUnchecked(&echo_pb.Pong{
		Msg: "this is pong from go",
	}))
}

步骤 6:生成最终代码

在当前 crate 下执行 cargo build cargo testcargo install,触发 build.rs 的执行,并生成代码。

注意:当 GoFfi 被定义后,在首次编译或更改代码后,可能会出现如下类似的警告,此时应执行 cargo build 两次

警告:... 重新执行 'cargo build' 以确保 'libgo_echo.a' 的正确性

因此,建议在 build.sh 脚本中直接重复执行 cargo build 两次

#!/bin/bash

cargo build --release
cargo build --release
cargo build --release

步骤 7:测试

  • Rust 调用 Go 测试,你可以在 lib.rs 中添加测试函数,
    示例代码如下
#![feature(test)]

extern crate test;

mod echo_pb_ffi;


#[cfg(test)]
mod tests {
    use test::Bencher;

    use fcplug::protobuf::PbMessage;
    use fcplug::TryIntoTBytes;

    use crate::echo_pb_ffi::{FfiImpl, GoFfiCall, Ping, Pong};

    #[test]
    fn test_call_echo_go() {
        let pong = unsafe {
            FfiImpl::echo_go::<Pong>(Ping {
                msg: "this is ping from rust".to_string(),
            }.try_into_tbytes::<PbMessage<_>>().unwrap())
        };
        println!("{:?}", pong);
    }

    #[bench]
    fn bench_call_echo_go(b: &mut Bencher) {
        let req = Ping {
            msg: "this is ping from rust".to_string(),
        }
            .try_into_tbytes::<PbMessage<_>>()
            .unwrap();
        b.iter(|| {
            let pong = unsafe { FfiImpl::echo_go::<Vec<u8>>(req.clone()) };
            let _ = test::black_box(pong);
        });
    }
}
  • Go 调用 Rust 测试,在根目录下添加文件 go_call_rust_test.go
    示例代码如下
package echo_pb_test

import (
	"testing"

	"github.com/andeya/fcplug/samples/echo_pb"
)

func TestEcho(t *testing.T) {
	ret := echo_pb.GlobalRustFfi.EchoRs(echo_pb.TBytesFromPbUnchecked[*echo_pb.Ping](&echo_pb.Ping{
		Msg: "this is ping from go",
	}))
	if ret.IsOk() {
		t.Logf("%#v", ret.PbUnmarshalUnchecked())
	} else {
		t.Logf("fail: err=%v", ret.AsError())
	}
	ret.Free()
}

异步编程

  • Rust Tokio 异步函数调用 Go 同步函数
use fcplug::protobuf::PbMessage;
use fcplug::TryIntoTBytes;
use fcplug-build::task;

use crate::echo_ffi::{FfiImpl, GoFfiCall, Ping, Pong};

let pong = task::spawn_blocking(move | | {
// The opened task runs in a dedicated thread pool. 
// If this task is blocked, it will not affect the completion of other tasks
unsafe {
FfiImpl::echo_go::< Pong > (Ping {
msg: "this is ping from rust".to_string(),
}.try_into_tbytes::< PbMessage < _ > > ().unwrap())
}
}).await?;

  • Go 调用 Rust,至少有一方是异步函数

开发中

基准测试

查看基准测试代码

goos: darwin
goarch: amd64
pkg: github.com/andeya/fcplug/demo
cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz

Benchmark: fcplug(cgo->rust) vs pure go

依赖

~12MB
~192K SLoC