use super::component::ICore;
use std::ffi::CString;
use std::os::raw::{c_char, c_int};
#[cfg(target_env = "msvc")]
const ILOGGER_OFFSET: isize = 56;
#[cfg(not(target_env = "msvc"))]
const ILOGGER_OFFSET: isize = 40;
const SLOT_PRINTLN: usize = 0;
const SLOT_LOGLN: usize = 2;
const SLOT_PRINTLN_U8: usize = 4;
const SLOT_LOGLN_U8: usize = 6;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Debug = 0,
Message = 1,
Warning = 2,
Error = 3,
}
type PrintLnFn = unsafe extern "C" fn(this: *mut u8, fmt: *const c_char, arg: *const c_char);
type LogLnFn =
unsafe extern "C" fn(this: *mut u8, level: c_int, fmt: *const c_char, arg: *const c_char);
unsafe fn logger_slot(core: *mut ICore, slot: usize) -> Option<(*mut u8, usize)> {
unsafe { super::vtable::secondary_call_target(core.cast::<u8>(), ILOGGER_OFFSET, slot) }
}
pub unsafe fn core_print_ln(core: *mut ICore, message: &str) -> bool {
let Some((this, slot)) = (unsafe { logger_slot(core, SLOT_PRINTLN) }) else {
return false;
};
let Ok(msg) = CString::new(message) else {
return false;
};
let fmt = c"%s";
let f: PrintLnFn = unsafe { std::mem::transmute(slot) };
unsafe { f(this, fmt.as_ptr(), msg.as_ptr()) };
true
}
pub unsafe fn core_log_ln(core: *mut ICore, level: LogLevel, message: &str) -> bool {
let Some((this, slot)) = (unsafe { logger_slot(core, SLOT_LOGLN) }) else {
return false;
};
let Ok(msg) = CString::new(message) else {
return false;
};
let fmt = c"%s";
let f: LogLnFn = unsafe { std::mem::transmute(slot) };
unsafe { f(this, level as c_int, fmt.as_ptr(), msg.as_ptr()) };
true
}
pub unsafe fn core_print_ln_u8(core: *mut ICore, message: &str) -> bool {
let Some((this, slot)) = (unsafe { logger_slot(core, SLOT_PRINTLN_U8) }) else {
return false;
};
let Ok(msg) = CString::new(message) else {
return false;
};
let fmt = c"%s";
let f: PrintLnFn = unsafe { std::mem::transmute(slot) };
unsafe { f(this, fmt.as_ptr(), msg.as_ptr()) };
true
}
pub unsafe fn core_log_ln_u8(core: *mut ICore, level: LogLevel, message: &str) -> bool {
let Some((this, slot)) = (unsafe { logger_slot(core, SLOT_LOGLN_U8) }) else {
return false;
};
let Ok(msg) = CString::new(message) else {
return false;
};
let fmt = c"%s";
let f: LogLnFn = unsafe { std::mem::transmute(slot) };
unsafe { f(this, level as c_int, fmt.as_ptr(), msg.as_ptr()) };
true
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CStr;
use std::sync::Mutex;
static TEST_LOCK: Mutex<()> = Mutex::new(());
#[derive(Default, Clone)]
struct Captured {
slot: Option<usize>,
level: Option<c_int>,
fmt: Option<String>,
message: Option<String>,
}
static CAPTURED: Mutex<Option<Captured>> = Mutex::new(None);
fn reset_captures() {
*CAPTURED.lock().unwrap() = Some(Captured::default());
}
fn last_capture() -> Captured {
CAPTURED.lock().unwrap().clone().unwrap_or_default()
}
fn cstr_to_string(ptr: *const c_char) -> Option<String> {
if ptr.is_null() {
return None;
}
unsafe { CStr::from_ptr(ptr) }
.to_str()
.ok()
.map(String::from)
}
unsafe extern "C" fn mock_print_ln(_this: *mut u8, fmt: *const c_char, arg: *const c_char) {
let mut guard = CAPTURED.lock().unwrap();
let c = guard.as_mut().unwrap();
c.slot = Some(SLOT_PRINTLN);
c.fmt = cstr_to_string(fmt);
c.message = cstr_to_string(arg);
}
unsafe extern "C" fn mock_log_ln(
_this: *mut u8,
level: c_int,
fmt: *const c_char,
arg: *const c_char,
) {
let mut guard = CAPTURED.lock().unwrap();
let c = guard.as_mut().unwrap();
c.slot = Some(SLOT_LOGLN);
c.level = Some(level);
c.fmt = cstr_to_string(fmt);
c.message = cstr_to_string(arg);
}
unsafe extern "C" fn mock_print_ln_u8(_this: *mut u8, fmt: *const c_char, arg: *const c_char) {
let mut guard = CAPTURED.lock().unwrap();
let c = guard.as_mut().unwrap();
c.slot = Some(SLOT_PRINTLN_U8);
c.fmt = cstr_to_string(fmt);
c.message = cstr_to_string(arg);
}
unsafe extern "C" fn mock_log_ln_u8(
_this: *mut u8,
level: c_int,
fmt: *const c_char,
arg: *const c_char,
) {
let mut guard = CAPTURED.lock().unwrap();
let c = guard.as_mut().unwrap();
c.slot = Some(SLOT_LOGLN_U8);
c.level = Some(level);
c.fmt = cstr_to_string(fmt);
c.message = cstr_to_string(arg);
}
unsafe extern "C" fn unused_slot() {}
static MOCK_VTABLE: std::sync::OnceLock<[usize; 10]> = std::sync::OnceLock::new();
fn mock_vtable() -> &'static [usize; 10] {
MOCK_VTABLE.get_or_init(|| {
[
mock_print_ln as *const () as usize, unused_slot as *const () as usize, mock_log_ln as *const () as usize, unused_slot as *const () as usize, mock_print_ln_u8 as *const () as usize, unused_slot as *const () as usize, mock_log_ln_u8 as *const () as usize, unused_slot as *const () as usize, 0,
0,
]
})
}
fn make_mock_core() -> [usize; 32] {
let mut buf = [0usize; 32];
let vptr = mock_vtable().as_ptr() as usize;
let idx = usize::try_from(ILOGGER_OFFSET).expect("ILOGGER_OFFSET must be >= 0")
/ std::mem::size_of::<usize>();
buf[idx] = vptr;
buf
}
#[test]
fn core_print_ln_calls_slot_0_at_logger_offset() {
let _g = TEST_LOCK.lock().unwrap();
reset_captures();
let mut core = make_mock_core();
let core_ptr = core.as_mut_ptr().cast::<ICore>();
let ok = unsafe { core_print_ln(core_ptr, "hello") };
assert!(ok, "core_print_ln must return true with a valid mock");
let c = last_capture();
assert_eq!(c.slot, Some(SLOT_PRINTLN));
assert_eq!(c.fmt.as_deref(), Some("%s"));
assert_eq!(c.message.as_deref(), Some("hello"));
assert_eq!(c.level, None, "printLn does not take a LogLevel");
}
#[test]
fn core_log_ln_calls_slot_2_with_level() {
let _g = TEST_LOCK.lock().unwrap();
reset_captures();
let mut core = make_mock_core();
let core_ptr = core.as_mut_ptr().cast::<ICore>();
let ok = unsafe { core_log_ln(core_ptr, LogLevel::Warning, "alert") };
assert!(ok);
let c = last_capture();
assert_eq!(c.slot, Some(SLOT_LOGLN));
assert_eq!(c.level, Some(LogLevel::Warning as c_int));
assert_eq!(c.fmt.as_deref(), Some("%s"));
assert_eq!(c.message.as_deref(), Some("alert"));
}
#[test]
fn core_print_ln_u8_calls_slot_4() {
let _g = TEST_LOCK.lock().unwrap();
reset_captures();
let mut core = make_mock_core();
let core_ptr = core.as_mut_ptr().cast::<ICore>();
let ok = unsafe { core_print_ln_u8(core_ptr, "hi") };
assert!(ok);
let c = last_capture();
assert_eq!(c.slot, Some(SLOT_PRINTLN_U8));
assert_eq!(c.message.as_deref(), Some("hi"));
}
#[test]
fn core_log_ln_u8_calls_slot_6_with_level() {
let _g = TEST_LOCK.lock().unwrap();
reset_captures();
let mut core = make_mock_core();
let core_ptr = core.as_mut_ptr().cast::<ICore>();
let ok = unsafe { core_log_ln_u8(core_ptr, LogLevel::Error, "critical failure") };
assert!(ok);
let c = last_capture();
assert_eq!(c.slot, Some(SLOT_LOGLN_U8));
assert_eq!(c.level, Some(LogLevel::Error as c_int));
assert_eq!(c.message.as_deref(), Some("critical failure"));
}
#[test]
fn all_log_fns_return_false_for_null_core() {
let _g = TEST_LOCK.lock().unwrap();
let nul = std::ptr::null_mut();
assert!(!unsafe { core_print_ln(nul, "x") });
assert!(!unsafe { core_log_ln(nul, LogLevel::Message, "x") });
assert!(!unsafe { core_print_ln_u8(nul, "x") });
assert!(!unsafe { core_log_ln_u8(nul, LogLevel::Message, "x") });
}
#[test]
fn log_fns_reject_message_with_interior_nul() {
let _g = TEST_LOCK.lock().unwrap();
let mut core = make_mock_core();
let core_ptr = core.as_mut_ptr().cast::<ICore>();
assert!(!unsafe { core_print_ln(core_ptr, "a\0b") });
assert!(!unsafe { core_log_ln(core_ptr, LogLevel::Message, "a\0b") });
}
}