Skip to main content

pmpx_plugin/
debug.rs

1//! Saying something to the person running pmpx.
2//!
3//! A plugin cannot print usefully on its own: it does not know the id the host shows for it, whether
4//! the person asked for detail, or where those lines should go. So it calls the macros here --
5//! [`debug!`](macro@crate::debug), [`info!`](macro@crate::info), [`warn!`](macro@crate::warn), [`error!`](macro@crate::error)
6//! -- and the **host** decides: it adds the prefix and the id, and drops anything louder than the
7//! level it was asked for.
8//!
9//! Two consequences worth knowing:
10//!
11//! - **Nothing is formatted when the host would not print it.** Every macro asks [`wants`] first, so
12//!   a run without a trace pays nothing for the lines a plugin writes.
13//! - **The trace is not an input.** The plugin always calls the same way; only the host's printing
14//!   changes, so a traced run executes byte-for-byte the same command as any other.
15//!
16//! Without a host -- a plugin's own `cargo test` -- the messages go to stderr directly, at every
17//! level, which is what an author wants while writing a plugin.
18
19use std::cell::RefCell;
20use std::fmt;
21use std::sync::atomic::{AtomicPtr, Ordering};
22use std::sync::OnceLock;
23
24use pmpx_plugin_abi::{PmpxLog, PmpxStr};
25
26/// How loud one message is.
27///
28/// Ordered: a host that prints [`Level::Warn`] prints everything up to and including it.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
30pub enum Level {
31    /// Something went wrong.
32    Error,
33    /// Something is off, but the command still runs.
34    Warn,
35    /// An ordinary note about what the plugin decided.
36    Info,
37    /// Detail for someone debugging.
38    Debug,
39}
40
41impl Level {
42    /// The number this level crosses the boundary as.
43    pub const fn to_abi(self) -> u32 {
44        match self {
45            Level::Error => pmpx_plugin_abi::PMPX_LEVEL_ERROR,
46            Level::Warn => pmpx_plugin_abi::PMPX_LEVEL_WARN,
47            Level::Info => pmpx_plugin_abi::PMPX_LEVEL_INFO,
48            Level::Debug => pmpx_plugin_abi::PMPX_LEVEL_DEBUG,
49        }
50    }
51}
52
53/// The host's logging table, or null when there is no host (the plugin is under its own tests).
54///
55/// It is only stored after its `size` was checked against what this build knows how to read, which
56/// is what makes reading `max_level` below safe.
57static LOG: AtomicPtr<PmpxLog> = AtomicPtr::new(std::ptr::null_mut());
58
59/// What the plugin calls itself, for the no-host case only -- the host knows its own id for it.
60static NAME: OnceLock<String> = OnceLock::new();
61
62// The call in progress on this thread, held here rather than passed around so that `context()` and
63// the macros can report what the host said without every plugin method threading it through. The
64// description is rendered by the shell, because only it has the raw context.
65thread_local! {
66    static CURRENT: RefCell<Option<String>> = const { RefCell::new(None) };
67}
68
69/// Install the host's logging table. Called by the `export!` shell; a plugin never calls this.
70///
71/// # Safety
72/// `table` must be the host's, with at least `size_of::<PmpxLog>()` bytes, and must stay valid for
73/// the life of the process.
74pub(crate) unsafe fn set_log_table(table: *const PmpxLog) {
75    LOG.store(table as *mut PmpxLog, Ordering::Release);
76}
77
78/// Remember the plugin's own name, for the no-host fallback. Called by the `export!` shell.
79pub(crate) fn remember_name(name: &str) {
80    let _ = NAME.set(name.to_string());
81}
82
83/// Whether the host would print a message at this level.
84///
85/// Cheap on purpose: the macros call it *before* formatting, so a silent host costs one atomic load.
86/// With no host the answer is "yes", because that is a plugin's own test run.
87pub fn wants(level: Level) -> bool {
88    match host() {
89        Some(host) => level.to_abi() <= host.max_level,
90        None => true,
91    }
92}
93
94/// Send one already-formatted message. The macros go through here.
95pub fn emit(level: Level, args: fmt::Arguments<'_>) {
96    if !wants(level) {
97        return;
98    }
99    send(level, &args.to_string());
100}
101
102/// Print the whole context of the call in progress, as one line.
103///
104/// The line is rendered by the shell that made the call, so it carries what the host actually said
105/// rather than a summary this side guessed at. Outside a call there is nothing to print.
106pub fn context() {
107    if !wants(Level::Debug) {
108        return;
109    }
110
111    if let Some(line) = CURRENT.with(|current| current.borrow().clone()) {
112        send(Level::Debug, &line);
113    }
114}
115
116/// Run `f` with `description` installed as the current call, restoring whatever was there before.
117pub(crate) fn with_call<T>(description: String, f: impl FnOnce() -> T) -> T {
118    let previous = CURRENT.with(|current| current.borrow_mut().replace(description));
119    let out = f();
120    CURRENT.with(|current| *current.borrow_mut() = previous);
121    out
122}
123
124/// Hand one message to the host, or print it here when there is none.
125fn send(level: Level, message: &str) {
126    match host() {
127        Some(host) => {
128            let borrowed = PmpxStr::new(message.as_ptr(), message.len());
129            // SAFETY: the table was checked when it was installed, and the host keeps it alive for
130            // the process; `message` outlives the call, which is what the contract asks for.
131            unsafe { (host.write)(level.to_abi(), borrowed) };
132        }
133        None => eprintln!("[{}] {message}", name()),
134    }
135}
136
137/// The logging table, if a host installed one.
138fn host() -> Option<&'static PmpxLog> {
139    let raw = LOG.load(Ordering::Acquire);
140    if raw.is_null() {
141        return None;
142    }
143
144    // SAFETY: only the shell calls `set_log_table`, with a pointer the host promises to keep alive
145    // and unchanged, and only after checking that the table is large enough to read.
146    Some(unsafe { &*raw })
147}
148
149/// The name to use when there is no host to ask.
150fn name() -> &'static str {
151    NAME.get().map(String::as_str).unwrap_or("plugin")
152}
153
154/// Say something that went wrong.
155///
156/// Printed by the host in its error style. This is also the channel for the text a
157/// [`PluginError`](crate::PluginError) carries: that text stays inside the plugin and is never sent
158/// across the boundary, so `error!` is how it reaches a person.
159#[macro_export]
160macro_rules! error {
161    ($($arg:tt)*) => {
162        $crate::debug::emit($crate::debug::Level::Error, format_args!($($arg)*))
163    };
164}
165
166/// Say that something is off, but the command still runs.
167#[macro_export]
168macro_rules! warn {
169    ($($arg:tt)*) => {
170        $crate::debug::emit($crate::debug::Level::Warn, format_args!($($arg)*))
171    };
172}
173
174/// Say what the plugin decided, in one line.
175#[macro_export]
176macro_rules! info {
177    ($($arg:tt)*) => {
178        $crate::debug::emit($crate::debug::Level::Info, format_args!($($arg)*))
179    };
180}
181
182/// Say something for whoever is debugging.
183///
184/// With no trace asked for the host drops it, and this costs an atomic load and no formatting.
185#[macro_export]
186macro_rules! debug {
187    ($($arg:tt)*) => {
188        $crate::debug::emit($crate::debug::Level::Debug, format_args!($($arg)*))
189    };
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    /// Without a host -- a plugin's own test run -- everything is wanted, so an author sees their own
197    /// lines.
198    #[test]
199    fn without_a_host_every_level_is_wanted() {
200        assert!(wants(Level::Error));
201        assert!(wants(Level::Warn));
202        assert!(wants(Level::Info));
203        assert!(wants(Level::Debug));
204    }
205
206    #[test]
207    fn levels_are_ordered_so_a_host_can_say_how_loud_it_wants_them() {
208        assert!(Level::Error < Level::Warn);
209        assert!(Level::Warn < Level::Info);
210        assert!(Level::Info < Level::Debug);
211    }
212
213    /// The description is only current inside a call: a plugin logging outside one prints nothing
214    /// from `context()`.
215    #[test]
216    fn the_call_description_is_scoped() {
217        context();
218
219        with_call("context: inside".to_string(), || {
220            let seen = CURRENT.with(|current| current.borrow().clone());
221            assert_eq!(seen.as_deref(), Some("context: inside"));
222        });
223
224        let after = CURRENT.with(|current| current.borrow().clone());
225        assert!(after.is_none(), "the description must not outlive the call");
226    }
227}