Zig & Go Interoperation

Jaime LopezJaime Lopez
3 min read

Zig & Go can be a convenient combination of programming languages. Zig provides fine-grained control over memory and performance, making it ideal to solve low-level tasks (similar to C), while Go excels in simplicity, easy of development, and built-in concurrency. Zig can be used to write performance-critical components and let Go to manage the main application logic.

One way to enable this combination is by writing Zig functions that can be called as C functions, and interfacing those functions to Go using cgo. In other words, one strategy is using Zig as a C replacement and taking advantage of the features that Go has available for using C generated code.

const std = @import("std");

fn strlen(s: [*:0]const u8) u64 {
    return std.mem.len(s);
}

test "String Length" {
    const s = "Hello, world!";
    try std.testing.expect(strlen(s.ptr) == 13);
}

In the above Zig code (example.zig), a strlen function is defined. It receives as argument a C string, i.e. a null terminated string, and returns its length. To show that this library can be used as a C replacement, below is a C program (client.c) that calls our custom version of strlen.

#include <assert.h>
#include <stddef.h>

extern size_t strlen(const char *s);

int main() {
    const char *s = "Hello, world!";
    assert(strlen(s) == 13);
    return 0;
}

To compile and test these examples, you can use the below commands. A static library is created which is used when linking the C program to resolve the location of the strlen function. In other words, we are using the strlen function written in Zig, instead of the one provided by the C standard library. Because the assertion in client.c is true, if everything is correct, no output should be produced when the binary client is run.

$ zig test example.zig
All 1 tests passed.
$ zig build-lib example.zig
$ gcc -o client client.c -L. -lexample
$ ./client

At this point, we have demonstrated that Zig can be used as a replacement for C. Now, we are able to use this example’s Zig code in a Go program. The below example (demo.go) is calling our custom strlen function in an assertion. According to the requirements of the cgo interface, we are declaring the library flags and the external function signature as pseudo-comments. Besides that, we are also taking care to allocate and de-allocate a proper c-string, to pass it as the argument for the strlen function.

package main

// #cgo LDFLAGS : -L. -lexample
// #include <stdlib.h>
// extern size_t strlen(const char *s);
import "C"
import "unsafe"

func main() {
    s := "Hello, world!"
    c_s := C.CString(s)
    defer C.free(unsafe.Pointer(c_s))
    if C.strlen(c_s) != 13 {
        panic("Incorrect size")
    }
}

You can test the previous example, using the below commands. Similar to the C example, given that the assertion is true, if everything is correct, no output should be produced when the binary demo is run.

$ go build demo.go
$ ./demo

In this presentation, I have avoided to discuss the nuances of defining proper type signatures for your functions in order to pass arguments and return values between Zig and Go. For primitive data types, Go provides direct translations to C and Zig types. Beyond that, the usage of string as the type for the argument of strlen, shown in this article, can give you a clue that in the most of the cases, for structured data types, what your are passing are just pointers.

One final word about Zig & Go, they both have out of the box cross-platform compatibility and scalability across different levels of computing. Instead of locking your projects to a restricting tooling, you can build relatively open components preserving at the same time performance and simplicity.

0
Subscribe to my newsletter

Read articles from Jaime Lopez directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jaime Lopez
Jaime Lopez