DEV Community

paschal
paschal

Posted on

Hello World in Go From Rust

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

gorust

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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);
    };
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

Then we can run go run main.go and it's alive:

hello world. "merry christmas"
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
ooosys profile image
oOosys • Edited

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.

Collapse
 
obbap profile image
paschal

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)

Collapse
 
ooosys profile image
oOosys • Edited

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.