C++/Go Library Loading

Imagine you only have access to a library (.a) and its header (.h) file. You can see the functions and their signatures, but you just can’t figure out how to call them from Go. One thing that you notice is that the functions are overloaded. The only way to do that is to compile the static library in C++. Overloaded functions are rarely found outside of C++, and they certainly aren’t in Go.

It’s still possible to call the library functions, we just need to write a wrapper.

As of the writing of this article, I haven’t found a good, safe way to call overloaded functions in a dynamic library. It’s possible to obtain the names of functions and methods by examining the dynamic library, but that way madness lies.

0000000000003e90 d _DYNAMIC
0000000000004000 d _GLOBAL_OFFSET_TABLE_
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
00000000000010f9 T _Z3sumii
0000000000001111 T _Z3sumiii
00000000000020c8 r __FRAME_END__
0000000000002000 r __GNU_EH_FRAME_HDR
0000000000004020 d __TMC_END__
                 w __cxa_finalize
00000000000010b0 t __do_global_dtors_aux
0000000000003e88 d __do_global_dtors_aux_fini_array_entry
0000000000004018 d __dso_handle
0000000000003e80 d __frame_dummy_init_array_entry
                 w __gmon_start__
0000000000001134 t _fini
0000000000001000 t _init
0000000000004020 b completed.0
0000000000001040 t deregister_tm_clones
00000000000010f0 t frame_dummy
0000000000001070 t register_tm_clones

Let’s create three libraries:

  • A static library with two overloaded functions
  • A dynamic library with a C-style function
  • A wrapper library that will provide the interface between the overloaded function library and Go

Here is the file structure we want to have at the end (some object files will be temporary):

  • src/
    • sums.cpp
    • sums.h
    • sums_dyn.cpp
    • sums_dyn.h
    • wrapper.cpp
    • wrapper.h
  • lib/
    • sums.a
    • sums_dyn.so
    • wrapper.a
  • main.go
  • go.mod
// sums.h
#pragma once
int sum(int i, int j);

int sum(int i, int j, int k);
// sums.cpp
#include "sums.h"

int sum(int i, int j)
{
        return i + j;
}

int sum(int i, int j, int k)
{
        return i + j + k;
}
// sums_dyn.h
#pragma once
#ifdef __cplusplus
extern "C"
{
#endif

int sum4(int i, int j, int k, int x);

#ifdef __cplusplus
}
#endif

For sums_dyn.h, note that we’ll be wrapping the whole thing in an extern “C” block. This informs the compiler to not mangle symbol names. That limits what we can do, but there’s no other way I know of than to search for symbols (functions) inside a dynamic library than by exact name.

// sums_dyn.cpp
#include "sums_dyn.h"

int sum4(int i, int j, int k, int x)
{
    return i + j + k + x;
}
// wrapper.h
#pragma once
#ifdef __cplusplus
extern "C"
{
#endif

int sum2(int i, int j);

int sum3(int t, int j, int k);

#ifdef __cplusplus
}
#endif

Here is were things get a bit interesting. We’re going to #include “sums.h” outside of the extern “C” block because we want the compiler to mangle its function names when we call them, but our own function names should remain un-mangled, so we’ll put them inside a conditional extern “C” block in the header file. The conditional block is include by g++ but not by Go’s C-compiler, cgo. In this configuration, the overloaded functions are exposed to wrapper.cpp, but they won’t be exposed to Go.

// wrapper.cpp
#include "wrapper.h"
#include "sums.h"

int sum2(int i, int j)
{
        return sum(i, j);
}

int sum3(int i, int j, int k)
{
        return sum(i, j, k);
}

It’s time to build our main.go file.

// main.go
package main

/*
#cgo LDFLAGS: -L. -l:./lib/wrapper.a -lstdc++
#include "./src/wrapper.h"
#cgo LDFLAGS: -L. -l:./lib/sums.a -lstdc++
#cgo LDFLAGS: -L. -l:./lib/sums_dyn.so -lstdc++
#include "./src/sums_dyn.h"
*/
import "C"

import "fmt"

func main() {
        fmt.Println("2 + 3 =", C.sum2(2, 3))

        fmt.Println("4 + 5 + 6 =", C.sum3(4, 5, 6))

        fmt.Println("7 + 8 + 9 + 10 =", C.sum4(7, 8, 9, 10))
}

There is a lot to unpack, but here is a quick guide. import “C” treats the immediately preceding code block as strictly C-code. There is no way to write C++ code there. Note that the -lstdc++ flag isn’t necessary since our code never calls functions such as new(), but I leave it here as an example.

  • We statically load and link the file lib/wrapper.a
  • We include the src/wrapper.h file to determine which functions are available to Go
  • We statically load and link the file lib/sums.a – Go doesn’t know what’s in it, but wrapper.a does
  • We dynamically link the file lib/sums_dyn.so (we’ll test that it’s dynamically linked and not statically loaded and linked)
  • We include the serc/sums_dyn.h file to determine which functions are available to Go

Let’s build. From a directory containing main.go:

g++ -c src/sums.cpp -o src/sums.o
g++ -c src/wrapper.cpp -o src/wrapper.o
ar rcs lib/sums.a src/sums.o
ar rcs lib/wrapper.a src/wrapper.o
g++ -fPIC -shared src/sums_dyn.cpp -o lib/sums_dyn.so
go build -a
./main
2 + 3 = 5
4 + 5 + 6 = 15
7 + 8 + 9 + 10 = 34

To test that sums_dyn.so gets loaded at runtime, let’s rename lib/sums_dyn.so to lib/sums_dyn.so2 and run the program again.

./main: error while loading shared libraries: ./lib/sums_dyn.so: cannot open shared object file: No such file or directory

This is merely the first step to loading and linking C++ libraries into Go, but, hopefully, it will help guide you along.