nemo_flow_ffi/error.rs
1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Error handling for the FFI layer.
5//!
6//! This module defines the [`NemoFlowStatus`] enum returned by every exported
7//! FFI function, along with thread-local storage for human-readable error
8//! messages. After any non-`Ok` return, the caller should invoke
9//! [`nemo_flow_last_error`] on the same thread to obtain a diagnostic string.
10//! The error message remains valid until the next FFI call on that thread clears
11//! it via [`clear_last_error`].
12
13use std::cell::RefCell;
14use std::ffi::CStr;
15use std::ffi::CString;
16
17use libc::c_char;
18
19use nemo_flow::error::FlowError;
20use nemo_flow::plugin::PluginError;
21
22/// Status codes returned by all FFI functions.
23///
24/// Every `extern "C"` function in this library returns an `NemoFlowStatus`.
25/// On non-`Ok` returns, call [`nemo_flow_last_error`] on the same thread to
26/// retrieve a human-readable error message.
27#[repr(i32)]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum NemoFlowStatus {
30 /// Operation completed successfully.
31 Ok = 0,
32 /// A resource with the given name already exists.
33 AlreadyExists = 1,
34 /// The requested resource was not found.
35 NotFound = 2,
36 /// The scope stack is empty (no active scope).
37 ScopeStackEmpty = 3,
38 /// A guardrail rejected the operation.
39 GuardrailRejected = 4,
40 /// An internal runtime error occurred.
41 Internal = 5,
42 /// A required pointer argument was null.
43 NullPointer = 6,
44 /// A JSON string argument could not be parsed.
45 InvalidJson = 7,
46 /// A C string argument contained invalid UTF-8.
47 InvalidUtf8 = 8,
48 /// A function argument had an invalid value (e.g. malformed UUID).
49 InvalidArg = 9,
50}
51
52thread_local! {
53 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
54}
55
56/// Store an error message in thread-local storage for later retrieval.
57pub fn set_last_error(msg: &str) {
58 LAST_ERROR.with(|cell| {
59 *cell.borrow_mut() = CString::new(msg).ok();
60 });
61}
62
63/// Clear the thread-local last-error message.
64pub fn clear_last_error() {
65 LAST_ERROR.with(|cell| {
66 *cell.borrow_mut() = None;
67 });
68}
69
70/// Retrieve the last error message set on this thread, if any.
71pub fn last_error_message() -> Option<String> {
72 LAST_ERROR.with(|cell| {
73 cell.borrow()
74 .as_ref()
75 .map(|s| s.to_string_lossy().into_owned())
76 })
77}
78
79/// Retrieve the last error message set on this thread, or null if no error
80/// has occurred since the last [`clear_last_error`] call.
81///
82/// The returned pointer borrows from thread-local storage and is valid only
83/// until the next FFI call on the same thread. Do **not** free the returned
84/// pointer.
85#[unsafe(no_mangle)]
86pub extern "C" fn nemo_flow_last_error() -> *const c_char {
87 LAST_ERROR.with(|cell| {
88 cell.borrow()
89 .as_ref()
90 .map(|s| s.as_ptr())
91 .unwrap_or(std::ptr::null())
92 })
93}
94
95/// Set the thread-local last-error message from foreign code.
96///
97/// Intended for callback trampolines that need to propagate an error through
98/// the existing FFI last-error channel.
99///
100/// # Safety
101/// `msg` must be either null or a valid, null-terminated C string for the
102/// duration of this call.
103#[unsafe(no_mangle)]
104pub unsafe extern "C" fn nemo_flow_set_last_error_message(msg: *const c_char) {
105 if msg.is_null() {
106 set_last_error("unknown callback error");
107 return;
108 }
109 match unsafe { CStr::from_ptr(msg) }.to_str() {
110 Ok(s) => set_last_error(s),
111 Err(_) => set_last_error("callback error was not valid UTF-8"),
112 }
113}
114
115impl From<&FlowError> for NemoFlowStatus {
116 fn from(e: &FlowError) -> Self {
117 match e {
118 FlowError::AlreadyExists(_) => NemoFlowStatus::AlreadyExists,
119 FlowError::NotFound(_) => NemoFlowStatus::NotFound,
120 FlowError::InvalidArgument(_) => NemoFlowStatus::InvalidArg,
121 FlowError::ScopeStackEmpty => NemoFlowStatus::ScopeStackEmpty,
122 FlowError::GuardrailRejected(_) => NemoFlowStatus::GuardrailRejected,
123 FlowError::Internal(_) => NemoFlowStatus::Internal,
124 }
125 }
126}
127
128/// Convert an `FlowError` to an `NemoFlowStatus`, storing the error message
129/// in thread-local storage.
130pub fn status_from_error(e: &FlowError) -> NemoFlowStatus {
131 set_last_error(&e.to_string());
132 NemoFlowStatus::from(e)
133}
134
135/// Convert a `PluginError` to an `NemoFlowStatus`, storing the error message
136/// in thread-local storage.
137pub fn status_from_plugin_error(e: &PluginError) -> NemoFlowStatus {
138 set_last_error(&e.to_string());
139 match e {
140 PluginError::NotFound(_) => NemoFlowStatus::NotFound,
141 PluginError::InvalidConfig(_) | PluginError::Serialization(_) => NemoFlowStatus::InvalidArg,
142 PluginError::Internal(_) | PluginError::RegistrationFailed(_) => NemoFlowStatus::Internal,
143 }
144}
145
146#[cfg(test)]
147#[path = "../tests/coverage/error_tests.rs"]
148mod tests;