Zig & Go Interoperation


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.
Subscribe to my newsletter
Read articles from Jaime Lopez directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
