Languages: English | 简体中文 | 日本語
A lightweight, high-performance code generator for Go-to-Zig FFI, inspired by purego and rust2go.
- Zig-native API declarations - Use standard Zig syntax as API description, no separate IDL needed
- Automatic type bridging - Seamlessly convert
string,[]byte, nested structs, enums, slices, and arrays - No cgo required - Direct assembly-based calling with up to 8x performance improvement over cgo
- Builder pattern - Generate Go wrappers and compile Zig dynamic libraries in one step
- Dual API style - Both
Clientmethods and top-level functions for flexible usage
go2zig now includes an experimental streaming bridge based on GoReader and GoWriter.
- Go-side helpers currently support
io.Reader,io.Writer,io.ReadCloser,io.WriteCloser, andio.Pipe - Go-side helpers also support
*os.File - Zig side consumes stream handles synchronously through
rt.streamRead(...)andrt.streamWrite(...) - The current implementation is a block-based, synchronous stream bridge; it is not yet an async or full-duplex protocol layer
- Streaming must be enabled explicitly with
WithStreamExperimental(true)or-stream-experimental
Borrowing the idea of support tiers from purego, the current main verified set is:
- Tier 1 -
windows/amd64,windows/arm64,linux/amd64,linux/arm64,darwin/arm64
These targets currently run tests, benchmarks, and build checks in main CI.
- ✅ Windows/amd64 - Full support with CI testing
- ✅ Windows/arm64 - Supported by the no-cgo asm runtime
- ✅ Linux/amd64 - Full support with CI testing
- ✅ Linux/arm64 - Supported by the no-cgo asm runtime
- ✅ Darwin/arm64 - Dynamic loading and generated wrappers supported
- ❌ Darwin/amd64 - Not supported
- ❌ Other architectures - Not currently supported
- Go 1.26+
- Zig 0.16.0
- Platform: Windows/Linux (
amd64andarm64) or Darwin (arm64only)
boolu8,u16,u32,u64,usizei8,i16,i32,i64,isizef32,f64
- Structs:
extern structwith nested fields - Enums:
enum(integer_type)with explicit values - Arrays: Fixed-length
[N]Typeand named aliases likepub const Digest = [4]u8 - Slices: Named aliases like
ScoreList = extern struct { ptr: ?[*]const u16, len: usize }, including aliases whose elements are structs - Optionals:
?POD(e.g.,?u32,?UserKind,?Digest)
- String: Maps to Go
string(Zig allocates, Go frees) - Bytes: Maps to Go
[]byte(Zig allocates, Go frees)
- Error unions: Named or inline
error{...}!ReturnTypemapped to Go(T, error)orerror
map[K]Vchan Tinterface{}- Function types (
func(...)) - Pointers (except in String/Bytes)
unsafe.Pointer
unioncomptime@import
- Optional support is currently centered on POD-style shapes
- Slice elements cannot be
StringorBytes - Experimental stream types (
GoReader/GoWriter) can only appear as top-level function parameters
// api.zig
pub const String = extern struct {
ptr: [*]const u8,
len: usize,
};
pub const Bytes = extern struct {
ptr: [*]const u8,
len: usize,
};
pub const UserKind = enum(u8) {
guest,
member,
admin,
};
pub const User = extern struct {
id: u64,
kind: UserKind,
name: String,
email: String,
};
pub extern fn health() bool;
pub extern fn login(user: User, password: String) String;# Generate only
go run ./cmd/go2zig -api ./api.zig -out ./gen.go -pkg main -lib mylib -no-build
# Generate and build dynamic library
go run ./cmd/go2zig -api ./api.zig -zig ./lib.zig -out ./gen.go -pkg main -lib mylib
# Generate without top-level forwarding functions
go run ./cmd/go2zig -api ./api.zig -zig ./lib.zig -out ./gen.go -pkg main -lib mylib -no-top-levelpackage main
import "fmt"
func main() {
// Load the dynamic library
if err := Default.Load(); err != nil {
panic(err)
}
// Call functions directly
if !Health() {
panic("Health check failed")
}
// Or use the client
client := NewGo2ZigClient("")
if err := client.Load(); err != nil {
panic(err)
}
greeting := client.Login(User{
ID: 1,
Kind: UserKindMember,
Name: "alice",
Email: "alice@example.com",
}, "password123")
fmt.Println(greeting)
}Based on benchmarks on Windows/amd64:
| Method | Performance | Relative |
|---|---|---|
| asmcall (go2zig) | 3.35 ns/op | 1x |
| cgo | 28.56 ns/op | ~8.5x slower |
The no-cgo approach provides approximately 8x performance improvement for short, synchronous FFI calls.
- Allocation: Zig side allocates memory for strings, bytes, and slices
- Deallocation: Go side frees memory through
go2zig_free_buf - Pattern: Copy-in for inputs, copy-out for outputs
- Overhead: Data copying required for each call
When you run the generator, it produces:
gen.go- Go wrapper with types and functionsgo2zig_runtime.zig- Zig runtime helpersgo2zig_exports.zig- Zig export bridge functionsmylib.dll/libmylib.so/libmylib.dylib- Dynamic library
When Build() also compiles Zig, it additionally writes go2zig_build_root.zig before invoking zig build-lib.
For programmatic usage in Go:
import "go2zig"
err := go2zig.NewBuilder().
WithAPI("./api.zig").
WithZigSource("./lib.zig").
WithOutput("./gen.go").
WithPackageName("main").
WithLibraryName("mylib").
Build()If your project already provides its own higher-level wrapper layer and you want to avoid generated package-level forwarding functions like Login(...), disable them explicitly:
err := go2zig.NewBuilder().
WithAPI("./api.zig").
WithZigSource("./lib.zig").
WithOutput("./gen.go").
WithPackageName("main").
WithLibraryName("mylib").
WithTopLevelFunctions(false).
Build()This keeps Go2ZigClient methods such as client.Login(...), but skips top-level forwarding functions and helps avoid symbol collisions with hand-written wrappers.
The current Builder API also includes WithHeaderOutput, WithRuntimeZig, WithBridgeZig, WithDynamicBuild, WithStreamExperimental, WithAPIModuleName, and WithImplModule.
See examples/basic/ for a complete working example demonstrating:
- Primitive types
- Structs and nested structs
- Enums with explicit values
- Arrays and array aliases
- Slices and slice aliases
- Optionals
- Error unions
- String and Bytes handling
- Docs Home
- Architecture Overview
- Usage Guide
- Generator Guide
- Runtime Design
- Testing & Benchmarks
- CI Guide
- Platform: Only Windows/Linux on
amd64andarm64, plus Darwin onarm64 - Types: No support for Go maps, channels, interfaces
- Memory: Fixed allocation pattern (Zig allocates, Go frees)
- Performance: Data copying required for each call
- Runtime loading: If you skip explicit
Load()and the first lazy load fails, generated call paths currently panic
- Support for
?Stringand?Bytesoptionals - Better error diagnostics
uniontype support- Custom allocator interface
- Performance optimizations
- Generic type support
- Toolchain integration
- Cross-platform improvements
- Run tests:
go test ./... - Run benchmarks:
go test -bench . ./asmcall - Test on both Windows and Linux
- Update documentation for new features
This project is licensed under the Mozilla Public License 2.0.