ZGo: Calling Zig from Go

submited by
Style Pass
2022-05-12 17:00:10

For those unfamiliar with Zig, it aims to be a modern C/C++ alternative with a focus on simplicity, and performance. There's no macros or hidden flow control, but there is comptime metaprogramming. This is a balanced approach that helps reduce hidden behaviors while keeping comptime features in the hands of the programmer. Interop with C has always been a key feature of Zig. And with progress on the Zig stage2 compiler doors are opened to more easy integrations. (For more information on how the internals of the compiler works, I recommend reading Mitchell Hashimoto's series of posts). It turns out to be quite simple to interface with Zig from Go. By utilizing Cgo and the new features of the zig compiler we can easily make calls into Zig.

bridge.go package main import "fmt" // #cgo CFLAGS: -I. // #cgo LDFLAGS: -L. -lzgo // #include ❬zgo.h❭ import "C" func main() { fmt.Printf("Invoking zig library!\n") fmt.Println("Done ", C.x(10)) }
Key is the Cgo decorations for CFLAGS including the current directory. And LDFLAGS to search for libraries in the current directory. We also include our autogenerated header file zgo.h which is emitted by the zig stage2 compiler. zgo.zig const std = @import("std"); pub export fn x(y: c_int) c_int { return y+2; }A simple example function, we export the symbol in the library. build.zig const std = @import("std"); const Builder = std.build.Builder; const builtin = std.builtin; pub fn build(b: *Builder) void { const mode = b.standardReleaseOptions(); const lib = b.addStaticLibrary("zgo", "zgo.zig"); lib.bundle_compiler_rt = true; lib.use_stage1 = false; lib.emit_h = true; lib.emit_bin = .{ .emit_to = "libzgo.a"}; lib.setBuildMode(mode); lib.install(); const go = build_go(b); const make_step = b.step("go", "Make go executable"); make_step.dependOn(&go.step); } fn build_go(b: *std.build.Builder) *std.build.RunStep { const go = b.addSystemCommand( &[_][]const u8{ "go", "build", "-ldflags", "-linkmode external -extldflags -static", "bridge.go", }, ); return go; }The builder breaks the process down into build step objects that can depend on eachother and contain build semantics. Generally compiler flags are exposed as fields or methods. Comparing this to the Makefile below will give you an idea of what this does. We override the default output location with .emit_bin for simplicity. Running zig build && zig build go will compile the executable. (alternatively) Makefile static: zig build-lib -fno-stage1 -femit-h zgo.zig go build -ldflags "-linkmode external -extldflags -static" bridge.go dynamic: zig build-lib -dynamic -fno-stage1 -femit-h zgo.zig go build -ldflags "-linkmode external -extldflags -dynamic" bridge.go Calling Zig![email protected] ~/P/zgo> ./bridge Invoking zig library! Done 12 Thoughts: ************* Zig makes a good compainion to Go in a number of regards. The integrated build system has been one of my favorite parts of the Zig experience. I rarely opt for writing Makefiles when I can write a build.zig. The portability of the Zig compiler and cross-target capabilities make for an ideal part of any build pipeline. Zig is a useful tool to interop with existing libraries, squeezing a bit more performance out of a hot path, or maintaining builds of exisiting projects.

Leave a Comment