use std::cell::RefCell;
use std::fmt;
use std::sync::atomic::{AtomicPtr, Ordering};
use std::sync::OnceLock;
use pmpx_plugin_abi::{PmpxLog, PmpxStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Error,
Warn,
Info,
Debug,
}
impl Level {
pub const fn to_abi(self) -> u32 {
match self {
Level::Error => pmpx_plugin_abi::PMPX_LEVEL_ERROR,
Level::Warn => pmpx_plugin_abi::PMPX_LEVEL_WARN,
Level::Info => pmpx_plugin_abi::PMPX_LEVEL_INFO,
Level::Debug => pmpx_plugin_abi::PMPX_LEVEL_DEBUG,
}
}
}
static LOG: AtomicPtr<PmpxLog> = AtomicPtr::new(std::ptr::null_mut());
static NAME: OnceLock<String> = OnceLock::new();
thread_local! {
static CURRENT: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub(crate) unsafe fn set_log_table(table: *const PmpxLog) {
LOG.store(table as *mut PmpxLog, Ordering::Release);
}
pub(crate) fn remember_name(name: &str) {
let _ = NAME.set(name.to_string());
}
pub fn wants(level: Level) -> bool {
match host() {
Some(host) => level.to_abi() <= host.max_level,
None => true,
}
}
pub fn emit(level: Level, args: fmt::Arguments<'_>) {
if !wants(level) {
return;
}
send(level, &args.to_string());
}
pub fn context() {
if !wants(Level::Debug) {
return;
}
if let Some(line) = CURRENT.with(|current| current.borrow().clone()) {
send(Level::Debug, &line);
}
}
pub(crate) fn with_call<T>(description: String, f: impl FnOnce() -> T) -> T {
let previous = CURRENT.with(|current| current.borrow_mut().replace(description));
let out = f();
CURRENT.with(|current| *current.borrow_mut() = previous);
out
}
fn send(level: Level, message: &str) {
match host() {
Some(host) => {
let borrowed = PmpxStr::new(message.as_ptr(), message.len());
unsafe { (host.write)(level.to_abi(), borrowed) };
}
None => eprintln!("[{}] {message}", name()),
}
}
fn host() -> Option<&'static PmpxLog> {
let raw = LOG.load(Ordering::Acquire);
if raw.is_null() {
return None;
}
Some(unsafe { &*raw })
}
fn name() -> &'static str {
NAME.get().map(String::as_str).unwrap_or("plugin")
}
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {
$crate::debug::emit($crate::debug::Level::Error, format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {
$crate::debug::emit($crate::debug::Level::Warn, format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! info {
($($arg:tt)*) => {
$crate::debug::emit($crate::debug::Level::Info, format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! debug {
($($arg:tt)*) => {
$crate::debug::emit($crate::debug::Level::Debug, format_args!($($arg)*))
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn without_a_host_every_level_is_wanted() {
assert!(wants(Level::Error));
assert!(wants(Level::Warn));
assert!(wants(Level::Info));
assert!(wants(Level::Debug));
}
#[test]
fn levels_are_ordered_so_a_host_can_say_how_loud_it_wants_them() {
assert!(Level::Error < Level::Warn);
assert!(Level::Warn < Level::Info);
assert!(Level::Info < Level::Debug);
}
#[test]
fn the_call_description_is_scoped() {
context();
with_call("context: inside".to_string(), || {
let seen = CURRENT.with(|current| current.borrow().clone());
assert_eq!(seen.as_deref(), Some("context: inside"));
});
let after = CURRENT.with(|current| current.borrow().clone());
assert!(after.is_none(), "the description must not outlive the call");
}
}