fcplug

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

Features

| ⇊Caller \ Callee⇉ | Go | Rust | |-------------------|:--:|:----:| | Go | - | ✅ | | Rust | ✅ | - |

Schematic

Fcplug Schematic

Prepare

shell curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup default nightly

Download Go

Version go≥1.18

Set environment variables: CGO_ENABLED=1

Use protoc v23.2

Use 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.

shell cargo new --lib {crate_name}

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

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

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

```protobuf 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

```rust

![allow(unused_imports)]

use fcplugbuild::{Config, generatecode, UnitLikeStructPath};

fn main() { generatecode(Config { idlfile: "./echo.proto".into(), // go command dir, default to find from $GOROOT > $PATH gorootpath: None, gomodparent: "github.com/andeya/fcplug/samples", targetcratedir: None, }); } ```

Step 4: Preliminary Code Generation

```shell cargo build

cargo test and cargo install will also trigger the execution of build.rs to generate code

```

Step 5: Implement the FFI interface

```rust

![allow(unused_variables)]

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

mod echopbgen;

impl RustFfi for FfiImpl { fn echors(mut req: ::fcplug::RustFfiArg) -> ::fcplug::ABIResult<::fcplug::TBytes> { let _req = req.trytoobject::>(); #[cfg(debugassertions)] println!("rust receive req: {:?}", req); Pong { msg: "this is pong from rust".tostring(), } .tryintotbytes::>() } }

impl GoFfi for FfiImpl { #[allow(unusedmut)] unsafe fn echogosetresult(mut goret: ::fcplug::RustFfiArg) -> ::fcplug::GoFfiResult { #[cfg(debugassertions)] return GoFfiResult::fromok(goret.trytoobject::>()?); #[cfg(not(debugassertions))] return GoFfiResult::fromok(goret.bytes().toowned()); } } ```

```go 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 echopb.TBytes[echopb.Ping]) gust.EnumResult[echopb.TBytes[*echopb.Pong], ResultMsg] { _ = req.PbUnmarshalUnchecked() fmt.Printf("go receive req: %v\n", req.PbUnmarshalUnchecked()) return gust.EnumOkechopb.TBytes[*echopb.Pong], ResultMsg }

```

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

```bash

!/bin/bash

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

Step 7: Testing

```rust

![feature(test)]

extern crate test;

mod echopbffi;

[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 package echopbtest

import ( "testing"

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

)

func TestEcho(t testing.T) { ret := echo_pb.GlobalRustFfi.EchoRs(echo_pb.TBytesFromPbUncheckedecho_pb.Ping) if ret.IsOk() { t.Logf("%#v", ret.PbUnmarshalUnchecked()) } else { t.Logf("fail: err=%v", ret.AsError()) } ret.Free() }

```

Asynchronous programming

```rust use fcplug::protobuf::PbMessage; use fcplug::TryIntoTBytes; use tokio::task;

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

let pong = task::spawnblocking(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::echogo::< Pong > (Ping { msg: "this is ping from rust".tostring(), }.tryinto_tbytes::< PbMessage < _ > > ().unwrap()) } }).await?;

```

in development

Benchmark

See benchmark code

text 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