Embedded development with C and GoLang (CGO)

Hi, I'm not an embedded developer. My coding experience is based on C#, Java, Go Desktop and Server development. Recently at Lobaro I was facing some challenges in embedded C programming and noticed - once again - the huge difference between the holy land of memory management and the C pointer maze where you have to think twice about every malloc call. C code is often not very generic and thus less reusable. There is no such thing as modules, but a bunch of global stuff that tend to break other global stuff. People rarely test C code, and if they do, they do it on the hardware it is written for, making it almost impossible to use any CI infrastructure.

And then there is Go. Go is like C but with solutions to most problems that come with C. First of all a garbage collector and smart pointer management. That's nice, but also the reason why you would never run a Go program on your ARM micro controller, even though Go compiles to native ARM code. To mentions some other problem areas that got solved nicely: Great re-usability supported by a nice Module system, Concurrency just works, very few run time issues due to a strict static type system (not even generics), and for all friends of C it has Structs that can be bound to functions. And then there are the two most important feature for this article: (1) Testing is build in. It's as simple as putting a Test* function into a *_test.go file and execute go test. (2) You can call C code from Go and vice versa using CGO.

Let's have a look how this works. A good starting point is the CGO documentation, the Go Wiki and this Blog Article. I like to focus on the topic I struggled with even after reading the linked pages.

In the following sections I want to take a closer look into:

  • Calling C code from GO
  • Calling Go code from C
  • Compiling C code from files
  • Dealing with function pointers
  • Convert types, access structs and pointer arithmetic

Calling C code from Go

package main
/*
// Everything in comments above the import "C" is C code and will be compiles with the GCC.
// Make sure you have a GCC installed.
int addInC(int a, int b) {
    return a + b;
}
 */
import "C
import "fmt"
func main() {
       a := 3
       b := 5
       
       c := C.addInC(C.int(a), C.int(b))
       fmt.Println("Add in C:", a, "+", b, "=", int(c))
}

From this we can already see a lot:

  • import "C provides a special packet that will tell the Go tool to compile with CGO. Comments directly above this import are compiled to C using the GCC.
  • We can simply define a C function like addInC and call it using the C package.
  • C types like int but also structs you define can be accessed via the C package. This way we can just convert our Go int to C.int and back.
    • The conversion inside Println is not necessary but makes the example more clear.

Calling Go code from C

package main
/*
static inline int multiplyInGo(int a, int b) {
    return go_multiply(a, b);
}
 */
import "C
import (
       "fmt"
)
func main() {
       a := 3
       b := 5
       
       c := C.multiplyInGo(C.int(a), C.int(b))
       fmt.Println("multiplyInGo:", a, "*", b, "=", int(c))
}
//export go_multiply
func go_multiply(a C.int, b C.int) C.int {
       return a * b
}

This code works but need some special attention. First there is the special //export comment above the Go function. This tells CGO to export the function into a separate C file. But that leads to the following restriction: "if your program uses any //export directives, then the C code in the comment may only include declarations (external int f();), not definitions (int f() { return 1; }). You can use //export directives to make Go functions accessible to C code." See also: CGO documentation

This is the reason why I defined multiplyInGo as static inline. This avoids the issue with the duplicate symbols.

Compiling C code from files

The good news is, all C files in your module are just compiled and usable from Go code, you can also use #include "my_c_file.h" to get the declarations. The bad news are: It does not work in your main package and your C code must not have any dependencies to C code outside of the Go module folder. Including globally available libs like #include works fine.

There is one possible workaround, that is to include C files from another folder. The C file content will get inlined at the #include statement and thus compiled. But be aware of unwanted side effects and make sure the same C code is not included twice.

Dealing with function pointers

package main
/*
int go_multiply(int a, int b);
typedef int (*multiply_f)(int a, int b);
multiply_f multiply;
static inline init() {
    multiply = go_multiply;
}
static inline int multiplyWithFp(int a, int b) {
    return multiply(a, b);
}
 */
import "C
import (
       "fmt"
)
func main() {
       a := 3
       b := 5
       C.init();
       c := C.multiplyWithFp(C.int(a), C.int(b))
       fmt.Println("multiplyInGo:", a, "+", b, "=", int(c))
}
//export go_multiply
func go_multiply(a C.int, b C.int) C.int {
       return a * b
}

The code above has an init function to setup the function pointer multiply to the go_multiply implementations. This requires to declare go_multiply before, since CGO compiles the declaration into a separate header file (_cgo_export.h) which we can not include prior generation. The typedef is not striclty necessary but makes the example more clear. In multiplyWithFp we simply call the function stored in the function pointer.

The assignment of the function pointer must happen in C. The following code will not work as replacement for the C.init() call:

C.multiply = C.multiply_f(go_multiply);

It fails with "cannot convert go_multiply (type func(C.int, C.int) C.int) to type C.multiply_f".

Convert types, access structs and pointer arithmetic

We already did some type conversion between int and C.int. The same works for all other types. If you have a struct my_struct_t in C you can just use is with C.my_struct_t in Go. You can access all fields of the C structs and the CGO compiler will even check the types and detect when you access non existing fields.

There are some types that are more difficult to handle. But CGO comes with some helper functions:

// 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
// free, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char
// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// free, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer
// 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

Most of them are pretty clear. I just got a little confused with the unsafe.pointer in C.GoBytes. Here is a little example:

func go_handleData(data *C.uint8_t, length C.uint8_t) []byte {
       return C.GoBytes(unsafe.Pointer(data), C.int(length))
}

As you can see, we can just convert our C pointer to unsafe.pointer. When you got an unsafe pointer, e.g. from C.CBytes and want to convert it into a C pointer you can do that like this:

goByteSlice := []byte {1, 2, 3}
goUnsafePointer := C.CBytes(goByteSlice)
cPointer := (*C.uint8_t)(goUnsafePointer)

Any C void pointer is represented by Go unsafe.Pointer.

Dealing with Pointer arithmetic is very dangerous but also possible:

func getPayload(packet *C.packet_t) []byte {
       dataPtr := unsafe.Pointer(packet.data)
       // Lets assume a 2 byte header before the payload.
       payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2))
       return payload
}

Conclusion

That's it. We have seen how to call C code from Go and vice versa. It's easy to compile a few C files along with your Go code but gets tricky when you have bigger C code bases. Using function pointers in C are a great way to replace native C implementations with Go implementations. This way you can have functions like GetTimestamp in C that are implemented with Go functions where you can fake time during tests.

For C structs you will need to write some conversion code to convert the C structs to Go structs, as soon as you want to use them in other parts of your Go program. I recommend to strictly separate the Go code that wraps C functions and only pass Go types between public Go functions.

All types but function pointers can be converted between C and Go and even function pointers can be stored as unsafe pointers and passed back and force between both worlds.

There are still some topics like #cgo flags that are documented in the linked resources above but not covered in this article.