From f84e1a5d9c014f50578046042614019bd6d87436 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Thu, 30 Jul 2026 15:08:46 +0800 Subject: [PATCH] feat: improve last_error and cffi_wrapper --- omrf/src/cffi.rs | 157 +++++++++++++++++++++++++++++ omrf/src/last_error.rs | 152 ++++++++++++++++++++++------ omrf/src/lib.rs | 121 +---------------------- omrf/tests/cffi_wrapper.rs | 198 +++++++++++++++++++++++++++++++++++++ omrf/tests/last_error.rs | 76 ++++++++++++++ 5 files changed, 555 insertions(+), 149 deletions(-) create mode 100644 omrf/src/cffi.rs create mode 100644 omrf/tests/cffi_wrapper.rs create mode 100644 omrf/tests/last_error.rs diff --git a/omrf/src/cffi.rs b/omrf/src/cffi.rs new file mode 100644 index 0000000..cc89853 --- /dev/null +++ b/omrf/src/cffi.rs @@ -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` 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` -- a type alias for `core::result::Result`. +/// - `UserError` -- the project's error type, which must implement [`Display`] and +/// `From 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 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() + } + } + }}; +} diff --git a/omrf/src/last_error.rs b/omrf/src/last_error.rs index 6f1997b..132f523 100644 --- a/omrf/src/last_error.rs +++ b/omrf/src/last_error.rs @@ -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`) 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 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 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` (typically provided +//! by implementing `From 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 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 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, + /// 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 + 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 = 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 + Display`. +pub fn set_last_error + 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()) } diff --git a/omrf/src/lib.rs b/omrf/src/lib.rs index 7f0059c..f777957 100644 --- a/omrf/src/lib.rs +++ b/omrf/src/lib.rs @@ -1,28 +1,8 @@ +pub mod cffi; pub mod cstr_ffi; pub mod last_error; 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_rules! resolve_enum { ($t:ty, $v:expr) => { @@ -43,102 +23,3 @@ macro_rules! pull_writer { $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 -/// }); -/// ``` -/// -/// 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 -/// }); -/// ``` -#[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 - } - } - }}; -} - diff --git a/omrf/tests/cffi_wrapper.rs b/omrf/tests/cffi_wrapper.rs new file mode 100644 index 0000000..bd54966 --- /dev/null +++ b/omrf/tests/cffi_wrapper.rs @@ -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 for CError { + fn from(e: TestErr) -> Self { + match e { + TestErr::NotFound => 2, + TestErr::Bad => 3, + TestErr::Parse(_) => 4, + } + } +} + +type Result = core::result::Result; + +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")); +} diff --git a/omrf/tests/last_error.rs b/omrf/tests/last_error.rs new file mode 100644 index 0000000..5b696e4 --- /dev/null +++ b/omrf/tests/last_error.rs @@ -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 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")); +}