Graham King

Solvitas perambulum

Building shared libraries in Go: Part 2

software go
Summary
In this example, I demonstrate how to call a Go shared library from C++. First, I create a Go function `Concat` that takes a string and a byte slice as inputs, and concatenates them into an output byte slice. I export this function using `cgo`, build the shared library (`libconcat.so`), and create a corresponding header file. I then write a C++ program that prepares inputs as `GoString` and `GoSlice` types, calls the `Concat` function, and prints the result. I ensure to handle types carefully, particularly using `static_cast<GoInt>` for size conversions, while avoiding passing pointers to Go-allocated memory back to C++. This approach is similar to working with any standard shared library, but with specific considerations for memory management between Go and C++.

In part 1 we called a very simple Go shared library from Python. Let’s do a more complex example, passing string and []byte, from C++, and getting back a []byte.

Calling Go from C++

Save the following as concat/main.go:

package main

import "C"

//export Concat
func Concat(sIn string, bIn []byte, bOut []byte) {
    n := copy(bOut, sIn)
    copy(bOut[n:], bIn)
}

func main() {}

We add the import "C" so that cgo gives us a header file to #include. Build the shared library and header:

go build -buildmode=c-shared -o libconcat.so concat

In libconcat.h we have this signature:

extern void Concat(GoString p0, GoSlice p1, GoSlice p2)

GoString and GoSlice are defined further up in the header file like this:

typedef struct { char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

Copy libconcat.so to /usr/lib/ (or wherever your libraries live). Now let’s call Concat from C++:

#include <vector>
#include <string>
#include <iostream>
#include "libconcat.h"

int main() {
    std::string s_in {"Hello "};
    std::vector<char> v_in {'W', 'o', 'r', 'l', 'd'};
    std::vector<char> v_out(11);

    GoString go_s_in{&s_in[0], static_cast<GoInt>(s_in.size())};
    GoSlice go_v_in{
        v_in.data(),
        static_cast<GoInt>(v_in.size()),
        static_cast<GoInt>(v_in.size()),
    };
    GoSlice go_v_out{
        v_out.data(),
        static_cast<GoInt>(v_out.size()),
        static_cast<GoInt>(v_out.size()),
    };

    Concat(go_s_in, go_v_in, go_v_out);

    for(auto& c : v_out) {
        std::cout << c;
    }
    std::cout << '\n';
}

Save that as concat.cpp. Copy libconcat.h into the same directory. Build and run:

g++ --std=c++14 concat.cpp -o concat -lconcat
./concat

I need the static_cast<GoInt> because a GoInt is a signed type (long long on my machine), but size() returns unsigned type size_t. Apart from wrapping things in Go[String|Slice|etc], this is exactly like calling an ordinary shared libary, because that’s what we built, an ordinary shared library.

The one very important caveat is that you should not pass pointers to Go allocated memory back to the C++ side; that’s why we use an output parameter (v_out). Even if you maintain a reference to it on the Go side so that the garbage collector doesn’t reclaim it, the Go runtime reserves the right to move that memory if they build a copying garbage collector. Lots of details in issue #8310.

Passing C++ allocated memory to Go is fine. Go will not garbage collect memory it did not allocate. There rules here are the same as for cgo.