Using C code from Go

19-08-2024



Background

In the previous article, testing C with Go , I looked at how easy it is to include simple C code in your Go application. I also demonstrated how you can use Go to test C code and how easy it is with Go's excellent testing support.

In this post I'll be digging a little bit deeper into the C and Go interoperability features by making use of some more complex C code and showing how C and Go can coexist in a project.

If you need to set up Go on your computer, please see the previous article for resources.


Easy embed C code in Go

Did you know you can embed C code directly inside your Go sources? Init a new Go project with go mod init in a new directory. I called my module cwithgo, use whatever works for you. Create a source file called main.go and add the following content:


    package main

    // #include <stdio.h>
    // void responder(void) {
    //    printf("This is C. Read you five by five.\n");
    //}
    import "C"
    import "fmt"

    // ======================================================================= main
    func main() {
	    fmt.Println("Attempting to call C.")
	    C.responder()
    }            
            

You can run it with the standard:


    go run .
            

On my system, I see:


    Attempting to call C.
    This is C. Read you five by five.
            

Pretty handy when you really just want to have a short bit of C. Of course, for larger projects, or existing code, you really want to use external source files, especially since you get zero help from most editors inside comments. I mainly use this technique when trying out some small samples or when I really just need to call a particular function in a library.

Speaking of editors, have you checked out Zed yet? It's definitely worth a visit.


Go, C and Strings

The next challenge I've run into is having Go and C work together with strings. I often need to either pass a string value to C or receive one from a C function. Once again, Go makes this less of a headache than one would expect. To enable interoperability between C and Go, Go includes utility functions that cast Go types to their C versions. For example, CString can convert a Go-style string to a C-style string.

Update the main.go file a bit:


    package main

    // #include <ctype.h>
    // #include <stdio.h>
    //
    // void responder(void) {
    //    printf("This is C. Read you five by five.\n");
    // }
    //
    // void sayHelloTo(char* person) {
    //    printf("Oh, howdy %s!\n", person);
    // }
    //
    // char* toUpper(char* s) {
    //  for (char *ptr=s; *ptr; ptr++) *ptr=toupper(*ptr);
    //  return s;
    // }
    import "C"
    import "fmt"

    // ======================================================================= main
    func main() {
	    fmt.Println("Attempting to call C.")
	    C.responder()
	    C.sayHelloTo(C.CString("Roger"))
	    fmt.Println("abcd becomes:", C.GoString(C.toUpper(C.CString("abcd"))))
    }
            

Run it with:


    go run . 
            

On my system I see:

            
    Attempting to call C.
    This is C. Read you five by five.
    Oh, howdy Roger!
    abcd becomes: ABCD
            

We can send strings to C and easily receive them as well. The only trick is to use the correct cast, as provided by Go, to cross the divide safely. The GoString function is used to convert a C-style string to a Go-style string. We can also include and access the standard C libraries, which is pretty handy.


Passing buffers to C

A lot of C code I need to integrate with Go requires some kind of a character buffer as a parameter. As they say, with great power comes great crashes. To really integrate with C, we need to resort to the realm of unsafe operations. Basically, we want to forego the convenience and safety of having Go manage program memory on our behalf and dip into the realm of handling memory management ourselves a bit. To do this, we need to do the following in Go:

            
    package main

    /*
    #cgo CFLAGS: -g
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>

    void responder(void) {
        printf("This is C. Read you five by five.\n");
    }

    void sayHelloTo(char* person) {
        printf("Oh, howdy %s!\n", person);
    }

    char* toUpper(char* s) {
        for (char *ptr=s; *ptr; ptr++) *ptr=toupper(*ptr);
        return s;
    }

    int populate(int quantity, const char *item, char *out) {
        return sprintf(out, "We have %d %s available.", quantity, item);
    }
    */
    import "C"
    import (
	    "fmt"
	    "unsafe"
    )

    // ======================================================================= main
    func main() {
	    fmt.Println("Attempting to call C.")
	    C.responder()
	    C.sayHelloTo(C.CString("Roger"))
	    fmt.Println("abcd becomes:", C.GoString(C.toUpper(C.CString("abcd"))))

	    // ---------- unsafe operations follow ----------

	    // create parameters
	    productName := C.CString("Groovy Cheese Straws")
	    defer C.free(unsafe.Pointer(productName))

	    stockCount := C.int(11)

	    // create buffer
	    bufferPtr := C.malloc(C.sizeof_char * 1024)
	    defer C.free(unsafe.Pointer(bufferPtr))

	    // make the call, passing the parameters and the buffer
	    size := C.populate(stockCount, productName, (*C.char)(bufferPtr))

	    // copy the buffer into a Go memory-managed slice
	    goSlice := C.GoBytes(bufferPtr, size)

	    // print the slice we created from the C buffer
	    fmt.Println(string(goSlice))
    }    
            

The first thing you'll notice is that I'm using a different style of Go comment, a block comment. This is my preferred way as it displays neater and cleaner, but you should stick to what works for you. Also, you'll notice that you can give compiler flags using the #cgo CFLAGS: -g statement. This directs CGO to use the specified flag(s) when compiling your C code.

Take note of the unsafe import. This allows us to do C-style pointer operations, like getting a pointer to a string or a buffer that can be passed to the C code. Because we are managing the memory when in unsafe mode, it is important to remember to set up the memory free calls. Defer is a great way of getting that done whenever you allocate the memory, so you don't forget!

When I run the code on my machine I see:


    Attempting to call C.
    This is C. Read you five by five.
    Oh, howdy Roger!
    abcd becomes: ABCD
    We have 11 Groovy Cheese Straws available.
            

Conclusion

Go has very good support for interoperability with C, as you've seen. It does require a bit of unsafe code, but then again, C code is not memory managed anyway! With some caution, and testing, this can be achieved safely and be a great tool when you need to make use of existing C code.


Resources

To learn more about Go or C, you can use the following resources as a starting point:



Return to the blog index