feat: improve last_error and cffi_wrapper
This commit is contained in:
@@ -0,0 +1,157 @@
|
|||||||
|
//! Ergonomic helpers for authoring `extern "C"` functions.
|
||||||
|
//!
|
||||||
|
//! Contains the following macros:
|
||||||
|
//!
|
||||||
|
//! - `in_param_ty!` / `out_param_ty!` -- annotate the direction (in / out) of an FFI parameter.
|
||||||
|
//! - `deref_out_param!` / `set_out_param!` -- write values through output parameter pointers.
|
||||||
|
//! - `cffi_wrapper!` -- wrap a `Result`-returning inner function into the C-style outcome pattern
|
||||||
|
//! (an [`CError`](crate::last_error::CError) return value), backed by
|
||||||
|
//! [`last_error`](crate::last_error) for outcome/message storage.
|
||||||
|
|
||||||
|
/// Annotate a parameter as an input parameter (passed by value). Expands to the type unchanged.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! in_param_ty {
|
||||||
|
($t:ty) => {
|
||||||
|
$t
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Annotate a parameter as an output parameter. Expands to `*mut T` -- a mutable raw pointer the
|
||||||
|
/// callee writes the result through.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! out_param_ty {
|
||||||
|
($t:ty) => {
|
||||||
|
*mut $t
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expand to a place expression that writes through an output parameter pointer (`*ptr`).
|
||||||
|
///
|
||||||
|
/// Intended for the left-hand side of an assignment within an `unsafe` block. Composing several of
|
||||||
|
/// these into a tuple enables structured destructuring assignment for multiple outputs, e.g.
|
||||||
|
/// `unsafe { (deref_out_param!(a), deref_out_param!(b)) = (x, y) };` expands to
|
||||||
|
/// `unsafe { (*a, *b) = (x, y) };`.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! deref_out_param {
|
||||||
|
($lhs:expr) => {
|
||||||
|
*$lhs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a single value through an output parameter pointer.
|
||||||
|
///
|
||||||
|
/// Convenience wrapper around [`deref_out_param!`] for the common single-output case; expands to
|
||||||
|
/// `unsafe { deref_out_param!($lhs) = $rhs };`.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! set_out_param {
|
||||||
|
($lhs:expr, $rhs:expr) => {
|
||||||
|
unsafe {
|
||||||
|
$crate::deref_out_param!($lhs) = $rhs;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrap an inner `Result`-returning function body into a C-style outcome.
|
||||||
|
///
|
||||||
|
/// The four accepted shapes are:
|
||||||
|
///
|
||||||
|
/// - no input, no output: `cffi_wrapper!(|| { ... })`
|
||||||
|
/// - inputs, no output: `cffi_wrapper!(|p1: T1, p2: T2| { ... })`
|
||||||
|
/// - no input, outputs: `cffi_wrapper!(|| -> (o1: O1, o2: O2) { ... })`
|
||||||
|
/// - inputs and outputs: `cffi_wrapper!(|p1: T1| -> (o1: O1, o2: O2, o3: O3) { ... })`
|
||||||
|
///
|
||||||
|
/// The output list accepts any number of outputs (one or more). The inner function must return
|
||||||
|
/// `Result<O>` for a single output or `Result<(O1, O2, ...)>` for multiple outputs; the wrapper
|
||||||
|
/// writes each value to its corresponding output parameter via structured destructuring assignment
|
||||||
|
/// through the pointers. On error no output parameter is written.
|
||||||
|
///
|
||||||
|
/// The identifiers used for inputs and outputs must match the surrounding function's parameter
|
||||||
|
/// names, since the expansion forwards them directly.
|
||||||
|
///
|
||||||
|
/// # Required environment conventions
|
||||||
|
///
|
||||||
|
/// This macro expands in the caller's scope and internally relies on
|
||||||
|
/// [`last_error`](crate::last_error), so the following names must be in scope there:
|
||||||
|
///
|
||||||
|
/// - `Result<T>` -- a type alias for `core::result::Result<T, UserError>`.
|
||||||
|
/// - `UserError` -- the project's error type, which must implement [`Display`] and
|
||||||
|
/// `From<UserError> for `[`CError`](crate::last_error::CError)` so it can be handed to
|
||||||
|
/// [`set_last_error`](crate::last_error::set_last_error).
|
||||||
|
///
|
||||||
|
/// On success the wrapper clears the thread-local last error and yields
|
||||||
|
/// [`CERROR_OK`](crate::last_error::CERROR_OK) (absolute success); on error it records the
|
||||||
|
/// error (message via [`Display`], code via the user's `From<UserError> for CError`) and yields the
|
||||||
|
/// recorded code. Both branches obtain their final value from
|
||||||
|
/// [`get_error_code`](crate::last_error::get_error_code).
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! cffi_wrapper {
|
||||||
|
// inputs + one or more outputs
|
||||||
|
(|$($p:ident: $t:ty),+| -> ($($o:ident: $ot:ty),+) $inner_body:block) => {{
|
||||||
|
fn inner($($p: $t),+) -> Result<($($ot),+)> $inner_body
|
||||||
|
|
||||||
|
match inner($($p),+) {
|
||||||
|
Ok(rv) => {
|
||||||
|
unsafe {
|
||||||
|
($($crate::deref_out_param!($o)),+) = rv;
|
||||||
|
}
|
||||||
|
$crate::last_error::clear_last_error();
|
||||||
|
$crate::last_error::get_error_code()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
$crate::last_error::set_last_error(e);
|
||||||
|
$crate::last_error::get_error_code()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
|
||||||
|
// no inputs + one or more outputs
|
||||||
|
(|| -> ($($o:ident: $ot:ty),+) $inner_body:block) => {{
|
||||||
|
fn inner() -> Result<($($ot),+)> $inner_body
|
||||||
|
|
||||||
|
match inner() {
|
||||||
|
Ok(rv) => {
|
||||||
|
unsafe {
|
||||||
|
($($crate::deref_out_param!($o)),+) = rv;
|
||||||
|
}
|
||||||
|
$crate::last_error::clear_last_error();
|
||||||
|
$crate::last_error::get_error_code()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
$crate::last_error::set_last_error(e);
|
||||||
|
$crate::last_error::get_error_code()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
|
||||||
|
// inputs + no outputs
|
||||||
|
(|$($p:ident: $t:ty),+| $inner_body:block) => {{
|
||||||
|
fn inner($($p: $t),+) -> Result<()> $inner_body
|
||||||
|
|
||||||
|
match inner($($p),+) {
|
||||||
|
Ok(_) => {
|
||||||
|
$crate::last_error::clear_last_error();
|
||||||
|
$crate::last_error::get_error_code()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
$crate::last_error::set_last_error(e);
|
||||||
|
$crate::last_error::get_error_code()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
|
||||||
|
// no inputs + no outputs
|
||||||
|
(|| $inner_body:block) => {{
|
||||||
|
fn inner() -> Result<()> $inner_body
|
||||||
|
|
||||||
|
match inner() {
|
||||||
|
Ok(_) => {
|
||||||
|
$crate::last_error::clear_last_error();
|
||||||
|
$crate::last_error::get_error_code()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
$crate::last_error::set_last_error(e);
|
||||||
|
$crate::last_error::get_error_code()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
+123
-29
@@ -1,58 +1,152 @@
|
|||||||
//! When calling function with dynamic library,
|
//! Thread-local storage for the outcome of the most recent FFI call on this thread -- both its
|
||||||
//! function return value indicates whether function has been successfully executed.
|
//! error *code* and its human-readable *message*.
|
||||||
//! When function return `false`, programmer may want to know which error occurs.
|
//!
|
||||||
//!
|
//! This is closer in spirit to Win32 `FormatMessage` or SQLite's `sqlite3_errmsg` than to
|
||||||
//! This module provide **thread independent** error message string storage,
|
//! `GetLastError`: the conventional pattern is for the FFI function to *return* the code as its
|
||||||
//! which is more like Win32 `GetLastError()` but return error message instead of error code.
|
//! return value (see [`get_error_code`]) while this module retains the matching message text for
|
||||||
//! These module provided functions will be called when executing main module functions.
|
//! 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::cell::RefCell;
|
||||||
use std::ffi::{CString, c_char};
|
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 {
|
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,
|
msg: CString,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LastError {
|
impl LastError {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
code: None,
|
||||||
msg: CString::new("").expect("empty string must be valid for CString"),
|
msg: CString::new("").expect("empty string must be valid for CString"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_msg(&mut self, msg: &str) {
|
/// Record an outcome: derive a message from `err`'s [`Display`] (sanitizing interior NUL) and
|
||||||
self.msg = CString::new(msg).expect("unexpected blank in error message output");
|
/// 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 {
|
/// Reset to the absolute-success state: no code, empty message.
|
||||||
self.msg.as_ptr()
|
pub fn clear_last_error(&mut self) {
|
||||||
}
|
self.code = None;
|
||||||
|
|
||||||
pub fn clear_msg(&mut self) {
|
|
||||||
self.msg = CString::new("").expect("empty string must be valid for CString");
|
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! {
|
thread_local! {
|
||||||
static LAST_ERROR: RefCell<LastError> = RefCell::new(LastError::new());
|
static LAST_ERROR: RefCell<LastError> = RefCell::new(LastError::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set thread local error message.
|
/// Record an outcome for the current thread.
|
||||||
pub fn set_last_error(msg: &str) {
|
///
|
||||||
LAST_ERROR.with(|e| {
|
/// Derives a message from `err`'s [`Display`] (interior NUL bytes are replaced with the replacement
|
||||||
e.borrow_mut().set_msg(msg);
|
/// 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.
|
/// Reset the current thread's recorded outcome to the absolute-success state (no code, empty
|
||||||
/// If there is no error, return pointer will point to empty string.
|
/// message). Should be called at the entry of every FFI function.
|
||||||
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).
|
|
||||||
pub fn clear_last_error() {
|
pub fn clear_last_error() {
|
||||||
LAST_ERROR.with(|e| {
|
LAST_ERROR.with(|e| e.borrow_mut().clear_last_error());
|
||||||
e.borrow_mut().clear_msg();
|
}
|
||||||
});
|
|
||||||
|
/// 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())
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-120
@@ -1,28 +1,8 @@
|
|||||||
|
pub mod cffi;
|
||||||
pub mod cstr_ffi;
|
pub mod cstr_ffi;
|
||||||
pub mod last_error;
|
pub mod last_error;
|
||||||
pub mod object_pool;
|
pub mod object_pool;
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! in_param_ty {
|
|
||||||
($t:ty) => {
|
|
||||||
$t
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! out_param_ty {
|
|
||||||
($t:ty) => {
|
|
||||||
*mut $t
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! set_out_param {
|
|
||||||
($lhs:expr, $rhs:expr) => {
|
|
||||||
unsafe { *$lhs = $rhs };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! resolve_enum {
|
macro_rules! resolve_enum {
|
||||||
($t:ty, $v:expr) => {
|
($t:ty, $v:expr) => {
|
||||||
@@ -43,102 +23,3 @@ macro_rules! pull_writer {
|
|||||||
$pool.write().map_err(|_| Error::PoisonRwLock)
|
$pool.write().map_err(|_| Error::PoisonRwLock)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Macro to wrap inner function execution with standard error handling pattern.
|
|
||||||
///
|
|
||||||
/// For functions with no output parameter and no input parameters:
|
|
||||||
/// ```ignore
|
|
||||||
/// cffi_wrapper!(|| {
|
|
||||||
/// // inner function body returning Result<()>
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// For functions with no output parameter and with input parameters:
|
|
||||||
/// ```ignore
|
|
||||||
/// cffi_wrapper!(|param1: Type1, param2: Type2| {
|
|
||||||
/// // inner function body using param1, param2 returning Result<()>
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// For functions with one output parameter and no input parameters:
|
|
||||||
/// ```ignore
|
|
||||||
/// cffi_wrapper!(|| -> (out_param, OutType) {
|
|
||||||
/// // inner function body returning Result<T>
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// For functions with one output parameter and with input parameters:
|
|
||||||
/// ```ignore
|
|
||||||
/// cffi_wrapper!(|param1: Type1| -> (out_param, OutType) {
|
|
||||||
/// // inner function body using param1 returning Result<T>
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! cffi_wrapper {
|
|
||||||
// Case with output parameter and input parameters
|
|
||||||
(|$($param:ident: $param_ty:ty),*| -> ($out_param:ident: $out_param_ty:ty) $inner_body:block) => {{
|
|
||||||
fn inner($($param: $param_ty),*) -> Result<$out_param_ty> $inner_body
|
|
||||||
|
|
||||||
match inner($($param),*) {
|
|
||||||
Ok(rv) => {
|
|
||||||
set_out_param!($out_param, rv);
|
|
||||||
last_error::clear_last_error();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
last_error::set_last_error(e.to_string().as_str());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
|
|
||||||
// Case with output parameter and no input parameters
|
|
||||||
(|| -> ($out_param:ident: $out_param_ty:ty) $inner_body:block) => {{
|
|
||||||
fn inner() -> Result<$out_param_ty> $inner_body
|
|
||||||
|
|
||||||
match inner() {
|
|
||||||
Ok(rv) => {
|
|
||||||
set_out_param!($out_param, rv);
|
|
||||||
last_error::clear_last_error();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
last_error::set_last_error(e.to_string().as_str());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
|
|
||||||
// Case without output parameter but with input parameters
|
|
||||||
(|$($param:ident: $param_ty:ty),*| $inner_body:block) => {{
|
|
||||||
fn inner($($param: $param_ty),*) -> Result<()> $inner_body
|
|
||||||
|
|
||||||
match inner($($param),*) {
|
|
||||||
Ok(_) => {
|
|
||||||
last_error::clear_last_error();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
last_error::set_last_error(e.to_string().as_str());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
|
|
||||||
// Case without output parameter and no input parameters
|
|
||||||
(|| $inner_body:block) => {{
|
|
||||||
fn inner() -> Result<()> $inner_body
|
|
||||||
|
|
||||||
match inner() {
|
|
||||||
Ok(_) => {
|
|
||||||
last_error::clear_last_error();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
last_error::set_last_error(e.to_string().as_str());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
//! Integration tests for the `cffi_wrapper!` macro and the parameter-direction macros.
|
||||||
|
//!
|
||||||
|
//! The wrapper functions below mirror the real FFI surface -- parameters typed via `in_param_ty!`
|
||||||
|
//! / `out_param_ty!` and a `CError` return -- but omit the `extern "C"` / `no_mangle` attributes.
|
||||||
|
//! Each is exercised by a `#[test]` caller, in the same shape as a real `WFProgramCreate`-style
|
||||||
|
//! FFI function.
|
||||||
|
//!
|
||||||
|
//! Note: on the error path the value of any output parameter is undefined behavior; the error-case
|
||||||
|
//! tests provide a valid pointer but intentionally never read it back.
|
||||||
|
|
||||||
|
use sarasacw_omrf::last_error::{self, CError, CERROR_OK, CStrPtr};
|
||||||
|
use sarasacw_omrf::{cffi_wrapper, in_param_ty, out_param_ty};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
// ---- project-side conventions required by `cffi_wrapper!` ----
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum TestErr {
|
||||||
|
#[error("not found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("bad input")]
|
||||||
|
Bad,
|
||||||
|
#[error("parse: {0}")]
|
||||||
|
Parse(#[from] core::num::ParseIntError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TestErr> for CError {
|
||||||
|
fn from(e: TestErr) -> Self {
|
||||||
|
match e {
|
||||||
|
TestErr::NotFound => 2,
|
||||||
|
TestErr::Bad => 3,
|
||||||
|
TestErr::Parse(_) => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result<T> = core::result::Result<T, TestErr>;
|
||||||
|
|
||||||
|
fn parse_cstr(ptr: CStrPtr) -> String {
|
||||||
|
assert!(!ptr.is_null());
|
||||||
|
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ FFI-style wrapper functions ============
|
||||||
|
|
||||||
|
/// Coverage: no inputs, no outputs -- success path. Returns absolute success and leaves no last
|
||||||
|
/// error.
|
||||||
|
fn ping() -> CError {
|
||||||
|
cffi_wrapper!(|| { Ok(()) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverage: no inputs, no outputs -- error path. Returns the mapped code and records the message.
|
||||||
|
fn ping_fail() -> CError {
|
||||||
|
cffi_wrapper!(|| { Err(TestErr::NotFound) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverage: one input, no outputs. Succeeds for non-negative input, fails (`Bad`) otherwise.
|
||||||
|
fn validate(x: in_param_ty!(i32)) -> CError {
|
||||||
|
cffi_wrapper!(|x: i32| {
|
||||||
|
if x < 0 {
|
||||||
|
Err(TestErr::Bad)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverage: no inputs, one output -- success path. Writes the result through the output pointer.
|
||||||
|
fn make_answer(out: out_param_ty!(i32)) -> CError {
|
||||||
|
cffi_wrapper!(|| -> (out: i32) { Ok(42) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverage: no inputs, one output -- error path. The output value is UB after the call.
|
||||||
|
fn make_fail(out: out_param_ty!(i32)) -> CError {
|
||||||
|
cffi_wrapper!(|| -> (out: i32) { Err(TestErr::NotFound) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverage: one input, one output.
|
||||||
|
fn scale(x: in_param_ty!(i32), out: out_param_ty!(i32)) -> CError {
|
||||||
|
cffi_wrapper!(|x: i32| -> (out: i32) { Ok(x * 2) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverage: two inputs, two outputs (structured destructuring assignment through the pointers).
|
||||||
|
fn add_mul(
|
||||||
|
a: in_param_ty!(i32),
|
||||||
|
b: in_param_ty!(i32),
|
||||||
|
sum: out_param_ty!(i32),
|
||||||
|
prod: out_param_ty!(i32),
|
||||||
|
) -> CError {
|
||||||
|
cffi_wrapper!(|a: i32, b: i32| -> (sum: i32, prod: i32) { Ok((a + b, a * b)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverage: no inputs, two outputs (the no-input branch of the multi-output shape).
|
||||||
|
fn make_pair(o1: out_param_ty!(i32), o2: out_param_ty!(i32)) -> CError {
|
||||||
|
cffi_wrapper!(|| -> (o1: i32, o2: i32) { Ok((10, 20)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverage: the `?` operator propagating a sub-error (`ParseIntError`) into `TestErr` via the
|
||||||
|
/// `#[from]` conversion, exactly like the `WFProgramCreate` example chains `?`.
|
||||||
|
fn parse_num(s: in_param_ty!(&str), out: out_param_ty!(i32)) -> CError {
|
||||||
|
cffi_wrapper!(|s: &str| -> (out: i32) {
|
||||||
|
let n: i32 = s.parse()?;
|
||||||
|
Ok(n)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ test callers ============
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ping_returns_success() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
assert_eq!(ping(), CERROR_OK);
|
||||||
|
assert!(!last_error::has_last_message());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ping_fail_records_error() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
assert_eq!(ping_fail(), 2);
|
||||||
|
assert_eq!(parse_cstr(last_error::get_error_message()), "not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_accepts_non_negative() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
assert_eq!(validate(5), CERROR_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_rejects_negative() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
assert_eq!(validate(-1), 3);
|
||||||
|
assert_eq!(parse_cstr(last_error::get_error_message()), "bad input");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn make_answer_writes_output() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
let mut out: i32 = 0;
|
||||||
|
assert_eq!(make_answer(&mut out), CERROR_OK);
|
||||||
|
assert_eq!(out, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn make_fail_records_error() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
let mut out: i32 = 0;
|
||||||
|
// A valid pointer is required, but its value is UB after an error and is intentionally unread.
|
||||||
|
assert_eq!(make_fail(&mut out), 2);
|
||||||
|
assert_eq!(parse_cstr(last_error::get_error_message()), "not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scale_writes_doubled() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
let mut out: i32 = 0;
|
||||||
|
assert_eq!(scale(21, &mut out), CERROR_OK);
|
||||||
|
assert_eq!(out, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_mul_writes_both_outputs() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
let mut sum: i32 = 0;
|
||||||
|
let mut prod: i32 = 0;
|
||||||
|
assert_eq!(add_mul(3, 4, &mut sum, &mut prod), CERROR_OK);
|
||||||
|
assert_eq!(sum, 7);
|
||||||
|
assert_eq!(prod, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn make_pair_writes_both_outputs() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
let mut a: i32 = 0;
|
||||||
|
let mut b: i32 = 0;
|
||||||
|
assert_eq!(make_pair(&mut a, &mut b), CERROR_OK);
|
||||||
|
assert_eq!(a, 10);
|
||||||
|
assert_eq!(b, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_num_success() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
let mut out: i32 = 0;
|
||||||
|
assert_eq!(parse_num("123", &mut out), CERROR_OK);
|
||||||
|
assert_eq!(out, 123);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_num_fail_propagates_suberror() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
let mut out: i32 = 0;
|
||||||
|
// Output value is UB after an error and is intentionally unread.
|
||||||
|
assert_eq!(parse_num("not_a_num", &mut out), 4);
|
||||||
|
assert!(parse_cstr(last_error::get_error_message()).contains("parse"));
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
//! Integration tests for the `last_error` module.
|
||||||
|
|
||||||
|
use sarasacw_omrf::last_error::{self, CError, CERROR_OK, CStrPtr};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum TestErr {
|
||||||
|
#[error("boom error")]
|
||||||
|
Boom,
|
||||||
|
#[error("bad\x00value")]
|
||||||
|
WithNul,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TestErr> for CError {
|
||||||
|
fn from(e: TestErr) -> Self {
|
||||||
|
match e {
|
||||||
|
TestErr::Boom => 7,
|
||||||
|
TestErr::WithNul => 9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cstr(ptr: CStrPtr) -> String {
|
||||||
|
assert!(!ptr.is_null());
|
||||||
|
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fresh_state_is_absolute_success() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
assert!(!last_error::has_last_message());
|
||||||
|
assert_eq!(last_error::get_error_code(), CERROR_OK);
|
||||||
|
assert_eq!(parse_cstr(last_error::get_error_message()), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_records_code_and_message() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
last_error::set_last_error(TestErr::Boom);
|
||||||
|
assert!(last_error::has_last_message());
|
||||||
|
assert_eq!(last_error::get_error_code(), 7);
|
||||||
|
assert_eq!(parse_cstr(last_error::get_error_message()), "boom error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_resets_to_absolute_success() {
|
||||||
|
last_error::set_last_error(TestErr::Boom);
|
||||||
|
assert!(last_error::has_last_message());
|
||||||
|
last_error::clear_last_error();
|
||||||
|
assert!(!last_error::has_last_message());
|
||||||
|
assert_eq!(last_error::get_error_code(), CERROR_OK);
|
||||||
|
assert_eq!(parse_cstr(last_error::get_error_message()), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interior_nul_in_message_is_sanitized() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
last_error::set_last_error(TestErr::WithNul);
|
||||||
|
assert_eq!(last_error::get_error_code(), 9);
|
||||||
|
let msg = parse_cstr(last_error::get_error_message());
|
||||||
|
assert!(!msg.contains('\0'));
|
||||||
|
assert!(msg.contains("bad"));
|
||||||
|
assert!(msg.contains("value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overwriting_replaces_previous_state() {
|
||||||
|
last_error::clear_last_error();
|
||||||
|
last_error::set_last_error(TestErr::Boom);
|
||||||
|
last_error::set_last_error(TestErr::WithNul);
|
||||||
|
assert_eq!(last_error::get_error_code(), 9);
|
||||||
|
let msg = parse_cstr(last_error::get_error_message());
|
||||||
|
assert!(!msg.contains("boom"));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user