Introduction
Suppose every software in the world was written in just one programming language Eg. C. In that case, there'll be no need for bindings and foreign function interfaces but since we have thousands of programming languages, genuine use cases arise where communicating across programming languages is necessary.
- Some programming languages are more performant than others and you'll need to hand off computation-intensive tasks to these languages.
- Some programming languages are state of the art in different areas, Eg, Javascript with Browsers, Python with Data, and C with Embedded systems, so it'll be easier to utilize the existing libraries instead of reinventing the wheel.
- Some languages are strict and strongly typed; you want to prototype fast but not compromise on performance. An example is Game development or NGINX module extensions with Lua and C.
The Base CÂ Layer
Golang uses cgo
to communicate with external C libraries and Rust uses FFI (Foreign function interfaces) to export C functions. Additionally, a library for automatically generating bindings can be used to generate the header files (.h)
. In our case, we'll only be exporting two C functions, so using cbindgen is overkill but we'll use it regardless because why not?Â
Firstly, we'll write the rust functions we want to export. We'll start by creating a new rust library:
cargo new r --lib
This means we're creating a new rust library called r
.Â
In our Cargo.toml, we'll specify that this crate is a dynamic library and then include the libc and cbindgen dependencies.
[package]
name = "r"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
libc = "0.2.151"
[build-dependencies]
cbindgen = "0.26.0"
We'll need libc
to access some C types, specifically the c_char
type. This is because strings are UTF-32 encoded (4 bytes) in C while they're UTF-8 encoded in Rust, meaning they can have variable sizes (1 to 4 bytes).
Rust Functions
To write our hello_world
function which will be called from Go, we'll need the no_mangle
annotation so the linker can find the function while linking without the Rust compiler mangling it.Â
Then we can write our functions hello_world
and hello_world_free
:
extern crate libc;
use std::ffi::CString;
use libc::c_char;
#[no_mangle]
pub extern "C" fn hello_world(item: *mut c_char) -> *mut c_char {
unsafe {
let c_str =
CString::new(format!("hello world {:?}", CString::from_raw(item))).expect("dont fail");
c_str.into_raw()
}
}
#[no_mangle]
pub extern "C" fn hello_world_free(item: *mut c_char) {
unsafe {
if item.is_null() {
return;
}
let _ = CString::from_raw(item);
};
}
hello_world
accepts a mutable pointer to a char array as an argument and returns the same. The unsafe
keyword is needed as the rust compiler can't guarantee that from_raw
which entails dereferencing a null pointer, won't lead to undefined behavior. Finally, we use into_raw
to hand ownership back to the caller, in this case, Go.
hello_world_free
accepts a mutable pointer, and calls from_raw
on it, so we can properly reconstruct it, claim ownership, and then drop it. This is to avoid memory leaks.
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_config(config)
.generate()
.expect("Unable to generate bindings")
.write_to_file("bindings.h");
To generate the bindings, we'll add a build script build.rs
which uses cbindgen
to generate the bindings.h
file. Then we can run:
cargo build
If everything goes smoothly, we should have a compiled binary and a newly created bindings.h
file with the following methods in it:
char *hello_world(char *item);
void hello_world_free(char *item);
Go
After building the Rust binary, we'll need to tell the linker where to find it and we can do this by passing the linker flags via cgo
. We'll also need to include (think import) the bindings.h
file so we can call the rust methods.
// #cgo LDFLAGS: -L/Users/pbaba/projects/go-rust/r/target/debug -lr
// #include <bindings.h>
import "C"
Finally, we can use the hello_world
function and include the "merry christmas" string as an argument. We'll also defer the hello_world_free
method call, which will free the memory referenced by the pointer b
afterward.
a := C.CString("merry christmas")
b := C.hello_world(a)
defer C.hello_world_free(b)
fmt.Println(C.GoString(b))
Then we can run go run main.go
and it's alive:
hello world. "merry christmas"
In some cases, we can do without the hello_world_free
function and replace it with the custom free function provided by cgo
defer C.free(unsafe.Pointer(b))
This should replace the function we already have so we don't end up freeing the same location in memory twice and causing a double free:
malloc: Double free of object 0x7f8c340040a0
But for CStrings, The Rust documentation recommends we free this explicitly by reconstructing and then dropping it instead of using C's free.
The rest of the code is on GitHub.