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
diff --git a/.github/workflows/test_bindings.yml b/.github/workflows/test_bindings.yml
index 3cf8064..e8639f2 100644
--- a/.github/workflows/test_bindings.yml
+++ b/.github/workflows/test_bindings.yml
@@ -14,7 +14,7 @@
 
 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 @@
         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
diff --git a/.gitignore b/.gitignore
index bc0c14a..53704eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,3 +67,6 @@
 dist/
 *.egg-info*
 uv.lock
+
+# Rust build stuff
+/bindings/rust/target/
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d27698a..f08a86e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -41,6 +41,7 @@
 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)
diff --git a/bindings/rust/Cargo.lock b/bindings/rust/Cargo.lock
new file mode 100644
index 0000000..c0ed487
--- /dev/null
+++ b/bindings/rust/Cargo.lock
@@ -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",
+]
diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml
new file mode 100644
index 0000000..0a6dda9
--- /dev/null
+++ b/bindings/rust/Cargo.toml
@@ -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"
diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs
new file mode 100644
index 0000000..02a2f1a
--- /dev/null
+++ b/bindings/rust/build.rs
@@ -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/");
+}
diff --git a/bindings/rust/src/ffi.rs b/bindings/rust/src/ffi.rs
new file mode 100644
index 0000000..d8ad766
--- /dev/null
+++ b/bindings/rust/src/ffi.rs
@@ -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);
+    }
+}
diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs
new file mode 100644
index 0000000..b13c7ec
--- /dev/null
+++ b/bindings/rust/src/lib.rs
@@ -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()
+}
diff --git a/bindings/rust/src/rust_api.cc b/bindings/rust/src/rust_api.cc
new file mode 100644
index 0000000..5efc5fa
--- /dev/null
+++ b/bindings/rust/src/rust_api.cc
@@ -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
diff --git a/bindings/rust/src/rust_api.h b/bindings/rust/src/rust_api.h
new file mode 100644
index 0000000..17ccd0f
--- /dev/null
+++ b/bindings/rust/src/rust_api.h
@@ -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
diff --git a/bindings/rust/tests/test_benchmark.rs b/bindings/rust/tests/test_benchmark.rs
new file mode 100644
index 0000000..025846b
--- /dev/null
+++ b/bindings/rust/tests/test_benchmark.rs
@@ -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);
+}
diff --git a/docs/releasing.md b/docs/releasing.md
index ab664a8..a58dacd 100644
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -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 @@
 __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:
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 374c09f..588b463 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -326,3 +326,12 @@
       "  --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()