Go Cgo for C code

2 minute read

Cgo lets us include C code in Go.

Inline C in go

You can write C code in go files by adding it as comments above an import "C" statement:

package main

import "fmt"

/*
#include <stdint.h>
uint8_t shift(uint8_t x, int y) {
  return x << y;
}
*/
import "C"

func main() {
  in, n := uint8(0x10), 2
  out := C.shift(C.uint8_t(in), C.int(2))
  fmt.Printf("Shifted %#x by %d, got %#x\n", in, n, out)
}

If we run this, we print:

Shifted 0x10 by 2, got 0x40

We can also use installed C libraries directly in go:

package main

import (
  "fmt"
  "math"
)

// #include <math.h>
import "C"

func main() {
  fmt.Printf(" C π: %.9f\nGo π: %.9f\n", C.M_PI, math.Pi)
}

Gives us:

 C π: 3.141593000
Go π: 3.141592654

Compiling C files

We may want to import C from source. Using the shift example from above, we can organise our go project into:

|-main.go
|-shift.c
|-shift.h

In main.go:

package main

import "fmt"

// #include "shift.h"
import "C"

func main() {
  in, n := uint8(0x10), 2
  out := C.shift(C.uint8_t(in), C.int(2))
  fmt.Printf("Shifted %#x by %d, got %#x\n", in, n, out)
}

In shift.h:

#include <stdint.h>

uint8_t shift(uint8_t x, int y);

In shift.c:

#include "shift.h"

uint8_t shift(uint8_t x, int y) {
	return x << y;
}

And we should be able to compile and run as normal, getting the same result as before:

Shifted 0x10 by 2, got 0x40

Calling Go in C

We may want to leverage Go to enhance the capabilities of our C libraries. Similar to the previous section, we will organize our code as follows:

|-main.go
|-gocaller.c
|-gocaller.h

In this example, we will use Go’s json library to encode some values provided by our C code.

In our main.go file, we will use the //export directive to export a go function that our C code can use:

package main

import (
  "encoding/json"
)

// #include "gocaller.h"
import "C"

//export Encode
func Encode(a C.int, b C.int, c C.int) *C.char {
  m := map[string]C.int{"a": a, "b": b, "c": c}
  bytes, _ := json.Marshal(&m)
  var result *C.char = C.CString(string(bytes))
  // Warning: `result` is a memory leak!
  return result
}

func main() {
  C.addAndEncodeWithGo(1, 2)
}

Inside gocaller.h, we have:

void addAndEncodeWithGo(int a, int b);

And inside gocaller.c, we #include "_cgo_export.h" so that we can call our Encode function from Go.

#include <stdio.h>
#include "gocaller.h"

#include "_cgo_export.h"

void addAndEncodeWithGo(int a, int b) {
    char* encoded = Encode(a, b, a + b);
    printf("Encoded message `%s`\n", encoded);
}

If we run this, our C code calls Go’s Encode function, and then prints the json object to the terminal:

Encoded message `{"a":1,"b":2,"c":3}`

Notes on CGo

  • There is no garbage collection for C pointers. The example for calling Go from C had a memory leak, where we assigned a *C.char pointer.
    • This should always be freed with C.free, usually using defer to ensure the lifetime is contained:
      var cstr *C.char = C.CString(gostr)
      defer C.free(unsafe.Pointer(cstr))
      
  • Compiled Go code using Cgo is only useful for the machine (OS/arch) that the code is compiled on.
    • Go cross compilation in Cgo is usually not worth the effort
    • Compiling your Go code for other machines is limited by the availablity of the C libraries you need.
  • Compared to pure Go, Cgo has very little native debugging support