#cgo #go-rust #rust-go

nightly fcplug

Foreign-Clang-Plugin solution, such as solving rust and go two-way calls

34 releases

0.4.5 Mar 29, 2024
0.4.4 Mar 23, 2024
0.4.3 Sep 21, 2023
0.3.21 Sep 5, 2023
0.1.0 Jun 8, 2023

#44 in FFI

Download history 1/week @ 2024-02-19 4/week @ 2024-02-26 4/week @ 2024-03-11 134/week @ 2024-03-18 105/week @ 2024-03-25 199/week @ 2024-04-01

1,236 downloads per month


644 lines


Foreign-Clang-Plugin solution, such as solving rust and go two-way calls.

Crates.io Apache-2.0 licensed API Docs


⇊Caller \ Callee⇉ Go Rust
Go -
Rust -
  • Protobuf IDL codec solution: Supported!
  • Thrift IDL codec solution: In development...
  • No codec solution: In development...


Fcplug Schematic


  • Install rust nightly
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup default nightly
  • Install go

Download Go

Version go≥1.18

Set environment variables: CGO_ENABLED=1

  • Install protoc

Use protoc v23.2

Use protoc-gen-go v1.5.3

go install github.com/golang/protobuf/protoc-gen-go@v1.5.3

Example of use

Take Protobuf IDL serialization solution as an example.

See the echo_pb

Step 1: create/prepare a crate

Generally, Fcplug is executed in a Crate's build.sh, and the code is automatically generated to the current Crate.

  • If you do not have a Crate, execute the following command to create it:
cargo new --lib {crate_name}
  • Add staticlib crate-type and some dependent packages, open debug log of build.rs, edited in Cargo.toml as follows:
crate-type = ["rlib", "staticlib"]

opt-level = 0
debug = true

fcplug = "0.3"
pilota = "0.7.0"
serde = "1"
serde_json = "1"

fcplug-build = "0.3"

Step 2: Write the IDL file that defines the FFI interface

Write the IDL file {ffi_name} .proto in ProtoBuf format, you can put it in the root directory of {crate_name}, the content example is as follows:

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) {}

Step 3: Scripting auto-generated code build.rs


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,

Step 4: Preliminary Code Generation

  • Execute under the current Crate:
cargo build
# `cargo test` and `cargo install` will also trigger the execution of build.rs to generate code
  • Attach the generated src/{ffi_name}_ffi mod to Crate, that is, add mod {ffi_name}_ffi to the lib.rs file

Step 5: Implement the FFI interface

  • On the rust side, you need to implement the specific trait RustFfi and trait GoFfi methods in the newly initialized file src/{ffi_name}_ffi/mod.rs.
    The complete sample code of the file is as follows:

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<_>>();
        println!("rust receive req: {:?}", _req);
        Pong {
            msg: "this is pong from rust".to_string(),

impl GoFfi for FfiImpl {
    unsafe fn echo_go_set_result(mut go_ret: ::fcplug::RustFfiArg<Pong>) -> ::fcplug::GoFfiResult {
        return GoFfiResult::from_ok(go_ret.try_to_object::<PbMessage<_>>()?);
        return GoFfiResult::from_ok(go_ret.bytes().to_owned());
  • Implement the go GoFfi interface in the one-time generated file ./cgobin/clib_goffi_impl.go.
    The complete sample code of this file is as follows:
package main

import (


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",

Step 6: Generate Final Code

Execute cargo build cargo test or cargo install under the current Crate, trigger the execution of build.rs, and generate code.

Note: When GoFfi is defined, after compiling or changing the code for the first time, a warning similar to the following will occur, and you should execute cargo build twice at this time

warning: ... to re-execute 'cargo build' to ensure the correctness of 'libgo_echo.a'

Therefore, it is recommended to repeat cargo build three times directly in the build.sh script


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

Step 7: Testing

  • Rust calls Go tests, you can add test functions in lib.rs,
    the sample code is as follows:

extern crate test;

mod echo_pb_ffi;

mod tests {
    use test::Bencher;

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

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

    fn test_call_echo_go() {
        let pong = unsafe {
            FfiImpl::echo_go::<Pong>(Ping {
                msg: "this is ping from rust".to_string(),
        println!("{:?}", pong);

    fn bench_call_echo_go(b: &mut Bencher) {
        let req = Ping {
            msg: "this is ping from rust".to_string(),
        b.iter(|| {
            let pong = unsafe { FfiImpl::echo_go::<Vec<u8>>(req.clone()) };
            let _ = test::black_box(pong);
  • Go calls Rust test, add the file go_call_rust_test.go in the root directory,
    the sample code is as follows:
package echo_pb_test

import (


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())

Asynchronous programming

  • Rust Tokio asynchronous function calling Go synchronous function
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())

  • Go calls Rust, at least one side is an async function

in development


See benchmark code

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


~181K SLoC