jedi.sh#> ZGo (part 1), Calling Zig from Go

2 min read Original article ↗
  • 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. Note we use the C ABI compatible type c_int.
  • 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!

    linux@solar ~/P/zgo> ./bridge
       Invoking zig library!
       Done  12

    Thoughts:

    *************

    Zig makes a good companion 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.


    (Part 2) Calling Go from Zig