use std::cell::{Cell, RefCell};
use std::ffi::{c_char, c_int, CString};
use std::panic::{catch_unwind, UnwindSafe};
use std::ptr;
use crate::kind;
pub type FfiError = (c_int, String);
#[derive(Debug, Default)]
pub struct LastErrorSlot {
message: RefCell<Option<CString>>,
kind: Cell<c_int>,
}
impl LastErrorSlot {
pub const fn new() -> Self {
Self {
message: RefCell::new(None),
kind: Cell::new(kind::NONE),
}
}
pub fn set(&self, kind: c_int, message: &str) {
*self.message.borrow_mut() = CString::new(message).ok();
self.kind.set(kind);
}
pub fn set_error(&self, error: &FfiError) {
self.set(error.0, &error.1);
}
pub fn clear(&self) {
*self.message.borrow_mut() = None;
self.kind.set(kind::NONE);
}
pub fn message_ptr(&self) -> *const c_char {
self.message
.borrow()
.as_ref()
.map(|message| message.as_ptr())
.unwrap_or(ptr::null())
}
pub fn kind(&self) -> c_int {
self.kind.get()
}
}
pub fn catch<T, F>(body: F) -> Result<T, FfiError>
where
F: FnOnce() -> Result<T, FfiError> + UnwindSafe,
{
match catch_unwind(body) {
Ok(result) => result,
Err(_) => Err((
kind::PANIC,
"a panic was caught at the ABI boundary".to_string(),
)),
}
}
pub fn invalid_argument(message: impl Into<String>) -> FfiError {
(kind::INVALID_ARGUMENT, message.into())
}
pub fn invalid_output() -> FfiError {
(
kind::INVALID_OUTPUT,
"output contains null byte".to_string(),
)
}
#[macro_export]
macro_rules! export_last_error_abi {
($slot:path, $message_fn:ident, $kind_fn:ident) => {
#[no_mangle]
pub extern "C" fn $message_fn() -> *const ::std::ffi::c_char {
$slot.with(|slot| $crate::ffi::LastErrorSlot::message_ptr(slot))
}
#[no_mangle]
pub extern "C" fn $kind_fn() -> ::std::ffi::c_int {
$slot.with(|slot| $crate::ffi::LastErrorSlot::kind(slot))
}
};
}
#[macro_export]
macro_rules! assert_stable_kinds {
($enum:ty, $test_name:ident, $($variant:ident = $value:expr),+ $(,)?) => {
#[test]
fn $test_name() {
$(
assert_eq!(
<$enum>::$variant as ::std::ffi::c_int,
$value as ::std::ffi::c_int,
concat!(
stringify!($variant),
" changed value. Discriminants are a public ABI contract: a new \
reason takes a new number, existing ones are never renumbered."
)
);
)+
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CStr;
thread_local! {
static LAST_ERROR: LastErrorSlot = const { LastErrorSlot::new() };
}
export_last_error_abi!(LAST_ERROR, test_last_error, test_last_error_kind);
fn recorded_message() -> Option<String> {
let ptr = test_last_error();
if ptr.is_null() {
None
} else {
Some(
unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned(),
)
}
}
#[test]
fn an_untouched_slot_reads_as_success() {
LAST_ERROR.with(|slot| slot.clear());
assert_eq!(test_last_error_kind(), kind::NONE);
assert_eq!(recorded_message(), None);
}
#[test]
fn setting_and_clearing_moves_message_and_kind_together() {
LAST_ERROR.with(|slot| slot.set(kind::IO, "file not found"));
assert_eq!(test_last_error_kind(), kind::IO);
assert_eq!(recorded_message().as_deref(), Some("file not found"));
LAST_ERROR.with(|slot| slot.clear());
assert_eq!(test_last_error_kind(), kind::NONE);
assert_eq!(recorded_message(), None);
}
#[test]
fn a_message_holding_a_nul_does_not_leave_the_previous_one_behind() {
LAST_ERROR.with(|slot| slot.set(kind::IO, "first failure"));
LAST_ERROR.with(|slot| slot.set(kind::PANIC, "second\0failure"));
assert_eq!(test_last_error_kind(), kind::PANIC);
assert_eq!(recorded_message(), None, "the stale message must be gone");
}
#[test]
fn set_error_records_both_halves() {
LAST_ERROR.with(|slot| slot.set_error(&invalid_output()));
assert_eq!(test_last_error_kind(), kind::INVALID_OUTPUT);
assert_eq!(
recorded_message().as_deref(),
Some("output contains null byte")
);
LAST_ERROR.with(|slot| slot.clear());
}
#[test]
fn a_slot_is_per_thread() {
LAST_ERROR.with(|slot| slot.set(kind::IO, "main thread"));
std::thread::spawn(|| {
assert_eq!(
test_last_error_kind(),
kind::NONE,
"another thread must start clean"
);
})
.join()
.expect("thread should not panic");
assert_eq!(
test_last_error_kind(),
kind::IO,
"this thread keeps its own"
);
LAST_ERROR.with(|slot| slot.clear());
}
#[test]
fn catch_passes_success_and_classified_failure_through() {
assert_eq!(catch(|| Ok(41 + 1)).unwrap(), 42);
let err = catch::<(), _>(|| Err(invalid_argument("path was null"))).unwrap_err();
assert_eq!(err.0, kind::INVALID_ARGUMENT);
assert_eq!(err.1, "path was null");
}
#[test]
fn catch_turns_a_panic_into_a_boundary_reason() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let err = catch::<(), _>(|| panic!("deliberate")).unwrap_err();
std::panic::set_hook(previous);
assert_eq!(err.0, kind::PANIC);
assert!(kind::is_boundary(err.0));
assert!(
!err.1.contains("deliberate"),
"the panic payload is an implementation detail: {}",
err.1
);
}
}