Testing C code using Go

16-08-2024



Background

Last week I looked at testing C with Zig and that got me thinking about why I haven't tried that with Go yet. As much as I enjoy working in Zig, and to some extent C, my goto language for building APIs is still Go, formerly known as Golang.

It's robust, battle-tested and provides me with a fantastic Java-like ecosystem with a lot less overheads. From startup to enterprise, Go remains a reliable and productive tool for me. So when I had a bit of time, I decided to dig into testing some C code with Go and it really wasn't a mission at all!


Calling C code from Go

To follow along, you need to install Go on your system. See the installation guide for tips and tricks. I'll be using Go 1.22.6, which is the latest stable version at the time of writing. Go calls into C code using CGO which requires a C compiler to be available on your system. I'm assuming you have this installed already, if you are interested in using C and Go together.

Let's make sure things are working by calling a super simple little C function using Go. 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 withgo.c and add the following content:


    int summer(int left, int right) {
      return left + right;
    }
            

The corresponding header file named withgo.h contains:

    
    #ifndef __WITH_GO__
    #define __WITH_GO__

    int summer(int left, int right);

    #endif            
            

Now that we have a source file, let's use Go to call it. This is my main.go file:

            
    package main

    /*
      #include "withgo.h"
    */
    import "C"
    import "fmt"

    func main() {
      left := 5
      right := 10
      result := C.summer(C.int(left), C.int(right))
      fmt.Printf("Sum of %d and %d is %d.\n", left, right, result)
    }
            

You can run it with the standard:


    go run .
            

On my system, I see:


    Sum of 5 and 10 is 15.
            

That doesn't look so traumatic, does it? In fact, that was pretty easy! The most important part is the import "C" bit, and the comment preceding it. This tells Go to include the C source file in the binary. From there on we can call into the C import, casting the Go int to a C int along the way. Out of interest, if you look at the type of result, you'll see it's a _Ctype_int. This goes to show that interop with C is a well-supported feature in Go, so let's dig a little deeper and attempt some tests.


Test C code with Go

Now that we've seen that Go can call into C, let's see if we can get some tests going as well. Go has excellent support for tests, so everything we need is already installed and ready to use.

Create a new file main_test.go with the following content:


    package main

    /*
      #include "withgo.c"
    */
    import "C"
    import "testing"

    func Test_BasicSum(t *testing.T) {
	  const val1 = C.int(20)
	  const val2 = C.int(30)
	  const expected = C.int(50)
	  result := C.summer(val1, val2)

	  if expected != result {
		t.Fatalf("Expected %d, got %d.\n", expected, result)
	  }
    }
            

Run it with:


    go test . 
            

Only to get...:

            
    use of cgo in test main_test.go not supported
            

Oops! We can't call C code directly in tests it looks like. Doh! Maybe we can wrap the code in a Go function? Let's update main.go a bit by adding a wrapper function around our C code call. For convenience, it will take and return standard Go types, handling the conversion and casting for us:

            
    package main

    /*
      #include "withgo.h"
    */
    import "C"
    import "fmt"

    // ======================================================================= main
    func main() {
	  left := 5
	  right := 10
	  fmt.Printf("Sum of %d and %d is %d.\n", left, right, goSum(left, right))
    }

    // ---------------------------------------------------------------------- goSum
    func goSum(one int, two int) int {
	  result := C.summer(C.int(one), C.int(two))
	  return int(result)
    }        
        

Next we adjust main_test.go to use this new function. We also don't need to import C or any C files anymore, as that's done in our main package for us. We already know we can't do it in tests anyway:


    package main

    import "testing"

    func Test_BasicSum(t *testing.T) {
	  const val1 = 20
	  const val2 = 30
	  const expected = 50
	  result := goSum(val1, val2)

	  if expected != result {
		t.Fatalf("Expected %d, got %d.\n", expected, result)
	  }
    }    
            

Let's try go test . again:


    PASS
    ok  	cwithgo	0.493s            
            

Conclusion

We have barely scratched the surface of Go and C interoperability, but it should be enough to get you started on your way to digging deeper into this powerful tool. Go is able to use C code and libraries, so there is a whole range of applications made possible by this feature.

With so much existing C code, there's definitely a use-case for calling into C from Go. On many of the legacy projects I work on, rewriting existing C libraries does not make sense, but it's also very challenging to keep the projects in C, as those skills are becoming scarce. Using Go as a wrapper around these libraries have proven to be a great step forward for the companies involved. Even if you are not using Go as a language at your company, it could be useful to consider building a test framework for your C code in it.


Resources

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



Return to the blog index