Skip to content

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

Description

@gvtret

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

  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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions