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/// Replace any interior null bytes with `'?'`, build a `CString`, cache it
66/// in `ERROR_CSTRING`, and return a raw pointer into the cached string.
67///
68/// The returned pointer is valid until the next call to `set_runtime_error`,
69/// `patch_seq_get_error`, `patch_seq_take_error`, or `patch_seq_clear_error`
70/// replaces or clears the cached `CString`.
71fn cache_error_cstring(msg: &str) -> *const i8 {
72 let safe_msg: String = msg
73 .chars()
74 .map(|c| if c == '\0' { '?' } else { c })
75 .collect();
76 let cstring = CString::new(safe_msg).expect("null bytes already replaced");
77 ERROR_CSTRING.with(|cs| {
78 let ptr = cstring.as_ptr();
79 *cs.borrow_mut() = Some(cstring);
80 ptr
81 })
82}
83
84/// Check if there's a pending runtime error (FFI-safe)
85#[unsafe(no_mangle)]
86pub extern "C" fn patch_seq_has_error() -> bool {
87 has_runtime_error()
88}
89
90/// Get the last error message as a C string pointer (FFI-safe)
91///
92/// Returns null if no error is pending.
93///
94/// # WARNING: Pointer Lifetime
95/// The returned pointer is only valid until the next call to `set_runtime_error`,
96/// `get_error`, `take_error`, or `clear_error`. Callers must copy the string
97/// immediately if they need to retain it.
98#[unsafe(no_mangle)]
99pub extern "C" fn patch_seq_get_error() -> *const i8 {
100 LAST_ERROR.with(|e| match e.borrow().as_deref() {
101 Some(msg) => cache_error_cstring(msg),
102 None => ptr::null(),
103 })
104}
105
106/// Take (and clear) the last error, returning it as a C string (FFI-safe)
107///
108/// Returns null if no error is pending.
109///
110/// # WARNING: Pointer Lifetime
111/// The returned pointer is only valid until the next call to `set_runtime_error`,
112/// `get_error`, `take_error`, or `clear_error`. Callers must copy the string
113/// immediately if they need to retain it.
114#[unsafe(no_mangle)]
115pub extern "C" fn patch_seq_take_error() -> *const i8 {
116 match take_runtime_error() {
117 Some(msg) => cache_error_cstring(&msg),
118 None => ptr::null(),
119 }
120}
121
122/// Clear any pending error (FFI-safe)
123#[unsafe(no_mangle)]
124pub extern "C" fn patch_seq_clear_error() {
125 clear_runtime_error();
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_set_and_take_error() {
134 clear_runtime_error();
135 assert!(!has_runtime_error());
136
137 set_runtime_error("test error");
138 assert!(has_runtime_error());
139
140 let error = take_runtime_error();
141 assert_eq!(error, Some("test error".to_string()));
142 assert!(!has_runtime_error());
143 }
144
145 #[test]
146 fn test_clear_error() {
147 set_runtime_error("another error");
148 assert!(has_runtime_error());
149
150 clear_runtime_error();
151 assert!(!has_runtime_error());
152 assert!(take_runtime_error().is_none());
153 }
154}