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.zig
Build Integration
- For Zig Users:
build.zig invokes 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=ON looks for zig and 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*) — State and Benchmark are passed as opaque void* through the C boundary. Zig wraps them in typed structs. This avoids fragile layout coupling to C++ internals.
- Comptime trampolines — Each
registerBenchmark call 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, AddCustomContext
State: KeepRunning, KeepRunningBatch, PauseTiming, ResumeTiming, SkipWithError, SetBytesProcessed, SetItemsProcessed, SetLabel, SetComplexityN, range, iterations, threads, threadIndex
Benchmark builder: 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 additional State/Benchmark internals 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).
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/benchmarkacross 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/benchmarkwithout 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
Build Integration
build.ziginvokes CMake to build libbenchmark + the C adapter together as a combined static archive, avoiding C++ ABI mismatches (libstdc++ vs libc++).-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:The comptime trampoline generates a unique static function per benchmark at compile time — zero heap allocation, zero dynamic dispatch.
Public Zig API
Key Design Decisions
void*) —StateandBenchmarkare passed as opaquevoid*through the C boundary. Zig wraps them in typed structs. This avoids fragile layout coupling to C++ internals.registerBenchmarkcall generates a unique C-compatible callback at compile time, eliminating heap allocation and runtime dispatch.[*: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,ComplexityTimeUnit,BigONot covered (yet):
ComputeStatistics,Fixture,ScopedPauseTiming, custom reporters.Testing
zig build test) and CMake (ctest -R zig_bindings_tests)Open Questions
void*+ comptime trampolines) sufficient, or do we need to expose additionalState/Benchmarkinternals for advanced use cases?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).