Skip to main content

seq_core/
error.rs

1//! Runtime Error Handling
2//!
3//! Provides thread-local error state for FFI functions to report errors
4//! without panicking across the FFI boundary.
5//!
6//! # Usage
7//!
8//! FFI functions can set an error instead of panicking:
9//! ```ignore
10//! if divisor == 0 {
11//!     set_runtime_error("divide: division by zero");
12//!     return stack; // Return unchanged stack
13//! }
14//! ```
15//!
16//! Callers can check for errors:
17//! ```ignore
18//! if patch_seq_has_error() {
19//!     let error = patch_seq_take_error();
20//!     // Handle error...
21//! }
22//! ```
23
24use std::cell::RefCell;
25use std::ffi::CString;
26use std::ptr;
27
28thread_local! {
29    /// Thread-local storage for the last runtime error message
30    static LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
31
32    /// Cached C string for FFI access (avoids allocation on every get)
33    static ERROR_CSTRING: RefCell<Option<CString>> = const { RefCell::new(None) };
34}
35
36/// Set the last runtime error message
37///
38/// Note: This clears any cached CString to prevent stale pointer access.
39pub fn set_runtime_error(msg: impl Into<String>) {
40    // Clear cached CString first to prevent stale pointers
41    ERROR_CSTRING.with(|cs| *cs.borrow_mut() = None);
42    LAST_ERROR.with(|e| {
43        *e.borrow_mut() = Some(msg.into());
44    });
45}
46
47/// Take (and clear) the last runtime error message
48pub fn take_runtime_error() -> Option<String> {
49    LAST_ERROR.with(|e| e.borrow_mut().take())
50}
51
52/// Check if there's a pending runtime error
53pub fn has_runtime_error() -> bool {
54    LAST_ERROR.with(|e| e.borrow().is_some())
55}
56
57/// Clear any pending runtime error
58pub fn clear_runtime_error() {
59    LAST_ERROR.with(|e| *e.borrow_mut() = None);
60    ERROR_CSTRING.with(|e| *e.borrow_mut() = None);
61}
62
63// FFI-safe error access functions
64
65/// Check if there's a pending runtime error (FFI-safe)
66#[unsafe(no_mangle)]
67pub extern "C" fn patch_seq_has_error() -> bool {
68    has_runtime_error()
69}
70
71/// Get the last error message as a C string pointer (FFI-safe)
72///
73/// Returns null if no error is pending.
74///
75/// # WARNING: Pointer Lifetime
76/// The returned pointer is only valid until the next call to `set_runtime_error`,
77/// `get_error`, `take_error`, or `clear_error`. Callers must copy the string
78/// immediately if they need to retain it.
79#[unsafe(no_mangle)]
80pub extern "C" fn patch_seq_get_error() -> *const i8 {
81    LAST_ERROR.with(|e| {
82        let error = e.borrow();
83        match &*error {
84            Some(msg) => {
85                // Cache the CString so the pointer remains valid
86                ERROR_CSTRING.with(|cs| {
87                    // Replace null bytes with '?' to preserve error content
88                    let safe_msg: String = msg
89                        .chars()
90                        .map(|c| if c == '\0' { '?' } else { c })
91                        .collect();
92                    let cstring = CString::new(safe_msg).expect("null bytes already replaced");
93                    let ptr = cstring.as_ptr();
94                    *cs.borrow_mut() = Some(cstring);
95                    ptr
96                })
97            }
98            None => ptr::null(),
99        }
100    })
101}
102
103/// Take (and clear) the last error, returning it as a C string (FFI-safe)
104///
105/// Returns null if no error is pending.
106///
107/// # WARNING: Pointer Lifetime
108/// The returned pointer is only valid until the next call to `set_runtime_error`,
109/// `get_error`, `take_error`, or `clear_error`. Callers must copy the string
110/// immediately if they need to retain it.
111#[unsafe(no_mangle)]
112pub extern "C" fn patch_seq_take_error() -> *const i8 {
113    let msg = take_runtime_error();
114    match msg {
115        Some(s) => ERROR_CSTRING.with(|cs| {
116            // Replace null bytes with '?' to preserve error content
117            let safe_msg: String = s.chars().map(|c| if c == '\0' { '?' } else { c }).collect();
118            let cstring = CString::new(safe_msg).expect("null bytes already replaced");
119            let ptr = cstring.as_ptr();
120            *cs.borrow_mut() = Some(cstring);
121            ptr
122        }),
123        None => ptr::null(),
124    }
125}
126
127/// Clear any pending error (FFI-safe)
128#[unsafe(no_mangle)]
129pub extern "C" fn patch_seq_clear_error() {
130    clear_runtime_error();
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_set_and_take_error() {
139        clear_runtime_error();
140        assert!(!has_runtime_error());
141
142        set_runtime_error("test error");
143        assert!(has_runtime_error());
144
145        let error = take_runtime_error();
146        assert_eq!(error, Some("test error".to_string()));
147        assert!(!has_runtime_error());
148    }
149
150    #[test]
151    fn test_clear_error() {
152        set_runtime_error("another error");
153        assert!(has_runtime_error());
154
155        clear_runtime_error();
156        assert!(!has_runtime_error());
157        assert!(take_runtime_error().is_none());
158    }
159}