google / google/benchmark

[FR] Add native Zig bindings via top-level `bindings/zig`

Open
#2,266 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.