weaveffi/abi/callback.rs
1//! Consumer-implemented callback interfaces: the producer half of the ABI's
2//! vtable contract.
3//!
4//! A callback-interface parameter arrives as two slots, a `void* ctx` the
5//! consumer owns and a pointer to a process-wide static vtable. Every vtable
6//! starts with the same header ([`VtableHeader`]: its `size`, reserved
7//! `flags`, and the `free(ctx)` release hook), followed by one `extern "C"`
8//! function pointer per method. The `#[weaveffi::module]` expansion wraps
9//! the pair in a [`ForeignCallback`], implements the producer's trait on top
10//! of it (each method lowers its arguments, calls the vtable entry, adopts
11//! the return, then checks `out_err`), and hands the producer an
12//! `Arc<dyn Trait>`. When the last `Arc` drops, [`ForeignCallback`] calls
13//! `free(ctx)` exactly once.
14//!
15//! The producer rejects a vtable whose `size` is smaller than the vtable it
16//! was built with, so a consumer generated from an older contract can't make
17//! the producer call through a slot that doesn't exist. A larger `size` is
18//! accepted, which lets a newer consumer pass methods this producer doesn't
19//! know yet.
20//!
21//! A callback method returns `Result<T, ForeignError>`. A failure the
22//! consumer reports through `out_err` becomes the `Err`: a method that
23//! `throws` keeps a declared code of its module's error domain (decode it
24//! with [`ForeignError::domain`]), and every other failure has the code
25//! [`FOREIGN_ERROR_CODE`].
26
27use std::ffi::c_void;
28use std::sync::Arc;
29
30use crate::abi::buffer::BufferReader;
31use crate::abi::error::{ErrorDomain, FfiError, FOREIGN_ERROR_CODE, MARSHAL_ERROR_CODE};
32
33/// The fixed header every callback-interface vtable starts with, in this
34/// order and at these offsets (`{prefix}_..._vtable` in C).
35#[repr(C)]
36#[derive(Debug, Clone, Copy)]
37pub struct VtableHeader {
38 /// `sizeof` the whole vtable as the consumer compiled it.
39 pub size: u32,
40 /// Reserved; consumers set it to `0`.
41 pub flags: u32,
42 /// The consumer's release hook, called exactly once (from any producer
43 /// thread) when the producer drops its last reference to the callback.
44 pub free: unsafe extern "C" fn(ctx: *mut c_void),
45}
46
47/// Implemented by every generated `#[repr(C)]` vtable struct, whose first
48/// field is its [`VtableHeader`].
49///
50/// # Safety
51///
52/// The implementing type must be `#[repr(C)]` and start with a
53/// [`VtableHeader`], so a pointer to it is also a valid pointer to the
54/// header; [`ForeignCallback`] reads the header before trusting the rest.
55pub unsafe trait Vtable: 'static {}
56
57/// Ties a callback interface's `dyn Trait` to its generated vtable and
58/// foreign wrapper.
59///
60/// The `#[weaveffi::module]` expansion implements this for `dyn Trait` of
61/// every `#[weaveffi::callback_interface]`, which lets a thunk that only knows
62/// the producer's written type (`Arc<dyn Trait>`) name the vtable slot type
63/// (`<dyn Trait as CallbackInterface>::Vtable`) and lift the pair into a
64/// shared trait object with [`lift_callback`].
65pub trait CallbackInterface {
66 /// The generated `#[repr(C)]` vtable struct for this interface.
67 type Vtable: Vtable;
68
69 /// Wrap a lifted foreign callback as a shared trait object.
70 fn from_foreign(cb: ForeignCallback<Self::Vtable>) -> Arc<Self>;
71}
72
73/// Lift a callback-interface parameter's `(ctx, vtable)` slots into the
74/// `Arc<dyn Trait>` the producer's function takes. `name` is the parameter's
75/// name, for the error message.
76///
77/// # Errors
78///
79/// Returns a [`MARSHAL_ERROR_CODE`] error for a null vtable or one smaller
80/// than the producer's.
81///
82/// # Safety
83///
84/// Same contract as [`ForeignCallback::from_raw`].
85pub unsafe fn lift_callback<C: CallbackInterface + ?Sized>(
86 ctx: *mut c_void,
87 vtable: *const C::Vtable,
88 name: &str,
89) -> Result<Arc<C>, FfiError> {
90 // SAFETY: forwarded from the caller.
91 match unsafe { lift_callback_opt::<C>(ctx, vtable, name) }? {
92 Some(cb) => Ok(cb),
93 None => Err(FfiError::new(
94 MARSHAL_ERROR_CODE,
95 &format!("{name}: null callback vtable"),
96 )),
97 }
98}
99
100/// Lift an optional callback-interface parameter (`Cb?`): a null vtable is
101/// `None`.
102///
103/// # Errors
104///
105/// Returns a [`MARSHAL_ERROR_CODE`] error for a vtable smaller than the
106/// producer's.
107///
108/// # Safety
109///
110/// Same contract as [`ForeignCallback::from_raw`].
111pub unsafe fn lift_callback_opt<C: CallbackInterface + ?Sized>(
112 ctx: *mut c_void,
113 vtable: *const C::Vtable,
114 name: &str,
115) -> Result<Option<Arc<C>>, FfiError> {
116 // SAFETY: forwarded from the caller.
117 match unsafe { ForeignCallback::from_raw(ctx, vtable) } {
118 Ok(Some(cb)) => Ok(Some(C::from_foreign(cb))),
119 Ok(None) => Ok(None),
120 Err(VtableTooSmall { size, expected }) => Err(FfiError::new(
121 MARSHAL_ERROR_CODE,
122 &format!(
123 "{name}: the callback vtable is {size} bytes but this library expects at least \
124 {expected}; regenerate the bindings"
125 ),
126 )),
127 }
128}
129
130/// A vtable's `size` is smaller than the vtable the producer was built with.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct VtableTooSmall {
133 /// The `size` the consumer wrote.
134 pub size: u32,
135 /// The size of the producer's vtable struct.
136 pub expected: usize,
137}
138
139/// A consumer-implemented callback interface held by the producer.
140///
141/// Holds the consumer's `ctx` and a pointer to its static vtable `V`. The
142/// generated `impl Trait for ForeignX` calls
143/// [`vtable`](Self::vtable)`().method(ctx, ...)` per method; dropping the
144/// last owner calls `free(ctx)`.
145pub struct ForeignCallback<V: Vtable> {
146 ctx: *mut c_void,
147 vtable: *const V,
148}
149
150// SAFETY: the ABI contract obliges the consumer to make every vtable entry
151// (including `free`) callable from any thread, and `ctx` is only ever handed
152// back to those entries. The producer never dereferences `ctx` itself.
153unsafe impl<V: Vtable> Send for ForeignCallback<V> {}
154// SAFETY: see the `Send` impl; the vtable is immutable static consumer data.
155unsafe impl<V: Vtable> Sync for ForeignCallback<V> {}
156
157impl<V: Vtable> ForeignCallback<V> {
158 /// Adopt a `(ctx, vtable)` pair lifted from a callback-interface
159 /// parameter's slots. Returns `Ok(None)` when the vtable pointer is null.
160 ///
161 /// # Errors
162 ///
163 /// Returns [`VtableTooSmall`] when the vtable's `size` is smaller than
164 /// `V`. The context is released through the header's `free` first (when
165 /// the header itself is complete), so a rejected callback doesn't leak.
166 ///
167 /// # Safety
168 ///
169 /// `vtable` must be null or point to a vtable whose header is
170 /// initialized and whose first `size` bytes stay valid and unmodified
171 /// for the life of the returned value (consumers use a process-wide
172 /// static). `ctx` must remain valid until the vtable's `free` entry is
173 /// called with it.
174 pub unsafe fn from_raw(
175 ctx: *mut c_void,
176 vtable: *const V,
177 ) -> Result<Option<Self>, VtableTooSmall> {
178 if vtable.is_null() {
179 return Ok(None);
180 }
181 // SAFETY: `V: Vtable` starts with the header, which the caller
182 // guarantees is initialized.
183 let header = unsafe { &*vtable.cast::<VtableHeader>() };
184 let expected = std::mem::size_of::<V>();
185 if (header.size as usize) < expected {
186 if header.size as usize >= std::mem::size_of::<VtableHeader>() {
187 // SAFETY: the header is complete, so `free` is the
188 // consumer's release hook for `ctx`; nothing else will
189 // release it.
190 unsafe { (header.free)(ctx) };
191 }
192 return Err(VtableTooSmall {
193 size: header.size,
194 expected,
195 });
196 }
197 crate::abi::leak::track(crate::abi::leak::CALLBACKS, 1);
198 Ok(Some(Self { ctx, vtable }))
199 }
200
201 /// The consumer's context pointer, passed as the first argument of every
202 /// vtable entry.
203 #[must_use]
204 pub fn ctx(&self) -> *mut c_void {
205 self.ctx
206 }
207
208 /// The consumer's vtable.
209 #[must_use]
210 pub fn vtable(&self) -> &V {
211 // SAFETY: `from_raw` rejected null and short vtables, and its
212 // contract keeps the vtable alive and immutable for the life of
213 // `self`.
214 unsafe { &*self.vtable }
215 }
216}
217
218impl<V: Vtable> Drop for ForeignCallback<V> {
219 fn drop(&mut self) {
220 // SAFETY: `V` starts with the header (see `Vtable`).
221 let free = unsafe { &*self.vtable.cast::<VtableHeader>() }.free;
222 crate::abi::leak::track(crate::abi::leak::CALLBACKS, -1);
223 // SAFETY: `ctx` is handed back to the consumer's own release hook
224 // exactly once, as the ABI contract requires.
225 unsafe { free(self.ctx) };
226 }
227}
228
229/// A failure a consumer's callback-interface implementation reported, or a
230/// value it returned that the producer couldn't accept.
231///
232/// Every callback trait method returns `Result<T, ForeignError>`. `code` is
233/// [`FOREIGN_ERROR_CODE`] for any consumer failure, except that a method
234/// declared `throws` keeps a code of its module's error domain, whose fields
235/// [`domain`](Self::domain) decodes. A malformed return value (a string that
236/// isn't UTF-8, a null object, a bad enum value or buffer) has the code
237/// [`MARSHAL_ERROR_CODE`].
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct ForeignError {
240 /// The failure's code (see the type's docs).
241 pub code: i32,
242 /// The consumer's message, copied out of `out_err` before it was freed.
243 pub message: String,
244 /// The domain code's fields as a value buffer, or empty.
245 pub payload: Vec<u8>,
246}
247
248impl ForeignError {
249 /// A failure with `code` and `message` and no payload.
250 #[must_use]
251 pub fn new(code: i32, message: impl Into<String>) -> Self {
252 Self {
253 code,
254 message: message.into(),
255 payload: Vec::new(),
256 }
257 }
258
259 /// Decode a domain error the consumer reported: `Some` when `code` is
260 /// one of `E`'s declared codes and the payload holds that code's fields.
261 ///
262 /// Object fields can't be decoded here (a callback's error payload may
263 /// not carry object references), so a code whose fields include an
264 /// interface yields `None`.
265 #[must_use]
266 pub fn domain<E: ErrorDomain>(&self) -> Option<E> {
267 if self.code <= 0 {
268 return None;
269 }
270 let mut r = BufferReader::token_free(&self.payload);
271 // SAFETY: a token-free reader never adopts an object token.
272 let decoded = unsafe { E::read_code(self.code, &mut r) }.ok().flatten()?;
273 r.expect_end().ok().map(|()| decoded)
274 }
275
276 fn marshal(message: &str) -> Self {
277 Self::new(MARSHAL_ERROR_CODE, message)
278 }
279}
280
281impl std::fmt::Display for ForeignError {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 f.write_str(&self.message)
284 }
285}
286
287impl std::error::Error for ForeignError {}
288
289/// A `ForeignError` is reported with its own code, message, and payload, so
290/// a producer function whose error type is `ForeignError` can propagate a
291/// callback failure with `?`.
292impl crate::abi::ErrorReport for ForeignError {
293 fn code(&self) -> i32 {
294 self.code
295 }
296 fn message(&self) -> String {
297 self.message.clone()
298 }
299 fn payload(&self) -> Vec<u8> {
300 self.payload.clone()
301 }
302}
303
304fn failure(err: &FfiError, code: i32) -> ForeignError {
305 // SAFETY: the consumer fills `out_err` only through `{prefix}_error_set`,
306 // so a non-null message is a NUL-terminated string this runtime owns.
307 let message = unsafe { err.message_str() }
308 .unwrap_or("callback interface implementation failed")
309 .to_string();
310 ForeignError {
311 code,
312 message,
313 payload: if code > 0 {
314 err.payload().to_vec()
315 } else {
316 Vec::new()
317 },
318 }
319}
320
321/// Read the `out_err` slot a vtable entry of a method that doesn't `throw`
322/// wrote: `Ok(())` when the consumer succeeded, otherwise its failure with
323/// the code [`FOREIGN_ERROR_CODE`] (whatever code it wrote).
324///
325/// # Errors
326///
327/// Returns the consumer's failure when `err.code` isn't `0`.
328pub fn callback_status(err: &FfiError) -> Result<(), ForeignError> {
329 if err.code == 0 {
330 Ok(())
331 } else {
332 Err(failure(err, FOREIGN_ERROR_CODE))
333 }
334}
335
336/// Read the `out_err` slot a vtable entry of a `throws` method wrote: a
337/// positive code that `E` declares (with a payload that decodes) is kept,
338/// and every other failure gets the code [`FOREIGN_ERROR_CODE`].
339///
340/// # Errors
341///
342/// Returns the consumer's failure when `err.code` isn't `0`.
343pub fn callback_status_in<E: ErrorDomain>(err: &FfiError) -> Result<(), ForeignError> {
344 if err.code == 0 {
345 return Ok(());
346 }
347 let kept = failure(err, err.code);
348 if kept.domain::<E>().is_some() {
349 Err(kept)
350 } else {
351 Err(failure(err, FOREIGN_ERROR_CODE))
352 }
353}
354
355/// Lift a C-style enum a vtable entry returned.
356///
357/// # Errors
358///
359/// Returns a [`MARSHAL_ERROR_CODE`] failure for a value `E` doesn't declare.
360pub fn callback_ret_enum<E: crate::abi::CEnum>(value: i32) -> Result<E, ForeignError> {
361 E::from_i32(value)
362 .ok_or_else(|| ForeignError::marshal("callback interface returned an invalid enum value"))
363}
364
365/// Adopt the strong object reference a vtable entry returned.
366///
367/// # Errors
368///
369/// Returns a [`MARSHAL_ERROR_CODE`] failure for a null pointer.
370///
371/// # Safety
372///
373/// `ptr` must be null or carry one strong reference to a live `T` (a fresh
374/// `_clone`), which this adopts.
375pub unsafe fn callback_ret_object<T>(ptr: *mut T) -> Result<Arc<T>, ForeignError> {
376 // SAFETY: forwarded from the caller.
377 unsafe { callback_ret_object_opt(ptr) }
378 .ok_or_else(|| ForeignError::marshal("callback interface returned a null object"))
379}
380
381/// Adopt the optional object reference a vtable entry returned (`I?`).
382///
383/// # Safety
384///
385/// Same contract as [`callback_ret_object`].
386#[must_use]
387pub unsafe fn callback_ret_object_opt<T>(ptr: *mut T) -> Option<Arc<T>> {
388 // SAFETY: an object pointer is its token widened; forwarded from the
389 // caller.
390 unsafe { crate::abi::object::object_from_token(ptr as usize as u64) }
391}
392
393/// Adopt the byte run a vtable entry wrote to its `out_ptr`/`out_len` slots.
394///
395/// # Errors
396///
397/// Returns a [`MARSHAL_ERROR_CODE`] failure for a null pointer with a
398/// non-zero length.
399///
400/// # Safety
401///
402/// `ptr` must be null or a run of `len` bytes from `{prefix}_alloc`, which
403/// this adopts and frees.
404pub unsafe fn callback_ret_bytes(ptr: *mut u8, len: usize) -> Result<Vec<u8>, ForeignError> {
405 // SAFETY: forwarded from the caller.
406 unsafe { crate::abi::convert::adopt_bytes(ptr, len) }
407 .ok_or_else(|| ForeignError::marshal("callback interface returned a null run"))
408}
409
410/// Adopt a string a vtable entry wrote to its `out_ptr`/`out_len` slots.
411///
412/// # Errors
413///
414/// Returns a [`MARSHAL_ERROR_CODE`] failure for a null pointer with a
415/// non-zero length or bytes that aren't UTF-8.
416///
417/// # Safety
418///
419/// Same contract as [`callback_ret_bytes`].
420pub unsafe fn callback_ret_string(ptr: *mut u8, len: usize) -> Result<String, ForeignError> {
421 // SAFETY: forwarded from the caller.
422 String::from_utf8(unsafe { callback_ret_bytes(ptr, len) }?)
423 .map_err(|_| ForeignError::marshal("callback interface returned a string that isn't UTF-8"))
424}
425
426/// Adopt and decode a value buffer a vtable entry wrote to its
427/// `out_ptr`/`out_len` slots.
428///
429/// # Errors
430///
431/// Returns a [`MARSHAL_ERROR_CODE`] failure for a null pointer with a
432/// non-zero length or a malformed buffer.
433///
434/// # Safety
435///
436/// Same contract as [`callback_ret_bytes`], and every object token in the
437/// buffer must carry an unadopted reference (see
438/// [`BufferValue::read_value`](crate::abi::BufferValue::read_value)).
439pub unsafe fn callback_ret_buffer<T: crate::abi::BufferValue>(
440 ptr: *mut u8,
441 len: usize,
442) -> Result<T, ForeignError> {
443 // SAFETY: forwarded from the caller.
444 let bytes = unsafe { callback_ret_bytes(ptr, len) }?;
445 // SAFETY: forwarded from the caller; the run is decoded once.
446 unsafe { crate::abi::decode_value(&bytes) }
447 .map_err(|e| ForeignError::marshal(&format!("callback interface returned {e}")))
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use std::sync::atomic::{AtomicUsize, Ordering};
454
455 #[repr(C)]
456 struct TestVtable {
457 header: VtableHeader,
458 ping: unsafe extern "C" fn(*mut c_void, i32, *mut FfiError) -> i32,
459 }
460
461 // SAFETY: `#[repr(C)]` and starts with the header.
462 unsafe impl Vtable for TestVtable {}
463
464 unsafe extern "C" fn ping(_ctx: *mut c_void, x: i32, out_err: *mut FfiError) -> i32 {
465 if x < 0 {
466 unsafe { crate::abi::error_set(out_err, 7, "negative") };
467 return 0;
468 }
469 x * 2
470 }
471
472 /// Each test owns its counter: the context points at it, so tests running
473 /// in parallel never observe each other's releases.
474 unsafe extern "C" fn free(ctx: *mut c_void) {
475 unsafe { &*ctx.cast::<AtomicUsize>() }.fetch_add(1, Ordering::SeqCst);
476 }
477
478 static VTABLE: TestVtable = TestVtable {
479 header: VtableHeader {
480 size: std::mem::size_of::<TestVtable>() as u32,
481 flags: 0,
482 free,
483 },
484 ping,
485 };
486
487 fn ctx(freed: &AtomicUsize) -> *mut c_void {
488 std::ptr::from_ref(freed).cast_mut().cast()
489 }
490
491 fn call(cb: &ForeignCallback<TestVtable>, x: i32) -> Result<i32, ForeignError> {
492 let mut err = FfiError::default();
493 let out = unsafe { (cb.vtable().ping)(cb.ctx(), x, &mut err) };
494 callback_status(&err).map(|()| out)
495 }
496
497 #[test]
498 fn calls_through_and_frees_once() {
499 let freed = AtomicUsize::new(0);
500 let cb = Arc::new(
501 unsafe { ForeignCallback::from_raw(ctx(&freed), &VTABLE) }
502 .unwrap()
503 .unwrap(),
504 );
505 assert_eq!(call(&cb, 21), Ok(42));
506 let second = Arc::clone(&cb);
507 drop(cb);
508 assert_eq!(freed.load(Ordering::SeqCst), 0);
509 drop(second);
510 assert_eq!(freed.load(Ordering::SeqCst), 1);
511 }
512
513 #[test]
514 fn every_failure_of_a_non_throwing_method_is_foreign() {
515 let freed = AtomicUsize::new(0);
516 let cb = unsafe { ForeignCallback::from_raw(ctx(&freed), &VTABLE) }
517 .unwrap()
518 .unwrap();
519 let err = call(&cb, -1).unwrap_err();
520 assert_eq!(err.code, FOREIGN_ERROR_CODE);
521 assert_eq!(err.message, "negative");
522 let err = FfiError::new(MARSHAL_ERROR_CODE, "kept?");
523 assert_eq!(callback_status(&err).unwrap_err().code, FOREIGN_ERROR_CODE);
524 }
525
526 #[test]
527 fn short_vtables_are_rejected_and_released() {
528 let freed = AtomicUsize::new(0);
529 let short = TestVtable {
530 header: VtableHeader {
531 size: std::mem::size_of::<VtableHeader>() as u32,
532 flags: 0,
533 free,
534 },
535 ping,
536 };
537 let err = unsafe { ForeignCallback::from_raw(ctx(&freed), &short) }
538 .err()
539 .unwrap();
540 assert_eq!(err.expected, std::mem::size_of::<TestVtable>());
541 assert_eq!(freed.load(Ordering::SeqCst), 1);
542 // A larger vtable (a newer consumer) is fine.
543 let long = TestVtable {
544 header: VtableHeader {
545 size: 1024,
546 flags: 0,
547 free,
548 },
549 ping,
550 };
551 let cb = unsafe { ForeignCallback::from_raw(ctx(&freed), &long) }.unwrap();
552 drop(cb);
553 assert_eq!(freed.load(Ordering::SeqCst), 2);
554 }
555
556 #[test]
557 fn null_vtable_is_none() {
558 assert!(matches!(
559 unsafe {
560 ForeignCallback::<TestVtable>::from_raw(std::ptr::null_mut(), std::ptr::null())
561 },
562 Ok(None)
563 ));
564 }
565
566 #[test]
567 fn returned_runs_are_adopted() {
568 let ptr = crate::abi::alloc(5);
569 unsafe { std::ptr::copy_nonoverlapping(b"hello".as_ptr(), ptr, 5) };
570 assert_eq!(unsafe { callback_ret_string(ptr, 5) }.unwrap(), "hello");
571 assert_eq!(
572 unsafe { callback_ret_bytes(std::ptr::null_mut(), 0) },
573 Ok(vec![])
574 );
575 assert_eq!(
576 unsafe { callback_ret_bytes(std::ptr::null_mut(), 3) }
577 .unwrap_err()
578 .code,
579 MARSHAL_ERROR_CODE
580 );
581 let bad = crate::abi::alloc(1);
582 unsafe { *bad = 0xff };
583 assert!(unsafe { callback_ret_string(bad, 1) }.is_err());
584 }
585}