# fcplug Foreign-Clang-Plugin solution, such as solving rust and go two-way calls. [![Crates.io][crates-badge]][crates-url] [![Apache-2.0 licensed][mit-badge]][mit-url] [![API Docs][docs-badge]][docs-url] [crates-badge]: https://img.shields.io/crates/v/fcplug-build.svg [crates-url]: https://crates.io/crates/fcplug-build [mit-badge]: https://img.shields.io/badge/license-Apache2.0-blue.svg [mit-url]: https://github.com/andeya/fcplug/blob/main/LICENSE [docs-badge]: https://img.shields.io/badge/API-Docs-green.svg [docs-url]: https://docs.rs/fcplug-build ## Features | ⇊Caller \ Callee⇉ | Go | Rust | |-------------------|:--:|:----:| | Go | - | ✅ | | Rust | ✅ | - | - Protobuf IDL codec solution: Supported! - Thrift IDL codec solution: In development... - No codec solution: In development... ## Schematic ![Fcplug Schematic](https://github.com/andeya/fcplug/raw/HEAD/doc/fcplug_schematic.png) ## Prepare - Install rust nightly ```shell curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup default nightly ``` - Install go > Download [Go](https://go.dev/doc/install) > > Version go≥1.18 > > Set environment variables: `CGO_ENABLED=1` - Install protoc > Use [protoc v23.2](https://github.com/protocolbuffers/protobuf/releases/tag/v23.2) > > Use [protoc-gen-go v1.5.3](https://pkg.go.dev/github.com/golang/protobuf@v1.5.3/protoc-gen-go) > ```shell > 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](https://github.com/andeya/fcplug/raw/HEAD/samples/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: ```shell 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: ```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 {ffi_name} .proto in ProtoBuf format, you can put it in the root directory of {crate_name}, > 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 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: ```shell 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: ```rust #![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) -> ::fcplug::ABIResult<::fcplug::TBytes> { let _req = req.try_to_object::>(); #[cfg(debug_assertions)] println!("rust receive req: {:?}", _req); Pong { msg: "this is pong from rust".to_string(), } .try_into_tbytes::>() } } impl GoFfi for FfiImpl { #[allow(unused_mut)] unsafe fn echo_go_set_result(mut go_ret: ::fcplug::RustFfiArg) -> ::fcplug::GoFfiResult { #[cfg(debug_assertions)] return GoFfiResult::from_ok(go_ret.try_to_object::>()?); #[cfg(not(debug_assertions))] 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: ```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 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 ```bash #!/bin/bash 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: ```rust #![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::(Ping { msg: "this is ping from rust".to_string(), }.try_into_tbytes::>().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::>() .unwrap(); b.iter(|| { let pong = unsafe { FfiImpl::echo_go::>(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: ```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() } ``` ## Asynchronous programming - Rust Tokio asynchronous function calling Go synchronous function ```rust 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 calls Rust, at least one side is an async function > in development ## Benchmark [See benchmark code](https://github.com/andeya/fcplug/blob/HEAD/samples/echo_pb/go_call_rust_test.go) ```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](https://github.com/andeya/fcplug/raw/HEAD/doc/benchmark.png)