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:
- 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?
- 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.
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/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 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/benchmarkwithout 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:
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 buildflow.bindings/rust/build.rsscript will programmatically invokeCMakein the background to build the C++ static library artifact, then link it to the Rust crate automatically.CMakeLists.txtwill ignore thebindings/rustdirectory by default. We can introduce an optional flag,-DBENCHMARK_ENABLE_RUST_BINDINGS=ON, which will look forcargoand 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
cxxcrate.cxxenforces 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: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:Critical Trade-offs & Open Questions
Before committing to an implementation, we would love community feedback on the following areas:
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 implementingstate::keep_running()usingKeepRunningBatchunder the hood?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.rsloop that successfully binds and runs a simple benchmark.