feat: improve last_error and cffi_wrapper
This commit is contained in:
+123
-29
@@ -1,58 +1,152 @@
|
||||
//! When calling function with dynamic library,
|
||||
//! function return value indicates whether function has been successfully executed.
|
||||
//! When function return `false`, programmer may want to know which error occurs.
|
||||
//!
|
||||
//! This module provide **thread independent** error message string storage,
|
||||
//! which is more like Win32 `GetLastError()` but return error message instead of error code.
|
||||
//! These module provided functions will be called when executing main module functions.
|
||||
//! Thread-local storage for the outcome of the most recent FFI call on this thread -- both its
|
||||
//! error *code* and its human-readable *message*.
|
||||
//!
|
||||
//! This is closer in spirit to Win32 `FormatMessage` or SQLite's `sqlite3_errmsg` than to
|
||||
//! `GetLastError`: the conventional pattern is for the FFI function to *return* the code as its
|
||||
//! return value (see [`get_error_code`]) while this module retains the matching message text for
|
||||
//! later retrieval (see [`get_error_message`]). The code is also retained here so it can be
|
||||
//! queried independently of the return value.
|
||||
//!
|
||||
//! # Call order
|
||||
//!
|
||||
//! The expected usage at each FFI function entry is:
|
||||
//!
|
||||
//! 1. Call [`clear_last_error`] to reset the thread state to absolute success (code = `None`,
|
||||
//! empty message). **This must be done at the entry of every FFI function**; the default state
|
||||
//! is "absolute success".
|
||||
//! 2. On failure, call [`set_last_error`] with the error value; this records the code (via
|
||||
//! `Into<CError>`) and derives the message (via `Display`).
|
||||
//! 3. The FFI function returns [`get_error_code`] (absolute success if nothing was set, otherwise
|
||||
//! the recorded code). The foreign side may call [`get_error_message`] for the text.
|
||||
//!
|
||||
//! The `cffi_wrapper!` macro automates this whole sequence.
|
||||
//!
|
||||
//! # The error code type
|
||||
//!
|
||||
//! [`CError`] is defined here as a plain `u32`. [`CERROR_OK`] is the single value that represents
|
||||
//! absolute, unambiguous success. Every other value's meaning is decided by the consuming project
|
||||
//! through `From<UserError> for CError`: it may denote a failure, or, like some Win32 functions, a
|
||||
//! *partial success*.
|
||||
//!
|
||||
//! # Migrating from a `bool` return value
|
||||
//!
|
||||
//! Projects that previously returned a plain `bool` can adopt the error-code scheme with minimal
|
||||
//! change: define exactly one non-success code (e.g. `const CERROR_FAIL: CError = 1;`) and map
|
||||
//! *every* error type to it via `From<UserError> for CError`. The result is equivalent to a `bool`
|
||||
//! -- [`CERROR_OK`] for success, `CERROR_FAIL` for any failure -- while leaving room to introduce
|
||||
//! finer-grained codes later.
|
||||
//!
|
||||
//! # Required trait implementations
|
||||
//!
|
||||
//! [`set_last_error`] accepts any error type `E` that implements `Into<CError>` (typically provided
|
||||
//! by implementing `From<UserError> for CError`) and [`Display`] (used to derive the message text).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{CString, c_char};
|
||||
use std::fmt::Display;
|
||||
|
||||
/// The raw pointer type to an immutable C/C++ NUL-terminated character string, as returned by
|
||||
/// [`get_error_message`].
|
||||
pub type CStrPtr = *const c_char;
|
||||
|
||||
/// The error code type returned by FFI functions.
|
||||
///
|
||||
/// This is a plain `u32`. [`CERROR_OK`] is the single value that represents absolute, unambiguous
|
||||
/// success. Any other value's meaning is defined by the consuming project through
|
||||
/// `From<UserError> for CError`: it may represent a failure, or, like some Win32 functions, a
|
||||
/// *partial success*.
|
||||
pub type CError = u32;
|
||||
|
||||
/// The sole error code that represents absolute, unambiguous success.
|
||||
///
|
||||
/// All other [`CError`] values carry project-defined meaning (failure or partial success) via the
|
||||
/// user's `From<UserError> for CError`.
|
||||
pub const CERROR_OK: CError = 0;
|
||||
|
||||
struct LastError {
|
||||
/// `None` means nothing has been recorded since the last clear (i.e. the last call was an
|
||||
/// absolute success). `Some(code)` means an outcome code has been recorded.
|
||||
code: Option<CError>,
|
||||
/// Cached message text derived from the error's [`Display`], with interior NUL bytes replaced
|
||||
/// by the replacement character so that building the [`CString`] never fails.
|
||||
msg: CString,
|
||||
}
|
||||
|
||||
impl LastError {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
code: None,
|
||||
msg: CString::new("").expect("empty string must be valid for CString"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_msg(&mut self, msg: &str) {
|
||||
self.msg = CString::new(msg).expect("unexpected blank in error message output");
|
||||
/// Record an outcome: derive a message from `err`'s [`Display`] (sanitizing interior NUL) and
|
||||
/// store `err.into()` as the code.
|
||||
pub fn set_last_error<E: Into<CError> + Display>(&mut self, err: E) {
|
||||
let text = format!("{err}").replace('\0', "\u{FFFD}");
|
||||
self.msg = CString::new(text).expect("NUL bytes were sanitized away");
|
||||
self.code = Some(err.into());
|
||||
}
|
||||
|
||||
pub fn get_msg(&self) -> *const c_char {
|
||||
self.msg.as_ptr()
|
||||
}
|
||||
|
||||
pub fn clear_msg(&mut self) {
|
||||
/// Reset to the absolute-success state: no code, empty message.
|
||||
pub fn clear_last_error(&mut self) {
|
||||
self.code = None;
|
||||
self.msg = CString::new("").expect("empty string must be valid for CString");
|
||||
}
|
||||
|
||||
/// Return whether an outcome has been recorded since the last clear.
|
||||
pub fn has_last_message(&self) -> bool {
|
||||
self.code.is_some()
|
||||
}
|
||||
|
||||
/// Return the recorded code, or [`CERROR_OK`] (absolute success) if none was recorded.
|
||||
pub fn get_error_code(&self) -> CError {
|
||||
self.code.unwrap_or(CERROR_OK)
|
||||
}
|
||||
|
||||
/// Return a pointer to the cached message string -- a NUL-terminated C string whose storage is
|
||||
/// private to this module and not shared with any other module's cache. If nothing was recorded
|
||||
/// the pointer addresses an empty string. The pointer stays valid until the next
|
||||
/// [`set_last_error`](Self::set_last_error) or [`clear_last_error`](Self::clear_last_error) on
|
||||
/// this thread.
|
||||
pub fn get_error_message(&self) -> CStrPtr {
|
||||
self.msg.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static LAST_ERROR: RefCell<LastError> = RefCell::new(LastError::new());
|
||||
}
|
||||
|
||||
/// Set thread local error message.
|
||||
pub fn set_last_error(msg: &str) {
|
||||
LAST_ERROR.with(|e| {
|
||||
e.borrow_mut().set_msg(msg);
|
||||
});
|
||||
/// Record an outcome for the current thread.
|
||||
///
|
||||
/// Derives a message from `err`'s [`Display`] (interior NUL bytes are replaced with the replacement
|
||||
/// character) and stores `err.into()` as the code. Requires `E: Into<CError> + Display`.
|
||||
pub fn set_last_error<E: Into<CError> + Display>(err: E) {
|
||||
LAST_ERROR.with(|e| e.borrow_mut().set_last_error(err));
|
||||
}
|
||||
|
||||
/// Get const pointer to thread local error message string.
|
||||
/// If there is no error, return pointer will point to empty string.
|
||||
pub fn get_last_error() -> *const c_char {
|
||||
LAST_ERROR.with(|e| e.borrow().get_msg())
|
||||
}
|
||||
|
||||
/// Clear thread local error message (reset to empty string).
|
||||
/// Reset the current thread's recorded outcome to the absolute-success state (no code, empty
|
||||
/// message). Should be called at the entry of every FFI function.
|
||||
pub fn clear_last_error() {
|
||||
LAST_ERROR.with(|e| {
|
||||
e.borrow_mut().clear_msg();
|
||||
});
|
||||
LAST_ERROR.with(|e| e.borrow_mut().clear_last_error());
|
||||
}
|
||||
|
||||
/// Return whether an outcome has been recorded for the current thread since the last clear.
|
||||
pub fn has_last_message() -> bool {
|
||||
LAST_ERROR.with(|e| e.borrow().has_last_message())
|
||||
}
|
||||
|
||||
/// Return the current thread's recorded code, or [`CERROR_OK`] (absolute success) if none was
|
||||
/// recorded.
|
||||
pub fn get_error_code() -> CError {
|
||||
LAST_ERROR.with(|e| e.borrow().get_error_code())
|
||||
}
|
||||
|
||||
/// Return a pointer to the current thread's cached message string -- a NUL-terminated C string
|
||||
/// whose storage is private to this module and not shared with any other module's cache. If nothing
|
||||
/// was recorded the pointer addresses an empty string. The pointer stays valid until the next
|
||||
/// [`set_last_error`] or [`clear_last_error`] on this thread.
|
||||
pub fn get_error_message() -> CStrPtr {
|
||||
LAST_ERROR.with(|e| e.borrow().get_error_message())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user