seq_runtime/
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/// Format a panic payload into an error message
64pub fn format_panic_payload(payload: &Box<dyn std::any::Any + Send>) -> String {
65    if let Some(s) = payload.downcast_ref::<&str>() {
66        s.to_string()
67    } else if let Some(s) = payload.downcast_ref::<String>() {
68        s.clone()
69    } else {
70        "unknown panic".to_string()
71    }
72}
73
74// FFI-safe error access functions
75
76/// Check if there's a pending runtime error (FFI-safe)
77#[unsafe(no_mangle)]
78pub extern "C" fn patch_seq_has_error() -> bool {
79    has_runtime_error()
80}
81
82/// Get the last error message as a C string pointer (FFI-safe)
83///
84/// Returns null if no error is pending.
85///
86/// # WARNING: Pointer Lifetime
87/// The returned pointer is only valid until the next call to `set_runtime_error`,
88/// `get_error`, `take_error`, or `clear_error`. Callers must copy the string
89/// immediately if they need to retain it.
90#[unsafe(no_mangle)]
91pub extern "C" fn patch_seq_get_error() -> *const i8 {
92    LAST_ERROR.with(|e| {
93        let error = e.borrow();
94        match &*error {
95            Some(msg) => {
96                // Cache the CString so the pointer remains valid
97                ERROR_CSTRING.with(|cs| {
98                    // Replace null bytes with '?' to preserve error content
99                    let safe_msg: String = msg
100                        .chars()
101                        .map(|c| if c == '\0' { '?' } else { c })
102                        .collect();
103                    let cstring = CString::new(safe_msg).expect("null bytes already replaced");
104                    let ptr = cstring.as_ptr();
105                    *cs.borrow_mut() = Some(cstring);
106                    ptr
107                })
108            }
109            None => ptr::null(),
110        }
111    })
112}
113
114/// Take (and clear) the last error, returning it as a C string (FFI-safe)
115///
116/// Returns null if no error is pending.
117///
118/// # WARNING: Pointer Lifetime
119/// The returned pointer is only valid until the next call to `set_runtime_error`,
120/// `get_error`, `take_error`, or `clear_error`. Callers must copy the string
121/// immediately if they need to retain it.
122#[unsafe(no_mangle)]
123pub extern "C" fn patch_seq_take_error() -> *const i8 {
124    let msg = take_runtime_error();
125    match msg {
126        Some(s) => ERROR_CSTRING.with(|cs| {
127            // Replace null bytes with '?' to preserve error content
128            let safe_msg: String = s.chars().map(|c| if c == '\0' { '?' } else { c }).collect();
129            let cstring = CString::new(safe_msg).expect("null bytes already replaced");
130            let ptr = cstring.as_ptr();
131            *cs.borrow_mut() = Some(cstring);
132            ptr
133        }),
134        None => ptr::null(),
135    }
136}
137
138/// Clear any pending error (FFI-safe)
139#[unsafe(no_mangle)]
140pub extern "C" fn patch_seq_clear_error() {
141    clear_runtime_error();
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_set_and_take_error() {
150        clear_runtime_error();
151        assert!(!has_runtime_error());
152
153        set_runtime_error("test error");
154        assert!(has_runtime_error());
155
156        let error = take_runtime_error();
157        assert_eq!(error, Some("test error".to_string()));
158        assert!(!has_runtime_error());
159    }
160
161    #[test]
162    fn test_clear_error() {
163        set_runtime_error("another error");
164        assert!(has_runtime_error());
165
166        clear_runtime_error();
167        assert!(!has_runtime_error());
168        assert!(take_runtime_error().is_none());
169    }
170
171    #[test]
172    fn test_format_panic_payload() {
173        let payload: Box<dyn std::any::Any + Send> = Box::new("panic message");
174        assert_eq!(format_panic_payload(&payload), "panic message");
175
176        let payload: Box<dyn std::any::Any + Send> = Box::new("owned panic".to_string());
177        assert_eq!(format_panic_payload(&payload), "owned panic");
178    }
179}