diff --git a/.github/workflows/test_bindings.yml b/.github/workflows/test_bindings.yml index 3cf8064475..e8639f2b78 100644 --- a/.github/workflows/test_bindings.yml +++ b/.github/workflows/test_bindings.yml @@ -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 diff --git a/.gitignore b/.gitignore index bc0c14acd7..53704eba79 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ CMakeSettings.json dist/ *.egg-info* uv.lock + +# Rust build stuff +/bindings/rust/target/ diff --git a/CMakeLists.txt b/CMakeLists.txt index d27698adc8..f08a86e40e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/bindings/rust/Cargo.lock b/bindings/rust/Cargo.lock new file mode 100644 index 0000000000..c0ed48717e --- /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 0000000000..0a6dda959a --- /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 0000000000..02a2f1a14e --- /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 0000000000..d8ad766d9e --- /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 0000000000..b13c7ecf67 --- /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) { + let mut c_args: Vec = 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 0000000000..5efc5fac69 --- /dev/null +++ b/bindings/rust/src/rust_api.cc @@ -0,0 +1,26 @@ +#include "rust_api.h" + +#include + +namespace benchmark { +namespace rust_api { + +void RegisterBenchmark(rust::Str name, rust::Fn func); +void Initialize(int* argc, size_t argv); +void SkipWithError(benchmark::State& state, rust::Str msg); + +void RegisterBenchmark(rust::Str name, rust::Fn 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 0000000000..17ccd0fd53 --- /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 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 0000000000..025846b654 --- /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 ab664a8640..a58dacd8e5 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 @@ 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: diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 374c09fa3f..588b463f59 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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()