gravel_ffi/
logging.rs

1//! Plumbing for using the [`log`] crate across FFI-boundaries.
2//!
3//! By setting up a chain from [`ForwardLogger`] => [`LogTarget`]
4//! => [`Log`], a dynamically loaded library can effectively log
5//! to the calling library's logging implementation.
6//!
7//! ### Usage
8//! - Set up logging as usual
9//! - Construct a [`StaticLogTarget`] in the same library
10//! - Pass the [`LogTarget`] through FFI
11//! - Register a [`ForwardLogger`] in the dynamic library
12//! - Use the [`log`] crate as usual
13//!
14//! Note: The max logging level is only set once, when
15//! the [`ForwardLogger`] is registered.
16
17use abi_stable::{sabi_trait, std_types::RBox};
18use log::{Log, Metadata, Record};
19use log_types::{RLevelFilter, RMetadata, RRecord};
20
21/// FFI-safe [`LogTarget`] trait object.
22pub type BoxDynLogTarget = LogTarget_TO<'static, RBox<()>>;
23
24/// FFI-safe version of [`Log`].
25#[sabi_trait]
26pub trait LogTarget: Send + Sync {
27	fn enabled(&self, metadata: RMetadata<'_>) -> bool;
28	fn log(&self, record: RRecord<'_>);
29	fn flush(&self);
30	fn max_level(&self) -> RLevelFilter;
31}
32
33/// [`LogTarget`] that forwards calls to the statically registered
34/// logger of the library it was constructed in.
35pub struct StaticLogTarget;
36
37impl StaticLogTarget {
38	pub fn get() -> BoxDynLogTarget {
39		BoxDynLogTarget::from_value(Self, sabi_trait::TD_Opaque)
40	}
41}
42
43impl LogTarget for StaticLogTarget {
44	fn enabled(&self, metadata: RMetadata<'_>) -> bool {
45		log::logger().enabled(&metadata.into())
46	}
47
48	fn log(&self, record: RRecord<'_>) {
49		record.log(log::logger());
50	}
51
52	fn flush(&self) {
53		log::logger().flush();
54	}
55
56	fn max_level(&self) -> RLevelFilter {
57		log::max_level().into()
58	}
59}
60
61/// [`LogTarget`] that does nothing.
62pub struct NoOpLogTarget;
63
64impl NoOpLogTarget {
65	pub fn get() -> BoxDynLogTarget {
66		BoxDynLogTarget::from_value(Self, sabi_trait::TD_Opaque)
67	}
68}
69
70impl LogTarget for NoOpLogTarget {
71	fn enabled(&self, _metadata: RMetadata<'_>) -> bool {
72		false
73	}
74
75	fn log(&self, _record: RRecord<'_>) {}
76
77	fn flush(&self) {}
78
79	fn max_level(&self) -> RLevelFilter {
80		RLevelFilter::Off
81	}
82}
83
84/// Logger that forwards calls to a [`LogTarget`].
85pub struct ForwardLogger {
86	target: BoxDynLogTarget,
87}
88
89impl ForwardLogger {
90	/// # Panics
91	/// When called more than once. See [`log::set_logger`].
92	pub fn register(target: BoxDynLogTarget, crate_name: &'static str) {
93		log::set_max_level(target.max_level().into());
94		log::set_logger(Box::leak(Box::new(Self { target }))).expect("logger must only be registered once per plugin");
95
96		log::trace!("initialized log forwarding for dynamic library '{crate_name}'");
97	}
98}
99
100impl Log for ForwardLogger {
101	fn enabled(&self, metadata: &Metadata<'_>) -> bool {
102		self.target.enabled(metadata.into())
103	}
104
105	fn log(&self, record: &Record<'_>) {
106		self.target.log(record.into());
107	}
108
109	fn flush(&self) {
110		self.target.flush();
111	}
112}
113
114/// Contains FFI-safe versions and associated conversions for [`log`] types.
115mod log_types {
116	use abi_stable::StableAbi;
117	use abi_stable::std_types::{ROption, RStr, RString};
118	use abi_stable::traits::{IntoReprC, IntoReprRust};
119	use log::{Level, LevelFilter, Log, Metadata, Record, RecordBuilder};
120	use std::fmt::Arguments;
121
122	/// FFI-safe version of [`log::Metadata`].
123	#[repr(C)]
124	#[derive(StableAbi)]
125	pub struct RMetadata<'a> {
126		level: RLevel,
127		target: RStr<'a>,
128	}
129
130	impl<'a> From<&Metadata<'a>> for RMetadata<'a> {
131		fn from(value: &Metadata<'a>) -> Self {
132			Self {
133				level: value.level().into(),
134				target: value.target().into_c(),
135			}
136		}
137	}
138
139	impl<'a> From<RMetadata<'a>> for Metadata<'a> {
140		fn from(value: RMetadata<'a>) -> Self {
141			Metadata::builder()
142				.level(value.level.into())
143				.target(value.target.as_str())
144				.build()
145		}
146	}
147
148	/// FFI-safe version of [`log::LevelFilter`].
149	#[repr(u8)]
150	#[derive(StableAbi, Clone, Copy)]
151	pub enum RLevelFilter {
152		Off,
153		Error,
154		Warn,
155		Info,
156		Debug,
157		Trace,
158	}
159
160	impl From<LevelFilter> for RLevelFilter {
161		fn from(value: LevelFilter) -> Self {
162			match value {
163				LevelFilter::Off => Self::Off,
164				LevelFilter::Error => Self::Error,
165				LevelFilter::Warn => Self::Warn,
166				LevelFilter::Info => Self::Info,
167				LevelFilter::Debug => Self::Debug,
168				LevelFilter::Trace => Self::Trace,
169			}
170		}
171	}
172
173	impl From<RLevelFilter> for LevelFilter {
174		fn from(value: RLevelFilter) -> Self {
175			match value {
176				RLevelFilter::Off => Self::Off,
177				RLevelFilter::Error => Self::Error,
178				RLevelFilter::Warn => Self::Warn,
179				RLevelFilter::Info => Self::Info,
180				RLevelFilter::Debug => Self::Debug,
181				RLevelFilter::Trace => Self::Trace,
182			}
183		}
184	}
185
186	/// FFI-safe version of [`log::Level`].
187	#[repr(u8)]
188	#[derive(StableAbi, Clone, Copy)]
189	pub enum RLevel {
190		Error,
191		Warn,
192		Info,
193		Debug,
194		Trace,
195	}
196
197	impl From<Level> for RLevel {
198		fn from(value: Level) -> Self {
199			match value {
200				Level::Error => Self::Error,
201				Level::Warn => Self::Warn,
202				Level::Info => Self::Info,
203				Level::Debug => Self::Debug,
204				Level::Trace => Self::Trace,
205			}
206		}
207	}
208
209	impl From<RLevel> for Level {
210		fn from(value: RLevel) -> Self {
211			match value {
212				RLevel::Error => Self::Error,
213				RLevel::Warn => Self::Warn,
214				RLevel::Info => Self::Info,
215				RLevel::Debug => Self::Debug,
216				RLevel::Trace => Self::Trace,
217			}
218		}
219	}
220
221	/// FFI-safe version of [`log::Record`].
222	#[repr(C)]
223	#[derive(StableAbi)]
224	pub struct RRecord<'a> {
225		// you can do this without allocations using DynTrait,
226		// but I found it's a few hundred nanoseconds slower overall
227		args: RString,
228		metadata: RMetadata<'a>,
229		module_path: ROption<RStr<'a>>,
230		file: ROption<RStr<'a>>,
231		line: ROption<u32>,
232	}
233
234	impl<'a> From<&'a Record<'a>> for RRecord<'a> {
235		fn from(value: &'a Record<'a>) -> Self {
236			Self {
237				args: value.args().to_string().into_c(),
238				metadata: value.metadata().into(),
239				module_path: value.module_path().map(IntoReprC::into_c).into_c(),
240				file: value.file().map(IntoReprC::into_c).into_c(),
241				line: value.line().into_c(),
242			}
243		}
244	}
245
246	impl RRecord<'_> {
247		pub fn log(self, logger: &dyn Log) {
248			fn inner<'a>(logger: &dyn Log, args: Arguments<'a>, mut builder: RecordBuilder<'a>) {
249				logger.log(&builder.args(args).build());
250			}
251
252			let mut builder = Record::builder();
253
254			builder
255				.metadata(self.metadata.into())
256				.module_path(self.module_path.map(IntoReprRust::into_rust).into_rust())
257				.file(self.file.map(IntoReprRust::into_rust).into_rust())
258				.line(self.line.into_rust());
259
260			// we have do do this song and dance to make format_args happy
261			inner(logger, format_args!("{}", self.args), builder);
262		}
263	}
264}