use crate::Htl;
use std::any::Any;
use std::cell::{Cell, RefCell, UnsafeCell};
use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::fmt;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::ThreadId;
pub const ABI_VERSION: c_int = 1;
pub const PAYLOAD_VERSION: i64 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Status {
Ok = 0,
Err = 1,
BadHandle = 2,
NotFound = 3,
Lua = 4,
Panic = 5,
WrongThread = 6,
Interrupted = 7,
}
impl Status {
pub const ALL: &'static [Status] = &[
Status::Ok,
Status::Err,
Status::BadHandle,
Status::NotFound,
Status::Lua,
Status::Panic,
Status::WrongThread,
Status::Interrupted,
];
pub const fn code(self) -> c_int {
self as c_int
}
pub const fn name(self) -> &'static str {
match self {
Status::Ok => "OK",
Status::Err => "ERR",
Status::BadHandle => "BAD_HANDLE",
Status::NotFound => "NOT_FOUND",
Status::Lua => "LUA",
Status::Panic => "PANIC",
Status::WrongThread => "WRONG_THREAD",
Status::Interrupted => "INTERRUPTED",
}
}
}
thread_local! {
static LAST: RefCell<Option<CString>> = const { RefCell::new(None) };
static LAST_STATUS: Cell<c_int> = const { Cell::new(0) };
}
pub fn set_error(status: Status, msg: impl fmt::Display) {
let text = msg.to_string().replace('\0', "?");
LAST.with(|c| *c.borrow_mut() = CString::new(text).ok());
LAST_STATUS.with(|c| c.set(status.code()));
}
pub fn clear_error() {
LAST.with(|c| *c.borrow_mut() = None);
LAST_STATUS.with(|c| c.set(Status::Ok.code()));
}
pub fn last_error_ptr() -> *const c_char {
LAST.with(|c| match &*c.borrow() {
Some(s) => s.as_ptr(),
None => std::ptr::null(),
})
}
pub fn last_status() -> c_int {
LAST_STATUS.with(|c| c.get())
}
pub unsafe fn last_error_into(buf: *mut c_char, len: c_int) -> c_int {
if buf.is_null() || len <= 0 {
return -1;
}
let cap = len as usize;
LAST.with(|c| {
let borrowed = c.borrow();
let bytes = borrowed.as_ref().map(|s| s.as_bytes()).unwrap_or(b"");
let n = bytes.len().min(cap - 1);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n);
*buf.add(n) = 0;
}
bytes.len() as c_int
})
}
pub fn give(s: impl Into<Vec<u8>>) -> *mut c_char {
match CString::new(s) {
Ok(c) => c.into_raw(),
Err(e) => {
set_error(
Status::Err,
format!(
"the value contains a NUL byte at index {} and cannot cross as a C string",
e.nul_position()
),
);
std::ptr::null_mut()
}
}
}
pub unsafe fn free(p: *mut c_char) {
if !p.is_null() {
drop(unsafe { CString::from_raw(p) });
}
}
pub unsafe fn arg_str<'a>(p: *const c_char, what: &str) -> Option<&'a str> {
if p.is_null() {
set_error(Status::Err, format!("argument `{what}` is NULL"));
return None;
}
match unsafe { CStr::from_ptr(p) }.to_str() {
Ok(s) => Some(s),
Err(e) => {
set_error(Status::Err, format!("argument `{what}` is not UTF-8: {e}"));
None
}
}
}
pub fn json<T: serde::Serialize + ?Sized>(value: &T) -> Result<String, String> {
let mut v = serde_json::to_value(value).map_err(|e| format!("serializing the result: {e}"))?;
if let serde_json::Value::Object(map) = &mut v {
map.entry("v")
.or_insert_with(|| serde_json::Value::from(PAYLOAD_VERSION));
}
serde_json::to_string(&v).map_err(|e| format!("serializing the result: {e}"))
}
pub fn give_json<T: serde::Serialize + ?Sized>(value: &T) -> *mut c_char {
match json(value) {
Ok(s) => give(s),
Err(e) => {
set_error(Status::Err, e);
std::ptr::null_mut()
}
}
}
pub fn from_json<T: serde::de::DeserializeOwned>(s: &str, what: &str) -> Result<T, String> {
serde_json::from_str(s).map_err(|e| format!("argument `{what}` is not valid JSON: {e}"))
}
pub fn fail<E: fmt::Display + Any>(e: E) -> Status {
let (status, message) = classify(&e as &dyn Any);
set_error(status, message.unwrap_or_else(|| e.to_string()));
status
}
fn classify(any: &dyn Any) -> (Status, Option<String>) {
if let Some(m) = any.downcast_ref::<mlua::Error>() {
return (lua_status(m), Some(crate::user_message_lua(m)));
}
if let Some(io) = any.downcast_ref::<std::io::Error>() {
return (io_status(io), None);
}
if let Some(a) = any.downcast_ref::<anyhow::Error>() {
if let Some(m) = a.downcast_ref::<mlua::Error>() {
return (lua_status(m), Some(crate::user_message(a)));
}
if let Some(io) = a.downcast_ref::<std::io::Error>() {
return (io_status(io), Some(crate::user_message(a)));
}
return (Status::Err, Some(crate::user_message(a)));
}
(Status::Err, None)
}
fn lua_status(e: &mlua::Error) -> Status {
if e.downcast_ref::<Interrupted>().is_some() {
Status::Interrupted
} else {
Status::Lua
}
}
fn io_status(e: &std::io::Error) -> Status {
if e.kind() == std::io::ErrorKind::NotFound {
Status::NotFound
} else {
Status::Err
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Interrupted;
impl fmt::Display for Interrupted {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("interrupted")
}
}
impl std::error::Error for Interrupted {}
pub fn guard<R>(sentinel: R, f: impl FnOnce() -> R) -> R {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(v) => v,
Err(p) => {
set_error(Status::Panic, panic_message(&p));
sentinel
}
}
}
fn panic_message(p: &Box<dyn Any + Send>) -> String {
if let Some(s) = p.downcast_ref::<&str>() {
format!("panic: {s}")
} else if let Some(s) = p.downcast_ref::<String>() {
format!("panic: {s}")
} else {
"panic: (no message)".to_string()
}
}
#[derive(Clone, Debug)]
pub struct Interrupt(Arc<AtomicBool>);
const HOOK_EVERY: u32 = 10_000;
impl Interrupt {
pub fn request(&self) {
self.0.store(true, Ordering::SeqCst);
}
pub fn is_set(&self) -> bool {
self.0.load(Ordering::SeqCst)
}
pub fn clear(&self) {
self.0.store(false, Ordering::SeqCst);
}
pub fn install(&self, h: &Htl) -> mlua::Result<()> {
self.install_every(h, HOOK_EVERY)
}
pub fn install_every(&self, h: &Htl, every: u32) -> mlua::Result<()> {
let flag = self.0.clone();
h.lua().set_global_hook(
mlua::HookTriggers::new().every_nth_instruction(every),
move |_lua, _debug| {
if flag.swap(false, Ordering::SeqCst) {
Err(mlua::Error::external(Interrupted))
} else {
Ok(mlua::VmState::Continue)
}
},
)
}
}
#[derive(Debug)]
struct Shared {
magic: u64,
owner: ThreadId,
interrupt: Arc<AtomicBool>,
}
struct Slot<T> {
value: T,
poisoned: bool,
entered: bool,
}
pub struct Handle<T> {
shared: Shared,
slot: UnsafeCell<Slot<T>>,
}
pub const fn magic(prefix: &str) -> u64 {
let b = prefix.as_bytes();
let mut h = 0xcbf2_9ce4_8422_2325u64;
let mut i = 0;
while i < b.len() {
h ^= b[i] as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
i += 1;
}
if h == 0 { 1 } else { h }
}
impl<T> Handle<T> {
pub fn open(magic: u64, make: impl FnOnce(Interrupt) -> Result<T, String>) -> *mut c_void {
guard(std::ptr::null_mut(), || {
clear_error();
let interrupt = Interrupt(Arc::new(AtomicBool::new(false)));
match make(interrupt.clone()) {
Ok(value) => {
let h = Box::new(Handle {
shared: Shared {
magic,
owner: std::thread::current().id(),
interrupt: interrupt.0,
},
slot: UnsafeCell::new(Slot {
value,
poisoned: false,
entered: false,
}),
});
Box::into_raw(h) as *mut c_void
}
Err(e) => {
set_error(Status::Err, e);
std::ptr::null_mut()
}
}
})
}
pub unsafe fn enter<R>(
ptr: *mut c_void,
magic: u64,
sentinel: R,
f: impl FnOnce(&mut T) -> R,
) -> R {
clear_error();
let Some(h) = (unsafe { Self::check(ptr, magic) }) else {
return sentinel;
};
let slot = h.slot.get();
unsafe {
if (*slot).poisoned {
set_error(
Status::Panic,
"the handle was poisoned by an earlier panic and cannot be used",
);
return sentinel;
}
if (*slot).entered {
set_error(
Status::Err,
"the handle is already inside a call (re-entering it is not supported)",
);
return sentinel;
}
(*slot).entered = true;
let out = catch_unwind(AssertUnwindSafe(|| f(&mut (*slot).value)));
(*slot).entered = false;
match out {
Ok(v) => v,
Err(p) => {
(*slot).poisoned = true;
set_error(Status::Panic, panic_message(&p));
sentinel
}
}
}
}
pub unsafe fn enter_status(
ptr: *mut c_void,
magic: u64,
f: impl FnOnce(&mut T) -> c_int,
) -> c_int {
let out = unsafe { Self::enter(ptr, magic, -1, f) };
if out < 0 { last_status() } else { out }
}
pub unsafe fn interrupt(ptr: *mut c_void, magic: u64) -> c_int {
clear_error();
if ptr.is_null() {
set_error(Status::BadHandle, "the handle is NULL");
return Status::BadHandle.code();
}
let h = unsafe { &*(ptr as *const Handle<T>) };
if h.shared.magic != magic {
set_error(
Status::BadHandle,
"the handle was not returned by this library",
);
return Status::BadHandle.code();
}
h.shared.interrupt.store(true, Ordering::SeqCst);
Status::Ok.code()
}
pub unsafe fn close(ptr: *mut c_void, magic: u64) {
clear_error();
if unsafe { Self::check(ptr, magic) }.is_none() {
return;
}
guard((), || {
drop(unsafe { Box::from_raw(ptr as *mut Handle<T>) });
});
}
unsafe fn check<'a>(ptr: *mut c_void, magic: u64) -> Option<&'a Handle<T>> {
if ptr.is_null() {
set_error(Status::BadHandle, "the handle is NULL");
return None;
}
let h = unsafe { &*(ptr as *const Handle<T>) };
if h.shared.magic != magic {
set_error(
Status::BadHandle,
"the handle was not returned by this library",
);
return None;
}
if h.shared.owner != std::thread::current().id() {
set_error(
Status::WrongThread,
"the handle belongs to the thread that opened it; \
open one per thread (see <prefix>_threadsafe)",
);
return None;
}
Some(h)
}
}
pub trait CExport {
const PREFIX: &'static str;
const HEADER: &'static str;
const ABI_VERSION: c_int;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_codes_are_pinned() {
let pairs: Vec<(&str, c_int)> = Status::ALL.iter().map(|s| (s.name(), s.code())).collect();
assert_eq!(
pairs,
vec![
("OK", 0),
("ERR", 1),
("BAD_HANDLE", 2),
("NOT_FOUND", 3),
("LUA", 4),
("PANIC", 5),
("WRONG_THREAD", 6),
("INTERRUPTED", 7),
]
);
}
#[test]
fn give_refuses_an_interior_nul_and_says_where() {
let p = give("ab\0cd".to_string());
assert!(p.is_null(), "a string with a NUL must not cross");
assert_eq!(last_status(), Status::Err.code());
let msg = unsafe { CStr::from_ptr(last_error_ptr()) }
.to_string_lossy()
.into_owned();
assert!(msg.contains("NUL byte at index 2"), "{msg}");
}
#[test]
fn free_of_null_is_a_no_op() {
unsafe { free(std::ptr::null_mut()) };
}
#[test]
fn a_round_trip_through_give_and_free_keeps_the_bytes() {
let p = give("hello".to_string());
assert!(!p.is_null());
assert_eq!(unsafe { CStr::from_ptr(p) }.to_str().unwrap(), "hello");
unsafe { free(p) };
}
#[test]
fn last_error_into_truncates_and_reports_the_full_length() {
set_error(Status::Err, "0123456789");
let mut buf = [0i8; 4];
let n = unsafe { last_error_into(buf.as_mut_ptr() as *mut c_char, 4) };
assert_eq!(n, 10, "the length asked for is the whole message");
let got = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
assert_eq!(got.to_str().unwrap(), "012");
assert_eq!(
unsafe { last_error_into(std::ptr::null_mut(), 4) },
-1,
"a NULL buffer is refused, not written"
);
}
#[test]
fn json_stamps_the_payload_version_on_an_object_only() {
#[derive(serde::Serialize)]
struct P {
depth: i32,
}
assert_eq!(json(&P { depth: 3 }).unwrap(), r#"{"depth":3,"v":1}"#);
assert_eq!(json(&[1, 2, 3]).unwrap(), "[1,2,3]");
}
#[test]
fn guard_turns_a_panic_into_the_sentinel_and_a_message() {
let n = guard(-1i32, || panic!("boom"));
assert_eq!(n, -1);
assert_eq!(last_status(), Status::Panic.code());
let msg = unsafe { CStr::from_ptr(last_error_ptr()) }
.to_string_lossy()
.into_owned();
assert!(msg.contains("boom"), "{msg}");
}
#[test]
fn magic_is_per_prefix_and_never_zero() {
assert_ne!(magic("hello"), magic("hell"));
assert_ne!(magic(""), 0);
}
}