pmpx_engine/log.rs
1#![allow(unsafe_code)] // the two C callbacks the host exports: this module is an ABI boundary
2//! The host's logging hooks: the one channel that goes **into** a plugin.
3//!
4//! A plugin cannot print usefully by itself -- it does not know the id pmpx shows for it, or whether
5//! the person asked for detail -- so it calls the hooks here and the host decides. What it decides is
6//! an [`Event`]: the text and the level, with nothing about colour, prefixes or destinations. That
7//! belongs to whoever is showing it.
8//!
9//! # Why the events are deferred
10//!
11//! The callback is a plain C function pointer: it gets a level and a string, and no way to reach a
12//! caller's closure. So messages are queued on the calling thread and drained into the event sink once
13//! the call returns -- which also means the queue needs no `unsafe` pointer to a borrowed sink.
14
15use std::cell::RefCell;
16use std::sync::atomic::{AtomicU32, Ordering};
17
18use pmpx_plugin::abi::{
19 PmpxHost, PmpxLog, PmpxStr, PMPX_LEVEL_DEBUG, PMPX_LEVEL_ERROR, PMPX_LEVEL_WARN,
20};
21
22use crate::Event;
23
24thread_local! {
25 /// Messages a plugin produced during the call on this thread.
26 static QUEUED: RefCell<Vec<Event>> = const { RefCell::new(Vec::new()) };
27}
28
29/// The level this run installed, so the host's capability lookup can answer with the matching table.
30static LEVEL: AtomicU32 = AtomicU32::new(PMPX_LEVEL_WARN);
31
32/// What a plugin's `debug!` reaches, when the trace is on: everything, including `debug!`.
33static LOG_TRACE: PmpxLog = PmpxLog {
34 size: std::mem::size_of::<PmpxLog>(),
35 write: log,
36 max_level: PMPX_LEVEL_DEBUG,
37};
38
39/// A normal run: warnings and errors, no notes.
40static LOG_NORMAL: PmpxLog = PmpxLog {
41 size: std::mem::size_of::<PmpxLog>(),
42 write: log,
43 max_level: PMPX_LEVEL_WARN,
44};
45
46/// `--quiet`: only errors.
47static LOG_QUIET: PmpxLog = PmpxLog {
48 size: std::mem::size_of::<PmpxLog>(),
49 write: log,
50 max_level: PMPX_LEVEL_ERROR,
51};
52
53/// How much of a plugin's output a run wants.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55pub struct Levels {
56 /// Only errors.
57 pub quiet: bool,
58 /// Everything, including notes and detail.
59 pub trace: bool,
60}
61
62impl Levels {
63 /// The level this combination installs.
64 ///
65 /// `trace` wins over `quiet`: asking for a trace is the more explicit of the two requests, and a
66 /// plugin's lines are part of that trace. `quiet` on its own still silences a plugin's notes and
67 /// warnings, leaving only its errors.
68 pub const fn max_level(self) -> u32 {
69 if self.trace {
70 PMPX_LEVEL_DEBUG
71 } else if self.quiet {
72 PMPX_LEVEL_ERROR
73 } else {
74 PMPX_LEVEL_WARN
75 }
76 }
77}
78
79/// The host's capability lookup: the one thing a plugin may ask for.
80///
81/// # Safety
82/// `name` must be valid for the duration of the call; the returned pointer is to one of the `'static`
83/// tables above.
84unsafe extern "C" fn capability(name: PmpxStr) -> *const std::ffi::c_void {
85 // SAFETY: the plugin passes a borrow that outlives the call.
86 let bytes = unsafe { name.as_bytes() }.unwrap_or(&[]);
87 if bytes != pmpx_plugin::abi::PMPX_CAP_LOG.as_bytes() {
88 // A capability this host does not have is "not here", which keeps an unknown one from being an
89 // error.
90 return std::ptr::null();
91 }
92
93 let table = match LEVEL.load(Ordering::Acquire) {
94 PMPX_LEVEL_DEBUG => &LOG_TRACE,
95 PMPX_LEVEL_ERROR => &LOG_QUIET,
96 _ => &LOG_NORMAL,
97 };
98 std::ptr::from_ref(table).cast()
99}
100
101/// The host's table, handed to a plugin once per load.
102static HOST: PmpxHost = PmpxHost {
103 abi_major: pmpx_plugin::abi::PMPX_ABI_MAJOR,
104 size: std::mem::size_of::<PmpxHost>(),
105 capability,
106};
107
108/// The hooks to install for this run.
109///
110/// Three tables rather than one mutable: the level is decided once, at startup, and the table a plugin
111/// reads `max_level` from has to be right when it is attached.
112pub fn hooks(levels: Levels) -> &'static PmpxHost {
113 LEVEL.store(levels.max_level(), Ordering::Release);
114 &HOST
115}
116
117/// Hand the queued plugin messages to `sink`, oldest first.
118pub fn drain(sink: &mut dyn FnMut(Event)) {
119 QUEUED.with(|queued| {
120 let mut queued = queued.borrow_mut();
121 for event in queued.drain(..) {
122 sink(event);
123 }
124 });
125}
126
127/// Forget anything queued on this thread.
128///
129/// Called before a call, so that a plugin's messages can never be attributed to the next one.
130pub fn clear() {
131 QUEUED.with(|queued| queued.borrow_mut().clear());
132}
133
134/// The callback a plugin calls through.
135///
136/// # Safety
137/// `message` must be valid for the duration of this call, which is what the contract promises.
138unsafe extern "C" fn log(level: u32, message: PmpxStr) {
139 // SAFETY: the plugin passes a borrowed view of a `String` that outlives this call.
140 let bytes = unsafe { message.as_bytes() }.unwrap_or(&[]);
141 let text = String::from_utf8_lossy(bytes).into_owned();
142
143 QUEUED.with(|queued| {
144 queued
145 .borrow_mut()
146 .push(Event::PluginMessage { level, text })
147 });
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 /// The level a run installs is what decides how much of a plugin's output is ever formatted.
155 #[test]
156 fn the_installed_level_follows_the_flags() {
157 let max = |levels: Levels| {
158 let hooks = hooks(levels);
159 // SAFETY: the host's own lookup, with a key borrowed from a literal.
160 let table = unsafe {
161 (hooks.capability)(PmpxStr::new(
162 pmpx_plugin::abi::PMPX_CAP_LOG.as_ptr(),
163 pmpx_plugin::abi::PMPX_CAP_LOG.len(),
164 ))
165 };
166 assert!(!table.is_null(), "the log capability is always offered");
167 // SAFETY: the lookup answered with one of this module's own tables.
168 unsafe { (*(table as *const PmpxLog)).max_level }
169 };
170
171 assert_eq!(max(Levels::default()), PMPX_LEVEL_WARN);
172 assert_eq!(
173 max(Levels {
174 quiet: true,
175 trace: false
176 }),
177 PMPX_LEVEL_ERROR
178 );
179 assert_eq!(
180 max(Levels {
181 quiet: false,
182 trace: true
183 }),
184 PMPX_LEVEL_DEBUG
185 );
186 assert_eq!(
187 max(Levels {
188 quiet: true,
189 trace: true
190 }),
191 PMPX_LEVEL_DEBUG,
192 "a trace is the more explicit request of the two"
193 );
194 }
195
196 /// A capability this host does not have is "not here", not an error.
197 #[test]
198 fn an_unknown_capability_answers_nothing() {
199 let name = "something.else";
200 // SAFETY: a key borrowed from a literal.
201 let answer = unsafe { capability(PmpxStr::new(name.as_ptr(), name.len())) };
202 assert!(answer.is_null());
203 }
204
205 /// The plugin's messages reach the sink as data, and only once.
206 #[test]
207 fn messages_are_queued_and_drained_once() {
208 clear();
209 // SAFETY: a borrowed view of a local `String` that outlives the call.
210 unsafe {
211 log(
212 PMPX_LEVEL_WARN,
213 PmpxStr::new("careful".as_ptr(), "careful".len()),
214 )
215 };
216
217 let mut seen = Vec::new();
218 drain(&mut |event| seen.push(event));
219 assert_eq!(
220 seen,
221 vec![Event::PluginMessage {
222 level: PMPX_LEVEL_WARN,
223 text: "careful".to_string()
224 }]
225 );
226
227 let mut again = Vec::new();
228 drain(&mut |event| again.push(event));
229 assert!(again.is_empty(), "a message is delivered once");
230 }
231
232 /// The pointer handed to a plugin must stay valid for its whole life, so it is one `'static`.
233 #[test]
234 fn the_hooks_are_static() {
235 let first = hooks(Levels::default()) as *const PmpxHost;
236 let second = hooks(Levels::default()) as *const PmpxHost;
237 assert_eq!(first, second);
238 // SAFETY: the pointer is to this module's own table.
239 assert_eq!(unsafe { &*first }.size, std::mem::size_of::<PmpxHost>());
240 }
241}