[FR] Add native Zig bindings via top-level `bindings/zig`
- Dominant language
- C++
- Stars
- 10.4k
- Forks
- 1.8k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 8
Description
## 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
```text
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`:
```zig
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
```zig
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
1. **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.
2. **Comptime trampolines** — Each `registerBenchmark` call generates a unique C-compatible callback at compile time, eliminating heap allocation and runtime dispatch.
3. **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).
4. **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
1. **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?
2. **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](https://github.com/google/benchmark/blob/main/AGENTS.md)).
Contributor guide
Research direction
Start by comparing the existing Python and Rust binding layouts, then read the top-level CMakeLists.txt and the proposed bindings/zig files. Verify the C adapter, Zig API, build integration, and listed CI commands against the scoped APIs. Done means the 14 unit tests, seven examples, zig build test, and ctest -R zig_bindings_tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cmake, cpp, github-actions, zig
- Domain
- build-system, testing, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100