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.
Top comments (3)
What about writing a Rust application writing to stdout and a golang application reading from stdin and then run from command line:
$ rustHelloWorld | golangGreetingsFromRustReceiver
? Wouldn't it make much more sense? Notice, that no matter the programming language the final binary executable will be written is same "language" assuming same CPU and hardware periphery ... This is the common point where all programming languages meet ... the binary executable ... with stdin and stdout provided by the system.For a trivial hello world application like what I have here that could work but that won’t scale for much more complex applications. There are many things using variables across the languages will give you like caching computation you had done prior, you won’t know all of this if you’re always writing to and reading from a file. There’ll also be file I/O overhead and security concerns (other applications can write to stdin)
Yes ... it seems that this kind of justification is so mainstream that ChatGPT responds with a same one if asked about which sense does it make, but ... the fact is that modern applications tend to over-complicate things keeping software developer busy with problems which where not there if not created by confused minds without clear vision of what is really needed, in first place. What can be done to improve this situation? I encourage you to check in first place where a problem originates from. This will help to solve it at its roots instead of patching the patches with each next patch generating some unexpected side-effects which remain unnoticed or need another patch later on. Emacs as text editor is an excellent example of a complex application screwed to a degree that it is as good as impossible to get things right there because of all the past which contributed to the chaos now only a hand full of experts are able to handle.