Embedding Go in a Rust Program
Recently, I’ve been using Rust and sometimes often miss the simplicity of Go and its huge ecosystem of battle-tested libraries. Fortunately, Rust provides a good Foreign Function Interface (FFI) story that makes it easy to call code written in Go. It’s somewhat tricky to setup if you’re new to Rust or FFI, so I hope this is helpful. Please let me know if I made a mistake!
Suppose you have some important business logic written in Go:
func Greet(name string) string {
return fmt.Sprintf("Hello from Go, %s!", name)
}
CGo
To make this callable from Rust (or any foreign language), we’ll have to use CGo to expose the function as a global symbol, and change its signature to use primitive types, like null-terminated C char*
, instead of Go strings:
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"unsafe"
)
//export Greet
func Greet(name *C.char) *C.char {
return C.CString(fmt.Sprintf("Hello from Go, %s!", C.GoString(name)))
}
//export GoFree
func GoFree(ptr *C.char) {
C.free(unsafe.Pointer(ptr))
}
func main() {}
How does memory management work here?
The name
argument is allocated in Rust, where it will be freed, typically when it drops out of scope. The *C.char
that is returned must be also be freed, but by whom?
The GoFree()
function is supplied so that foreign code can easily free any pointer originating in Go land. It may be possible to pass ownership of this pointer to the Rust side (where it could be freed), but I haven’t figured out a simple way to do that, nor does it feel like a good idea, especially if Go and Rust are using different memory allocators. The Rust documentation for CString::from_raw even hints at this.
Nevertheless, the process of calling this function would be:
- Allocate a C string in Rust for the
name
argument - Call Greet with it
- Take the return value pointer and initialize a Rust
String
with it (this is a copy) - Tell Go to free the pointer by calling
GoFree
pub fn greet(name: &str) -> String {
let name = CString::new(name).unwrap();
unsafe {
let cstr = CStr::from_ptr(Greet(name.as_ptr() as *mut c_char));
let s = String::from_utf8_lossy(cstr.to_bytes()).to_string();
GoFree(cstr.as_ptr() as *mut c_char);
s
}
}
As long as you remember to free every C pointer returned by Go, you won’t have to worry about memory leaks. For my application, this is good enough, but you’ll have to do more work if you wanted to support passing byte buffers (e.g., with embedded null bytes) back, or returning tuples from Go, etc. Things get complicated if you try to pass fancier structs, due to FFI restrictions and memory ownership.
Take note of the function signature. The //export Greet
comment is necessary to let the Go linker know to export the Greet
symbol in the static library it produces. On the other hand, you could pass Go-style strings as arguments into the function, if you use the GoString
type that bindgen generates, but the return type need to be a primitive type, otherwise you’d get a crash:
panic: runtime error: cgo result has Go pointer
Let’s do a compile check:
$ go build -buildmode=c-archive -o libgo.a go/lib.go
This produces two files: libgo.a and libgo.h. The C header file will be used to generate the bindings on the Rust side.
While it may be reasonable to build the Go library separately, here, I’m going to integrate it into the Rust build by using a custom cargo build script. Presumably, your project layout looks like this:
rust_go
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs
Keep it simple and add a new crate to contain our FFI shim and Go library code:
$ cargo new go_interop --lib
Created library `go_interop` package
Copy the Go code from earlier into a go
subdirectory inside go_interop
.
Rust Bindings
For all but the simplest of C bindings, you’ll want to use bindgen to create FFI definitions based on our library header. In the go_interop
directory, update Cargo.toml
and add bindgen as a build-time dependency:
[build-dependencies]
bindgen = "*"
Create a new file build.rs
that will build the Go library and generate the Rust bindings:
extern crate bindgen;
use std::env;
use std::path::PathBuf;
use std::process::Command;
fn main() {
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
let mut go_build = Command::new("go");
go_build
.arg("build")
.arg("-buildmode=c-archive")
.arg("-o")
.arg(out_path.join("libgo.a"))
.arg("./go/lib.go");
go_build.status().expect("Go build failed");
let bindings = bindgen::Builder::default()
.header(out_path.join("libgo.h").to_str().unwrap())
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
println!("cargo:rerun-if-changed=go/lib.go");
println!(
"cargo:rustc-link-search=native={}",
out_path.to_str().unwrap()
);
println!("cargo:rustc-link-lib=static={}", "go");
}
Replace lib.rs
with:
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
use std::ffi::{c_char, CStr, CString};
pub fn greet(name: &str) -> String {
let name = CString::new(name).unwrap();
unsafe {
let cstr = CStr::from_ptr(Greet(name.as_ptr() as *mut c_char));
let s = String::from_utf8_lossy(cstr.to_bytes()).to_string();
GoFree(cstr.as_ptr() as *mut c_char);
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = greet("Rust");
assert_eq!(result, "Hello from Go, Rust!");
}
}
At the top-level, add the new crate as a dependency in Cargo.toml
:
[dependencies]
go_interop = { path = "./go_interop" }
Finally, the top-level main.rs
becomes:
use go_interop;
fn main() {
println!("{}", go_interop::greet("Rust"));
}
Run it:
$ cargo run
Hello from Go, Rust!
The final project structure:
rust_go
├── Cargo.lock
├── Cargo.toml
├── go_interop
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── build.rs
│ ├── go
│ │ └── lib.go
│ └── src
│ └── lib.rs
└── src
└── main.rs
That’s it for now.