diff --git a/MODULE.bazel b/MODULE.bazel index f048bc3..b359b9a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -120,3 +120,7 @@ git_override( commit = "3c65b223e9f516f95935bb4cd2e83d6088ca016f", remote = "https://github.com/eclipse-score/baselibs.git", ) + +# Rust dependencies + +bazel_dep(name = "score_baselibs_rust", version = "0.0.3") diff --git a/score/mw/log/rust/mw_logger/BUILD b/score/mw/log/rust/mw_logger/BUILD deleted file mode 100644 index acc2499..0000000 --- a/score/mw/log/rust/mw_logger/BUILD +++ /dev/null @@ -1,56 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -load("@rules_rust//cargo:defs.bzl", "cargo_build_script") -load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") - -cc_library( - name = "librust_cpp_log_adapter", - srcs = ["src/rust_cpp_log_adapter.cpp"], - visibility = ["//visibility:private"], - deps = [ - "//score/mw/log/detail/common:recorder_factory", - "@score_baselibs//score/mw/log:frontend", - ], -) - -rust_library( - name = "mw_logger", - srcs = glob(["src/**/*.rs"]), - crate_name = "mw_logger", - edition = "2021", - visibility = ["//visibility:public"], - deps = [ - ":librust_cpp_log_adapter", - "@score_baselibs//score/mw/log/rust:log", - ], -) - -rust_binary( - name = "example", - srcs = ["examples/main.rs"], - data = [ - "examples/config/logging.json", - ], - edition = "2021", - rustc_flags = [ - "-Clink-arg=-lstdc++", - "-Clink-arg=-lm", - "-Clink-arg=-lc", - ], - visibility = ["//visibility:public"], - deps = [ - ":mw_logger", - "@score_baselibs//score/mw/log/rust:log", - ], -) diff --git a/score/mw/log/rust/mw_logger/examples/main.rs b/score/mw/log/rust/mw_logger/examples/main.rs deleted file mode 100644 index a02bfee..0000000 --- a/score/mw/log/rust/mw_logger/examples/main.rs +++ /dev/null @@ -1,74 +0,0 @@ -// -// Copyright (c) 2025 Contributors to the Eclipse Foundation -// -// See the NOTICE file(s) distributed with this work for additional -// information regarding copyright ownership. -// -// This program and the accompanying materials are made available under the -// terms of the Apache License Version 2.0 which is available at -// -// -// SPDX-License-Identifier: Apache-2.0 -// - -use std::path::PathBuf; - -use log::{debug, error, info, trace, warn}; -use mw_logger::MwLoggerBuilder; - -fn main() { - //Setup for example using config file - let path = PathBuf::from(std::env::current_dir().unwrap()) - .join(file!()) - .parent() - .unwrap() - .join("config") - .join("logging.json"); - - std::env::set_var("MW_LOG_CONFIG_FILE", path.as_os_str()); - - // Just initialize and set as default logger - MwLoggerBuilder::new().set_as_default_logger::(); - - trace!("This is a trace log"); - debug!("This is a debug log"); - error!("This is an error log"); - info!("This is an info log"); - warn!("This is a warn log"); - - error!( - "This is an log that will be trimmed: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc - ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd - eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee - fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg - END MARKER NOT VISIBLE" - ); - - error!( - "This is an log that will be trimmed {} {} {} {} {} {} {}. END MARKER NOT VISIBLE", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg" - ); - - // Using logger instance with context - let logger = MwLoggerBuilder::new() - .with_context("ALFA") - .build::(); - - trace!( - logger : logger, - "This is a trace log" - ); - debug!(logger : logger, "This is a debug log"); - error!(logger : logger, "This is an error log"); - info!(logger : logger, "This is an info log"); - warn!(logger : logger, "This is a warn log"); -} diff --git a/score/mw/log/rust/mw_logger/src/lib.rs b/score/mw/log/rust/mw_logger/src/lib.rs deleted file mode 100644 index 41aecfe..0000000 --- a/score/mw/log/rust/mw_logger/src/lib.rs +++ /dev/null @@ -1,213 +0,0 @@ -// -// Copyright (c) 2025 Contributors to the Eclipse Foundation -// -// See the NOTICE file(s) distributed with this work for additional -// information regarding copyright ownership. -// -// This program and the accompanying materials are made available under the -// terms of the Apache License Version 2.0 which is available at -// -// -// SPDX-License-Identifier: Apache-2.0 -// - -mod mw_log_ffi; - -use crate::mw_log_ffi::*; - -use core::ffi::c_char; -use core::fmt::{self, Write}; -use log::{Level, Log, Metadata, Record}; -use std::ffi::CString; -use std::mem::MaybeUninit; - -const MSG_SIZE: usize = 512; - -/// Builder for the MwLogger -pub struct MwLoggerBuilder { - context: Option, -} - -impl MwLoggerBuilder { - pub fn new() -> Self { - Self { context: None } - } - - /// Builds the MwLogger with the specified context and configuration and returns it. - pub fn build(self) -> MwLogger { - let context_cstr = self.context.unwrap_or(CString::new("DFLT").unwrap()); - let c_logger_ptr = unsafe { mw_log_create_logger(context_cstr.as_ptr().cast::()) }; - MwLogger { - ptr: c_logger_ptr, - log_fn: log::, - } - } - - /// Builds and sets the MwLogger as the default logger with the specified configuration. - pub fn set_as_default_logger(self) { - let logger = self.build::(); - log::set_max_level(mw_log_logger_level(logger.ptr)); - log::set_boxed_logger(Box::new(logger)) - .expect("Failed to initialize MwLogger as default logger - logger may already be set"); - } - - /// Sets the context for currently build logger. - pub fn with_context(mut self, context: &str) -> Self { - self.context = Some(CString::new(context).expect( - "Failed to create CString: - input contains null bytes", - )); - self - } -} - -/// A simple buffer writer that implements `core::fmt::Write` -/// and writes into a fixed-size(`BUF_SIZE) buffer. -/// Used in `Log` implementation to format log messages -/// before passing them to the underlying C++ logger. -struct BufWriter { - buf: [MaybeUninit; BUF_SIZE], - pos: usize, -} - -impl BufWriter { - fn new() -> Self { - Self { - buf: [MaybeUninit::uninit(); BUF_SIZE], - pos: 0, - } - } - - /// Returns a slice to filled part of buffer. This is not null-terminated. - fn as_slice(&self) -> &[c_char] { - // SAFETY: We only expose already initialized part of the buffer - unsafe { core::slice::from_raw_parts(self.buf.as_ptr().cast::(), self.len()) } - } - - /// Returns the current length of the filled part of the buffer. - fn len(&self) -> usize { - self.pos - } - - /// Reverts the current position (consumes) by `cnt` bytes, saturating at 0. - fn revert_pos(&mut self, cnt: usize) { - self.pos = self.pos.saturating_sub(cnt); - } - - // Finds the right index (around requested `index`) to trim utf8 string to a valid char boundary - fn floor_char_boundary(view: &str, index: usize) -> usize { - if index >= view.len() { - view.len() - } else { - let lower_bound = index.saturating_sub(3); - let new_index = view.as_bytes()[lower_bound..=index] - .iter() - .rposition(|b| Self::is_utf8_char_boundary(*b)); - - // SAFETY: we know that the character boundary will be within four bytes - unsafe { lower_bound + new_index.unwrap_unchecked() } - } - } - - const fn is_utf8_char_boundary(i: u8) -> bool { - // This is bit magic equivalent to: b < 128 || b >= 192 - (i as i8) >= -0x40 - } -} - -impl Write for BufWriter { - fn write_str(&mut self, s: &str) -> fmt::Result { - let bytes = s.as_bytes(); - let to_write = bytes.len().min(self.buf.len() - self.pos - 1); // Cutting write, instead error when we don't fit - let bounded_to_write = Self::floor_char_boundary(s, to_write); - - if bounded_to_write == 0 { - return Err(fmt::Error); - } - - let dest = self.buf[self.pos..self.pos + bounded_to_write] - .as_mut_ptr() - .cast::(); - - unsafe { core::ptr::copy_nonoverlapping(bytes.as_ptr(), dest, bounded_to_write) }; - - self.pos += bounded_to_write; - Ok(()) - } -} - -pub struct MwLogger { - ptr: *const Logger, - log_fn: fn(&mut BufWriter, &Record), -} - -// SAFETY: The underlying C++ logger is known to be safe to change thread -unsafe impl Send for MwLogger {} - -// SAFETY: The underlying C++ logger is known to be thread-safe. -unsafe impl Sync for MwLogger {} - -impl MwLogger { - fn write_log(&self, level: Level, msg: &BufWriter) { - let slice = msg.as_slice(); - - unsafe { - match level { - Level::Error => mw_log_error_logger(self.ptr, slice.as_ptr(), slice.len() as u32), - Level::Warn => mw_log_warn_logger(self.ptr, slice.as_ptr(), slice.len() as u32), - Level::Info => mw_log_info_logger(self.ptr, slice.as_ptr(), slice.len() as u32), - Level::Debug => mw_log_debug_logger(self.ptr, slice.as_ptr(), slice.len() as u32), - Level::Trace => mw_log_verbose_logger(self.ptr, slice.as_ptr(), slice.len() as u32), - } - } - } -} - -impl Log for MwLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - mw_log_is_log_level_enabled(self.ptr, metadata.level()) - } - fn log(&self, record: &Record) { - if !self.enabled(record.metadata()) { - return; - } - - let mut msg_writer = BufWriter::::new(); - (self.log_fn)(&mut msg_writer, record); - - self.write_log(record.level(), &msg_writer); - } - - fn flush(&self) { - // No-op for this logger, as it does not buffer logs - } -} - -fn log( - msg_writer: &mut BufWriter, - record: &Record, -) { - if SHOW_FILE || SHOW_LINE || SHOW_MODULE { - let _ = write!(msg_writer, "["); - if SHOW_MODULE { - if let Some(module) = record.module_path() { - let _ = write!(msg_writer, "{}:", module); - } - } - if SHOW_FILE { - if let Some(file) = record.file() { - let _ = write!(msg_writer, "{}:", file); - } - } - if SHOW_LINE { - if let Some(line) = record.line() { - let _ = write!(msg_writer, "{}:", line); - } - } - - msg_writer.revert_pos(1); - let _ = write!(msg_writer, "] "); - } - - let _ = msg_writer.write_fmt(*record.args()); -} diff --git a/score/mw/log/rust/mw_logger/src/mw_log_ffi.rs b/score/mw/log/rust/mw_logger/src/mw_log_ffi.rs deleted file mode 100644 index 339fc84..0000000 --- a/score/mw/log/rust/mw_logger/src/mw_log_ffi.rs +++ /dev/null @@ -1,66 +0,0 @@ -// -// Copyright (c) 2025 Contributors to the Eclipse Foundation -// -// See the NOTICE file(s) distributed with this work for additional -// information regarding copyright ownership. -// -// This program and the accompanying materials are made available under the -// terms of the Apache License Version 2.0 which is available at -// -// -// SPDX-License-Identifier: Apache-2.0 -// - -use core::ffi::c_char; - -use log::{Level, LevelFilter}; - -// Opaque type representing the C++ logger ptr -#[repr(C)] -pub(crate) struct Logger { - _private: [u8; 0], // Opaque -} - -pub(crate) fn mw_log_logger_level(logger: *const Logger) -> LevelFilter { - let level = unsafe { mw_log_logger_level_internal(logger) }; - log_level_from_ffi(level) -} - -pub(crate) fn mw_log_is_log_level_enabled(logger: *const Logger, level: Level) -> bool { - let level_byte = match level { - Level::Error => 0x02, - Level::Warn => 0x03, - Level::Info => 0x04, - Level::Debug => 0x05, - Level::Trace => 0x06, - }; - unsafe { mw_log_is_log_level_enabled_internal(logger, level_byte) } -} - -/// Get the max log level from C++ as a LevelFilter directly -fn log_level_from_ffi(level: u8) -> LevelFilter { - match level { - 0x00 => LevelFilter::Off, - 0x01 => LevelFilter::Error, // Currently Fatal treated as Error - 0x02 => LevelFilter::Error, - 0x03 => LevelFilter::Warn, - 0x04 => LevelFilter::Info, - 0x05 => LevelFilter::Debug, - 0x06 => LevelFilter::Trace, // Verbose is Trace - _ => LevelFilter::Info, // fallback - } -} - -extern "C" { - - pub(crate) fn mw_log_create_logger(context: *const c_char) -> *mut Logger; - pub(crate) fn mw_log_error_logger(logger: *const Logger, message: *const c_char, len: u32); - pub(crate) fn mw_log_warn_logger(logger: *const Logger, message: *const c_char, len: u32); - pub(crate) fn mw_log_info_logger(logger: *const Logger, message: *const c_char, len: u32); - pub(crate) fn mw_log_debug_logger(logger: *const Logger, message: *const c_char, len: u32); - pub(crate) fn mw_log_verbose_logger(logger: *const Logger, message: *const c_char, len: u32); - - fn mw_log_is_log_level_enabled_internal(logger: *const Logger, level: u8) -> bool; - fn mw_log_logger_level_internal(logger: *const Logger) -> u8; - -} diff --git a/score/mw/log/rust/mw_logger/src/rust_cpp_log_adapter.cpp b/score/mw/log/rust/mw_logger/src/rust_cpp_log_adapter.cpp deleted file mode 100644 index 22ec0ed..0000000 --- a/score/mw/log/rust/mw_logger/src/rust_cpp_log_adapter.cpp +++ /dev/null @@ -1,105 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2025 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ - -#include "score/mw/log/logging.h" -#include "score/mw/log/configuration/configuration.h" -#include "score/mw/log/logger.h" -#include "score/mw/log/log_level.h" - -namespace score::mw::log -{ - extern "C" - { - - Logger *mw_log_create_logger(const char *context) - { - return &CreateLogger(context); - } - - bool mw_log_is_log_level_enabled_internal(const Logger *logger, uint8_t level) - { - return logger->IsLogEnabled(GetLogLevelFromU8(level)); - } - - void mw_log_fatal_logger(const Logger *logger, const char *message, uint32_t size) - { - logger->LogFatal() << LogString{message, size}; - } - - void mw_log_error_logger(const Logger *logger, const char *message, uint32_t size) - { - logger->LogError() << LogString{message, size}; - } - - void mw_log_warn_logger(const Logger *logger, const char *message, uint32_t size) - { - logger->LogWarn() << LogString{message, size}; - } - - void mw_log_info_logger(const Logger *logger, const char *message, uint32_t size) - { - logger->LogInfo() << LogString{message, size}; - } - - void mw_log_debug_logger(const Logger *logger, const char *message, uint32_t size) - { - logger->LogDebug() << LogString{message, size}; - } - - void mw_log_verbose_logger(const Logger *logger, const char *message, uint32_t size) - { - logger->LogVerbose() << LogString{message, size}; - } - - uint8_t mw_log_logger_level_internal(const Logger *logger) - { - // TODO: This is adapter code, as there seems to be no way to get log level for Logger - if (logger->IsLogEnabled(LogLevel::kInfo)) - { - // Between Verbose, Debug, Info - if (logger->IsLogEnabled(LogLevel::kDebug)) - { - if (logger->IsLogEnabled(LogLevel::kVerbose)) - { - return static_cast(LogLevel::kVerbose); - } - - return static_cast(LogLevel::kDebug); - } - - return static_cast(LogLevel::kInfo); - } - else - { - // Lower half: Warn, Error, Fatal - if (logger->IsLogEnabled(LogLevel::kError)) - { - if (logger->IsLogEnabled(LogLevel::kWarn)) - { - return static_cast(LogLevel::kWarn); - } - - return static_cast(LogLevel::kError); - } - - if (logger->IsLogEnabled(LogLevel::kFatal)) - { - return static_cast(LogLevel::kFatal); - } - } - - // fallback - return static_cast(LogLevel::kOff); - } - } -} // namespace score::mw::log diff --git a/score/mw/log/rust/score_log_bridge/BUILD b/score/mw/log/rust/score_log_bridge/BUILD new file mode 100644 index 0000000..31523e1 --- /dev/null +++ b/score/mw/log/rust/score_log_bridge/BUILD @@ -0,0 +1,111 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") + +config_setting( + name = "x86_64-linux", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], +) + +config_setting( + name = "arm64-qnx", + constraint_values = [ + "@platforms//cpu:arm64", + "@platforms//os:qnx", + ], +) + +cc_library( + name = "adapter", + srcs = ["src/adapter.cpp"], + # C++/Rust interface objects must share layout. + defines = select({ + ":x86_64-linux": ["x86_64_linux"], + ":arm64-qnx": ["arm64_qnx"], + "//conditions:default": [], + }), + visibility = ["//visibility:private"], + deps = [ + "//score/mw/log/detail/common:recorder_factory", + "@score_baselibs//score/mw/log:frontend", + ], +) + +rust_library( + name = "score_log_bridge", + srcs = glob(["src/**/*.rs"]), + # C++/Rust interface objects must share layout. + crate_features = select({ + ":x86_64-linux": ["x86_64_linux"], + ":arm64-qnx": ["arm64_qnx"], + "//conditions:default": [], + }), + edition = "2021", + visibility = ["//visibility:public"], + deps = [ + ":adapter", + "@score_baselibs_rust//src/log/score_log", + ], +) + +RUSTC_FLAGS = select({ + "@platforms//os:qnx": [ + "-Clink-arg=-lc++", + "-Clink-arg=-lm", + ], + "//conditions:default": [ + "-Clink-arg=-lstdc++", + "-Clink-arg=-lm", + "-Clink-arg=-lc", + ], +}) + +rust_test( + name = "tests", + crate = "score_log_bridge", + crate_features = select({ + ":x86_64-linux": ["x86_64_linux"], + ":arm64-qnx": ["arm64_qnx"], + "//conditions:default": [], + }), + edition = "2021", + rustc_flags = RUSTC_FLAGS, + tags = [ + "unit_tests", + "ut", + ], + deps = [ + "@score_crates//:libc", + "@score_crates//:tempfile", + ], +) + +rust_binary( + name = "example", + srcs = ["examples/main.rs"], + data = [ + "examples/config/logging.json", + ], + edition = "2021", + rustc_flags = RUSTC_FLAGS, + visibility = ["//visibility:public"], + deps = [ + ":score_log_bridge", + "@score_baselibs_rust//src/log/score_log", + ], +) diff --git a/score/mw/log/rust/mw_logger/examples/config/logging.json b/score/mw/log/rust/score_log_bridge/examples/config/logging.json similarity index 62% rename from score/mw/log/rust/mw_logger/examples/config/logging.json rename to score/mw/log/rust/score_log_bridge/examples/config/logging.json index cf4f333..ac97a33 100644 --- a/score/mw/log/rust/mw_logger/examples/config/logging.json +++ b/score/mw/log/rust/score_log_bridge/examples/config/logging.json @@ -1,6 +1,6 @@ { - "appId": "JSON", - "appDesc": "JSON example programs", + "appId": "EXMP", + "appDesc": "Logger example", "logMode" : "kConsole", "logLevel": "kVerbose", "logLevelThresholdConsole": "kInfo" diff --git a/score/mw/log/rust/score_log_bridge/examples/main.rs b/score/mw/log/rust/score_log_bridge/examples/main.rs new file mode 100644 index 0000000..cf7a4d7 --- /dev/null +++ b/score/mw/log/rust/score_log_bridge/examples/main.rs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use score_log::{debug, error, fatal, info, trace, warn, Log}; +use score_log_bridge::ScoreLoggerBuilder; +use std::path::PathBuf; + +fn main() { + // Setup for example using config file + let config_path = PathBuf::from(std::env::current_dir().unwrap()) + .join(file!()) + .parent() + .unwrap() + .join("config") + .join("logging.json"); + + // Just initialize and set as default logger + ScoreLoggerBuilder::new() + .show_module(false) + .show_file(true) + .show_line(false) + .config(config_path) + .set_as_default_logger(); + + trace!("This is a trace log - hidden"); + debug!("This is a debug log - hidden"); + info!("This is an info log"); + warn!("This is a warn log"); + error!("This is an error log"); + fatal!("This is a fatal log"); + + error!(context: "X", "This log contains single letter context"); + + let x1 = 123.4; + let x2 = 111; + let x3 = true; + let x4 = -0x3Fi8; + error!( + "This is an error log with numeric values: {} {} {} {:x}", + x1, x2, x3, x4, + ); + + // Using logger instance with context + let logger = ScoreLoggerBuilder::new() + .context("ALFA") + .show_module(false) + .show_file(true) + .show_line(false) + .build(); + + trace!( + logger: logger, + "This is a trace log - hidden" + ); + debug!(logger: logger, "This is a debug log - hidden"); + info!(logger: logger, "This is an info log"); + warn!(logger: logger, "This is a warn log"); + error!(logger: logger, "This is an error log"); + fatal!(logger: logger, "This is an fatal log"); +} diff --git a/score/mw/log/rust/score_log_bridge/src/adapter.cpp b/score/mw/log/rust/score_log_bridge/src/adapter.cpp new file mode 100644 index 0000000..48b1f23 --- /dev/null +++ b/score/mw/log/rust/score_log_bridge/src/adapter.cpp @@ -0,0 +1,246 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/mw/log/runtime.h" +#include "score/mw/log/slot_handle.h" + +using namespace score::mw::log; +using namespace score::mw::log::detail; + +// Verify configuration. +#if defined(x86_64_linux) || defined(arm64_qnx) +static_assert(sizeof(SlotHandle) == 24); +static_assert(alignof(SlotHandle) == 8); +#else +#error "Unknown configuration, unable to check layout" +#endif + +extern "C" { +/// @brief Get current recorder from runtime. +/// @return Current recorder. +Recorder* recorder_get() { return &Runtime::GetRecorder(); } + +/// @brief Start recording log message. +/// @param recorder Recorder. +/// @param context Message context name. +/// @param context_size Message context name size. +/// @param log_level Message log level. +/// @param slot `SlotHandle`-sized buffer. +/// @return `slot` if acquired, `nullptr` otherwise. +SlotHandle* recorder_start(Recorder* recorder, const char* context, size_t context_size, + LogLevel log_level, SlotHandle* slot) { + auto start_result{recorder->StartRecord(std::string_view{context, context_size}, log_level)}; + if (start_result) { + return new (slot) SlotHandle{*start_result}; + } else { + return nullptr; + } +} + +/// @brief Get current log level for provided context. +/// @param recorder Recorder. +/// @param context Message context name. +/// @param context_size Message context name size. +/// @return Current log level. +LogLevel recorder_log_level(const Recorder* recorder, const char* context, size_t context_size) { + auto first{static_cast(LogLevel::kOff)}; + auto last{static_cast(LogLevel::kVerbose)}; + // Reversed order - `kOff` always seem to report true. + for (uint8_t i{last}; i > first; --i) { + auto current = static_cast(i); + if (recorder->IsLogEnabled(current, context)) { + return current; + } + } + + // Fall-back. + return LogLevel::kOff; +} + +/// @brief Stop recording log message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +void recorder_stop(Recorder* recorder, SlotHandle* slot) { recorder->StopRecord(*slot); } + +/// @brief Add bool value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_bool(Recorder* recorder, SlotHandle* slot, const bool* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add f32 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_f32(Recorder* recorder, SlotHandle* slot, const float* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add f64 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_f64(Recorder* recorder, SlotHandle* slot, const double* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add string value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_string(Recorder* recorder, SlotHandle* slot, const char* value, size_t size) { + recorder->Log(*slot, std::string_view{value, size}); +} + +/// @brief Add i8 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_i8(Recorder* recorder, SlotHandle* slot, const int8_t* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add i16 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_i16(Recorder* recorder, SlotHandle* slot, const int16_t* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add i32 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_i32(Recorder* recorder, SlotHandle* slot, const int32_t* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add i64 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_i64(Recorder* recorder, SlotHandle* slot, const int64_t* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add u8 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_u8(Recorder* recorder, SlotHandle* slot, const uint8_t* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add u16 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_u16(Recorder* recorder, SlotHandle* slot, const uint16_t* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add u32 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_u32(Recorder* recorder, SlotHandle* slot, const uint32_t* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add u64 value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_u64(Recorder* recorder, SlotHandle* slot, const uint64_t* value) { + recorder->Log(*slot, *value); +} + +/// @brief Add 8-bit binary value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_bin8(Recorder* recorder, SlotHandle* slot, const uint8_t* value) { + recorder->Log(*slot, LogBin8{*value}); +} + +/// @brief Add 16-bit binary value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_bin16(Recorder* recorder, SlotHandle* slot, const uint16_t* value) { + recorder->Log(*slot, LogBin16{*value}); +} + +/// @brief Add 32-bit binary value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_bin32(Recorder* recorder, SlotHandle* slot, const uint32_t* value) { + recorder->Log(*slot, LogBin32{*value}); +} + +/// @brief Add 64-bit binary value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_bin64(Recorder* recorder, SlotHandle* slot, const uint64_t* value) { + recorder->Log(*slot, LogBin64{*value}); +} + +/// @brief Add 8-bit hexadecimal value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_hex8(Recorder* recorder, SlotHandle* slot, const uint8_t* value) { + recorder->Log(*slot, LogHex8{*value}); +} + +/// @brief Add 16-bit hexadecimal value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_hex16(Recorder* recorder, SlotHandle* slot, const uint16_t* value) { + recorder->Log(*slot, LogHex16{*value}); +} + +/// @brief Add 32-bit hexadecimal value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_hex32(Recorder* recorder, SlotHandle* slot, const uint32_t* value) { + recorder->Log(*slot, LogHex32{*value}); +} + +/// @brief Add 64-bit hexadecimal value to message. +/// @param recorder Recorder. +/// @param slot Acquired slot. +/// @param value Value. +void log_hex64(Recorder* recorder, SlotHandle* slot, const uint64_t* value) { + recorder->Log(*slot, LogHex64{*value}); +} + +/// @brief Get size of `SlotHandle`. +/// @return Size. +size_t slot_handle_size() { + return sizeof(SlotHandle); +} + +/// @brief Get alignment of `SlotHandle`. +/// @return Alignment. +size_t slot_handle_alignment() { + return alignof(SlotHandle); +} +} diff --git a/score/mw/log/rust/score_log_bridge/src/ffi.rs b/score/mw/log/rust/score_log_bridge/src/ffi.rs new file mode 100644 index 0000000..9f348eb --- /dev/null +++ b/score/mw/log/rust/score_log_bridge/src/ffi.rs @@ -0,0 +1,470 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use core::alloc::Layout; +use core::cmp::min; +use core::ffi::c_char; + +/// Represents severity of a log message. +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] +#[repr(u8)] +pub enum LogLevel { + #[allow(dead_code)] + Off = 0x00, + Fatal = 0x01, + Error = 0x02, + Warn = 0x03, + Info = 0x04, + Debug = 0x05, + Verbose = 0x06, +} + +impl From for LogLevel { + fn from(score_log_level: score_log::Level) -> Self { + match score_log_level { + score_log::Level::Fatal => LogLevel::Fatal, + score_log::Level::Error => LogLevel::Error, + score_log::Level::Warn => LogLevel::Warn, + score_log::Level::Info => LogLevel::Info, + score_log::Level::Debug => LogLevel::Debug, + score_log::Level::Trace => LogLevel::Verbose, + } + } +} + +impl From for score_log::Level { + fn from(log_level: LogLevel) -> Self { + match log_level { + LogLevel::Fatal => score_log::Level::Fatal, + LogLevel::Error => score_log::Level::Error, + LogLevel::Warn => score_log::Level::Warn, + LogLevel::Info => score_log::Level::Info, + LogLevel::Debug => score_log::Level::Debug, + LogLevel::Verbose => score_log::Level::Trace, + _ => panic!("Log level not supported"), + } + } +} + +impl From for score_log::LevelFilter { + fn from(level: LogLevel) -> score_log::LevelFilter { + match level { + LogLevel::Off => score_log::LevelFilter::Off, + LogLevel::Fatal => score_log::LevelFilter::Fatal, + LogLevel::Error => score_log::LevelFilter::Error, + LogLevel::Warn => score_log::LevelFilter::Warn, + LogLevel::Info => score_log::LevelFilter::Info, + LogLevel::Debug => score_log::LevelFilter::Debug, + LogLevel::Verbose => score_log::LevelFilter::Trace, + } + } +} + +/// Name of the context. +/// Max 4 bytes containing ASCII characters. +struct Context { + data: [c_char; 4], + size: usize, +} + +impl Context { + pub fn from(context: &str) -> Self { + // Disallow non-ASCII strings. + // ASCII characters are single byte in UTF-8. + if !context.is_ascii() { + panic!("Provided context contains non-ASCII characters: {context}"); + } + + // Get number of characters. + let size = min(context.len(), 4); + + // Copy data into array. + let mut data: [c_char; 4] = [0; 4]; + unsafe { + core::ptr::copy_nonoverlapping(context.as_ptr(), data.as_mut_ptr() as *mut u8, size); + } + + Self { data, size } + } +} + +/// Opaque type representing `Recorder`. +#[repr(C)] +struct RecorderPtr { + _private: [u8; 0], +} + +/// Recorder instance. +pub(crate) struct Recorder { + inner: *mut RecorderPtr, +} + +impl Recorder { + pub fn new() -> Self { + let inner = unsafe { recorder_get() }; + Self { inner } + } + + pub fn log_level(&self, context: &str) -> LogLevel { + let context = Context::from(context); + unsafe { recorder_log_level(self.inner, context.data.as_ptr(), context.size) } + } +} + +/// Opaque type representing `SlotHandle`. +#[cfg(any(feature = "x86_64_linux", feature = "arm64_qnx"))] +#[repr(C, align(8))] +pub(crate) struct SlotHandlePtr { + _private: [u8; 24], +} + +impl SlotHandlePtr { + pub fn layout_rust() -> Layout { + Layout::new::() + } + + pub fn layout_cpp() -> Layout { + let size = unsafe { slot_handle_size() }; + let align = unsafe { slot_handle_alignment() }; + Layout::from_size_align(size, align).expect("Invalid SlotHandle layout, size: {size}, alignment: {align}") + } +} + +/// Single log message stream. +pub struct LogStream<'a> { + recorder: &'a Recorder, + slot: Option, +} + +impl<'a> LogStream<'a> { + pub fn new(recorder: &'a Recorder, context: &str, log_level: LogLevel) -> Self { + // Create context object. + let context = Context::from(context); + + // Start record. + // `SlotHandle` is allocated on stack. + let mut slot_buffer = SlotHandlePtr { _private: [0; 24] }; + let slot_result = unsafe { + recorder_start( + recorder.inner, + context.data.as_ptr(), + context.size, + log_level, + &mut slot_buffer as *mut SlotHandlePtr, + ) + }; + + // Store buffer only if acquired. + let slot = if !slot_result.is_null() { + Some(slot_buffer) + } else { + None + }; + + Self { recorder, slot } + } + + pub fn log_bool(&mut self, v: &bool) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_bool(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const bool); + } + } + } + + pub fn log_f32(&mut self, v: &f32) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_f32(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const f32); + } + } + } + + pub fn log_f64(&mut self, v: &f64) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_f64(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const f64); + } + } + } + + pub fn log_string(&mut self, v: &str) { + // Disallow non-ASCII strings. + if !v.is_ascii() { + panic!("Provided string contains non-ASCII characters: {v}"); + } + + // Get string as pointer and size. + // ASCII characters are single byte in UTF-8. + let v_ptr = v.as_ptr() as *const c_char; + let v_size = v.len(); + + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_string(self.recorder.inner, slot as *mut SlotHandlePtr, v_ptr, v_size); + } + } + } + + pub fn log_i8(&mut self, v: &i8) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_i8(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const i8); + } + } + } + + pub fn log_i16(&mut self, v: &i16) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_i16(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const i16); + } + } + } + + pub fn log_i32(&mut self, v: &i32) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_i32(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const i32); + } + } + } + + pub fn log_i64(&mut self, v: &i64) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_i64(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const i64); + } + } + } + + pub fn log_u8(&mut self, v: &u8) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_u8(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u8); + } + } + } + + pub fn log_u16(&mut self, v: &u16) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_u16(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u16); + } + } + } + + pub fn log_u32(&mut self, v: &u32) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_u32(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u32); + } + } + } + + pub fn log_u64(&mut self, v: &u64) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_u64(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u64); + } + } + } + + pub fn log_bin8(&mut self, v: &u8) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_bin8(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u8); + } + } + } + + pub fn log_bin16(&mut self, v: &u16) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_bin16(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u16); + } + } + } + + pub fn log_bin32(&mut self, v: &u32) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_bin32(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u32); + } + } + } + + pub fn log_bin64(&mut self, v: &u64) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_bin64(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u64); + } + } + } + + pub fn log_hex8(&mut self, v: &u8) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_hex8(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u8); + } + } + } + + pub fn log_hex16(&mut self, v: &u16) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_hex16(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u16); + } + } + } + + pub fn log_hex32(&mut self, v: &u32) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_hex32(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u32); + } + } + } + + pub fn log_hex64(&mut self, v: &u64) { + if let Some(slot) = self.slot.as_mut() { + unsafe { + log_hex64(self.recorder.inner, slot as *mut SlotHandlePtr, v as *const u64); + } + } + } +} + +impl Drop for LogStream<'_> { + fn drop(&mut self) { + if let Some(slot) = self.slot.as_mut() { + unsafe { recorder_stop(self.recorder.inner, slot) } + } + } +} + +unsafe extern "C" { + fn recorder_get() -> *mut RecorderPtr; + fn recorder_start( + recorder: *mut RecorderPtr, + context: *const c_char, + context_size: usize, + log_level: LogLevel, + slot: *mut SlotHandlePtr, + ) -> *mut SlotHandlePtr; + fn recorder_stop(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr); + fn recorder_log_level(recorder: *const RecorderPtr, context: *const c_char, context_size: usize) -> LogLevel; + fn log_bool(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const bool); + fn log_f32(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const f32); + fn log_f64(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const f64); + fn log_string(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const c_char, size: usize); + fn log_i8(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const i8); + fn log_i16(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const i16); + fn log_i32(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const i32); + fn log_i64(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const i64); + fn log_u8(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u8); + fn log_u16(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u16); + fn log_u32(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u32); + fn log_u64(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u64); + fn log_bin8(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u8); + fn log_bin16(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u16); + fn log_bin32(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u32); + fn log_bin64(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u64); + fn log_hex8(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u8); + fn log_hex16(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u16); + fn log_hex32(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u32); + fn log_hex64(recorder: *mut RecorderPtr, slot: *mut SlotHandlePtr, value: *const u64); + fn slot_handle_size() -> usize; + fn slot_handle_alignment() -> usize; +} + +#[cfg(test)] +mod tests { + use crate::ffi::{Context, LogLevel}; + + #[test] + fn test_log_level_from_score_log_level() { + let levels = [ + (score_log::Level::Fatal, LogLevel::Fatal), + (score_log::Level::Error, LogLevel::Error), + (score_log::Level::Warn, LogLevel::Warn), + (score_log::Level::Info, LogLevel::Info), + (score_log::Level::Debug, LogLevel::Debug), + (score_log::Level::Trace, LogLevel::Verbose), + ]; + + for (score_log_level, ffi_level) in levels.into_iter() { + assert_eq!(LogLevel::from(score_log_level), ffi_level); + } + } + + #[test] + fn test_score_log_level_from_log_level() { + let levels = [ + (score_log::Level::Fatal, LogLevel::Fatal), + (score_log::Level::Error, LogLevel::Error), + (score_log::Level::Warn, LogLevel::Warn), + (score_log::Level::Info, LogLevel::Info), + (score_log::Level::Debug, LogLevel::Debug), + (score_log::Level::Trace, LogLevel::Verbose), + ]; + + for (score_log_level, ffi_level) in levels.into_iter() { + assert_eq!(score_log::Level::from(ffi_level), score_log_level); + } + } + + #[test] + fn test_score_log_level_filter_from_log_level() { + let levels = [ + (score_log::LevelFilter::Off, LogLevel::Off), + (score_log::LevelFilter::Fatal, LogLevel::Fatal), + (score_log::LevelFilter::Error, LogLevel::Error), + (score_log::LevelFilter::Warn, LogLevel::Warn), + (score_log::LevelFilter::Info, LogLevel::Info), + (score_log::LevelFilter::Debug, LogLevel::Debug), + (score_log::LevelFilter::Trace, LogLevel::Verbose), + ]; + + for (score_log_level_filter, ffi_level) in levels.into_iter() { + assert_eq!(score_log::LevelFilter::from(ffi_level), score_log_level_filter); + } + } + + #[test] + fn test_context_single_char() { + let context = Context::from("X"); + assert_eq!(context.size, 1); + assert_eq!(context.data, [88, 0, 0, 0]); + } + + #[test] + fn test_context_four_chars() { + let context = Context::from("TEST"); + assert_eq!(context.size, 4); + assert_eq!(context.data, [84, 69, 83, 84]); + } + + #[test] + fn test_context_trimmed() { + let context = Context::from("trimmed"); + assert_eq!(context.size, 4); + assert_eq!(context.data, [116, 114, 105, 109]); + } + + #[test] + #[should_panic(expected = "Provided context contains non-ASCII characters: ")] + fn test_context_non_ascii() { + let _ = Context::from("❌"); + } +} diff --git a/score/mw/log/rust/score_log_bridge/src/lib.rs b/score/mw/log/rust/score_log_bridge/src/lib.rs new file mode 100644 index 0000000..fbec09c --- /dev/null +++ b/score/mw/log/rust/score_log_bridge/src/lib.rs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! C++-based backend for `score_log`. + +#![warn(missing_docs)] +#![warn(clippy::std_instead_of_core)] +#![warn(clippy::alloc_instead_of_core)] + +mod ffi; +mod score_logger; +mod score_logger_writer; + +pub use crate::score_logger::{ScoreLogger, ScoreLoggerBuilder}; diff --git a/score/mw/log/rust/score_log_bridge/src/score_logger.rs b/score/mw/log/rust/score_log_bridge/src/score_logger.rs new file mode 100644 index 0000000..a4fac4e --- /dev/null +++ b/score/mw/log/rust/score_log_bridge/src/score_logger.rs @@ -0,0 +1,341 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! C++-based logger implementation + +use crate::ffi::{LogLevel, LogStream, Recorder, SlotHandlePtr}; +use crate::score_logger_writer::ScoreLoggerWriter; +use score_log::fmt::{score_write, write}; +use score_log::{Log, Metadata, Record}; +use std::env::{set_var, var_os}; +use std::path::PathBuf; +use std::sync::Once; + +/// Perform layout check, panic on mismatch. +fn layout_check() { + let slot_layout_rust = SlotHandlePtr::layout_rust(); + let slot_layout_cpp = SlotHandlePtr::layout_cpp(); + if slot_layout_rust != slot_layout_cpp { + panic!("SlotHandle layout mismatch, Rust: {slot_layout_rust:?}, C++: {slot_layout_cpp:?}"); + } +} + +/// Builder for the [`ScoreLogger`]. +pub struct ScoreLoggerBuilder { + context: String, + show_module: bool, + show_file: bool, + show_line: bool, + config_path: Option, +} + +impl ScoreLoggerBuilder { + /// Create builder with default parameters. + /// + /// # Note + /// + /// This operation perform data layout check. + /// This might cause panic if layout of FFI structures is mismatched. + pub fn new() -> Self { + // Perform layout check - only once. + static LAYOUT_CHECK: Once = Once::new(); + LAYOUT_CHECK.call_once(layout_check); + + Self::default() + } + + /// Set context for the [`ScoreLogger`]. + /// + /// Only ASCII characters are allowed. + /// Max 4 characters are used. Rest of the provided string will be trimmed. + pub fn context(mut self, context: &str) -> Self { + self.context = context.to_string(); + self + } + + /// Show module name in logs. + pub fn show_module(mut self, show_module: bool) -> Self { + self.show_module = show_module; + self + } + + /// Show file name in logs. + pub fn show_file(mut self, show_file: bool) -> Self { + self.show_file = show_file; + self + } + + /// Show line number in logs. + pub fn show_line(mut self, show_line: bool) -> Self { + self.show_line = show_line; + self + } + + /// Set `MW_LOG_CONFIG_FILE` environment variable during [`Self::build`]. + /// + /// Following conditions must be met: + /// - Variable is set only during the first call to [`Self::set_as_default_logger`]. + /// - Variable is set only if not set externally. + pub fn config(mut self, config_path: PathBuf) -> Self { + self.config_path = Some(config_path); + self + } + + /// Build the [`ScoreLogger`] with provided context and configuration. + pub fn build(self) -> ScoreLogger { + let recorder = Recorder::new(); + ScoreLogger { + context: self.context, + show_module: self.show_module, + show_file: self.show_file, + show_line: self.show_line, + recorder, + } + } + + /// Build the [`ScoreLogger`] and set it as the default logger. + pub fn set_as_default_logger(self) { + // Set `MW_LOG_CONFIG_FILE`. + { + const KEY: &str = "MW_LOG_CONFIG_FILE"; + + // Set variable only if: + // - environment variable is not set + // - `config_path` is set and not empty + if var_os(KEY).is_none() { + if let Some(ref path) = self.config_path { + let path_os_str = path.as_os_str(); + if !path_os_str.is_empty() { + unsafe { set_var(KEY, path_os_str) }; + } + } + } + } + + // Build logger and set as default. + let context = self.context.clone(); + let logger = self.build(); + score_log::set_max_level(logger.log_level(&context).into()); + if let Err(e) = score_log::set_global_logger(Box::new(logger)) { + panic!("unable to set logger: {e}"); + } + } +} + +impl Default for ScoreLoggerBuilder { + fn default() -> Self { + Self { + context: "DFLT".to_string(), + show_module: false, + show_file: false, + show_line: false, + config_path: None, + } + } +} + +/// C++-based logger implementation. +pub struct ScoreLogger { + context: String, + show_module: bool, + show_file: bool, + show_line: bool, + recorder: Recorder, +} + +impl ScoreLogger { + /// Current log level for provided context. + pub(crate) fn log_level(&self, context: &str) -> LogLevel { + self.recorder.log_level(context) + } +} + +impl Log for ScoreLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + self.log_level(metadata.context()) >= metadata.level().into() + } + + fn context(&self) -> &str { + &self.context + } + + fn log(&self, record: &Record) { + // Finish early if not enabled for requested level. + let metadata = record.metadata(); + if !self.enabled(metadata) { + return; + } + + // Create log stream. + let context = metadata.context(); + let log_level = metadata.level().into(); + let log_stream = LogStream::new(&self.recorder, context, log_level); + + // Create writer. + let mut writer = ScoreLoggerWriter::new(log_stream); + + // Write module, file and line. + if self.show_module || self.show_file || self.show_line { + let _ = score_write!(&mut writer, "["); + if self.show_module { + let _ = score_write!(&mut writer, "{}:", record.module_path()); + } + if self.show_file { + let _ = score_write!(&mut writer, "{}:", record.file()); + } + if self.show_line { + let _ = score_write!(&mut writer, "{}", record.line()); + } + let _ = score_write!(&mut writer, "]"); + } + + // Write log data. + let _ = write(&mut writer, *record.args()); + // Written data is flushed on log stream drop. + } + + fn flush(&self) { + // No-op. + } +} + +// SAFETY: The underlying C++ logger is known to be thread-safe. +unsafe impl Send for ScoreLogger {} + +// SAFETY: The underlying C++ logger is known to be thread-safe. +unsafe impl Sync for ScoreLogger {} + +#[cfg(test)] +mod tests { + use crate::ScoreLoggerBuilder; + use std::env::var_os; + use std::path::PathBuf; + + #[test] + fn test_builder_new() { + let builder = ScoreLoggerBuilder::new(); + assert_eq!(builder.context, "DFLT"); + assert!(!builder.show_module); + assert!(!builder.show_file); + assert!(!builder.show_line); + assert_eq!(builder.config_path, None); + } + + #[test] + fn test_builder_default() { + let builder = ScoreLoggerBuilder::default(); + assert_eq!(builder.context, "DFLT"); + assert!(!builder.show_module); + assert!(!builder.show_file); + assert!(!builder.show_line); + assert_eq!(builder.config_path, None); + } + + #[test] + fn test_builder_context() { + let builder = ScoreLoggerBuilder::new().context("NEW_CONTEXT"); + assert_eq!(builder.context, "NEW_CONTEXT"); + assert!(!builder.show_module); + assert!(!builder.show_file); + assert!(!builder.show_line); + assert_eq!(builder.config_path, None); + } + + #[test] + fn test_builder_show_module() { + let builder = ScoreLoggerBuilder::new().show_module(true); + assert_eq!(builder.context, "DFLT"); + assert!(builder.show_module); + assert!(!builder.show_file); + assert!(!builder.show_line); + assert_eq!(builder.config_path, None); + } + + #[test] + fn test_builder_show_file() { + let builder = ScoreLoggerBuilder::new().show_file(true); + assert_eq!(builder.context, "DFLT"); + assert!(!builder.show_module); + assert!(builder.show_file); + assert!(!builder.show_line); + assert_eq!(builder.config_path, None); + } + + #[test] + fn test_builder_show_line() { + let builder = ScoreLoggerBuilder::new().show_line(true); + assert_eq!(builder.context, "DFLT"); + assert!(!builder.show_module); + assert!(!builder.show_file); + assert!(builder.show_line); + assert_eq!(builder.config_path, None); + } + + #[test] + fn test_builder_config_path() { + let builder = ScoreLoggerBuilder::new().config(PathBuf::from("/some/path")); + assert_eq!(builder.context, "DFLT"); + assert!(!builder.show_module); + assert!(!builder.show_file); + assert!(!builder.show_line); + assert_eq!(builder.config_path, Some(PathBuf::from("/some/path"))); + } + + #[test] + fn test_builder_chained() { + let builder = ScoreLoggerBuilder::new() + .context("NEW_CONTEXT") + .show_module(true) + .show_file(true) + .show_line(true) + .config(PathBuf::from("/some/path")); + assert_eq!(builder.context, "NEW_CONTEXT"); + assert!(builder.show_module); + assert!(builder.show_file); + assert!(builder.show_line); + assert_eq!(builder.config_path, Some(PathBuf::from("/some/path"))); + } + + #[test] + fn test_builder_build() { + let logger = ScoreLoggerBuilder::new() + .context("NEW_CONTEXT") + .show_module(true) + .show_file(true) + .show_line(true) + .build(); + assert_eq!(logger.context, "NEW_CONTEXT"); + assert!(logger.show_module); + assert!(logger.show_file); + assert!(logger.show_line); + } + + #[test] + fn test_builder_set_as_default_logger() { + // Pre-check `MW_LOG_CONFIG_FILE` is not set. + const KEY: &str = "MW_LOG_CONFIG_FILE"; + assert!(var_os(KEY).is_none()); + + let config_path = PathBuf::from("/some/path"); + ScoreLoggerBuilder::new() + .context("NEW_CONTEXT") + .show_module(true) + .show_file(true) + .show_line(true) + .config(config_path.clone()) + .set_as_default_logger(); + + // Check environment variable. + assert!(var_os(KEY).is_some_and(|p| p == config_path)); + } +} diff --git a/score/mw/log/rust/score_log_bridge/src/score_logger_writer.rs b/score/mw/log/rust/score_log_bridge/src/score_logger_writer.rs new file mode 100644 index 0000000..a08f256 --- /dev/null +++ b/score/mw/log/rust/score_log_bridge/src/score_logger_writer.rs @@ -0,0 +1,155 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! [`score_log::fmt::ScoreWrite`] writer implementation used by [`crate::ScoreLogger`]. + +use crate::ffi::LogStream; +use score_log::fmt::{DisplayHint, Error, FormatSpec, Result as FmtResult, ScoreWrite}; + +/// [`score_log::fmt::ScoreWrite`] writer implementation used by [`crate::ScoreLogger`]. +/// Adds values to the log stream with selected formatting. +pub(crate) struct ScoreLoggerWriter<'a> { + log_stream: LogStream<'a>, +} + +impl<'a> ScoreLoggerWriter<'a> { + pub fn new(log_stream: LogStream<'a>) -> Self { + Self { log_stream } + } +} + +impl ScoreWrite for ScoreLoggerWriter<'_> { + fn write_bool(&mut self, v: &bool, _spec: &FormatSpec) -> FmtResult { + self.log_stream.log_bool(v); + Ok(()) + } + + fn write_f32(&mut self, v: &f32, _spec: &FormatSpec) -> FmtResult { + self.log_stream.log_f32(v); + Ok(()) + } + + fn write_f64(&mut self, v: &f64, _spec: &FormatSpec) -> FmtResult { + self.log_stream.log_f64(v); + Ok(()) + } + + fn write_i8(&mut self, v: &i8, spec: &FormatSpec) -> FmtResult { + match spec.get_display_hint() { + DisplayHint::NoHint => self.log_stream.log_i8(v), + DisplayHint::LowerHex | DisplayHint::UpperHex => { + let v_u8 = unsafe { core::mem::transmute(v) }; + self.log_stream.log_hex8(v_u8); + }, + DisplayHint::Binary => { + let v_u8 = unsafe { core::mem::transmute(v) }; + self.log_stream.log_bin8(v_u8); + }, + _ => return Err(Error), + } + Ok(()) + } + + fn write_i16(&mut self, v: &i16, spec: &FormatSpec) -> FmtResult { + match spec.get_display_hint() { + DisplayHint::NoHint => self.log_stream.log_i16(v), + DisplayHint::LowerHex | DisplayHint::UpperHex => { + let v_u16 = unsafe { core::mem::transmute(v) }; + self.log_stream.log_hex16(v_u16); + }, + DisplayHint::Binary => { + let v_u16 = unsafe { core::mem::transmute(v) }; + self.log_stream.log_bin16(v_u16); + }, + _ => return Err(Error), + } + Ok(()) + } + + fn write_i32(&mut self, v: &i32, spec: &FormatSpec) -> FmtResult { + match spec.get_display_hint() { + DisplayHint::NoHint => self.log_stream.log_i32(v), + DisplayHint::LowerHex | DisplayHint::UpperHex => { + let v_u32 = unsafe { core::mem::transmute(v) }; + self.log_stream.log_hex32(v_u32); + }, + DisplayHint::Binary => { + let v_u32 = unsafe { core::mem::transmute(v) }; + self.log_stream.log_bin32(v_u32); + }, + _ => return Err(Error), + } + Ok(()) + } + + fn write_i64(&mut self, v: &i64, spec: &FormatSpec) -> FmtResult { + match spec.get_display_hint() { + DisplayHint::NoHint => self.log_stream.log_i64(v), + DisplayHint::LowerHex | DisplayHint::UpperHex => { + let v_u64 = unsafe { core::mem::transmute(v) }; + self.log_stream.log_hex64(v_u64); + }, + DisplayHint::Binary => { + let v_u64 = unsafe { core::mem::transmute(v) }; + self.log_stream.log_bin64(v_u64); + }, + _ => return Err(Error), + } + Ok(()) + } + + fn write_u8(&mut self, v: &u8, spec: &FormatSpec) -> FmtResult { + match spec.get_display_hint() { + DisplayHint::NoHint => self.log_stream.log_u8(v), + DisplayHint::LowerHex | DisplayHint::UpperHex => self.log_stream.log_hex8(v), + DisplayHint::Binary => self.log_stream.log_bin8(v), + _ => return Err(Error), + } + Ok(()) + } + + fn write_u16(&mut self, v: &u16, spec: &FormatSpec) -> FmtResult { + match spec.get_display_hint() { + DisplayHint::NoHint => self.log_stream.log_u16(v), + DisplayHint::LowerHex | DisplayHint::UpperHex => self.log_stream.log_hex16(v), + DisplayHint::Binary => self.log_stream.log_bin16(v), + _ => return Err(Error), + } + Ok(()) + } + + fn write_u32(&mut self, v: &u32, spec: &FormatSpec) -> FmtResult { + match spec.get_display_hint() { + DisplayHint::NoHint => self.log_stream.log_u32(v), + DisplayHint::LowerHex | DisplayHint::UpperHex => self.log_stream.log_hex32(v), + DisplayHint::Binary => self.log_stream.log_bin32(v), + _ => return Err(Error), + } + Ok(()) + } + + fn write_u64(&mut self, v: &u64, spec: &FormatSpec) -> FmtResult { + match spec.get_display_hint() { + DisplayHint::NoHint => self.log_stream.log_u64(v), + DisplayHint::LowerHex | DisplayHint::UpperHex => self.log_stream.log_hex64(v), + DisplayHint::Binary => self.log_stream.log_bin64(v), + _ => return Err(Error), + } + Ok(()) + } + + fn write_str(&mut self, v: &str, _spec: &FormatSpec) -> FmtResult { + self.log_stream.log_string(v); + Ok(()) + } +}