diff --git a/modules/logger/telegraf-rs/src/lib.rs b/modules/logger/telegraf-rs/src/lib.rs index eceede51e3182c94c5deeaa5d5891998b52e63f0..fce30d9964f648b53b11c54e7ab8d229616370b2 100644 --- a/modules/logger/telegraf-rs/src/lib.rs +++ b/modules/logger/telegraf-rs/src/lib.rs @@ -15,30 +15,20 @@ // along with this program; if not, write to the Free Software Foundation, // Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. -use std::{cell::UnsafeCell, ffi::CStr, sync::Mutex}; -use telegraf::{Client, IntoFieldData, Point}; -use vlcrs_core::tracer::{sys::vlc_tracer_value_type, TracerCapability, TracerModuleLoader}; +use std::{cell::UnsafeCell, sync::Mutex}; +use telegraf::{Client, IntoFieldData}; +use vlcrs_core::tracer::{TraceValue, TracerCapability, TracerModuleLoader}; use vlcrs_macros::module; -struct TraceField(vlcrs_core::tracer::TraceField); +struct TraceValueWrapper<'a>(TraceValue<'a>); -impl IntoFieldData for TraceField { +impl<'a> IntoFieldData for TraceValueWrapper<'a> { fn field_data(&self) -> telegraf::FieldData { - match self.0.kind() { - vlc_tracer_value_type::String => unsafe { - let value = CStr::from_ptr(self.0.value().string); - telegraf::FieldData::Str(value.to_str().unwrap().to_string()) - }, - vlc_tracer_value_type::Integer => unsafe { - telegraf::FieldData::Number(self.0.value().integer) - }, - vlc_tracer_value_type::Unsigned => unsafe { - telegraf::FieldData::UNumber(self.0.value().unsigned) - }, - vlc_tracer_value_type::Double => unsafe { - telegraf::FieldData::Float(self.0.value().double) - }, - _ => unreachable!(), + match self.0 { + TraceValue::String(value) => telegraf::FieldData::Str(String::from(value)), + TraceValue::Integer(value) => telegraf::FieldData::Number(value), + TraceValue::Unsigned(value) => telegraf::FieldData::UNumber(value), + TraceValue::Double(value) => telegraf::FieldData::Float(value), } } } @@ -61,43 +51,34 @@ impl TracerCapability for TelegrafTracer { Some(Self { endpoint }) } - fn trace(&self, _tick: vlcrs_core::tracer::Tick, entries: &'_ vlcrs_core::tracer::Trace) { - if !entries - .into_iter() - .any(|e| e.kind() != vlcrs_core::tracer::sys::vlc_tracer_value_type::String) - { + fn trace(&self, _tick: vlcrs_core::tracer::Tick, trace: &vlcrs_core::tracer::Trace) { + let (tags, records) = trace.entries().fold( + (Vec::new(), Vec::new()), + |(mut tags, mut records), entry| { + let name = String::from(entry.key); + if let TraceValue::String(value) = entry.value { + let value = String::from(value); + tags.push(telegraf::protocol::Tag { name, value }); + } else { + let value = TraceValueWrapper(entry.value).field_data(); + records.push(telegraf::protocol::Field { name, value }); + } + + (tags, records) + }, + ); + + if records.is_empty() { /* We cannot support events for now. */ return; } - let tags: Vec<(String, String)> = entries - .into_iter() - .filter(|e| e.kind() == vlcrs_core::tracer::sys::vlc_tracer_value_type::String) - .map(|entry| unsafe { - let value = CStr::from_ptr(entry.value().string); - ( - String::from(entry.key()), - String::from(value.to_str().unwrap()), - ) - }) - .collect(); - let record: Vec<(String, Box<dyn IntoFieldData + 'static>)> = entries - .into_iter() - .filter(|e| e.kind() != vlcrs_core::tracer::sys::vlc_tracer_value_type::String) - .map(|entry| { - ( - String::from(entry.key()), - Box::new(TraceField(entry)) as Box<dyn IntoFieldData>, - ) - }) - .collect(); - - let p = Point::new( - String::from("measurement"), + let p = telegraf::Point { + measurement: String::from("measurement"), tags, - record, - None, //Some(tick.0 as u64), - ); + fields: records, + timestamp: None, //Some(tick.0 as u64), + }; let mut endpoint = self.endpoint.lock().unwrap(); if let Err(err) = endpoint.get_mut().write_point(&p) { diff --git a/src/rust/vlcrs-core/src/convert.rs b/src/rust/vlcrs-core/src/convert.rs new file mode 100644 index 0000000000000000000000000000000000000000..864f33dae9e406512adbea62b54d6ffb5e3c19bc --- /dev/null +++ b/src/rust/vlcrs-core/src/convert.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2025 Alaric Senat <alaric@videolabs.io> +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + +/// Unchecked conversions on internal VLC values. +/// +/// VLC core APIs often have implicit constraints or internal check on exposed data that allow +/// conversions without the usual safety checks. +/// +/// Implementers of this trait must ensure that the conversion is safe within the context of +/// VLC, and new implementations must be thoroughly reviewed. +/// +/// # Safety +/// +/// Calling `assume_valid` is **always unsafe**, as it bypasses Rust's usual validity checks. +/// The caller must ensure that the input is indeed valid and checked in some way beforehand by VLC. +/// +/// # Examples +/// +/// [`AssumeValid`]`<&`[`str`](std::str)`>` is implemented for [`CStr`](std::ffi::CStr) as the internals of +/// VLC manipulate already checked UTF8 strings. This lift the bindings from the obligation to +/// perform UTF8 validity verifications: +/// +/// ``` +/// extern "C" fn tracer_trace(trace: *const std::ffi::c_char) { +/// // `trace` is from VLC and is assumed to be valid UTF8. +/// let trace: &str = unsafe { std::ffi::CStr::from_ptr(trace).assume_valid() }; +/// } +/// ``` +pub trait AssumeValid<T> { + /// Converts `self` into `T`, without safety checks. + unsafe fn assume_valid(self) -> T; +} + +impl<'a> AssumeValid<&'a str> for &'a std::ffi::CStr { + /// Convert a [`CStr`](std::ffi::CStr) into an `&`[`str`](std::str). + /// + /// # Safety + /// + /// Should only be used with VLC strings which are already UTF8 validated. Otherwise, the + /// string will be left unchecked and will lead to undefined behaviors. + /// + /// # Panics + /// + /// In **debug builds only**, this function will perform the UTF8 checks and panic on failure to + /// highlight and help resolve VLC core missing checks. + unsafe fn assume_valid(self) -> &'a str { + if cfg!(debug_assertions) { + str::from_utf8(self.to_bytes()).expect("Unexpected invalid UTF8 coming from VLC") + } else { + unsafe { str::from_utf8_unchecked(self.to_bytes()) } + } + } +} diff --git a/src/rust/vlcrs-core/src/lib.rs b/src/rust/vlcrs-core/src/lib.rs index 90950409bca3b4d9cbaa971ae5927499bb8bb70c..5f9a99a2c4b09e8db9e606b39cc533c46d177958 100644 --- a/src/rust/vlcrs-core/src/lib.rs +++ b/src/rust/vlcrs-core/src/lib.rs @@ -34,3 +34,5 @@ pub mod plugin; pub mod object; pub mod tracer; + +pub(crate) mod convert; diff --git a/src/rust/vlcrs-core/src/tracer/mod.rs b/src/rust/vlcrs-core/src/tracer/mod.rs index 48b3c18b34affe4e9c1ce46c837d00c9eb03af25..d0656ae07e4ec07fb850954f463e8cbd98ac3298 100644 --- a/src/rust/vlcrs-core/src/tracer/mod.rs +++ b/src/rust/vlcrs-core/src/tracer/mod.rs @@ -1,19 +1,20 @@ -/// SPDX-License-Identifier: LGPL-2.1-or-later -/// Copyright (C) 2024-2025 Alexandre Janniaux <ajanni@videolabs.io> -/// -/// This program is free software; you can redistribute it and/or modify it -/// under the terms of the GNU Lesser General Public License as published by -/// the Free Software Foundation; either version 2.1 of the License, or -/// (at your option) any later version. -/// -/// This program is distributed in the hope that it will be useful, -/// but WITHOUT ANY WARRANTY; without even the implied warranty of -/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -/// GNU Lesser General Public License for more details. -/// -/// You should have received a copy of the GNU Lesser General Public License -/// along with this program; if not, write to the Free Software Foundation, -/// Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024-2025 Alexandre Janniaux <ajanni@videolabs.io> +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + use std::{ cell::UnsafeCell, ffi::{c_char, c_void, CStr}, @@ -21,10 +22,9 @@ use std::{ ptr::NonNull, }; -use crate::{object::Object, plugin::ModuleProtocol}; +use crate::{convert::AssumeValid, object::Object, plugin::ModuleProtocol}; -pub mod sys; -pub use sys::{Trace, TraceField}; +mod sys; pub struct Tick(pub i64); @@ -53,7 +53,7 @@ pub struct Tick(pub i64); /// Some(Self{ last_trace_tick: Cell::from(Tick(0)) }) /// } /// -/// fn trace(&self, tick: Tick, entries: &Trace) { +/// fn trace(&self, tick: Tick, trace: &Trace) { /// let mut state = self.last_trace_tick.get_mut(); /// *state = tick; /// } @@ -83,7 +83,7 @@ pub trait TracerCapability: Sync { where Self: Sized; - fn trace(&self, tick: Tick, entries: &Trace); + fn trace(&self, tick: Tick, trace: &Trace); } #[allow(non_camel_case_types)] @@ -93,11 +93,16 @@ pub type TracerCapabilityActivate = opaque: &mut MaybeUninit<*mut c_void>, ) -> Option<&'static sys::vlc_tracer_operations>; -extern "C" fn tracer_trace(opaque: *const c_void, tick: sys::vlc_tick, entries: sys::Trace) { +extern "C" fn tracer_trace( + opaque: *const c_void, + tick: sys::vlc_tick, + trace: NonNull<sys::vlc_tracer_trace>, +) { { let tracer: &dyn TracerCapability = unsafe { &**(opaque as *const Box<dyn TracerCapability>) }; - tracer.trace(Tick(tick), &entries); + let trace = Trace(trace); + tracer.trace(Tick(tick), &trace); } } @@ -144,7 +149,7 @@ extern "C" fn activate_tracer<T: TracerCapability>( /// fn open(obj: &mut Object) -> Option<impl TracerCapability> { /// Some(Self{}) /// } -/// fn trace(&self, _tick: Tick, _entries: &Trace) {} +/// fn trace(&self, _tick: Tick, _trace: &Trace) {} /// } /// /// module!{ @@ -197,14 +202,14 @@ impl Tracer { /// /// Register the new point at time [tick] with the metadata [entries]. /// - pub fn trace(&self, tick: Tick, entries: Trace) { + pub fn trace(&self, tick: Tick, trace: Trace) { unsafe { // SAFETY: TODO let tracer = *self.tracer.get(); // SAFETY: the pointer `tracer` is guaranteed to be non-null and // nobody else has reference to it. - sys::vlc_tracer_TraceWithTs(tracer, tick.0, entries); + sys::vlc_tracer_TraceWithTs(tracer, tick.0, trace.0); } } } @@ -213,3 +218,138 @@ impl Tracer { macro_rules! trace { ($tracer: ident, $ts: expr, $($key:ident = $value:expr)*) => {}; } + +/// A trace record obtained from VLC. +/// +/// Trace records are a set of `entries`, a list of key-values describing the traced event. +/// +/// # Examples +/// +/// ``` +/// impl TracerCapability for T { +/// fn trace(&self, trace: &Trace) { +/// for entry in trace.entries() { +/// println!("{}", entry.key); +/// } +/// } +/// } +/// ``` +#[derive(PartialEq, Copy, Clone)] +#[repr(transparent)] +pub struct Trace(NonNull<sys::vlc_tracer_trace>); + +impl Trace { + /// Get an iterator over the trace entries. + pub fn entries(&self) -> TraceIterator { + self.into_iter() + } +} + +impl<'a> IntoIterator for &'a Trace { + type Item = TraceEntry<'a>; + type IntoIter = TraceIterator<'a>; + fn into_iter(self) -> Self::IntoIter { + TraceIterator { + current_field: unsafe { self.0.read().entries }, + _plt: std::marker::PhantomData, + } + } +} + +/// Iterate over trace record entries. +pub struct TraceIterator<'a> { + current_field: NonNull<sys::vlc_tracer_entry>, + _plt: std::marker::PhantomData<&'a sys::vlc_tracer_entry>, +} + +impl<'a> Iterator for TraceIterator<'a> { + type Item = TraceEntry<'a>; + + fn next(&mut self) -> Option<Self::Item> { + unsafe { + if self.current_field.read().key.is_null() { + return None; + } + let output = Some(TraceEntry::from(self.current_field.read())); + self.current_field = self.current_field.add(1); + output + } + } +} + +/// A key-value pair recorded in a trace event. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TraceEntry<'a> { + pub key: &'a str, + pub value: TraceValue<'a>, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TraceValue<'a> { + Integer(i64), + Double(f64), + String(&'a str), + Unsigned(u64), +} + +impl<'a> From<sys::vlc_tracer_entry> for TraceEntry<'a> { + fn from(entry: sys::vlc_tracer_entry) -> Self { + // SAFETY: Key is guaranteed to be non-null by the iterator. + let key = unsafe { CStr::from_ptr(entry.key).assume_valid() }; + + // SAFETY: Union accesses are only made with the associated entry tag. + let value = unsafe { + match entry.kind { + sys::vlc_tracer_value_type::Integer => TraceValue::Integer(entry.value.integer), + sys::vlc_tracer_value_type::Double => TraceValue::Double(entry.value.double), + sys::vlc_tracer_value_type::String => { + TraceValue::String(CStr::from_ptr(entry.value.string).assume_valid()) + } + sys::vlc_tracer_value_type::Unsigned => TraceValue::Unsigned(entry.value.unsigned), + } + }; + + Self { key, value } + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_trace_interop() { + use super::*; + use sys::*; + let entries = [ + vlc_tracer_entry { + kind: vlc_tracer_value_type::String, + value: vlc_tracer_value { + string: c"value1".as_ptr(), + }, + key: c"test1".as_ptr(), + }, + vlc_tracer_entry { + kind: vlc_tracer_value_type::Integer, + value: vlc_tracer_value { integer: 0 }, + key: std::ptr::null(), + }, + ]; + + let trace_field = TraceEntry::from(entries[0]); + assert_eq!(trace_field.key, "test1"); + assert_eq!(trace_field.value, TraceValue::String("value1")); + + let trace = vlc_tracer_trace { + entries: NonNull::from(&entries[0]), + }; + + let trace = Trace(NonNull::from(&trace)); + + let mut iterator = trace.entries(); + + let first = iterator.next().expect("First field must be valid"); + assert_eq!(first.value, TraceValue::String("value1")); + assert_eq!(first.key, "test1"); + + assert_eq!(iterator.next(), None); + } +} diff --git a/src/rust/vlcrs-core/src/tracer/sys.rs b/src/rust/vlcrs-core/src/tracer/sys.rs index 7ddabf0dcafdc98f42a0caa63068a4e99d1d157b..4501bac5f23884dd1bc3eed111b2b2a4761e67de 100644 --- a/src/rust/vlcrs-core/src/tracer/sys.rs +++ b/src/rust/vlcrs-core/src/tracer/sys.rs @@ -1,27 +1,28 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024-2025 Alexandre Janniaux <ajanni@videolabs.io> +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + #![allow(rustdoc::bare_urls)] #![allow(rustdoc::broken_intra_doc_links)] #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] -/// SPDX-License-Identifier: LGPL-2.1-or-later -/// Copyright (C) 2024-2025 Alexandre Janniaux <ajanni@videolabs.io> -/// -/// This program is free software; you can redistribute it and/or modify it -/// under the terms of the GNU Lesser General Public License as published by -/// the Free Software Foundation; either version 2.1 of the License, or -/// (at your option) any later version. -/// -/// This program is distributed in the hope that it will be useful, -/// but WITHOUT ANY WARRANTY; without even the implied warranty of -/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -/// GNU Lesser General Public License for more details. -/// -/// You should have received a copy of the GNU Lesser General Public License -/// along with this program; if not, write to the Free Software Foundation, -/// Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. use std::{ - ffi::{c_char, c_double, c_void, CStr}, + ffi::{c_char, c_double, c_void}, ptr::NonNull, }; @@ -68,128 +69,18 @@ pub struct vlc_tracer { _unused: [u8; 0], } -#[derive(PartialEq, Copy, Clone)] -#[repr(transparent)] -pub struct Trace { - pub entries: NonNull<vlc_tracer_trace>, -} - -#[derive(PartialEq, Copy, Clone, Debug)] -#[repr(transparent)] -pub struct TraceField { - pub entry: NonNull<vlc_tracer_entry>, -} - -impl TraceField { - pub fn key(&self) -> &str { - unsafe { - let key = CStr::from_ptr(self.entry.read().key); - key.to_str().unwrap() - } - } - - pub fn kind(&self) -> vlc_tracer_value_type { - unsafe { self.entry.read().kind } - } - - pub fn value(&self) -> vlc_tracer_value { - unsafe { self.entry.read().value } - } -} - -pub struct TraceIterator { - current_field: NonNull<vlc_tracer_entry>, -} - -impl IntoIterator for Trace { - type Item = TraceField; - type IntoIter = TraceIterator; - fn into_iter(self) -> Self::IntoIter { - TraceIterator { - current_field: unsafe { self.entries.read().entries }, - } - } -} - -impl Iterator for TraceIterator { - type Item = TraceField; - - fn next(&mut self) -> Option<Self::Item> { - unsafe { - if self.current_field.read().key.is_null() { - return None; - } - let output = Some(TraceField { - entry: self.current_field, - }); - self.current_field = self.current_field.add(1); - output - } - } -} - #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct vlc_tracer_operations { - pub trace: extern "C" fn(opaque: *const c_void, ts: vlc_tick, entries: Trace), + pub trace: extern "C" fn(opaque: *const c_void, ts: vlc_tick, trace: NonNull<vlc_tracer_trace>), pub destroy: extern "C" fn(*mut c_void), } extern "C" { pub fn vlc_tracer_Create(parent: &Object, name: *const c_char) -> Option<NonNull<vlc_tracer>>; - - pub fn vlc_tracer_TraceWithTs(tracer: NonNull<vlc_tracer>, tick: vlc_tick, entries: Trace); -} - -#[cfg(test)] -mod test { - #[test] - fn test_trace_interop() { - use super::*; - let entries = [ - vlc_tracer_entry { - kind: vlc_tracer_value_type::String, - value: vlc_tracer_value { - string: c"value1".as_ptr(), - }, - key: c"test1".as_ptr(), - }, - vlc_tracer_entry { - kind: vlc_tracer_value_type::Integer, - value: vlc_tracer_value { integer: 0 }, - key: std::ptr::null(), - }, - ]; - - let trace_field = TraceField { - entry: NonNull::from(&entries[0]), - }; - assert_eq!(trace_field.kind(), vlc_tracer_value_type::String); - assert_eq!(trace_field.key(), "test1"); - - let trace_field = TraceField { - entry: NonNull::from(&entries[1]), - }; - assert_eq!(trace_field.kind(), vlc_tracer_value_type::Integer); - - let trace = vlc_tracer_trace { - entries: NonNull::from(&entries[0]), - }; - - let trace = Trace { - entries: NonNull::from(&trace), - }; - - let mut iterator = trace.into_iter(); - - let first = iterator.next().expect("First field must be valid"); - assert_eq!(first.kind(), vlc_tracer_value_type::String); - assert_eq!(first.key(), "test1"); - unsafe { - assert_eq!(CStr::from_ptr(first.value().string), c"value1"); - } - - let second = iterator.next(); - assert_eq!(second, None); - } + pub fn vlc_tracer_TraceWithTs( + tracer: NonNull<vlc_tracer>, + tick: vlc_tick, + trace: NonNull<vlc_tracer_trace>, + ); }