From 2fd9d929e7eb342b9b5f8245154594dce129c12d Mon Sep 17 00:00:00 2001 From: "Simon B. Gasse" Date: Tue, 4 Mar 2025 16:19:57 +0100 Subject: [PATCH] feat: add Rust-to-C++ log example Change-Id: Iee71d8d93a5d55253466a832ab28d68596eb1ede --- .bazelrc | 7 ++ .github/workflows/ci.yml | 47 +++++++++ .gitignore | 13 +++ BUILD.bazel | 8 ++ Cargo.lock | 32 ++++++ Cargo.toml | 11 ++ MODULE.bazel | 62 +++++++++++ clippy.toml | 3 + examples/rust/mw_log/BUILD.bazel | 28 +++++ examples/rust/mw_log/Cargo.toml | 14 +++ examples/rust/mw_log/build.rs | 16 +++ examples/rust/mw_log/src/bin/main.rs | 44 ++++++++ examples/rust/mw_log/src/ffi.cc | 68 +++++++++++++ examples/rust/mw_log/src/ffi.rs | 23 +++++ examples/rust/mw_log/src/include/mw_log.h | 23 +++++ examples/rust/mw_log/src/lib.rs | 119 ++++++++++++++++++++++ examples/rust/mw_log/src/mw_log.cc | 53 ++++++++++ 17 files changed, 571 insertions(+) create mode 100644 .bazelrc create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 BUILD.bazel create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 MODULE.bazel create mode 100644 clippy.toml create mode 100644 examples/rust/mw_log/BUILD.bazel create mode 100644 examples/rust/mw_log/Cargo.toml create mode 100644 examples/rust/mw_log/build.rs create mode 100644 examples/rust/mw_log/src/bin/main.rs create mode 100644 examples/rust/mw_log/src/ffi.cc create mode 100644 examples/rust/mw_log/src/ffi.rs create mode 100644 examples/rust/mw_log/src/include/mw_log.h create mode 100644 examples/rust/mw_log/src/lib.rs create mode 100644 examples/rust/mw_log/src/mw_log.cc diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..43a5a1a --- /dev/null +++ b/.bazelrc @@ -0,0 +1,7 @@ +# Copyright 2025 Accenture. +# +# SPDX-License-Identifier: Apache-2.0 + +build:lint-rust --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect +build:lint-rust --output_groups=+clippy_checks +build:lint-rust --@rules_rust//:clippy.toml=//:clippy.toml \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dc2e284 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +# Copyright 2025 Accenture +# +# SPDX-License-Identifier: Apache-2.0 + +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["*"] + workflow_dispatch: + +jobs: + bazel-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: bazelbuild/setup-bazelisk@v3 + + - uses: actions/cache@v4 + with: + path: "~/.cache/bazel" + key: bazel + + - name: Build all workspace targets + run: bazel build //... + + - name: Lint rust code (with clippy) + run: bazel build --config=lint-rust //... + + cargo-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.83.0 + components: clippy, rustfmt + + - run: cargo clippy --all-features + + - run: cargo fmt --check + + - run: cargo build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7852de --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Copyright 2025 Accenture. +# +# SPDX-License-Identifier: Apache-2.0 + +# Cargo +target + +# Bazel +bazel-* +MODULE.bazel.lock + +# IDEs +.vscode diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..1684bae --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,8 @@ +# Copyright 2025 Accenture. +# +# SPDX-License-Identifier: Apache-2.0 + +exports_files([ + "clippy.toml", + "MODULE.bazel", +]) diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a1e49ca --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,32 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "mw_log" +version = "0.1.0" +dependencies = [ + "cc", + "log", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1e0751f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +# Copyright 2025 Accenture. +# +# SPDX-License-Identifier: Apache-2.0 + +[workspace] +members = ["examples/rust/mw_log"] +resolver = "2" + +[workspace.dependencies] +cc = "1.2.16" +log = "0.4.26" diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..281927c --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,62 @@ +# Copyright 2025 Accenture. +# +# SPDX-License-Identifier: Apache-2.0 + +module( + name = "score_mw_log", + version = "0.1", + compatibility_level = 0, +) + +# Rust setup +bazel_dep(name = "rules_rust", version = "0.56.0") + +RUST_EDITION = "2021" + +RUST_VERSION = "1.83.0" + +rust = use_extension("@rules_rust//rust:extensions.bzl", "rust") +rust.toolchain( + edition = RUST_EDITION, + sha256s = { + "rustc-1.83.0-x86_64-unknown-linux-gnu.tar.xz": "6ec40e0405c8cbed3b786a97d374c144b012fc831b7c22b535f8ecb524f495ad", + "clippy-1.83.0-x86_64-unknown-linux-gnu.tar.xz": "ef6c05abcfd861ff0bca41d408e126dda195dc966ee35abee57645a12d418f5b", + "cargo-1.83.0-x86_64-unknown-linux-gnu.tar.xz": "de834a4062d9cd200f8e0cdca894c0b98afe26f1396d80765df828880a39b98c", + "llvm-tools-1.83.0-x86_64-unknown-linux-gnu.tar.xz": "b931673b309c229e234f03271aaa777ea149c3c41f0fb43f3ef13a272540299a", + "rust-std-1.83.0-x86_64-unknown-linux-gnu.tar.xz": "c88fe6cb22f9d2721f26430b6bdd291e562da759e8629e2b4c7eb2c7cad705f2", + }, + versions = [RUST_VERSION], +) +use_repo(rust, "rust_toolchains") + +crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate") +crate.from_cargo( + name = "cargo", + cargo_lockfile = "//:Cargo.lock", + manifests = [ + "//:Cargo.toml", + "//examples/rust/mw_log:Cargo.toml", + ], +) +use_repo(crate, "cargo") + +# To update, run `bazel run @rules_rust//tools/rust_analyzer:gen_rust_project`. +rust_analyzer = use_extension("@rules_rust//tools/rust_analyzer:deps.bzl", "rust") +rust_analyzer.rust_analyzer_dependencies() + +# C/C++ setup +bazel_dep(name = "googletest", version = "1.15.0") +bazel_dep(name = "rules_cc", version = "0.0.17") +bazel_dep(name = "toolchains_llvm", version = "1.3.0") + +# Configure and register the toolchain. +llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm") +llvm.toolchain( + llvm_version = "19.1.3", +) +use_repo(llvm, "llvm_toolchain_llvm") + +register_toolchains("@llvm_toolchain_llvm//:all") + +# Buildifier dependency for formatting and linting bazel files. +bazel_dep(name = "buildifier_prebuilt", version = "6.4.0") diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..eb96341 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,3 @@ +# Copyright 2025 Accenture. +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/examples/rust/mw_log/BUILD.bazel b/examples/rust/mw_log/BUILD.bazel new file mode 100644 index 0000000..c214ff1 --- /dev/null +++ b/examples/rust/mw_log/BUILD.bazel @@ -0,0 +1,28 @@ +# Copyright 2025 Accenture. +# +# SPDX-License-Identifier: Apache-2.0 + +load("@cargo//:defs.bzl", "all_crate_deps") +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "mw_log", + srcs = glob(["src/**/*.rs"]), + deps = all_crate_deps( + normal = True, + ) + [":libmw_log_cc"], +) + +cc_library( + name = "libmw_log_cc", + srcs = [ + "src/ffi.cc", + "src/mw_log.cc", + ], + hdrs = [ + "src/include/mw_log.h", + ], + includes = [ + "src/include", + ], +) diff --git a/examples/rust/mw_log/Cargo.toml b/examples/rust/mw_log/Cargo.toml new file mode 100644 index 0000000..df1bc09 --- /dev/null +++ b/examples/rust/mw_log/Cargo.toml @@ -0,0 +1,14 @@ +# Copyright 2025 Accenture. +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "mw_log" +version = "0.1.0" +edition = "2021" + +[dependencies] +log = { workspace = true, features = ["std"] } + +[build-dependencies] +cc = { workspace = true } diff --git a/examples/rust/mw_log/build.rs b/examples/rust/mw_log/build.rs new file mode 100644 index 0000000..4daaa63 --- /dev/null +++ b/examples/rust/mw_log/build.rs @@ -0,0 +1,16 @@ +// Copyright 2025 Accenture. +// +// SPDX-License-Identifier: Apache-2.0 + +fn main() { + println!("cargo::rerun-if-changed=src/include/mw_log.h"); + println!("cargo::rerun-if-changed=src/mw_log.cc"); + println!("cargo::rerun-if-changed=src/ffi.cc"); + + cc::Build::new() + .cpp(true) + .file("src/include/mw_log.h") + .file("src/mw_log.cc") + .file("src/ffi.cc") + .compile("libmw_log_cc"); +} diff --git a/examples/rust/mw_log/src/bin/main.rs b/examples/rust/mw_log/src/bin/main.rs new file mode 100644 index 0000000..fddbc97 --- /dev/null +++ b/examples/rust/mw_log/src/bin/main.rs @@ -0,0 +1,44 @@ +// Copyright 2025 Accenture. +// +// SPDX-License-Identifier: Apache-2.0 + +use std::{thread, time::Duration}; + +use log::{debug, error, info, trace, warn, LevelFilter}; +use mw_log::init_logging; + +fn main() { + init_logging(LevelFilter::Debug); + + trace!("This is a trace log, and won't show at the current level"); + debug!("This is a debug log"); + info!("This is an info log"); + warn!("This is a warn log"); + error!("This is an error log"); + + println!("\nRunning parallel logs\n"); + + let mut threads = Vec::with_capacity(10); + for msg in [ + "Short message", + "This is a bit of a longer log message but still, it does not go over the buffer", + "I hope I won't be mixed with other messages", + ] { + let msg = msg.to_owned(); + threads.push(thread::spawn(move || { + // Messages will intermix in the output for e.g. Duration::ZERO + log_in_a_loop(&msg, 10, Duration::from_nanos(100)) + })) + } + + for th in threads { + th.join().unwrap(); + } +} + +fn log_in_a_loop(msg: &str, number_of_times: usize, timeout: Duration) { + for _ in 0..number_of_times { + info!("{msg}"); + thread::sleep(timeout); + } +} diff --git a/examples/rust/mw_log/src/ffi.cc b/examples/rust/mw_log/src/ffi.cc new file mode 100644 index 0000000..adb195e --- /dev/null +++ b/examples/rust/mw_log/src/ffi.cc @@ -0,0 +1,68 @@ +// Copyright 2025 Accenture. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "include/mw_log.h" + +// FFI API reachable from Rust +extern "C" +{ + + // Create a new logger instance + void *new_logger(uint8_t max_level) + { + Logger *logger = new Logger(max_level); + return (void *)logger; + } + + // Free this logger + void free_logger(void *logger_p) + { + if (logger_p != nullptr) + { + Logger *logger = (Logger *)logger_p; + delete logger; + } + } + + // Check if this logger is enabled for the given metadata + // + // Has to be thread-safe + bool logger_enabled(void *logger_p, uint8_t level) + { + if (logger_p != nullptr) + { + Logger *logger = (Logger *)logger_p; + return logger->enabled(level); + } + else + { + return false; + } + } + + // Log the specified message with this logger + // + // Has to be thread-safe + void logger_log(void *logger_p, uint8_t level, const unsigned char *msg_ptr, uint64_t msg_len) + { + if (logger_p != nullptr) + { + + Logger *logger = (Logger *)logger_p; + logger->log(level, msg_ptr, msg_len); + } + } + + // Flush this logger + // + // Has to be thread-safe + void logger_flush(void *logger_p) + { + if (logger_p != nullptr) + { + Logger *logger = (Logger *)logger_p; + logger->flush(); + } + } +} diff --git a/examples/rust/mw_log/src/ffi.rs b/examples/rust/mw_log/src/ffi.rs new file mode 100644 index 0000000..a9e9ede --- /dev/null +++ b/examples/rust/mw_log/src/ffi.rs @@ -0,0 +1,23 @@ +// Copyright 2025 Accenture. +// +// SPDX-License-Identifier: Apache-2.0 + +use std::ffi::c_void; + +#[link(name = "libmw_log_cc")] +unsafe extern "C" { + /// Create a new logger instance + pub unsafe fn new_logger(max_level: u8) -> *mut c_void; + + /// Free this logger + pub unsafe fn free_logger(logger_p: *mut c_void); + + /// Check if this logger is enabled for the given level + pub unsafe fn logger_enabled(logger_p: *mut c_void, level: u8) -> bool; + + /// Log the specified message with this logger + pub unsafe fn logger_log(logger_p: *mut c_void, level: u8, msg_ptr: *const u8, msg_len: u64); + + /// Flush this logger + pub unsafe fn logger_flush(); +} diff --git a/examples/rust/mw_log/src/include/mw_log.h b/examples/rust/mw_log/src/include/mw_log.h new file mode 100644 index 0000000..6617588 --- /dev/null +++ b/examples/rust/mw_log/src/include/mw_log.h @@ -0,0 +1,23 @@ +// Copyright 2025 Accenture. +// +// SPDX-License-Identifier: Apache-2.0 + +#include + +// Logger class which Rust logging forwards to +class Logger +{ +public: + Logger(uint8_t max_level) : max_level(max_level) {} + + // All methods called by the Rust logger implementation have to be thread-safe + + bool enabled(uint8_t level); + void log(uint8_t level, const unsigned char *msg_ptr, uintptr_t msg_len); + void flush(); + +private: + uint8_t max_level; +}; + +void write_level(uint8_t level); \ No newline at end of file diff --git a/examples/rust/mw_log/src/lib.rs b/examples/rust/mw_log/src/lib.rs new file mode 100644 index 0000000..03057e1 --- /dev/null +++ b/examples/rust/mw_log/src/lib.rs @@ -0,0 +1,119 @@ +// Copyright 2025 Accenture. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Rust-to-C++ logging example +//! +//! This library contains example code showing how a Rust logger can forward logging calls +//! to a logger implementation in C++. + +use std::{ + cell::UnsafeCell, + ffi::c_void, + io::{Cursor, Write as _}, +}; + +use log::{LevelFilter, Log}; + +mod ffi; + +/// Initialize logging with the given level filter +pub fn init_logging(level: LevelFilter) { + let logger = MwLogger::new(level); + log::set_max_level(level); + log::set_boxed_logger(Box::new(logger)).expect("failed to set logger"); +} + +/// Size of the buffer into which we write log messages +const LOG_MSG_BUFFER_SIZE: usize = 1024; + +/// Logger implementation forwarding to `mw_log` in C++ +struct MwLogger { + /// Pointer to initialized logger instance in C++ + logger: *mut c_void, +} + +// The `log::Log` trait is a subtrait of `Send` and `Sync`. +// The mutable pointer does not implement `Send` and `Sync`. +// To be able to implement `log::Log`, we need to (unsafely) implement `Send` and `Sync`. +// This is safe only if all calls we make through the FFI are thread-safe. +// At the moment, the implementation is thread-safe in the sense of not leading to memory corruption, +// but messages can get interleaved, which we do not want. +// We can prevent this on either side of the FFI boundary, e.g.: +// - Writing messages to a channel on the Rust side and forward with a defined order in a separate thread. +// - Rely on a thread-safe mechanism of the underlying C++ implementation. +// - ... +// SAFETY: All calls which we do using the pointer are thread-safe from a memory perspective. +unsafe impl Send for MwLogger {} +// SAFETY: All calls which we do using the pointer are thread-safe from a memory perspective. +unsafe impl Sync for MwLogger {} + +impl MwLogger { + /// Create a new instance. + fn new(level: LevelFilter) -> Self { + let level = level as u8; + let logger = unsafe { ffi::new_logger(level) }; + Self { logger } + } +} + +impl Drop for MwLogger { + fn drop(&mut self) { + if !self.logger.is_null() && self.logger.is_aligned() { + // SAFETY: `self.logger` is a valid pointer to an initialized logger on the C++ side + unsafe { ffi::free_logger(self.logger) }; + } + } +} + +impl Log for MwLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + let level = metadata.level() as u8; + // SAFETY: `self.logger` is a valid pointer to an initialized logger on the C++ side + unsafe { ffi::logger_enabled(self.logger, level) } + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + // Create thread-local buffer + // Despite its name, the `UnsafeCell` is not inherently unsafe. + // We just need `unsafe` to access the data mutably. + // If you prefer a buffer without `unsafe`, `RefCell` is an option, + // although it introduces a very small runtime overhead. + thread_local! { + static BUFFER: UnsafeCell<[u8; LOG_MSG_BUFFER_SIZE]> = const {UnsafeCell::new([0; LOG_MSG_BUFFER_SIZE])}; + } + + // Access thread-local buffer + BUFFER.with(|buffer| { + // SAFETY: During the complete lifetime of this borrow, there is no other borrow alive + // (mutable or immutable). + let buffer = unsafe { &mut *buffer.get() }; + + // Write log to the buffer + let mut cursor = Cursor::new(&mut buffer[..]); + if write!(cursor, "{}", record.args()).is_err() { + // Skip logging if we did not write successfully + return; + }; + let msg_ptr = cursor.get_ref().as_ptr(); + let msg_len = cursor.position(); + + let level = record.level() as u8; + + // SAFETY: + // - `self.logger` is a valid pointer to an initialized logger on the C++ side + // - `msg_ptr` is created from an initialized buffer within the cursor + // - `msg_len` is shorter or equal to the size of the buffer behind `msg_ptr`, + // all bytes until `msg_len` are written to + unsafe { ffi::logger_log(self.logger, level, msg_ptr, msg_len) }; + }) + } + + fn flush(&self) { + unsafe { ffi::logger_flush() }; + } +} diff --git a/examples/rust/mw_log/src/mw_log.cc b/examples/rust/mw_log/src/mw_log.cc new file mode 100644 index 0000000..a57f729 --- /dev/null +++ b/examples/rust/mw_log/src/mw_log.cc @@ -0,0 +1,53 @@ +// Copyright 2025 Accenture. +// +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "include/mw_log.h" + +// Check if this logger is enabled for the given level +bool Logger::enabled(uint8_t level) +{ + return level <= this->max_level; +} + +// Log the message +void Logger::log(uint8_t level, const unsigned char *msg_ptr, uint64_t msg_len) +{ + + if (msg_ptr != nullptr) + { + write_level(level); + std::cout.write(reinterpret_cast(msg_ptr), msg_len) << std::endl; + } +} + +// Flush the logger +void Logger::flush() +{ + std::cout.flush(); +} + +// Write the log level +void write_level(uint8_t level) +{ + switch (level) + { + case 1: + std::cout << "ERROR: "; + break; + case 2: + std::cout << "WARN : "; + break; + case 3: + std::cout << "INFO : "; + break; + case 4: + std::cout << "DEBUG: "; + break; + case 5: + default: + std::cout << "TRACE: "; + } +} \ No newline at end of file