The foreign function interface to call Rust library from Golang

August 24, 2022

FFI

Intro

Imagin that you have a project written in C/C++ or even Rust and you would like to call functions from this project library in your Golang application. Wonderful thign is that there is a solution for that called FFI. A foreign function interface (FFI) is a mechanism by which a program written in one programming language can call routines or make use of services written in another.

The primary function of a foreign function interface is to mate the semantics and calling conventions of one programming language (the host language, or the language which defines the FFI), with the semantics and conventions of another (the guest language). This process must also take into consideration the runtime environments and/or application binary interfaces of both. This can be done in several ways:

Requiring that guest-language functions which are to be host-language callable be specified or implemented in a particular way, often using a compatibility library of some sort. Use of a tool to automatically “wrap” guest-language functions with appropriate glue code, which performs any necessary translation. Use of wrapper libraries Restricting the set of host language capabilities which can be used cross-language. For example, C++ functions called from C may not (in general) include reference parameters or throw exceptions. FFIs may be complicated by the following considerations:

If one language supports garbage collection (GC) and the other does not; care must be taken that the non-GC language code does nothing to cause GC in the other to fail. In JNI, for example, C code which “holds on to” object references that it receives from Java must “register” this fact with the Java runtime environment (JRE); otherwise, Java may delete objects before C has finished with them. (The C code must also explicitly release its link to any such object once C has no further need of that object.) Complicated or non-trivial objects or datatypes may be difficult to map from one environment to another. It may not be possible for both languages to maintain references to the same instance of a mutable object, due to the mapping issue above. One or both languages may be running on a virtual machine (VM); moreover, if both are, these will probably be different VMs. Cross-language inheritance and other differences, such as between type systems or between object-composition models, may be especially difficult.

Solution

Let’s look at a simple example of solving this problem: I prepared Some code samples located on My Github repository.

https://github.com/VadzimBelski-ScienceSoft/go-call-rust-ffi

  1. On the Rust side - there is nothing specific. Juat need to build project as library

  2. On Golang side we need to use “C” bindings which allow up to import dynamic libraries. Please also pay attention on datat type conversions needed for mapping from go to C types.

Other than that everyhting works as a charm. My use case is to use C++ libraties in golang - as example dozend of crypto algorythms implemented only in C++ especially if we are speaking about post quantum algorythms.

Rust library for crypto


extern crate libc;

use sha256::digest_bytes;
use std::ffi::{CStr, CString};

#[no_mangle] 
pub extern "C" fn rustdemo(name: *const libc::c_char) -> *const libc::c_char {
    let cstr_name = unsafe { CStr::from_ptr(name) };
    let bytes = cstr_name.to_bytes();
    CString::new(digest_bytes(bytes)).unwrap().into_raw()
}

Golang main application


package main

/*
#cgo LDFLAGS: -L./lib -lrustdemo
#include <stdlib.h>
#include "./lib/rustdemo.h"
*/
import "C"

import (
	"fmt"
	"os"
	"unsafe"
)

func main() {

	argsWithoutProg := os.Args[1:]

	for _, arg := range argsWithoutProg {

		input := C.CString(arg)
		defer C.free(unsafe.Pointer(input))
		o := C.rustdemo(input)
		output := C.GoString(o)
		fmt.Printf("%s\n", output)

	}

}

How to put all together


git clone git@github.com:VadzimBelski-ScienceSoft/go-call-rust-ffi.git

cd go-call-rust-ffi/rustdemo
cargo build --release
cd ..

cp rustdemo/target/release/librustdemo.dylib ./lib

go build -o go-rust  -ldflags="-r ./lib" main.go

echo -n "hello" | shasum -a 256

./go-rust "hello" 

Go references to C

Within the Go file, C’s struct field names that are keywords in Go can be accessed by prefixing them with an underscore: if x points at a C struct with a field named “type”, x._type accesses the field. C struct fields that cannot be expressed in Go, such as bit fields or misaligned data, are omitted in the Go struct, replaced by appropriate padding to reach the next field or the end of the struct.

The standard C numeric types are available under the names C.char, C.schar (signed char), C.uchar (unsigned char), C.short, C.ushort (unsigned short), C.int, C.uint (unsigned int), C.long, C.ulong (unsigned long), C.longlong (long long), C.ulonglong (unsigned long long), C.float, C.double, C.complexfloat (complex float), and C.complexdouble (complex double). The C type void* is represented by Go’s unsafe.Pointer. The C types __int128_t and __uint128_t are represented by [16]byte.

To access a struct, union, or enum type directly, prefix it with struct_, union_, or enum_, as in C.struct_stat.

The size of any C type T is available as C.sizeof_T, as in C.sizeof_struct_stat.

As Go doesn’t have support for C’s union type in the general case, C’s union types are represented as a Go byte array with the same length.

Go structs cannot embed fields with C types.

Go code can not refer to zero-sized fields that occur at the end of non-empty C structs. To get the address of such a field (which is the only operation you can do with a zero-sized field) you must take the address of the struct and add the size of the struct.

Cgo translates C types into equivalent unexported Go types. Because the translations are unexported, a Go package should not expose C types in its exported API: a C type used in one Go package is different from the same C type used in another.

Any C function (even void functions) may be called in a multiple assignment context to retrieve both the return value (if any) and the C errno variable as an error (use _ to skip the result value if the function returns void). For example:


n, err := C.sqrt(-1)
_, err := C.voidFunc()

Calling C function pointers is currently not supported, however you can declare Go variables which hold C function pointers and pass them back and forth between Go and C. C code may call function pointers received from Go. For example:

package main

// typedef int (*intFunc) ();
//
// int
// bridge_int_func(intFunc f)
// {
//		return f();
// }
//
// int fortytwo()
// {
//	    return 42;
// }
import "C"
import "fmt"

func main() {
	f := C.intFunc(C.fortytwo)
	fmt.Println(int(C.bridge_int_func(f)))
	// Output: 42
}

In C, a function argument written as a fixed size array actually requires a pointer to the first element of the array. C compilers are aware of this calling convention and adjust the call accordingly, but Go cannot. In Go, you must pass the pointer to the first element explicitly:


C.f(&C.x[0]).

A few special functions convert between Go and C types by making copies of the data. In pseudo-Go definitions:


// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte