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}