Skip to main content

weaveffi/abi/
error.rs

1//! The error struct every fallible symbol reports through, the reserved
2//! runtime codes, and the [`ErrorReport`] mapping from producer errors.
3//!
4//! A synchronous call takes a caller-owned `{prefix}_error* out_err` slot; an
5//! async completion receives a heap-boxed one it releases with
6//! `{prefix}_error_free`. Either way the producer allocates the message (a
7//! NUL-terminated C string) and the optional payload (a value buffer), so the
8//! producer is also the one that frees them.
9
10use std::ffi::{CStr, CString};
11use std::os::raw::c_char;
12use std::ptr;
13
14use crate::abi::buffer::{BufferDecodeError, BufferReader};
15
16/// The reserved error code for an **untyped producer error**: the default
17/// [`ErrorReport::code`], used by `Result<T, String>` and other error types
18/// that don't map onto a declared domain code.
19pub const GENERIC_ERROR_CODE: i32 = -1;
20
21/// The reserved error code reporting a producer **panic**.
22///
23/// Generated thunks wrap the producer call in `catch_unwind`; a panic is
24/// reported through `out_err` with this code so the consumer can distinguish
25/// "the producer has a bug" from any declared domain error. Validation rejects
26/// error domains that try to claim a reserved code (`0`, which means success,
27/// or any negative value).
28pub const PANIC_ERROR_CODE: i32 = -2;
29
30/// The reserved error code reporting a **marshalling failure**: an argument
31/// that could not be lifted at the boundary (a null pointer with a non-zero
32/// length, a non-UTF-8 string, an out-of-range enum discriminant, or a
33/// malformed value buffer).
34///
35/// Both sides are generated from the same IDL, so a marshalling failure is a
36/// producer/consumer contract violation, not a domain error; wrappers surface
37/// it through the same trap channel as [`PANIC_ERROR_CODE`].
38pub const MARSHAL_ERROR_CODE: i32 = -3;
39
40/// The reserved error code reporting that a **consumer callback-interface
41/// implementation failed**.
42///
43/// A consumer's method implementation that raises reports it through the
44/// vtable entry's `out_err` slot (via `{prefix}_error_set`). The producer
45/// sees every failure with this code, except a declared domain code from a
46/// method that `throws` (see
47/// [`ForeignError`](crate::abi::ForeignError)).
48pub const FOREIGN_ERROR_CODE: i32 = -4;
49
50/// The reserved error code reporting that an async call was **cancelled**:
51/// its cancel token fired, or the executor dropped the future before it
52/// completed. The completion callback still fires exactly once, with this
53/// code.
54pub const CANCELLED_ERROR_CODE: i32 = -5;
55
56/// The error struct passed across the C ABI boundary (`{prefix}_error` in C).
57///
58/// `code == 0` means success and every pointer is null. Otherwise `message`
59/// is a NUL-terminated UTF-8 string and `payload_ptr`/`payload_len`
60/// optionally hold the matched error code's fields serialized in the
61/// [`buffer`](crate::abi::buffer) format. The producer allocates both, and
62/// [`error_clear`] (the body of `{prefix}_error_clear`) frees them.
63///
64/// The struct owns its allocations, so it must not be copied bitwise while
65/// it holds any; a copy would double-free on clear.
66#[repr(C)]
67#[derive(Debug)]
68pub struct FfiError {
69    /// Status code. `0` means success; a positive value is a domain error
70    /// code and a negative value is one of the reserved runtime codes.
71    pub code: i32,
72    /// Owned, NUL-terminated UTF-8 message describing the failure, or null
73    /// when [`code`](Self::code) is `0`.
74    pub message: *const c_char,
75    /// Owned value buffer holding the matched error code's payload fields, or
76    /// null when the code declares no fields.
77    pub payload_ptr: *const u8,
78    /// Byte length of [`payload_ptr`](Self::payload_ptr); `0` when null.
79    pub payload_len: usize,
80}
81
82impl Default for FfiError {
83    fn default() -> Self {
84        Self {
85            code: 0,
86            message: ptr::null(),
87            payload_ptr: ptr::null(),
88            payload_len: 0,
89        }
90    }
91}
92
93// SAFETY: an `FfiError` exclusively owns the message and payload it points
94// to (both are producer heap allocations with no thread affinity), so moving
95// it to another thread moves that ownership with it. Async launchers rely on
96// this to carry a boxed error from the future to the completion.
97unsafe impl Send for FfiError {}
98
99impl FfiError {
100    /// Build an owned error with `code` and `message`. Interior NUL bytes are
101    /// dropped from the message, since C reads it as a NUL-terminated string.
102    #[must_use]
103    pub fn new(code: i32, message: &str) -> Self {
104        let mut err = Self::default();
105        err.set(code, message);
106        err
107    }
108
109    /// Build the error an [`ErrorReport`] value describes, including its
110    /// serialized payload.
111    #[must_use]
112    pub fn from_report<E: ErrorReport + ?Sized>(e: &E) -> Self {
113        let mut err = Self::new(e.code(), &e.message());
114        err.set_payload(e.payload());
115        err
116    }
117
118    /// Build the error a caught unwind payload describes: a producer panic,
119    /// reported with [`PANIC_ERROR_CODE`].
120    #[must_use]
121    pub fn from_panic(payload: &(dyn std::any::Any + Send)) -> Self {
122        Self::new(
123            PANIC_ERROR_CODE,
124            &format!("producer panicked: {}", panic_message(payload)),
125        )
126    }
127
128    /// The error a cancelled async call completes with.
129    #[must_use]
130    pub fn cancelled() -> Self {
131        Self::new(CANCELLED_ERROR_CODE, "cancelled")
132    }
133
134    /// The message as a string slice, or `None` when it's null or not UTF-8.
135    ///
136    /// # Safety
137    ///
138    /// `message` must be null or point to a NUL-terminated string that stays
139    /// valid for the borrow, which holds for any error this runtime filled.
140    #[must_use]
141    pub unsafe fn message_str(&self) -> Option<&str> {
142        if self.message.is_null() {
143            return None;
144        }
145        // SAFETY: the caller guarantees `message` is NUL-terminated and live.
146        unsafe { CStr::from_ptr(self.message) }.to_str().ok()
147    }
148
149    /// Release the message and payload, leaving the error in the success
150    /// state.
151    fn release(&mut self) {
152        if !self.message.is_null() {
153            // SAFETY: every non-null message was produced by
154            // `CString::into_raw` in `set`, and is released exactly once
155            // because the pointer is nulled right after.
156            unsafe { drop(CString::from_raw(self.message.cast_mut())) };
157            self.message = ptr::null();
158        }
159        if !self.payload_ptr.is_null() {
160            // SAFETY: every non-null payload was produced by
161            // `bytes_into_raw` in `set_payload` with this length.
162            unsafe { crate::abi::free_bytes(self.payload_ptr.cast_mut(), self.payload_len) };
163            self.payload_ptr = ptr::null();
164        }
165        self.payload_len = 0;
166        self.code = 0;
167    }
168
169    // `CString::new` is infallible here: interior NUL bytes are removed first.
170    #[allow(clippy::missing_panics_doc)]
171    fn set(&mut self, code: i32, message: &str) {
172        self.release();
173        self.code = code;
174        let message = if message.as_bytes().contains(&0) {
175            message.replace('\0', "")
176        } else {
177            message.to_owned()
178        };
179        self.message = CString::new(message)
180            .expect("interior NUL bytes were removed")
181            .into_raw();
182    }
183
184    fn set_payload(&mut self, payload: Vec<u8>) {
185        if !self.payload_ptr.is_null() {
186            // SAFETY: every non-null payload was produced by
187            // `bytes_into_raw` with this length, and is replaced right after.
188            unsafe { crate::abi::free_bytes(self.payload_ptr.cast_mut(), self.payload_len) };
189        }
190        let (ptr, len) = crate::abi::convert::bytes_into_raw(payload);
191        self.payload_ptr = ptr;
192        self.payload_len = len;
193    }
194
195    /// The payload as a byte slice (empty when there is none).
196    #[must_use]
197    pub fn payload(&self) -> &[u8] {
198        if self.payload_ptr.is_null() {
199            return &[];
200        }
201        // SAFETY: a non-null payload is an owned run of `payload_len` bytes
202        // (see `set_payload`), alive as long as `self`.
203        unsafe { std::slice::from_raw_parts(self.payload_ptr, self.payload_len) }
204    }
205}
206
207impl Drop for FfiError {
208    fn drop(&mut self) {
209        self.release();
210    }
211}
212
213/// Fill the caller's `out_err` slot with `code` and a copy of `message`,
214/// releasing anything it held before. A null `out_err` is a no-op.
215///
216/// # Safety
217///
218/// `out_err` must be null or point to a valid, writable [`FfiError`] whose
219/// pointers are null or owned by this runtime.
220pub unsafe fn error_set(out_err: *mut FfiError, code: i32, message: &str) {
221    // SAFETY: forwarded from the caller.
222    if let Some(err) = unsafe { out_err.as_mut() } {
223        err.set(code, message);
224    }
225}
226
227/// Move `value` into the caller's `out_err` slot, releasing anything it held
228/// before. A null `out_err` drops `value`.
229///
230/// # Safety
231///
232/// Same contract as [`error_set`].
233pub unsafe fn error_store(out_err: *mut FfiError, value: FfiError) {
234    // SAFETY: forwarded from the caller; the old value is dropped (and its
235    // allocations released) by the assignment.
236    if let Some(err) = unsafe { out_err.as_mut() } {
237        *err = value;
238    }
239}
240
241/// The body of `{prefix}_error_set`: fill `out_err` from a borrowed C string
242/// the consumer owns.
243///
244/// Callback-interface implementations call it to report a failure so that
245/// the message is allocated by the producer, which is the side that frees
246/// it. A null or non-UTF-8 `message` yields an empty message; the code is
247/// always recorded.
248///
249/// # Safety
250///
251/// Same contract as [`error_set`] for `out_err`. `message` must be null or
252/// point to a NUL-terminated string that stays valid for the call.
253pub unsafe fn error_set_c(out_err: *mut FfiError, code: i32, message: *const c_char) {
254    let text = if message.is_null() {
255        ""
256    } else {
257        // SAFETY: the caller guarantees a NUL-terminated string.
258        unsafe { CStr::from_ptr(message) }.to_str().unwrap_or("")
259    };
260    // SAFETY: forwarded from the caller.
261    unsafe { error_set(out_err, code, text) };
262}
263
264/// The body of `{prefix}_error_set_payload`: replace `out_err`'s payload
265/// with a producer-owned copy of the `len` bytes at `ptr`. A null `out_err`
266/// is a no-op; a null `ptr` (or a `len` of `0`) clears the payload.
267///
268/// A callback-interface implementation of a `throws` method calls it after
269/// `{prefix}_error_set` to attach a domain code's fields, encoded as a value
270/// buffer, so the producer can decode the typed error.
271///
272/// # Safety
273///
274/// Same contract as [`error_set`] for `out_err`. `ptr` must be null or point
275/// to `len` readable bytes for the duration of the call.
276pub unsafe fn error_set_payload_c(out_err: *mut FfiError, ptr: *const u8, len: usize) {
277    // SAFETY: forwarded from the caller.
278    let Some(err) = (unsafe { out_err.as_mut() }) else {
279        return;
280    };
281    // SAFETY: forwarded from the caller.
282    let bytes = unsafe { crate::abi::lift_byte_slice(ptr, len) }.unwrap_or(&[]);
283    err.set_payload(bytes.to_vec());
284}
285
286/// The body of `{prefix}_error_clear`: free any message and payload and set
287/// the code to `0`. A null `err` is a no-op.
288///
289/// # Safety
290///
291/// Same contract as [`error_set`].
292pub unsafe fn error_clear(err: *mut FfiError) {
293    // SAFETY: forwarded from the caller.
294    if let Some(err) = unsafe { err.as_mut() } {
295        err.release();
296    }
297}
298
299/// The body of `{prefix}_error_free`: release a heap-boxed error delivered
300/// through an async completion, including the box. A null `err` is a no-op.
301///
302/// # Safety
303///
304/// `err` must be null or a pointer this runtime handed to a completion
305/// callback (see [`boxed_error`]), released exactly once.
306pub unsafe fn error_free(err: *mut FfiError) {
307    if !err.is_null() {
308        // SAFETY: the caller guarantees `err` came from `Box::into_raw`.
309        drop(unsafe { Box::from_raw(err) });
310    }
311}
312
313/// Box `err` for an async completion callback, which hands ownership to the
314/// consumer (released with `{prefix}_error_free`).
315#[must_use]
316pub fn boxed_error(err: FfiError) -> *mut FfiError {
317    Box::into_raw(Box::new(err))
318}
319
320/// Maps a producer error onto the ABI's `(code, message, payload)` triple.
321///
322/// A fallible `#[weaveffi::export]` function returning `Result<T, E>` reports
323/// `Err(e)` through its trailing `out_err` slot using this trait. `String` and
324/// `&str` errors are covered out of the box with the generic code `-1`, so
325/// `Result<T, String>` needs no extra code. The `#[weaveffi::error]`
326/// expansion implements it for an error domain's enum: the code is the
327/// variant's discriminant, the message is the enum's
328/// [`Display`](std::fmt::Display) output, and the payload is the variant's
329/// fields.
330///
331/// To implement the trait by hand:
332///
333/// ```
334/// use weaveffi::abi::ErrorReport;
335///
336/// enum KvError {
337///     KeyNotFound,
338///     Io(String),
339/// }
340///
341/// impl ErrorReport for KvError {
342///     fn code(&self) -> i32 {
343///         match self {
344///             KvError::KeyNotFound => 1001,
345///             KvError::Io(_) => 1004,
346///         }
347///     }
348///     fn message(&self) -> String {
349///         match self {
350///             KvError::KeyNotFound => "key not found".to_string(),
351///             KvError::Io(detail) => format!("I/O error: {detail}"),
352///         }
353///     }
354/// }
355/// ```
356pub trait ErrorReport {
357    /// The non-zero status code written to [`FfiError::code`]. Defaults to
358    /// [`GENERIC_ERROR_CODE`].
359    fn code(&self) -> i32 {
360        GENERIC_ERROR_CODE
361    }
362
363    /// The human-readable message written to [`FfiError::message`].
364    fn message(&self) -> String;
365
366    /// The serialized payload fields written to [`FfiError::payload_ptr`], or
367    /// an empty vector for a code with no fields.
368    fn payload(&self) -> Vec<u8> {
369        Vec::new()
370    }
371}
372
373/// An error type the `#[weaveffi::error]` expansion generates: a module's
374/// error domain, decodable from a code and its payload.
375///
376/// [`ForeignError::domain`](crate::abi::ForeignError::domain) uses it to turn
377/// a domain error a consumer's callback reported into the typed error.
378pub trait ErrorDomain: ErrorReport + Sized {
379    /// Decode the error `code` names, reading its payload fields (if any)
380    /// from `r`. Returns `Ok(None)` when `code` isn't one of the domain's
381    /// declared codes.
382    ///
383    /// # Errors
384    ///
385    /// Returns an error when the payload is malformed for the code.
386    ///
387    /// # Safety
388    ///
389    /// Same contract as
390    /// [`BufferValue::read_value`](crate::abi::BufferValue::read_value).
391    unsafe fn read_code(
392        code: i32,
393        r: &mut BufferReader<'_>,
394    ) -> Result<Option<Self>, BufferDecodeError>;
395}
396
397impl ErrorReport for String {
398    fn message(&self) -> String {
399        self.clone()
400    }
401}
402
403impl ErrorReport for &str {
404    fn message(&self) -> String {
405        (*self).to_string()
406    }
407}
408
409impl ErrorReport for Box<dyn std::error::Error> {
410    fn message(&self) -> String {
411        self.to_string()
412    }
413}
414
415impl ErrorReport for Box<dyn std::error::Error + Send + Sync> {
416    fn message(&self) -> String {
417        self.to_string()
418    }
419}
420
421/// Best-effort extraction of a panic payload's message (`&str` and `String`
422/// payloads; anything else yields a fixed placeholder).
423#[must_use]
424pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
425    if let Some(s) = payload.downcast_ref::<&str>() {
426        (*s).to_string()
427    } else if let Some(s) = payload.downcast_ref::<String>() {
428        s.clone()
429    } else {
430        "producer panicked".to_string()
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    fn message(err: &FfiError) -> Option<String> {
439        // SAFETY: every error in these tests is filled by this module.
440        unsafe { err.message_str() }.map(str::to_string)
441    }
442
443    #[test]
444    fn default_is_ok() {
445        let err = FfiError::default();
446        assert_eq!(err.code, 0);
447        assert!(err.message.is_null());
448        assert!(err.payload_ptr.is_null());
449    }
450
451    #[test]
452    fn set_replaces_and_clear_resets() {
453        let mut err = FfiError::default();
454        unsafe { error_set(&mut err, 1, "first") };
455        unsafe { error_set(&mut err, 2, "second") };
456        assert_eq!(err.code, 2);
457        assert_eq!(message(&err).as_deref(), Some("second"));
458        unsafe { error_clear(&mut err) };
459        assert_eq!(err.code, 0);
460        assert!(err.message.is_null());
461    }
462
463    #[test]
464    fn null_slots_are_no_ops() {
465        unsafe {
466            error_set(ptr::null_mut(), 1, "x");
467            error_set_c(ptr::null_mut(), 1, ptr::null());
468            error_clear(ptr::null_mut());
469            error_free(ptr::null_mut());
470            error_store(ptr::null_mut(), FfiError::new(3, "dropped"));
471        }
472    }
473
474    #[test]
475    fn interior_nul_is_removed_from_messages() {
476        let err = FfiError::new(1, "hel\0lo");
477        assert_eq!(message(&err).as_deref(), Some("hello"));
478    }
479
480    #[test]
481    fn set_c_copies_a_borrowed_message() {
482        let mut err = FfiError::default();
483        let msg = CString::new("from the consumer").unwrap();
484        unsafe { error_set_c(&mut err, FOREIGN_ERROR_CODE, msg.as_ptr()) };
485        drop(msg);
486        assert_eq!(err.code, FOREIGN_ERROR_CODE);
487        assert_eq!(message(&err).as_deref(), Some("from the consumer"));
488        unsafe { error_set_c(&mut err, 3, ptr::null()) };
489        assert_eq!(err.code, 3);
490        assert_eq!(message(&err).as_deref(), Some(""));
491    }
492
493    enum DomainError {
494        NotFound,
495        Io(String),
496    }
497
498    impl ErrorReport for DomainError {
499        fn code(&self) -> i32 {
500            match self {
501                DomainError::NotFound => 1001,
502                DomainError::Io(_) => 1004,
503            }
504        }
505        fn message(&self) -> String {
506            match self {
507                DomainError::NotFound => "not found".to_string(),
508                DomainError::Io(detail) => format!("io: {detail}"),
509            }
510        }
511        fn payload(&self) -> Vec<u8> {
512            match self {
513                DomainError::NotFound => Vec::new(),
514                DomainError::Io(detail) => crate::abi::encode_value(detail),
515            }
516        }
517    }
518
519    #[test]
520    fn reports_carry_code_message_and_payload() {
521        let err = FfiError::from_report(&DomainError::NotFound);
522        assert_eq!(err.code, 1001);
523        assert_eq!(message(&err).as_deref(), Some("not found"));
524        assert!(err.payload_ptr.is_null());
525
526        let err = FfiError::from_report(&DomainError::Io("disk".into()));
527        assert_eq!(err.code, 1004);
528        assert_eq!(
529            unsafe { crate::abi::decode_value::<String>(err.payload()) }.unwrap(),
530            "disk"
531        );
532
533        let mut slot = FfiError::default();
534        unsafe { error_store(&mut slot, err) };
535        assert_eq!(slot.code, 1004);
536    }
537
538    #[test]
539    fn string_errors_use_the_generic_code() {
540        let e = "boom".to_string();
541        assert_eq!(ErrorReport::code(&e), GENERIC_ERROR_CODE);
542        assert_eq!(ErrorReport::message(&e), "boom");
543        assert_eq!(ErrorReport::code(&"boom"), GENERIC_ERROR_CODE);
544    }
545
546    #[test]
547    fn set_payload_copies_and_replaces() {
548        let mut err = FfiError::new(3, "with fields");
549        let bytes = [1u8, 2, 3];
550        unsafe { error_set_payload_c(&mut err, bytes.as_ptr(), bytes.len()) };
551        assert_eq!(err.payload(), &bytes);
552        unsafe { error_set_payload_c(&mut err, ptr::null(), 0) };
553        assert!(err.payload().is_empty());
554        unsafe { error_set_payload_c(ptr::null_mut(), bytes.as_ptr(), bytes.len()) };
555    }
556
557    #[test]
558    fn panic_payloads_map_to_the_panic_code() {
559        let payload: Box<dyn std::any::Any + Send> = Box::new("bug");
560        let err = FfiError::from_panic(&*payload);
561        assert_eq!(err.code, PANIC_ERROR_CODE);
562        assert_eq!(message(&err).as_deref(), Some("producer panicked: bug"));
563    }
564
565    #[test]
566    fn boxed_errors_free_through_error_free() {
567        let raw = boxed_error(FfiError::cancelled());
568        assert_eq!(unsafe { (*raw).code }, CANCELLED_ERROR_CODE);
569        unsafe { error_free(raw) };
570    }
571}