Call Between Golang and Rust Asynchronously
See the codeRust2Go is a project that provides users with a simple and efficient way to call Golang from Rust with native async support. It also support user calling Rust from Golang.
rust2go-cli --src src/user.rs --dst go/gen.go
--package-name <name> to set the package name of the generated go file (defaults to main).--without-main to omit the go main function, --go118 for Go 1.18/1.19 compatibility, and --no-fmt to skip formatting the generated file.build.rs for you project (see docs/build-rs.md for the full build script helper reference, including dynamic linking and custom go build arguments).For detailed example, please checkout the example projects.
i8/i16/i32/i64/isize, u8/u16/u32/u64/usize, f32/f64, bool, char, String, Vec<T>, user-defined structs, and non-generic type aliases (e.g. pub type Amount = i64;, expanded during code generation). Option<T> is treated as Vec<T>: None maps to an empty list on the Go side.#[derive(...)]) in the generated code, and #[rust2go::r2g_struct_tag(json = "snake_case")] adds tags to the generated Go struct fields. See docs/trait-attrs.md for the full attribute reference.&self methods)For the Go-to-Rust direction (#[rust2go::g2r]), if every method of the trait takes &self, the trait becomes stateful: instead of expecting you to implement the trait on the generated unit struct, the macro generates a process-wide instance registry plus a register function, and the FFI entries dispatch through the registered instance. (Mixing &self and non-&self methods in one trait is a compile error, as are &mut self or by-value self receivers.)
#[rust2go::g2r]
pub trait G2RCounter {
fn incr(&self, by: u64) -> u64;
fn current(&self) -> u64;
}
struct Counter { count: AtomicU64 }
impl G2RCounter for Counter {
fn incr(&self, by: u64) -> u64 { self.count.fetch_add(by, Ordering::SeqCst) + by }
fn current(&self) -> u64 { self.count.load(Ordering::SeqCst) }
}
// Call once at startup, before Go invokes any method:
G2RCounterImpl::register(Counter { count: AtomicU64::new(0) })
.unwrap_or_else(|_| panic!("register once"));
Rules to know:
Send + Sync + 'static: Go may call the methods from any thread, so keep mutable state behind atomics or a Mutex.register call returns Err (the global OnceLock is consumed).See examples/example-go2rust for a complete runnable demo (including the rust_lib_init cgo init hook used to register from a staticlib).
Detailed design details can be found in this article: Design and Implementation of a Rust-Go FFI Framework.
In order to achieve the ultimate performance, this project is not purely based on communication, but on FFI to pass specially encoded data. In order to reduce memory operations to a minimum, data that satisfies a specific memory layout is passed directly by reference rather than copied.
For example, Vec<u8> and String is represented as a pointer and a length. However, structs like Vec<String> or Vec<Vec<u8>> require intermediate representation. In order to reduce the number of memory allocations to one, I use a precomputed size buffer to store these intermediate structures.
On the Golang side, the data it receives is referenced from Rust. The Rust side will do its best to ensure the validity of this data during the call. So the Golang side can implement the handler arbitrarily, but manually deep copy when leaking data outside the function life cycle.
On the Rust side, it is needed to ensure that the slot pointer of the callback ffi operation, and the user parameters are valid when the future drops. This is archieved by implementing an atomic slot structure and providing a [drop_safe] attribute to require user passing parameters with ownership.
Note: Since golang may scan the stack, and when it meets peer pointer, it may panic. You should run the program with GODEBUG=invalidptr=0,cgocheck=0 env to bypass it.
--go118#[mem]/#[shm]) requires unix..go.tmpl filesgenerate() extracted into the rust2go-gen library crate; the CLI is a thin shell over itsyn::Error diagnostics instead of compiler panicsgofmt/go vet gates, and linux/macOS/Windows coverage across amd64 and arm64 legsThis project is inspired by fcplug.
Rust
74.7%
Go
20.4%
Go Template
3.2%
Assembly
1.8%
Call Between Golang and Rust Asynchronously
See the codeRust2Go is a project that provides users with a simple and efficient way to call Golang from Rust with native async support. It also support user calling Rust from Golang.
rust2go-cli --src src/user.rs --dst go/gen.go
--package-name <name> to set the package name of the generated go file (defaults to main).--without-main to omit the go main function, --go118 for Go 1.18/1.19 compatibility, and --no-fmt to skip formatting the generated file.build.rs for you project (see docs/build-rs.md for the full build script helper reference, including dynamic linking and custom go build arguments).For detailed example, please checkout the example projects.
i8/i16/i32/i64/isize, u8/u16/u32/u64/usize, f32/f64, bool, char, String, Vec<T>, user-defined structs, and non-generic type aliases (e.g. pub type Amount = i64;, expanded during code generation). Option<T> is treated as Vec<T>: None maps to an empty list on the Go side.#[derive(...)]) in the generated code, and #[rust2go::r2g_struct_tag(json = "snake_case")] adds tags to the generated Go struct fields. See docs/trait-attrs.md for the full attribute reference.&self methods)For the Go-to-Rust direction (#[rust2go::g2r]), if every method of the trait takes &self, the trait becomes stateful: instead of expecting you to implement the trait on the generated unit struct, the macro generates a process-wide instance registry plus a register function, and the FFI entries dispatch through the registered instance. (Mixing &self and non-&self methods in one trait is a compile error, as are &mut self or by-value self receivers.)
#[rust2go::g2r]
pub trait G2RCounter {
fn incr(&self, by: u64) -> u64;
fn current(&self) -> u64;
}
struct Counter { count: AtomicU64 }
impl G2RCounter for Counter {
fn incr(&self, by: u64) -> u64 { self.count.fetch_add(by, Ordering::SeqCst) + by }
fn current(&self) -> u64 { self.count.load(Ordering::SeqCst) }
}
// Call once at startup, before Go invokes any method:
G2RCounterImpl::register(Counter { count: AtomicU64::new(0) })
.unwrap_or_else(|_| panic!("register once"));
Rules to know:
Send + Sync + 'static: Go may call the methods from any thread, so keep mutable state behind atomics or a Mutex.register call returns Err (the global OnceLock is consumed).See examples/example-go2rust for a complete runnable demo (including the rust_lib_init cgo init hook used to register from a staticlib).
Detailed design details can be found in this article: Design and Implementation of a Rust-Go FFI Framework.
In order to achieve the ultimate performance, this project is not purely based on communication, but on FFI to pass specially encoded data. In order to reduce memory operations to a minimum, data that satisfies a specific memory layout is passed directly by reference rather than copied.
For example, Vec<u8> and String is represented as a pointer and a length. However, structs like Vec<String> or Vec<Vec<u8>> require intermediate representation. In order to reduce the number of memory allocations to one, I use a precomputed size buffer to store these intermediate structures.
On the Golang side, the data it receives is referenced from Rust. The Rust side will do its best to ensure the validity of this data during the call. So the Golang side can implement the handler arbitrarily, but manually deep copy when leaking data outside the function life cycle.
On the Rust side, it is needed to ensure that the slot pointer of the callback ffi operation, and the user parameters are valid when the future drops. This is archieved by implementing an atomic slot structure and providing a [drop_safe] attribute to require user passing parameters with ownership.
Note: Since golang may scan the stack, and when it meets peer pointer, it may panic. You should run the program with GODEBUG=invalidptr=0,cgocheck=0 env to bypass it.
--go118#[mem]/#[shm]) requires unix..go.tmpl filesgenerate() extracted into the rust2go-gen library crate; the CLI is a thin shell over itsyn::Error diagnostics instead of compiler panicsgofmt/go vet gates, and linux/macOS/Windows coverage across amd64 and arm64 legsThis project is inspired by fcplug.
Rust
74.7%
Go
20.4%
Go Template
3.2%
Assembly
1.8%