1use crate::memory::{FfiBuffer, FfiString};
14
15#[repr(i32)]
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum FfiErrorCode {
24 Ok = 0,
25 NullPointer = 1,
26 BufferTooSmall = 2,
27 InvalidUtf8 = 3,
28 Serialization = 4,
29 Panic = 5,
30 Timeout = 6,
31 NotFound = 7,
32 LockPoisoned = 8,
33 Unknown = 99,
34}
35
36#[derive(Debug)]
40pub enum FfiError {
41 NullPointer,
42 BufferTooSmall { needed: usize, available: usize },
43 InvalidUtf8(String),
44 Serialization(String),
45 Panic(String),
46 Timeout,
47 NotFound(String),
48 LockPoisoned,
49 Unknown(String),
50}
51
52impl FfiError {
53 pub fn code(&self) -> FfiErrorCode {
55 match self {
56 Self::NullPointer => FfiErrorCode::NullPointer,
57 Self::BufferTooSmall { .. } => FfiErrorCode::BufferTooSmall,
58 Self::InvalidUtf8(_) => FfiErrorCode::InvalidUtf8,
59 Self::Serialization(_) => FfiErrorCode::Serialization,
60 Self::Panic(_) => FfiErrorCode::Panic,
61 Self::Timeout => FfiErrorCode::Timeout,
62 Self::NotFound(_) => FfiErrorCode::NotFound,
63 Self::LockPoisoned => FfiErrorCode::LockPoisoned,
64 Self::Unknown(_) => FfiErrorCode::Unknown,
65 }
66 }
67
68 pub fn message(&self) -> String {
70 match self {
71 Self::NullPointer => "null pointer received".into(),
72 Self::BufferTooSmall { needed, available } => {
73 format!("buffer too small: need {needed} bytes, have {available}")
74 }
75 Self::InvalidUtf8(ctx) => format!("invalid UTF-8 in {ctx}"),
76 Self::Serialization(detail) => format!("serialization error: {detail}"),
77 Self::Panic(msg) => format!("panic caught at FFI boundary: {msg}"),
78 Self::Timeout => "operation timed out".into(),
79 Self::NotFound(name) => format!("not found: {name}"),
80 Self::LockPoisoned => "mutex lock is poisoned".into(),
81 Self::Unknown(detail) => format!("unknown error: {detail}"),
82 }
83 }
84}
85
86impl std::fmt::Display for FfiError {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "{}", self.message())
89 }
90}
91
92impl std::error::Error for FfiError {}
93
94#[repr(C)]
107pub struct FfiResult {
108 pub error_code: FfiErrorCode,
109 pub error_message: FfiString,
110 pub payload: FfiBuffer,
111}
112
113impl FfiResult {
114 pub fn ok(payload: FfiBuffer) -> Self {
116 FfiResult {
117 error_code: FfiErrorCode::Ok,
118 error_message: FfiString::null(),
119 payload,
120 }
121 }
122
123 pub fn err(error: FfiError) -> Self {
125 let msg = FfiString::new(&error.message());
126 FfiResult {
127 error_code: error.code(),
128 error_message: msg,
129 payload: FfiBuffer::null(),
130 }
131 }
132
133 #[inline]
135 pub fn is_ok(&self) -> bool {
136 self.error_code == FfiErrorCode::Ok
137 }
138}
139
140#[no_mangle]
149pub extern "C" fn ffi_result_free(result: FfiResult) {
150 unsafe {
152 result.error_message.dealloc();
153 result.payload.dealloc();
154 }
155}
156
157pub fn catch_panic<F>(f: F) -> FfiResult
177where
178 F: FnOnce() -> Result<FfiBuffer, FfiError> + std::panic::UnwindSafe,
179{
180 match std::panic::catch_unwind(f) {
181 Ok(Ok(buf)) => FfiResult::ok(buf),
182 Ok(Err(e)) => FfiResult::err(e),
183 Err(panic_payload) => {
184 let msg = panic_payload
185 .downcast_ref::<&str>()
186 .copied()
187 .or_else(|| panic_payload.downcast_ref::<String>().map(|s| s.as_str()))
188 .unwrap_or("unknown panic payload");
189 FfiResult::err(FfiError::Panic(msg.to_string()))
190 }
191 }
192}
193
194#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn ffi_result_ok_is_ok() {
202 let buf = FfiBuffer::new(8);
203 let result = FfiResult::ok(buf);
204 assert!(result.is_ok());
205 assert_eq!(result.error_code, FfiErrorCode::Ok);
206 ffi_result_free(result);
207 }
208
209 #[test]
210 fn ffi_result_err_carries_code_and_message() {
211 let result = FfiResult::err(FfiError::NullPointer);
212 assert!(!result.is_ok());
213 assert_eq!(result.error_code, FfiErrorCode::NullPointer);
214 let msg = unsafe { result.error_message.as_str() };
215 assert!(msg.contains("null pointer"));
216 ffi_result_free(result);
217 }
218
219 #[test]
220 fn catch_panic_ok_path() {
221 let result = catch_panic(|| Ok(FfiBuffer::new(4)));
222 assert!(result.is_ok());
223 ffi_result_free(result);
224 }
225
226 #[test]
227 fn catch_panic_err_path() {
228 let result = catch_panic(|| Err::<FfiBuffer, _>(FfiError::Timeout));
229 assert_eq!(result.error_code, FfiErrorCode::Timeout);
230 ffi_result_free(result);
231 }
232
233 #[test]
234 fn catch_panic_catches_panic() {
235 let result = catch_panic(|| {
236 panic!("intentional test panic");
237 });
238 assert_eq!(result.error_code, FfiErrorCode::Panic);
239 let msg = unsafe { result.error_message.as_str() };
240 assert!(msg.contains("intentional test panic"));
241 ffi_result_free(result);
242 }
243
244 #[test]
245 fn ffi_error_buffer_too_small_message() {
246 let e = FfiError::BufferTooSmall {
247 needed: 100,
248 available: 10,
249 };
250 assert!(e.message().contains("100"));
251 assert!(e.message().contains("10"));
252 }
253}