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}