[FR] Add native Zig bindings via top-level `bindings/zig`
Problem Statement
As Zig gains traction in systems programming, an increasing number of polyglot codebases (C++ and Zig) require uniform performance tracking. Currently, organizations using google/benchmark across their C++ infrastructure lack a native way to harness the same execution engine, CLI flags (--benchmark_filter), and structured output formats (JSON/CSV) for their Zig services.
While Zig has excellent C interop, creating bindings against a pure C++ library requires a thin adapter layer. We want to explore adding official, native Zig bindings to google/benchmark without disrupting the existing C++ developer experience, build architecture, or performance characteristics.
Proposed Architecture
The Zig bindings are hosted co-located within the main repository under bindings/zig/, following the same pattern as existing Python and Rust bindings.
Repository Layout
google/benchmark/
├── CMakeLists.txt
├── src/
├── include/
└── bindings/
├── python/
├── rust/
└── zig/
├── CMakeLists.txt
├── build.zig
├── build.zig.zon
└── src/
├── zig_api.h # C adapter header
├── zig_api.cc # C adapter implementation
├── benchmark.zig # Idiomatic Zig public API
└── benchmark_test.zigBuild Integration
- For Zig Users:
build.ziginvokes CMake to build libbenchmark + the C adapter together as a combined static archive, avoiding C++ ABI mismatches (libstdc++ vs libc++). - For C++ Users: An optional flag
-DBENCHMARK_ENABLE_ZIG_BINDINGS=ONlooks forzigand runs the Zig test suite during CI.
The Interop Layer
Since Zig has zero-cost C interop (no FFI bridge crate needed), we use a thin extern "C" adapter layer (zig_api.h/cc) that wraps C++ methods. Zig calls these via @cImport:
const c = @cImport(@cInclude("zig_api.h"));
// Benchmark registration with comptime trampoline
pub fn registerBenchmark(name: [*:0]const u8, comptime func: fn (*State) void) Benchmark {
const S = struct {
fn trampoline(state_ptr: ?*anyopaque) callconv(.c) void {
if (state_ptr) |ptr| {
var state = State{ .ptr = ptr };
func(&state);
}
}
};
return Benchmark{ .ptr = c.benchmark_zig_register_benchmark(name, &S.trampoline) };
}The comptime trampoline generates a unique static function per benchmark at compile time — zero heap allocation, zero dynamic dispatch.
Public Zig API
fn my_benchmark(state: *benchmark.State) void {
while (state.keepRunning()) {
// your code to benchmark
}
}
pub fn main() void {
const args = std.process.argsAlloc(std.heap.page_allocator) catch return;
defer std.process.argsFree(std.heap.page_allocator, args);
benchmark.initialize(args);
_ = benchmark.registerBenchmark("BM_MyBenchmark", my_benchmark)
.range(8, 1 << 20)
.threads(4)
.unit(.microsecond);
_ = benchmark.run();
}Key Design Decisions
- Opaque pointers (
void*) —StateandBenchmarkare passed as opaquevoid*through the C boundary. Zig wraps them in typed structs. This avoids fragile layout coupling to C++ internals. - Comptime trampolines — Each
registerBenchmarkcall generates a unique C-compatible callback at compile time, eliminating heap allocation and runtime dispatch. - Combined static archive — The C adapter is compiled with the same g++ as libbenchmark via CMake, ensuring a single consistent C++ ABI (no libstdc++/libc++ conflicts).
- String convention — Zig uses
[*:0]const u8(sentinel-terminated) at the boundary, enforcing null-termination at compile time.
Scope
Covered:
Initialize,RunSpecifiedBenchmarks,RegisterBenchmark,ClearRegisteredBenchmarks,AddCustomContextState:KeepRunning,KeepRunningBatch,PauseTiming,ResumeTiming,SkipWithError,SetBytesProcessed,SetItemsProcessed,SetLabel,SetComplexityN,range,iterations,threads,threadIndexBenchmarkbuilder:Arg,Range,DenseRange,Args,Unit,Threads,ThreadRange,MinTime,Iterations,Repetitions,UseRealTime,UseManualTime,Complexity- Enums:
TimeUnit,BigO
Not covered (yet): ComputeStatistics, Fixture, ScopedPauseTiming, custom reporters.
Testing
- 14 unit tests covering all bound APIs
- 7 usage examples (basic, throughput, parameterized, threaded, pause/resume, skip, etc.)
- CI integration via GitHub Actions (
zig build test) and CMake (ctest -R zig_bindings_tests)
Open Questions
- Thread safety: Is the current approach (opaque
void*+ comptime trampolines) sufficient, or do we need to expose additionalState/Benchmarkinternals for advanced use cases? - Fixture support: Would Zig users benefit from a
Fixture-like pattern (equivalent to C++BENCHMARK_F)?
AI Usage
Code was generated with AI assistance and reviewed by the contributor (as per AGENTS.md).
Source: google/benchmark