Skip to main content

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}