Introduce Rust bindings (#2221)

* add Rust bindings

* clang-format

* pre-commit fix

* windows static builds

* Rust prefers MD on windows

* check in lock file to avoid source poisoning

* use a rust crate name that isn't taken

* add CMake target for running Rust binding tests and integrate into CI

* complete crate rename

* enable test dependency downloads in Rust bindings CI
This commit is contained in:
dominic
2026-06-15 14:29:03 +01:00
committed by GitHub
parent 1b3abc896c
commit eb15fc80dd
13 changed files with 542 additions and 3 deletions
+26 -2
View File
@@ -14,7 +14,7 @@ permissions:
jobs:
python_bindings:
name: Test GBM Python ${{ matrix.python-version }} bindings on ${{ matrix.os }}
name: Test Python ${{ matrix.python-version }} bindings on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
@@ -30,7 +30,31 @@ jobs:
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
- name: Install GBM Python bindings on ${{ matrix.os }}
- name: Install Python bindings on ${{ matrix.os }}
run: python -m pip install .
- name: Run example on ${{ matrix.os }} under Python ${{ matrix.python-version }}
run: python bindings/python/google_benchmark/example.py
rust_bindings:
name: Test Rust bindings on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install Ninja (macOS)
if: runner.os == 'macOS'
run: brew install ninja
- name: Run Rust tests natively via Cargo
run: cargo test
working-directory: bindings/rust
- name: Run Rust tests via CMake target
run: |
cmake -S . -B build -DBENCHMARK_ENABLE_RUST_BINDINGS=ON -DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON
cmake --build build --target test_rust_bindings
+3
View File
@@ -67,3 +67,6 @@ CMakeSettings.json
dist/
*.egg-info*
uv.lock
# Rust build stuff
/bindings/rust/target/
+1
View File
@@ -41,6 +41,7 @@ option(BENCHMARK_ENABLE_GTEST_TESTS "Enable building the unit tests which depend
option(BENCHMARK_USE_BUNDLED_GTEST "Use bundled GoogleTest. If disabled, the find_package(GTest) will be used." ON)
option(BENCHMARK_ENABLE_LIBPFM "Enable performance counters provided by libpfm" OFF)
option(BENCHMARK_ENABLE_RUST_BINDINGS "Enable testing of the Rust bindings" OFF)
# Export only public symbols
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
+301
View File
@@ -0,0 +1,301 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "cc"
version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "codespan-reporting"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
"unicode-width",
]
[[package]]
name = "cxx"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e"
dependencies = [
"cc",
"cxx-build",
"cxxbridge-cmd",
"cxxbridge-flags",
"cxxbridge-macro",
"foldhash",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e"
dependencies = [
"cc",
"codespan-reporting",
"indexmap",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-cmd"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328"
dependencies = [
"clap",
"codespan-reporting",
"indexmap",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a"
[[package]]
name = "cxxbridge-macro"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf"
dependencies = [
"indexmap",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "google-benchmark-rs"
version = "1.9.5"
dependencies = [
"cmake",
"cxx",
"cxx-build",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "link-cplusplus"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82"
dependencies = [
"cc",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "scratch"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "google-benchmark-rs"
version = "1.9.5"
edition = "2021"
description = "Rust bindings for google/benchmark"
license = "Apache-2.0"
[dependencies]
cxx = "1.0"
[build-dependencies]
cmake = "0.1"
cxx-build = "1.0"
+40
View File
@@ -0,0 +1,40 @@
fn main() {
let mut config = cmake::Config::new("../../");
config
.define("BENCHMARK_ENABLE_TESTING", "OFF")
.define("BENCHMARK_ENABLE_LTO", "OFF")
.define("BENCHMARK_ENABLE_WERROR", "OFF")
.build_target("benchmark");
// Rust defaults to the Release CRT (/MD) on Windows even in Debug mode.
// Force CMake to use the Release profile so `google/benchmark` uses `/MD`,
// avoiding a mismatch with `cxx_build` (which uses `/MD`).
if cfg!(target_os = "windows") {
config.profile("Release");
}
let dst = config.build();
println!("cargo:rustc-link-search=native={}/build/src", dst.display());
println!("cargo:rustc-link-search=native={}/build/src/Debug", dst.display());
println!("cargo:rustc-link-search=native={}/build/src/Release", dst.display());
println!("cargo:rustc-link-lib=static=benchmark");
cxx_build::bridge("src/ffi.rs")
.file("src/rust_api.cc")
.include("../../include")
.include("src")
.std("c++17")
.define("BENCHMARK_STATIC_DEFINE", None)
.compile("benchmark_rust_ffi");
if cfg!(target_os = "windows") {
println!("cargo:rustc-link-lib=shlwapi");
}
println!("cargo:rerun-if-changed=src/ffi.rs");
println!("cargo:rerun-if-changed=src/rust_api.cc");
println!("cargo:rerun-if-changed=src/rust_api.h");
println!("cargo:rerun-if-changed=../../src/");
println!("cargo:rerun-if-changed=../../include/");
}
+21
View File
@@ -0,0 +1,21 @@
#[cxx::bridge]
pub mod ffi {
#[namespace = "benchmark"]
unsafe extern "C++" {
include!("benchmark/benchmark.h");
type State;
fn KeepRunning(self: Pin<&mut State>) -> bool;
fn RunSpecifiedBenchmarks() -> usize;
}
#[namespace = "benchmark::rust_api"]
unsafe extern "C++" {
include!("rust_api.h");
unsafe fn SkipWithError(state: Pin<&mut State>, msg: &str);
unsafe fn RegisterBenchmark(name: &str, func: fn(Pin<&mut State>));
unsafe fn Initialize(argc: *mut i32, argv: usize);
}
}
+66
View File
@@ -0,0 +1,66 @@
pub mod ffi;
use std::ffi::CString;
use std::os::raw::c_char;
use std::pin::Pin;
pub struct State<'a> {
#[doc(hidden)]
pub inner: Pin<&'a mut ffi::ffi::State>,
}
impl<'a> State<'a> {
/// Returns true if the benchmark should continue running.
///
/// **Note:** `keep_running()` currently has a small per-iteration overhead due to the FFI boundary.
/// In the future, this could be optimized using `KeepRunningBatch` under the hood.
#[inline]
pub fn keep_running(&mut self) -> bool {
self.inner.as_mut().KeepRunning()
}
pub fn skip_with_error(&mut self, msg: &str) {
unsafe {
ffi::ffi::SkipWithError(self.inner.as_mut(), msg);
}
}
}
/// Initialize the benchmark library.
/// This should be called before `run_specified_benchmarks`.
pub fn initialize(args: &Vec<String>) {
let mut c_args: Vec<CString> = args.iter()
.map(|arg| CString::new(arg.as_str()).unwrap())
.collect();
let mut c_ptrs: Vec<*mut c_char> = c_args.iter_mut()
.map(|c| c.as_ptr() as *mut c_char)
.collect();
let mut argc = c_ptrs.len() as i32;
let argv = c_ptrs.as_mut_ptr();
unsafe {
ffi::ffi::Initialize(&mut argc as *mut _, argv as usize);
}
}
#[macro_export]
macro_rules! register_benchmark {
($name:expr, $func:path) => {
{
fn trampoline(mut state: std::pin::Pin<&mut $crate::ffi::ffi::State>) {
let mut wrapped = $crate::State { inner: state.as_mut() };
$func(&mut wrapped);
}
unsafe {
$crate::ffi::ffi::RegisterBenchmark($name, trampoline);
}
}
};
}
/// Run all registered benchmarks.
pub fn run_specified_benchmarks() -> usize {
ffi::ffi::RunSpecifiedBenchmarks()
}
+26
View File
@@ -0,0 +1,26 @@
#include "rust_api.h"
#include <string>
namespace benchmark {
namespace rust_api {
void RegisterBenchmark(rust::Str name, rust::Fn<void(benchmark::State&)> func);
void Initialize(int* argc, size_t argv);
void SkipWithError(benchmark::State& state, rust::Str msg);
void RegisterBenchmark(rust::Str name, rust::Fn<void(benchmark::State&)> func) {
::benchmark::RegisterBenchmark(std::string(name).c_str(),
[func](benchmark::State& st) { func(st); });
}
void Initialize(int* argc, size_t argv) {
::benchmark::Initialize(argc, (char**)argv);
}
void SkipWithError(benchmark::State& state, rust::Str msg) {
state.SkipWithError(std::string(msg).c_str());
}
} // namespace rust_api
} // namespace benchmark
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "benchmark/benchmark.h"
#include "rust/cxx.h"
namespace benchmark {
namespace rust_api {
void RegisterBenchmark(rust::Str name, rust::Fn<void(benchmark::State&)> func);
void Initialize(int* argc, size_t argv);
void SkipWithError(benchmark::State& state, rust::Str msg);
} // namespace rust_api
} // namespace benchmark
+16
View File
@@ -0,0 +1,16 @@
use google_benchmark_rs::{initialize, register_benchmark, run_specified_benchmarks, State};
fn my_benchmark(state: &mut State) {
while state.keep_running() {
// do nothing
}
}
#[test]
fn test_bindings() {
let args = vec!["--benchmark_format=console".to_string(), "--benchmark_min_time=0.01".to_string()];
initialize(&args);
register_benchmark!("BM_MyBenchmark", my_benchmark);
let count = run_specified_benchmarks();
assert!(count > 0);
}
+6 -1
View File
@@ -9,7 +9,7 @@
commits between the last annotated tag and HEAD
* Pick the most interesting.
* Create one last commit that updates the version saved in `CMakeLists.txt`, `MODULE.bazel`,
and `bindings/python/google_benchmark/__init__.py` to the release version you're creating.
`bindings/python/google_benchmark/__init__.py`, and `bindings/rust/Cargo.toml` to the release version you're creating.
(This version will be used if benchmark is installed from the archive you'll be creating
in the next step.)
@@ -28,6 +28,11 @@ module(name = "com_github_google_benchmark", version="1.9.0")
__version__ = "1.9.0"
```
```toml
# bindings/rust/Cargo.toml
version = "1.9.0"
```
* Create a release through github's interface
* Note this will create a lightweight tag.
* Update this to an annotated tag:
+9
View File
@@ -326,3 +326,12 @@ if (${CMAKE_BUILD_TYPE_LOWER} MATCHES "coverage")
" --coverage flag: ${CXX_FLAG_COVERAGE_MESSAGE}")
endif()
endif()
if (BENCHMARK_ENABLE_RUST_BINDINGS)
find_program(CARGO_EXECUTABLE cargo REQUIRED)
add_custom_target(test_rust_bindings ALL
COMMAND ${CARGO_EXECUTABLE} test
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/bindings/rust
COMMENT "Running Rust bindings tests"
)
endif()