Writing And Using C Code From Go

20 Mar 2015

Yesterday's post introduced a specialized integer set which took 1/2 the space as a map[int]struct{} and generally did membership checks (Exists) in 1/2 the time. The next (logical?) step is to see what would happen if we rewrote the code in C and called it from Go. A word of warning: my C isn't that sharp.

The first thing we'll do is add a header file which will contain our IntSet type definition as well as the functions we'll be calling from Go (this goes in a file called intset.h which sits besides our Go code):

The vector type is just a simple dynamic array implementation which grows by a fixed amount as needed. The key here is that we've defined a type and four functions. How do we call these in Go? It's quite simple: inside of our Go code, we add:

/*
#include "intset.h"
*/
import "C"

It's very important that the import "C" line be placed immediately after the c-style include (and go fmt won't help you with this).

While we still need to implement the C code, the above is the big piece needed to glue Go and C together. With it, we have a package C with a type called C.IntSet and a function called C.intset_new. C isn't the same as other packages, but as far as how it's used, it's very similar. Assuming we had an implementation, we could do:

Now, this works nicely because our values are all constants and thus we get the proper typing at compile time. However, if we were dealing with variables, a C int doesn't map directly to an Go int, and a char* certainly doesn't map directly to a string. A more realistic example would look like:

A lot more type casting. With strings, we'd also be responsible for freeing the generated char*. It'd be nice if we could shield consumers from knowing that our implementation is written in C. Well, since C.IntSet is a normal Go type, we can do:

You can see that the original Go code is the fastest, while the built-in map comes in second. The C code is quite a bit slower. Why? Because of the overhead of calling into C. This type of code, where you're doing very small amounts of work, cannot make up for the overhead. However, what happens if instead of testing individual membership we do a basic intersection of two large sets?