#![allow(unsafe_code)]
use std::cell::RefCell;
use std::sync::atomic::{AtomicU32, Ordering};
use pmpx_plugin::abi::{
PmpxHost, PmpxLog, PmpxStr, PMPX_LEVEL_DEBUG, PMPX_LEVEL_ERROR, PMPX_LEVEL_WARN,
};
use crate::Event;
thread_local! {
static QUEUED: RefCell<Vec<Event>> = const { RefCell::new(Vec::new()) };
}
static LEVEL: AtomicU32 = AtomicU32::new(PMPX_LEVEL_WARN);
static LOG_TRACE: PmpxLog = PmpxLog {
size: std::mem::size_of::<PmpxLog>(),
write: log,
max_level: PMPX_LEVEL_DEBUG,
};
static LOG_NORMAL: PmpxLog = PmpxLog {
size: std::mem::size_of::<PmpxLog>(),
write: log,
max_level: PMPX_LEVEL_WARN,
};
static LOG_QUIET: PmpxLog = PmpxLog {
size: std::mem::size_of::<PmpxLog>(),
write: log,
max_level: PMPX_LEVEL_ERROR,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Levels {
pub quiet: bool,
pub trace: bool,
}
impl Levels {
pub const fn max_level(self) -> u32 {
if self.trace {
PMPX_LEVEL_DEBUG
} else if self.quiet {
PMPX_LEVEL_ERROR
} else {
PMPX_LEVEL_WARN
}
}
}
unsafe extern "C" fn capability(name: PmpxStr) -> *const std::ffi::c_void {
let bytes = unsafe { name.as_bytes() }.unwrap_or(&[]);
if bytes != pmpx_plugin::abi::PMPX_CAP_LOG.as_bytes() {
return std::ptr::null();
}
let table = match LEVEL.load(Ordering::Acquire) {
PMPX_LEVEL_DEBUG => &LOG_TRACE,
PMPX_LEVEL_ERROR => &LOG_QUIET,
_ => &LOG_NORMAL,
};
std::ptr::from_ref(table).cast()
}
static HOST: PmpxHost = PmpxHost {
abi_major: pmpx_plugin::abi::PMPX_ABI_MAJOR,
size: std::mem::size_of::<PmpxHost>(),
capability,
};
pub fn hooks(levels: Levels) -> &'static PmpxHost {
LEVEL.store(levels.max_level(), Ordering::Release);
&HOST
}
pub fn drain(sink: &mut dyn FnMut(Event)) {
QUEUED.with(|queued| {
let mut queued = queued.borrow_mut();
for event in queued.drain(..) {
sink(event);
}
});
}
pub fn clear() {
QUEUED.with(|queued| queued.borrow_mut().clear());
}
unsafe extern "C" fn log(level: u32, message: PmpxStr) {
let bytes = unsafe { message.as_bytes() }.unwrap_or(&[]);
let text = String::from_utf8_lossy(bytes).into_owned();
QUEUED.with(|queued| {
queued
.borrow_mut()
.push(Event::PluginMessage { level, text })
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_installed_level_follows_the_flags() {
let max = |levels: Levels| {
let hooks = hooks(levels);
let table = unsafe {
(hooks.capability)(PmpxStr::new(
pmpx_plugin::abi::PMPX_CAP_LOG.as_ptr(),
pmpx_plugin::abi::PMPX_CAP_LOG.len(),
))
};
assert!(!table.is_null(), "the log capability is always offered");
unsafe { (*(table as *const PmpxLog)).max_level }
};
assert_eq!(max(Levels::default()), PMPX_LEVEL_WARN);
assert_eq!(
max(Levels {
quiet: true,
trace: false
}),
PMPX_LEVEL_ERROR
);
assert_eq!(
max(Levels {
quiet: false,
trace: true
}),
PMPX_LEVEL_DEBUG
);
assert_eq!(
max(Levels {
quiet: true,
trace: true
}),
PMPX_LEVEL_DEBUG,
"a trace is the more explicit request of the two"
);
}
#[test]
fn an_unknown_capability_answers_nothing() {
let name = "something.else";
let answer = unsafe { capability(PmpxStr::new(name.as_ptr(), name.len())) };
assert!(answer.is_null());
}
#[test]
fn messages_are_queued_and_drained_once() {
clear();
unsafe {
log(
PMPX_LEVEL_WARN,
PmpxStr::new("careful".as_ptr(), "careful".len()),
)
};
let mut seen = Vec::new();
drain(&mut |event| seen.push(event));
assert_eq!(
seen,
vec![Event::PluginMessage {
level: PMPX_LEVEL_WARN,
text: "careful".to_string()
}]
);
let mut again = Vec::new();
drain(&mut |event| again.push(event));
assert!(again.is_empty(), "a message is delivered once");
}
#[test]
fn the_hooks_are_static() {
let first = hooks(Levels::default()) as *const PmpxHost;
let second = hooks(Levels::default()) as *const PmpxHost;
assert_eq!(first, second);
assert_eq!(unsafe { &*first }.size, std::mem::size_of::<PmpxHost>());
}
}