Skip to main content

goud_engine/core/error/
ffi_bridge.rs

1//! FFI error bridge infrastructure.
2//!
3//! Provides thread-local error storage and the [`GoudFFIResult`] type for passing
4//! errors across the FFI boundary to C#, Python, and other language bindings.
5//!
6//! # Usage Pattern
7//!
8//! 1. Rust function encounters an error
9//! 2. Rust function calls `set_last_error(error)`
10//! 3. Rust function returns error code via `GoudFFIResult`
11//! 4. Language binding checks if `success` is false
12//! 5. Language binding calls `goud_last_error_code()` and `goud_last_error_message()`
13//! 6. Language binding calls `take_last_error()` to clear the error
14//!
15//! # Thread Safety
16//!
17//! Each thread has its own error storage. Errors do not cross thread boundaries.
18//! This matches the behavior of `errno` in C and is safe for multi-threaded use.
19
20use std::cell::RefCell;
21
22use super::codes::{GoudErrorCode, SUCCESS};
23use super::context::GoudErrorContext;
24use super::types::GoudError;
25
26thread_local! {
27    /// Thread-local storage for the last error.
28    ///
29    /// Each thread has its own error storage, ensuring that errors from one
30    /// thread do not affect another. This is critical for thread-safe FFI.
31    static LAST_ERROR: RefCell<Option<GoudError>> = const { RefCell::new(None) };
32
33    /// Thread-local storage for error context (subsystem + operation metadata).
34    ///
35    /// Set alongside LAST_ERROR when `set_last_error_with_context` is used.
36    /// Cleared by `set_last_error`, `clear_last_error`, and `take_last_error`.
37    static LAST_ERROR_CONTEXT: RefCell<Option<GoudErrorContext>> = const { RefCell::new(None) };
38}
39
40/// Sets the last error for the current thread.
41///
42/// This function stores the error in thread-local storage where it can be
43/// retrieved by `last_error_code()` and `last_error_message()`.
44///
45/// # Thread Safety
46///
47/// The error is stored in thread-local storage and will not affect other threads.
48///
49/// # Example
50///
51/// ```
52/// use goud_engine::core::error::{GoudError, set_last_error, last_error_code, ERR_NOT_INITIALIZED};
53///
54/// set_last_error(GoudError::NotInitialized);
55/// assert_eq!(last_error_code(), ERR_NOT_INITIALIZED);
56/// ```
57pub fn set_last_error(error: GoudError) {
58    LAST_ERROR.with(|e| {
59        *e.borrow_mut() = Some(error);
60    });
61    LAST_ERROR_CONTEXT.with(|c| {
62        *c.borrow_mut() = None;
63    });
64}
65
66/// Sets the last error and its context for the current thread.
67///
68/// This function stores both the error and context metadata (subsystem, operation)
69/// in thread-local storage. Use `last_error_subsystem()` and `last_error_operation()`
70/// to retrieve the context.
71///
72/// # Thread Safety
73///
74/// Both values are stored in thread-local storage and will not affect other threads.
75pub fn set_last_error_with_context(error: GoudError, ctx: GoudErrorContext) {
76    LAST_ERROR.with(|e| {
77        *e.borrow_mut() = Some(error);
78    });
79    LAST_ERROR_CONTEXT.with(|c| {
80        *c.borrow_mut() = Some(ctx);
81    });
82}
83
84/// Takes the last error from the current thread, clearing it.
85///
86/// This function removes the error from thread-local storage and returns it.
87/// Subsequent calls will return `None` until a new error is set.
88///
89/// # Thread Safety
90///
91/// Only affects the current thread's error storage.
92///
93/// # Example
94///
95/// ```
96/// use goud_engine::core::error::{GoudError, set_last_error, take_last_error};
97///
98/// set_last_error(GoudError::NotInitialized);
99/// let error = take_last_error();
100/// assert!(error.is_some());
101/// assert!(take_last_error().is_none()); // Cleared after take
102/// ```
103pub fn take_last_error() -> Option<GoudError> {
104    LAST_ERROR_CONTEXT.with(|c| {
105        *c.borrow_mut() = None;
106    });
107    LAST_ERROR.with(|e| e.borrow_mut().take())
108}
109
110/// Gets the last error from the current thread without clearing it.
111///
112/// This function clones the error from thread-local storage. Use `take_last_error()`
113/// if you want to clear the error after retrieval.
114///
115/// # Thread Safety
116///
117/// Only accesses the current thread's error storage.
118///
119/// # Example
120///
121/// ```
122/// use goud_engine::core::error::{GoudError, set_last_error, get_last_error};
123///
124/// set_last_error(GoudError::NotInitialized);
125/// let error1 = get_last_error();
126/// let error2 = get_last_error();
127/// assert_eq!(error1, error2); // Error not cleared
128/// ```
129pub fn get_last_error() -> Option<GoudError> {
130    LAST_ERROR.with(|e| e.borrow().clone())
131}
132
133/// Returns the error code of the last error for the current thread.
134///
135/// Returns `SUCCESS` (0) if no error is set. This does not clear the error.
136///
137/// # Thread Safety
138///
139/// Only accesses the current thread's error storage.
140///
141/// # Example
142///
143/// ```
144/// use goud_engine::core::error::{set_last_error, last_error_code, clear_last_error, GoudError, SUCCESS, ERR_NOT_INITIALIZED};
145///
146/// clear_last_error();
147/// assert_eq!(last_error_code(), SUCCESS);
148///
149/// set_last_error(GoudError::NotInitialized);
150/// assert_eq!(last_error_code(), ERR_NOT_INITIALIZED);
151/// ```
152pub fn last_error_code() -> GoudErrorCode {
153    LAST_ERROR.with(|e| {
154        e.borrow()
155            .as_ref()
156            .map(|err| err.error_code())
157            .unwrap_or(SUCCESS)
158    })
159}
160
161/// Returns the error message of the last error for the current thread.
162///
163/// Returns `None` if no error is set. This does not clear the error.
164/// The returned string is a copy, safe to use across FFI.
165///
166/// # Thread Safety
167///
168/// Only accesses the current thread's error storage.
169///
170/// # Example
171///
172/// ```
173/// use goud_engine::core::error::{GoudError, set_last_error, last_error_message};
174///
175/// set_last_error(GoudError::InitializationFailed("GPU not found".to_string()));
176/// let msg = last_error_message();
177/// assert_eq!(msg, Some("GPU not found".to_string()));
178/// ```
179pub fn last_error_message() -> Option<String> {
180    LAST_ERROR.with(|e| e.borrow().as_ref().map(|err| err.message().to_string()))
181}
182
183/// Clears the last error for the current thread.
184///
185/// After calling this, `last_error_code()` will return `SUCCESS` and
186/// `last_error_message()` will return `None`.
187///
188/// # Thread Safety
189///
190/// Only affects the current thread's error storage.
191///
192/// # Example
193///
194/// ```
195/// use goud_engine::core::error::{GoudError, set_last_error, clear_last_error, last_error_code, SUCCESS};
196///
197/// set_last_error(GoudError::NotInitialized);
198/// clear_last_error();
199/// assert_eq!(last_error_code(), SUCCESS);
200/// ```
201pub fn clear_last_error() {
202    LAST_ERROR.with(|e| {
203        *e.borrow_mut() = None;
204    });
205    LAST_ERROR_CONTEXT.with(|c| {
206        *c.borrow_mut() = None;
207    });
208}
209
210/// Returns the subsystem from the last error's context, if set.
211///
212/// Returns `None` if no error context has been set or if the error was
213/// set without context via `set_last_error()`.
214pub fn last_error_subsystem() -> Option<&'static str> {
215    LAST_ERROR_CONTEXT.with(|c| c.borrow().as_ref().map(|ctx| ctx.subsystem))
216}
217
218/// Returns the operation from the last error's context, if set.
219///
220/// Returns `None` if no error context has been set or if the error was
221/// set without context via `set_last_error()`.
222pub fn last_error_operation() -> Option<&'static str> {
223    LAST_ERROR_CONTEXT.with(|c| c.borrow().as_ref().map(|ctx| ctx.operation))
224}
225
226// =============================================================================
227// FFI Result Type
228// =============================================================================
229
230/// FFI-safe result type for returning success/failure status across the FFI boundary.
231///
232/// This struct is designed to be passed by value across FFI and provides both
233/// a boolean success flag and the error code for detailed error handling.
234///
235/// # Memory Layout
236///
237/// Uses `#[repr(C)]` for predictable memory layout across language boundaries.
238/// The struct is 8 bytes (4 bytes for code, 4 bytes for success with padding).
239///
240/// # Usage
241///
242/// ```
243/// use goud_engine::core::error::{GoudFFIResult, GoudError, SUCCESS};
244///
245/// // Success case
246/// let result = GoudFFIResult::success();
247/// assert!(result.success);
248/// assert_eq!(result.code, SUCCESS);
249///
250/// // Error case
251/// let result = GoudFFIResult::from_error(GoudError::NotInitialized);
252/// assert!(!result.success);
253/// ```
254#[repr(C)]
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256pub struct GoudFFIResult {
257    /// The error code. `SUCCESS` (0) on success, error code on failure.
258    pub code: GoudErrorCode,
259    /// True if the operation succeeded, false otherwise.
260    pub success: bool,
261}
262
263impl GoudFFIResult {
264    /// Creates a successful result.
265    ///
266    /// # Example
267    ///
268    /// ```
269    /// use goud_engine::core::error::{GoudFFIResult, SUCCESS};
270    ///
271    /// let result = GoudFFIResult::success();
272    /// assert!(result.success);
273    /// assert_eq!(result.code, SUCCESS);
274    /// ```
275    #[inline]
276    pub const fn success() -> Self {
277        Self {
278            code: SUCCESS,
279            success: true,
280        }
281    }
282
283    /// Creates a result from an error code.
284    ///
285    /// # Example
286    ///
287    /// ```
288    /// use goud_engine::core::error::{GoudFFIResult, ERR_NOT_INITIALIZED};
289    ///
290    /// let result = GoudFFIResult::from_code(ERR_NOT_INITIALIZED);
291    /// assert!(!result.success);
292    /// assert_eq!(result.code, ERR_NOT_INITIALIZED);
293    /// ```
294    #[inline]
295    pub const fn from_code(code: GoudErrorCode) -> Self {
296        Self {
297            code,
298            success: code == SUCCESS,
299        }
300    }
301
302    /// Creates a result from a `GoudError`.
303    ///
304    /// This also sets the thread-local last error for message retrieval.
305    ///
306    /// # Example
307    ///
308    /// ```
309    /// use goud_engine::core::error::{GoudFFIResult, GoudError, ERR_NOT_INITIALIZED, last_error_message};
310    ///
311    /// let result = GoudFFIResult::from_error(GoudError::NotInitialized);
312    /// assert!(!result.success);
313    /// assert_eq!(result.code, ERR_NOT_INITIALIZED);
314    /// ```
315    #[inline]
316    pub fn from_error(error: GoudError) -> Self {
317        let code = error.error_code();
318        set_last_error(error);
319        Self {
320            code,
321            success: false,
322        }
323    }
324
325    /// Creates a result from a `GoudResult<T>`.
326    ///
327    /// On success, clears any previous error. On error, sets the last error.
328    ///
329    /// # Example
330    ///
331    /// ```
332    /// use goud_engine::core::error::{GoudFFIResult, GoudResult, GoudError, SUCCESS, ERR_NOT_INITIALIZED};
333    ///
334    /// let ok_result: GoudResult<i32> = Ok(42);
335    /// let ffi_result = GoudFFIResult::from_result(ok_result);
336    /// assert!(ffi_result.success);
337    ///
338    /// let err_result: GoudResult<i32> = Err(GoudError::NotInitialized);
339    /// let ffi_result = GoudFFIResult::from_result(err_result);
340    /// assert!(!ffi_result.success);
341    /// assert_eq!(ffi_result.code, ERR_NOT_INITIALIZED);
342    /// ```
343    #[inline]
344    pub fn from_result<T>(result: super::GoudResult<T>) -> Self {
345        match result {
346            Ok(_) => {
347                clear_last_error();
348                Self::success()
349            }
350            Err(error) => Self::from_error(error),
351        }
352    }
353
354    /// Returns true if the result indicates success.
355    #[inline]
356    pub const fn is_success(&self) -> bool {
357        self.success
358    }
359
360    /// Returns true if the result indicates failure.
361    #[inline]
362    pub const fn is_error(&self) -> bool {
363        !self.success
364    }
365
366    // =========================================================================
367    // Compatibility aliases (used by GoudResult type alias and proc macros)
368    // =========================================================================
369
370    /// Alias for `success()` — creates a success result.
371    #[inline]
372    pub const fn ok() -> Self {
373        Self::success()
374    }
375
376    /// Alias for `from_code()` — creates an error result from a code.
377    #[inline]
378    pub const fn err(code: GoudErrorCode) -> Self {
379        Self::from_code(code)
380    }
381
382    /// Alias for `is_success()` — returns true if the result is ok.
383    #[inline]
384    pub const fn is_ok(&self) -> bool {
385        self.success
386    }
387
388    /// Alias for `is_error()` — returns true if the result is an error.
389    #[inline]
390    pub const fn is_err(&self) -> bool {
391        !self.success
392    }
393}
394
395impl Default for GoudFFIResult {
396    /// Default is success.
397    fn default() -> Self {
398        Self::success()
399    }
400}
401
402impl From<GoudError> for GoudFFIResult {
403    fn from(error: GoudError) -> Self {
404        Self::from_error(error)
405    }
406}
407
408impl From<GoudErrorCode> for GoudFFIResult {
409    fn from(code: GoudErrorCode) -> Self {
410        if code == SUCCESS {
411            Self::success()
412        } else {
413            Self::from_code(code)
414        }
415    }
416}
417
418impl<T> From<super::GoudResult<T>> for GoudFFIResult {
419    fn from(result: super::GoudResult<T>) -> Self {
420        Self::from_result(result)
421    }
422}
423
424impl std::fmt::Display for GoudFFIResult {
425    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426        if self.success {
427            write!(f, "GoudResult::Success")
428        } else {
429            write!(f, "GoudResult::Error(code={})", self.code)
430        }
431    }
432}