Skip to main content

weaveffi_abi/
lib.rs

1//! C ABI runtime: error struct, memory helpers, and utility functions.
2#![deny(missing_docs)]
3#![warn(clippy::missing_errors_doc)]
4#![warn(clippy::missing_panics_doc)]
5#![warn(clippy::doc_markdown)]
6#![allow(non_camel_case_types)]
7#![allow(unsafe_code)]
8#![allow(clippy::not_unsafe_ptr_arg_deref)]
9
10pub mod arena;
11pub mod convert;
12mod macros;
13
14pub use convert::{
15    lift_byte_slice, lift_bytes, lift_opt_scalar, lift_opt_string, lift_ptr_vec, lift_scalar_vec,
16    lift_string_vec, lower_bytes, lower_opt_scalar, lower_opt_string, lower_ptr_vec,
17    lower_scalar_vec, lower_string_vec, write_map_out,
18};
19
20use std::ffi::{CStr, CString};
21use std::os::raw::c_char;
22use std::ptr;
23use std::sync::atomic::{AtomicBool, Ordering};
24
25/// Public opaque handle type exposed to foreign callers.
26pub type weaveffi_handle_t = u64;
27
28/// Error struct passed across the C ABI boundary.
29///
30/// # Safety
31///
32/// - `message` is a NUL-terminated UTF-8 C string allocated by Rust and must be
33///   released by calling `weaveffi_error_clear` or `weaveffi_free_string`.
34/// - This struct must not be copied while it owns a message pointer, as that
35///   would lead to double-free on clear.
36#[repr(C)]
37#[derive(Debug)]
38pub struct weaveffi_error {
39    /// Status code. `0` means success; any non-zero value indicates failure.
40    pub code: i32,
41    /// Owned, NUL-terminated UTF-8 message describing the failure, or null when
42    /// [`code`](Self::code) is `0`. Freed by `weaveffi_error_clear`.
43    pub message: *const c_char,
44}
45
46impl Default for weaveffi_error {
47    fn default() -> Self {
48        Self {
49            code: 0,
50            message: ptr::null(),
51        }
52    }
53}
54
55/// Set the error to OK (code = 0) and free any prior message.
56pub fn error_set_ok(out_err: *mut weaveffi_error) {
57    if out_err.is_null() {
58        return;
59    }
60    // SAFETY: pointer checked for null above
61    let err = unsafe { &mut *out_err };
62    if !err.message.is_null() {
63        // SAFETY: message was allocated via `CString::into_raw` in this module
64        unsafe { drop(CString::from_raw(err.message as *mut c_char)) };
65    }
66    err.code = 0;
67    err.message = ptr::null();
68}
69
70/// Populate an error with the given code and message (copying message).
71// `CString::new` is infallible here because interior NUL bytes are stripped
72// from `message` immediately below, so there is no reachable panic to document.
73#[allow(clippy::missing_panics_doc)]
74pub fn error_set(out_err: *mut weaveffi_error, code: i32, message: &str) {
75    if out_err.is_null() {
76        return;
77    }
78    // SAFETY: pointer checked for null above
79    let err = unsafe { &mut *out_err };
80    if !err.message.is_null() {
81        // SAFETY: message was allocated via `CString::into_raw` in this module
82        unsafe { drop(CString::from_raw(err.message as *mut c_char)) };
83    }
84    err.code = code;
85    let owned_message = message.replace('\0', "");
86    let cstr = CString::new(owned_message).expect("CString::new sanitized input");
87    err.message = cstr.into_raw();
88}
89
90/// Maps a producer error onto the ABI's `(code, message)` pair.
91///
92/// A fallible `#[weaveffi::export]` function returning `Result<T, E>` reports
93/// `Err(e)` through its trailing `out_err` slot by writing
94/// [`ErrorReport::code`] and [`ErrorReport::message`] into the caller's
95/// [`weaveffi_error`]. A blanket implementation covers every [`Display`] type,
96/// reporting the generic code `-1`, so `Result<T, String>` and
97/// `Result<T, MyDisplayError>` need no extra code.
98///
99/// To surface the named codes of an IDL error domain so consumers can react to
100/// each case, implement this trait directly on your error type (and do not
101/// implement [`Display`] for it, which would collide with the blanket impl):
102///
103/// ```
104/// use weaveffi_abi::ErrorReport;
105///
106/// enum KvError {
107///     KeyNotFound,
108///     Io(String),
109/// }
110///
111/// impl ErrorReport for KvError {
112///     fn code(&self) -> i32 {
113///         match self {
114///             KvError::KeyNotFound => 1001,
115///             KvError::Io(_) => 1004,
116///         }
117///     }
118///     fn message(&self) -> String {
119///         match self {
120///             KvError::KeyNotFound => "key not found".to_string(),
121///             KvError::Io(detail) => format!("I/O error: {detail}"),
122///         }
123///     }
124/// }
125/// ```
126///
127/// [`Display`]: std::fmt::Display
128pub trait ErrorReport {
129    /// The non-zero status code written to [`weaveffi_error::code`]. Defaults to
130    /// the generic error code `-1`.
131    fn code(&self) -> i32 {
132        -1
133    }
134
135    /// The human-readable message written to [`weaveffi_error::message`].
136    fn message(&self) -> String;
137}
138
139impl<E: std::fmt::Display> ErrorReport for E {
140    fn message(&self) -> String {
141        self.to_string()
142    }
143}
144
145/// Convenience adapter: map a `Result<T, E>` to `Option<T>` by writing into `out_err`.
146///
147/// `Err(e)` is reported through [`ErrorReport`], so the generic `-1` code is
148/// used for [`Display`](std::fmt::Display) errors and the domain code for types
149/// that implement [`ErrorReport`] directly.
150pub fn result_to_out_err<T, E: ErrorReport>(
151    result: Result<T, E>,
152    out_err: *mut weaveffi_error,
153) -> Option<T> {
154    match result {
155        Ok(value) => {
156            error_set_ok(out_err);
157            Some(value)
158        }
159        Err(e) => {
160            error_set(out_err, e.code(), &e.message());
161            None
162        }
163    }
164}
165
166/// The reserved error code reporting a producer **panic**.
167///
168/// Generated thunks wrap the producer call in `catch_unwind`; a panic is
169/// reported through `out_err` with this code so the consumer can distinguish
170/// "the producer has a bug" from any declared domain error. Validation rejects
171/// error domains that try to claim this value (or `0`, which means success).
172pub const PANIC_ERROR_CODE: i32 = -2;
173
174/// Best-effort extraction of a panic payload's message (`&str` and `String`
175/// payloads; anything else yields a fixed placeholder).
176pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
177    if let Some(s) = payload.downcast_ref::<&str>() {
178        (*s).to_string()
179    } else if let Some(s) = payload.downcast_ref::<String>() {
180        s.clone()
181    } else {
182        "producer panicked".to_string()
183    }
184}
185
186/// Report a caught panic through `out_err` with [`PANIC_ERROR_CODE`] and the
187/// payload's message. Generated thunks call this from their `catch_unwind`
188/// error arm.
189pub fn error_set_panic(out_err: *mut weaveffi_error, payload: &(dyn std::any::Any + Send)) {
190    error_set(
191        out_err,
192        PANIC_ERROR_CODE,
193        &format!("producer panicked: {}", panic_message(payload)),
194    );
195}
196
197/// Allocate a new C string from a Rust string, returning an owned pointer.
198/// Caller must later free with `weaveffi_free_string` or `weaveffi_error_clear`.
199// `CString::new` is infallible here because interior NUL bytes are stripped
200// before the call, so there is no reachable panic to document.
201#[allow(clippy::missing_panics_doc)]
202pub fn string_to_c_ptr(s: impl AsRef<str>) -> *const c_char {
203    let s = s.as_ref();
204    let sanitized = if s.as_bytes().contains(&0) {
205        s.replace('\0', "")
206    } else {
207        s.to_owned()
208    };
209    let cstr = CString::new(sanitized).expect("string_to_c_ptr: unexpected NUL after sanitization");
210    cstr.into_raw()
211}
212
213/// Free a C string previously allocated by this runtime.
214pub fn free_string(ptr: *const c_char) {
215    if ptr.is_null() {
216        return;
217    }
218    // SAFETY: pointer must be returned from `CString::into_raw`
219    unsafe { drop(CString::from_raw(ptr as *mut c_char)) };
220}
221
222/// Free a byte buffer previously allocated by Rust and returned to foreign code.
223pub fn free_bytes(ptr: *mut u8, len: usize) {
224    if ptr.is_null() {
225        return;
226    }
227    // SAFETY: reconstructs the original Box<[u8]> for deallocation
228    unsafe { drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len))) };
229}
230
231/// Fixed alignment used for every Wasm linear-memory allocation handed to JS.
232///
233/// 8 bytes over-aligns scalar/byte buffers but is required for the `{i32 ptr,
234/// i32 len}` and wider return slots that JS reads back through `DataView`.
235#[cfg(target_arch = "wasm32")]
236const WASM_ALLOC_ALIGN: usize = 8;
237
238/// Allocate `size` bytes in this module's Wasm linear memory.
239///
240/// The Wasm backend has no host-provided allocator, so generated JS glue calls
241/// the `weaveffi_alloc` thunk (emitted by [`export_runtime!`]) to stage input
242/// strings/byte buffers and to reserve struct-return (`sret`) slots. The caller
243/// must release the block with [`wasm_dealloc`] using the *same* `size`.
244#[cfg(target_arch = "wasm32")]
245pub fn wasm_alloc(size: usize) -> *mut u8 {
246    let size = size.max(1);
247    let layout = std::alloc::Layout::from_size_align(size, WASM_ALLOC_ALIGN)
248        .expect("weaveffi_alloc: invalid layout");
249    // SAFETY: `size >= 1` and the alignment is a non-zero power of two.
250    unsafe { std::alloc::alloc(layout) }
251}
252
253/// Release a block previously returned by [`wasm_alloc`].
254///
255/// `size` must match the original allocation request (JS retains it).
256#[cfg(target_arch = "wasm32")]
257pub fn wasm_dealloc(ptr: *mut u8, size: usize) {
258    if ptr.is_null() {
259        return;
260    }
261    let size = size.max(1);
262    let layout = std::alloc::Layout::from_size_align(size, WASM_ALLOC_ALIGN)
263        .expect("weaveffi_dealloc: invalid layout");
264    // SAFETY: `ptr` came from `wasm_alloc` with this exact layout.
265    unsafe { std::alloc::dealloc(ptr, layout) };
266}
267
268/// Clear an error by freeing any message and zeroing fields.
269pub fn error_clear(err: *mut weaveffi_error) {
270    error_set_ok(err);
271}
272
273/// Opaque cancellation token passed across the C ABI boundary.
274///
275/// Foreign callers obtain a token via `weaveffi_cancel_token_create`, signal
276/// cancellation with `weaveffi_cancel_token_cancel`, and release it with
277/// `weaveffi_cancel_token_destroy`.
278#[repr(C)]
279pub struct weaveffi_cancel_token {
280    cancelled: AtomicBool,
281}
282
283/// Allocate a new cancel token. The caller owns the returned pointer and must
284/// eventually call `weaveffi_cancel_token_destroy`.
285pub fn cancel_token_create() -> *mut weaveffi_cancel_token {
286    Box::into_raw(Box::new(weaveffi_cancel_token {
287        cancelled: AtomicBool::new(false),
288    }))
289}
290
291/// Signal cancellation on the token (thread-safe).
292pub fn cancel_token_cancel(token: *mut weaveffi_cancel_token) {
293    if token.is_null() {
294        return;
295    }
296    // SAFETY: pointer checked for null above
297    let t = unsafe { &*token };
298    t.cancelled.store(true, Ordering::Release);
299}
300
301/// Check whether the token has been cancelled (thread-safe).
302pub fn cancel_token_is_cancelled(token: *const weaveffi_cancel_token) -> bool {
303    if token.is_null() {
304        return false;
305    }
306    // SAFETY: pointer checked for null above
307    let t = unsafe { &*token };
308    t.cancelled.load(Ordering::Acquire)
309}
310
311/// Destroy a cancel token previously created by `cancel_token_create`.
312pub fn cancel_token_destroy(token: *mut weaveffi_cancel_token) {
313    if token.is_null() {
314        return;
315    }
316    // SAFETY: pointer was returned from `Box::into_raw` in `cancel_token_create`
317    unsafe { drop(Box::from_raw(token)) };
318}
319
320/// A safe, `Send` view of a foreign [`weaveffi_cancel_token`] handed to a
321/// cancellable `async fn`.
322///
323/// A producer marks an exported `async fn` `#[weaveffi::cancellable]` and
324/// accepts a `CancelToken` as its final parameter; the `#[weaveffi::module]`
325/// expansion lifts the launcher's `cancel_token` slot into one of these and
326/// moves it onto the worker thread. The function polls [`CancelToken::is_cancelled`]
327/// at safe points and returns early (typically `Err`) when cancellation is observed.
328/// The token carries no parameter in the IDL: it is part of the async calling
329/// convention, not the function's logical signature.
330///
331/// # Safety
332///
333/// The wrapped pointer is owned by the foreign caller, which (per the cancel
334/// token contract) keeps it alive until the completion callback fires, so
335/// reading the atomic flag from the worker thread is sound. The wrapper is
336/// therefore `Send`/`Sync`.
337pub struct CancelToken {
338    raw: *const weaveffi_cancel_token,
339}
340
341// SAFETY: the wrapped token is an atomic flag behind a pointer the foreign
342// caller keeps valid for the whole async operation; reading it from the worker
343// thread the launcher spawns races only on the atomic, which is synchronized.
344unsafe impl Send for CancelToken {}
345// SAFETY: see the `Send` impl; `is_cancelled` is a shared atomic load.
346unsafe impl Sync for CancelToken {}
347
348impl CancelToken {
349    /// Wrap a raw cancel-token pointer. A null pointer is permitted and reads as
350    /// "never cancelled".
351    ///
352    /// This is the entry point the `#[weaveffi::module]` expansion calls; it is
353    /// `#[doc(hidden)]` because producers receive an already-built token.
354    #[doc(hidden)]
355    #[must_use]
356    pub fn from_raw(raw: *const weaveffi_cancel_token) -> Self {
357        Self { raw }
358    }
359
360    /// Whether the foreign caller has requested cancellation.
361    #[must_use]
362    pub fn is_cancelled(&self) -> bool {
363        cancel_token_is_cancelled(self.raw)
364    }
365}
366
367/// An owned, type-erased iterator handed across the C ABI boundary.
368///
369/// A producer function whose IDL return type is `iter<T>` returns a
370/// `weaveffi::Iter<T>`, built from any iterator with [`Iter::new`]. The
371/// `#[weaveffi::module]` expansion boxes it behind an opaque iterator handle,
372/// pulls one element per `_next` call, and drops it in `_destroy`. Pulling
373/// elements lazily (rather than materializing a `Vec`) is what distinguishes an
374/// `iter<T>` return from a `[T]` (list) return.
375pub struct Iter<T> {
376    inner: Box<dyn Iterator<Item = T> + Send>,
377}
378
379impl<T> Iter<T> {
380    /// Wrap any `Send + 'static` iterator as a WeaveFFI iterator handle.
381    ///
382    /// Accepts anything `IntoIterator`, so `Iter::new(vec)`, `Iter::new(0..n)`,
383    /// and `Iter::new(map.into_values())` all work.
384    pub fn new<I>(iter: I) -> Self
385    where
386        I: IntoIterator<Item = T>,
387        I::IntoIter: Send + 'static,
388    {
389        Self {
390            inner: Box::new(iter.into_iter()),
391        }
392    }
393}
394
395impl<T> Iterator for Iter<T> {
396    type Item = T;
397
398    fn next(&mut self) -> Option<T> {
399        self.inner.next()
400    }
401}
402
403/// Drive a future to completion on the current thread, blocking until it
404/// resolves.
405///
406/// This is the minimal, dependency-free executor the `#[weaveffi::module]`
407/// expansion uses to run an exported `async fn` on the worker thread it spawns
408/// for each async launch, then invoke the completion callback with the result.
409/// It parks the thread between polls and wakes on `Waker::wake`, so a future
410/// that yields (for example, one awaiting a channel woken from another thread)
411/// makes progress without busy-spinning. There is no reactor, so a future that
412/// depends on an external runtime's I/O driver (such as Tokio's) will not be
413/// driven by this helper.
414///
415/// # Examples
416///
417/// ```
418/// let n = weaveffi_abi::block_on(async { 1 + 2 });
419/// assert_eq!(n, 3);
420/// ```
421pub fn block_on<F: std::future::Future>(fut: F) -> F::Output {
422    use std::sync::Arc;
423    use std::task::{Context, Poll, Wake, Waker};
424    use std::thread::{self, Thread};
425
426    struct ThreadWaker(Thread);
427    impl Wake for ThreadWaker {
428        fn wake(self: Arc<Self>) {
429            self.0.unpark();
430        }
431        fn wake_by_ref(self: &Arc<Self>) {
432            self.0.unpark();
433        }
434    }
435
436    let mut fut = Box::pin(fut);
437    let waker = Waker::from(Arc::new(ThreadWaker(thread::current())));
438    let mut cx = Context::from_waker(&waker);
439    loop {
440        match fut.as_mut().poll(&mut cx) {
441            Poll::Ready(out) => return out,
442            Poll::Pending => thread::park(),
443        }
444    }
445}
446
447/// Convert a NUL-terminated C string pointer to an owned `String`.
448/// Returns `None` if `ptr` is null or not valid UTF-8.
449pub fn c_ptr_to_string(ptr: *const c_char) -> Option<String> {
450    if ptr.is_null() {
451        return None;
452    }
453    // SAFETY: caller guarantees `ptr` points to a NUL-terminated string
454    let c = unsafe { CStr::from_ptr(ptr) };
455    c.to_str().ok().map(|s| s.to_owned())
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    #[test]
463    fn string_roundtrip_and_free() {
464        let ptr = string_to_c_ptr("hello world");
465        assert!(!ptr.is_null());
466        let recovered = c_ptr_to_string(ptr).unwrap();
467        assert_eq!(recovered, "hello world");
468        free_string(ptr);
469    }
470
471    #[test]
472    fn free_string_null_is_safe() {
473        free_string(ptr::null());
474    }
475
476    #[test]
477    fn free_bytes_null_is_safe() {
478        free_bytes(ptr::null_mut(), 0);
479    }
480
481    #[test]
482    fn bytes_alloc_and_free() {
483        let data: Vec<u8> = vec![1, 2, 3, 4, 5];
484        let len = data.len();
485        let boxed = data.into_boxed_slice();
486        let ptr = Box::into_raw(boxed) as *mut u8;
487        free_bytes(ptr, len);
488    }
489
490    #[test]
491    fn error_default_is_ok() {
492        let err = weaveffi_error::default();
493        assert_eq!(err.code, 0);
494        assert!(err.message.is_null());
495    }
496
497    #[test]
498    fn error_set_and_clear() {
499        let mut err = weaveffi_error::default();
500        error_set(&mut err, -1, "something went wrong");
501        assert_eq!(err.code, -1);
502        assert!(!err.message.is_null());
503        let msg = c_ptr_to_string(err.message).unwrap();
504        assert_eq!(msg, "something went wrong");
505        error_clear(&mut err);
506        assert_eq!(err.code, 0);
507        assert!(err.message.is_null());
508    }
509
510    #[test]
511    fn error_clear_null_is_safe() {
512        error_clear(ptr::null_mut());
513    }
514
515    #[test]
516    fn error_set_ok_frees_prior_message() {
517        let mut err = weaveffi_error::default();
518        error_set(&mut err, 1, "first");
519        error_set_ok(&mut err);
520        assert_eq!(err.code, 0);
521        assert!(err.message.is_null());
522    }
523
524    #[test]
525    fn error_set_replaces_prior_message() {
526        let mut err = weaveffi_error::default();
527        error_set(&mut err, 1, "first");
528        error_set(&mut err, 2, "second");
529        assert_eq!(err.code, 2);
530        let msg = c_ptr_to_string(err.message).unwrap();
531        assert_eq!(msg, "second");
532        error_clear(&mut err);
533    }
534
535    #[test]
536    fn result_to_out_err_ok_path() {
537        let mut err = weaveffi_error::default();
538        let val: Result<i32, String> = Ok(42);
539        let opt = result_to_out_err(val, &mut err);
540        assert_eq!(opt, Some(42));
541        assert_eq!(err.code, 0);
542        assert!(err.message.is_null());
543    }
544
545    #[test]
546    fn result_to_out_err_error_path() {
547        let mut err = weaveffi_error::default();
548        let val: Result<i32, String> = Err("bad input".to_string());
549        let opt = result_to_out_err(val, &mut err);
550        assert_eq!(opt, None);
551        assert_eq!(err.code, -1);
552        let msg = c_ptr_to_string(err.message).unwrap();
553        assert_eq!(msg, "bad input");
554        error_clear(&mut err);
555    }
556
557    // A domain error type that does *not* implement `Display`, so it can carry
558    // its own `ErrorReport` impl without colliding with the blanket one.
559    enum DomainError {
560        NotFound,
561        Io(String),
562    }
563
564    impl ErrorReport for DomainError {
565        fn code(&self) -> i32 {
566            match self {
567                DomainError::NotFound => 1001,
568                DomainError::Io(_) => 1004,
569            }
570        }
571        fn message(&self) -> String {
572            match self {
573                DomainError::NotFound => "not found".to_string(),
574                DomainError::Io(detail) => format!("io: {detail}"),
575            }
576        }
577    }
578
579    #[test]
580    fn error_report_blanket_display_uses_generic_code() {
581        let e = "boom".to_string();
582        assert_eq!(ErrorReport::code(&e), -1);
583        assert_eq!(ErrorReport::message(&e), "boom");
584    }
585
586    #[test]
587    fn error_report_domain_error_carries_its_code() {
588        let mut err = weaveffi_error::default();
589        let val: Result<i32, DomainError> = Err(DomainError::NotFound);
590        assert_eq!(result_to_out_err(val, &mut err), None);
591        assert_eq!(err.code, 1001);
592        assert_eq!(c_ptr_to_string(err.message).unwrap(), "not found");
593        error_clear(&mut err);
594
595        let val: Result<i32, DomainError> = Err(DomainError::Io("disk".to_string()));
596        assert_eq!(result_to_out_err(val, &mut err), None);
597        assert_eq!(err.code, 1004);
598        assert_eq!(c_ptr_to_string(err.message).unwrap(), "io: disk");
599        error_clear(&mut err);
600    }
601
602    #[test]
603    fn string_with_interior_nul_is_sanitized() {
604        let ptr = string_to_c_ptr("hel\0lo");
605        let recovered = c_ptr_to_string(ptr).unwrap();
606        assert_eq!(recovered, "hello");
607        free_string(ptr);
608    }
609
610    #[test]
611    fn c_ptr_to_string_null_returns_none() {
612        assert_eq!(c_ptr_to_string(ptr::null()), None);
613    }
614
615    #[test]
616    fn cancel_token_lifecycle() {
617        let token = cancel_token_create();
618        assert!(!token.is_null());
619        assert!(!cancel_token_is_cancelled(token));
620        cancel_token_cancel(token);
621        assert!(cancel_token_is_cancelled(token));
622        cancel_token_destroy(token);
623    }
624
625    #[test]
626    fn cancel_token_null_is_safe() {
627        cancel_token_cancel(ptr::null_mut());
628        assert!(!cancel_token_is_cancelled(ptr::null()));
629        cancel_token_destroy(ptr::null_mut());
630    }
631}