Skip to content

[RFC] Add native Rust bindings via top-level bindings/rust #2212

Description

@dmah42

Problem Statement

As Rust continues to mature as a systems programming language, an increasing number of polyglot codebases (C++ and Rust) 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 Rust services.

While the Rust ecosystem has profiling tools (e.g., criterion, divan), using a separate framework breaks parity in mixed-language environments and complicates unified CI/CD performance regression tracking. One of our maintainers has also reached out to the maintainers of criterion and has confirmed that it's only supported "best effort".

We want to explore adding official, native Rust bindings to google/benchmark without disrupting the existing C++ developer experience, build architecture, or performance characteristics.

Proposed Architecture

To minimize maintenance overhead and guarantee that the bindings do not fall out of sync with core C++ API changes, we propose hosting the Rust bindings co-located within the main repository under a new bindings/rust/ directory.

Repository Layout

The proposed directory structure isolates the Rust toolchain dependencies entirely to its own subdirectory:

google/benchmark/
├── CMakeLists.txt
├── src/
├── include/
└── bindings/
    ├── python/          # Existing python bindings
    └── rust/            # Proposed Rust bindings
        ├── Cargo.toml   # Rust package manifest
        ├── build.rs     # Cargo build script (drives CMake)
        └── src/
            ├── ffi.rs   # Declarative CXX bridge boundary
            └── lib.rs   # Safe, idiomatic Rust public API

Build Integration (Bi-directional Isolation)

A key requirement is that C++ maintainers should not need a Rust toolchain installed to work on the library, and Rust users should experience a standard cargo build flow.

  • For Rust Users: The bindings/rust/build.rs script will programmatically invoke CMake in the background to build the C++ static library artifact, then link it to the Rust crate automatically.
  • For C++ Users: The top-level CMakeLists.txt will ignore the bindings/rust directory by default. We can introduce an optional flag, -DBENCHMARK_ENABLE_RUST_BINDINGS=ON, which will look for cargo and run the Rust test suite during CI.

The Interop Layer (cxx)

Rather than maintaining a raw, unsafe C-string and pointer-heavy FFI layer, we propose using the cxx crate. cxx enforces type safety and static guardrails at the C++/Rust boundary, generating the required header plumbing automatically during compilation.

A preliminary layout of the bridge module (ffi.rs) would target the runtime loop and initialization:

#[cxx::bridge(namespace = "benchmark")]
mod ffi {
    unsafe extern "C++" {
        include!("benchmark/benchmark.h");

        type State;
        fn KeepRunning(self: Pin<&mut State>) -> bool;
        fn SkipWithError(self: Pin<&mut State>, msg: &str);
        
        fn Initialize(argc: &mut i32, argv: *mut *mut c_char);
        fn RunSpecifiedBenchmarks() -> usize;
    }
}

Public Rust API & Static Initialization Challenge

Because Rust intentionally lacks life-before-main (__attribute__((constructor))) macro mechanisms for safety reasons, we cannot easily replicate the C++ BENCHMARK(BM_StringCopy); macro style.

Instead, clients of the public Rust bindings will utilize explicit runtime registration inside their own fn main(), mapping a Rust closure to the underlying C++ runner:

// Example of how an end-user might write a benchmark binary
fn main() {
    // Initialize arguments passed from the command line
    benchmark::initialize(); 

    // Explicitly register the benchmark with the C++ engine
    benchmark::register_benchmark("BM_RustVectorPush", |state| {
        while state.keep_running() {
            let mut vec = Vec::new();
            vec.push(42);
            benchmark::do_not_optimize(vec);
        }
    });

    // Hand over control to the C++ test runner
    benchmark::run_specified_benchmarks();
}

Critical Trade-offs & Open Questions

Before committing to an implementation, we would love community feedback on the following areas:

  1. FFI Overhead: Crossing the FFI boundary via state.KeepRunning() on every single iteration loop adds a small execution cost. For ultra-fast microbenchmarks (sub-nanosecond), could this skew timing accuracy compared to pure C++ loops? Could we hide this by implementing state::keep_running() using KeepRunningBatch under the hood?
  2. Community Maintenance: Is there sufficient long-term interest from contributors with both C++ and Rust expertise to help review and maintain the bindings/rust/ directory as the core engine evolves?

Next Steps

If the community are supportive of this direction, we will start by submitting a minimal Proof of Concept (PoC) PR demonstrating a working build.rs loop that successfully binds and runs a simple benchmark.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions