Skip to main content

weaveffi_model/
plan.rs

1//! The **marshalling plan**: the language-neutral calling contracts every
2//! backend renders, stated once.
3//!
4//! The [`crate::model`] layer answers *which symbols exist and what their C
5//! signatures are*. This module answers the questions one level up, the ones
6//! the eleven generators used to answer independently (and inconsistently):
7//!
8//! * **Passing** ([`ArgPass`], [`RetPass`]): how each argument crosses into
9//!   its ABI slots (by value, pinned string/bytes, serialized value buffer,
10//!   borrowed object pointer, or callback context plus vtable) and what the
11//!   wrapper does with the result (use, copy, decode, or adopt) before any
12//!   owed release.
13//! * **Errors** ([`ErrorStrategy`]): when a call reports through `out_err`,
14//!   is that a typed domain error the caller can catch, or a producer bug the
15//!   wrapper must trap on?
16//! * **Ownership** ([`RetPass::frees_bytes`]): after copying a returned value
17//!   into a native one, does the wrapper owe `{prefix}_free_bytes`?
18//! * **Iterators** ([`IteratorProtocol`]): the pull contract of `iter<T>`,
19//!   including the requirement that wrappers stay **lazy** (one producer
20//!   `next` per consumer step, never a hidden drain into a list).
21//! * **Async** ([`AsyncProtocol`]): the completion-callback contract,
22//!   including the rule that results and errors are owned by the consumer
23//!   and released through the runtime free symbols.
24//! * **Callback interfaces** ([`CallbackProtocol`]): the contract for the
25//!   consumer-implemented vtable the producer calls back into.
26//!
27//! Every classification here derives from [`Ty::family`], so a backend that
28//! renders these plans in its own syntax cannot drift from the others on
29//! semantics; only the spelling differs.
30
31use crate::abi::split_qualified;
32use crate::abi::AbiParam;
33use crate::model::{
34    AsyncBinding, CallbackInterfaceBinding, Family, FnBinding, IteratorBinding, ParamBinding, Ty,
35};
36
37/// How a callable's `out_err` slot is interpreted by idiomatic wrappers.
38///
39/// Every synchronous C ABI entry point carries a trailing `out_err`, and every
40/// async completion callback carries an `err` slot, regardless of `throws`.
41/// What differs is the *meaning* of a non-zero code, and every backend must
42/// agree on it.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum ErrorStrategy {
45    /// The function declares `throws: true`: a non-zero code is a typed
46    /// domain error. The wrapper maps the code onto the module's error
47    /// domain (an exception subclass, a Swift `Error` enum case, a Go
48    /// `error` value, ...), decodes any payload fields from the error's
49    /// `payload_ptr`/`payload_len` buffer, and surfaces it through the
50    /// target's normal error channel so callers can catch and match on it.
51    /// A *negative* code (generic `-1`, panic `-2`, marshalling failure
52    /// `-3`, foreign callback failure `-4`, cancelled `-5`) surfaces through
53    /// the same channel as the target's runtime error type, distinct from
54    /// every domain error type, and `-5` as the language's cancellation error
55    /// where it has one.
56    Throws,
57    /// The function does not throw: the only way `out_err` reports failure
58    /// is a producer bug or a runtime trap (a caught panic, code `-2`; a
59    /// consumer callback that raised, code `-4`). The wrapper surfaces it
60    /// through the target's *programming-error* idiom (an unchecked exception
61    /// where the language has them, a Go `panic`, a Swift `fatalError`).
62    /// It must never be silently ignored, and it must never be dressed up as
63    /// a typed domain error.
64    Trap,
65}
66
67impl FnBinding {
68    /// The error strategy of this callable: [`ErrorStrategy::Throws`] when the
69    /// IDL declares `throws: true`, otherwise [`ErrorStrategy::Trap`].
70    pub fn error_strategy(&self) -> ErrorStrategy {
71        if self.throws {
72            ErrorStrategy::Throws
73        } else {
74            ErrorStrategy::Trap
75        }
76    }
77}
78
79/// How one parameter crosses the call boundary: the passing contract a
80/// wrapper renders when marshalling its native argument into ABI slots.
81///
82/// Exactly one variant applies to any parameter, and the borrowed
83/// [`AbiParam`] references point at the parameter's own precomputed slots,
84/// so a backend that dispatches on this enum cannot disagree with the C
85/// header about arity or slot order.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum ArgPass<'a> {
88    /// One slot passed by value: scalars, bools, and C-style enums.
89    Direct {
90        /// The single ABI slot.
91        slot: &'a AbiParam,
92    },
93    /// A borrowed `(ptr, len)` pair of UTF-8 bytes, not NUL-terminated. The
94    /// wrapper encodes the string and keeps the encoding alive for the
95    /// duration of the call; the producer copies what it needs.
96    String {
97        /// The `const uint8_t*` slot.
98        ptr: &'a AbiParam,
99        /// The `size_t` length slot.
100        len: &'a AbiParam,
101    },
102    /// A borrowed `(ptr, len)` byte pair. The wrapper pins its native byte
103    /// storage for the call; the producer copies what it needs.
104    Bytes {
105        /// The `uint8_t*` data slot.
106        ptr: &'a AbiParam,
107        /// The `size_t` length slot.
108        len: &'a AbiParam,
109    },
110    /// A buffered value (record, rich enum, optional, list, or map): the
111    /// wrapper serializes it into the value-buffer wire format
112    /// ([`Ty::wire`]), passes the encoding as a borrowed `(ptr, len)` pair,
113    /// and releases its own encoding after the call returns. Any object
114    /// token written into the encoding must be a freshly cloned reference
115    /// (see [`clone_symbol`]).
116    Buffer {
117        /// The `const uint8_t*` data slot.
118        ptr: &'a AbiParam,
119        /// The `size_t` length slot.
120        len: &'a AbiParam,
121    },
122    /// A borrowed object pointer: the wrapper passes the wrapped object's
123    /// native handle and retains its own reference. When `nullable`, the IDL
124    /// type is `Interface?` and null means none.
125    Object {
126        /// The single object-pointer slot.
127        slot: &'a AbiParam,
128        /// `true` for `Interface?`: null is a legal "none" argument.
129        nullable: bool,
130    },
131    /// A callback interface: the wrapper registers its native implementation
132    /// in a handle table, passes the table key as `ctx` and the interface's
133    /// static vtable as `vtable`, and removes the entry when the producer
134    /// calls the vtable's `free` (see [`CallbackProtocol`]).
135    Callback {
136        /// The `void*` context slot.
137        ctx: &'a AbiParam,
138        /// The `const {vtable}*` slot.
139        vtable: &'a AbiParam,
140    },
141}
142
143impl ParamBinding {
144    /// The passing contract for this parameter.
145    ///
146    /// # Panics
147    ///
148    /// Panics if the parameter's precomputed ABI slots disagree with its
149    /// type's family, which would be a bug in the model construction, not a
150    /// user error.
151    pub fn arg_pass(&self) -> ArgPass<'_> {
152        let pair = || {
153            assert!(
154                self.abi.len() == 2,
155                "two-slot parameter '{}' must have exactly two ABI slots",
156                self.name
157            );
158            (&self.abi[0], &self.abi[1])
159        };
160        let single = || {
161            assert!(
162                self.abi.len() == 1,
163                "single-slot parameter '{}' must have exactly one ABI slot",
164                self.name
165            );
166            &self.abi[0]
167        };
168        match self.ty.family() {
169            Family::Direct => ArgPass::Direct { slot: single() },
170            Family::String => {
171                let (ptr, len) = pair();
172                ArgPass::String { ptr, len }
173            }
174            Family::Bytes => {
175                let (ptr, len) = pair();
176                ArgPass::Bytes { ptr, len }
177            }
178            Family::Buffer => {
179                let (ptr, len) = pair();
180                ArgPass::Buffer { ptr, len }
181            }
182            Family::Object { nullable } => ArgPass::Object {
183                slot: single(),
184                nullable,
185            },
186            Family::Callback => {
187                let (ctx, vtable) = pair();
188                ArgPass::Callback { ctx, vtable }
189            }
190            Family::Iterator => unreachable!("iterators are never parameters"),
191        }
192    }
193}
194
195/// How a value produced by the producer crosses back to the wrapper: the
196/// receiving contract, including the decode step and the release obligation.
197///
198/// Sync returns, async results, and iterator elements all use this.
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum RetPass {
201    /// No return value.
202    Void,
203    /// By-value return (scalar, bool, C-style enum): use directly, nothing to
204    /// free.
205    Direct,
206    /// Owned `(const uint8_t*, out_len)` UTF-8: decode into the native string,
207    /// then release with
208    /// `{prefix}_free_bytes`.
209    String,
210    /// Owned `(const uint8_t*, out_len)` raw bytes: copy, then release with
211    /// `{prefix}_free_bytes`.
212    Bytes,
213    /// Owned `(const uint8_t*, out_len)` value buffer: decode via the wire
214    /// format ([`Ty::wire`]), adopting any object tokens it carries, then
215    /// release with `{prefix}_free_bytes`.
216    Buffer,
217    /// One strong object reference the wrapper adopts into its disposal
218    /// idiom. When `nullable`, the IDL type is `Interface?` and a null return
219    /// means none.
220    Object {
221        /// The `{prefix}_{module}_{Name}_destroy` symbol the adopted
222        /// reference eventually owes.
223        destroy_symbol: String,
224        /// The `{prefix}_{module}_{Name}_clone` symbol the wrapper calls when
225        /// it needs a second reference (for example to write the object into
226        /// a value buffer).
227        clone_symbol: String,
228        /// `true` for `Interface?`: a null return is a legal "none" result.
229        nullable: bool,
230    },
231}
232
233impl RetPass {
234    /// Whether the wrapper owes `{prefix}_free_bytes(ptr, len)` after copying
235    /// or decoding a *returned* value: true for strings, bytes, and value
236    /// buffers. Adopted objects owe their `destroy` symbol instead (see
237    /// [`RetPass::Object`]). Callback-method *arguments* reuse this plan but
238    /// are borrowed for the call, so a trampoline never frees them.
239    #[must_use]
240    pub fn frees_bytes(&self) -> bool {
241        matches!(self, RetPass::String | RetPass::Bytes | RetPass::Buffer)
242    }
243}
244
245/// The receiving contract for a value of type `ty` produced by a callable
246/// declared inside `module` under `prefix`. `None` (a void return) is
247/// [`RetPass::Void`].
248///
249/// # Panics
250///
251/// Panics on an iterator return, whose contract is [`IteratorProtocol`], not
252/// a value-passing plan (backends dispatch on
253/// [`CallShape`](crate::model::CallShape) before consulting this), and on a
254/// callback interface, which validation never admits as a return.
255pub fn ret_pass(ty: Option<&Ty>, prefix: &str) -> RetPass {
256    let Some(ty) = ty else {
257        return RetPass::Void;
258    };
259    match ty.family() {
260        Family::Direct => RetPass::Direct,
261        Family::String => RetPass::String,
262        Family::Bytes => RetPass::Bytes,
263        Family::Buffer => RetPass::Buffer,
264        Family::Object { nullable } => {
265            let iface = ty
266                .interface_name()
267                .expect("object family names an interface");
268            RetPass::Object {
269                destroy_symbol: destroy_symbol(iface, prefix),
270                clone_symbol: clone_symbol(iface, prefix),
271                nullable,
272            }
273        }
274        Family::Callback => panic!("callback interfaces are never returned"),
275        Family::Iterator => panic!("iterator returns follow IteratorProtocol, not a RetPass"),
276    }
277}
278
279/// The `{prefix}_{module}_{Name}_destroy` symbol for a (possibly
280/// dot-qualified) interface name referenced from `current_module`.
281pub fn destroy_symbol(name: &str, prefix: &str) -> String {
282    let (module, name) = split_qualified(name, "");
283    format!("{prefix}_{module}_{name}_destroy")
284}
285
286/// The `{prefix}_{module}_{Name}_clone` symbol for a (possibly
287/// dot-qualified) interface name referenced from `current_module`.
288pub fn clone_symbol(name: &str, prefix: &str) -> String {
289    let (module, name) = split_qualified(name, "");
290    format!("{prefix}_{module}_{name}_clone")
291}
292
293/// The `iter<T>` pull contract every backend renders.
294///
295/// The producer returns an opaque iterator handle; the consumer then calls
296/// `next` once per element and `destroy` exactly once when done. The binding
297/// contract has three clauses every wrapper must satisfy:
298///
299/// 1. **Laziness.** The wrapper exposes the target's native lazy iteration
300///    idiom (a Python iterator, a Ruby `Enumerator`, a Go `iter.Seq2`, a C#
301///    `IEnumerable`, a Dart `Iterable`, a JS iterable, a Swift `Sequence`, a
302///    Kotlin `Iterator`) and issues **one producer `next` call per consumer
303///    step**. Draining the producer into a hidden list defeats the point of
304///    `iter<T>` (constant-memory streaming) and is a contract violation.
305/// 2. **Element ownership.** Each `next` writes an element the consumer now
306///    owns; the wrapper receives it exactly as it would a return of the same
307///    type ([`elem`](Self::elem)): copy and free a string or bytes, decode
308///    and free a buffer, adopt an object.
309/// 3. **Handle lifecycle.** `destroy` is called exactly once: eagerly on
310///    exhaustion, and from the wrapper's disposal idiom (RAII destructor,
311///    finalizer, `close()`, generator cleanup) when iteration is abandoned
312///    early.
313///
314/// Each `next` call also carries `out_err` and follows the owning function's
315/// [`ErrorStrategy`].
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct IteratorProtocol<'a> {
318    /// The lowered iterator surface: launcher, `next`, and destroy symbols.
319    pub binding: &'a IteratorBinding,
320    /// How each element written by `next` is received.
321    pub elem: RetPass,
322    /// How `out_err` reports from the launcher and each `next` call are
323    /// interpreted.
324    pub error: ErrorStrategy,
325}
326
327impl IteratorBinding {
328    /// Build the full pull contract for this iterator, resolving the element
329    /// plan against the declaring `module` and `prefix`.
330    pub fn protocol<'a>(&'a self, f: &FnBinding, prefix: &str) -> IteratorProtocol<'a> {
331        let elem = ret_pass(Some(&self.elem), prefix);
332        IteratorProtocol {
333            binding: self,
334            elem,
335            error: f.error_strategy(),
336        }
337    }
338}
339
340/// The async completion contract every backend renders.
341///
342/// The launcher returns immediately; the producer later invokes the completion
343/// callback exactly once, from an arbitrary producer thread. The contract has
344/// three clauses:
345///
346/// 1. **Single completion.** The callback fires exactly once per launch; the
347///    wrapper resolves its native future idiom (a Python `asyncio` future, a
348///    JS `Promise`, a Swift continuation, a C# `TaskCompletionSource`, a Go
349///    channel) exactly once and then releases the registration.
350/// 2. **Owned results.** Everything passed to the callback is owned by the
351///    consumer. String results are released with `{prefix}_free_bytes`,
352///    byte and buffered-value results with `{prefix}_free_bytes`, and
353///    interface-object results transfer one strong reference (the wrapper
354///    adopts the pointer). This is what lets runtimes that defer callback
355///    bodies past the native return (Dart's `NativeCallable.listener`, for
356///    example) decode safely; a wrapper that processes results inline still
357///    copies or decodes first and then frees.
358/// 3. **Foreign-thread delivery.** The callback runs on a producer thread,
359///    so the wrapper must hop back to its native scheduler before touching
360///    consumer state (`call_soon_threadsafe`, a threadsafe function, a
361///    dispatched continuation) rather than resolving inline where the
362///    target's runtime forbids it.
363///
364/// The callback's `err` slot follows the owning function's [`ErrorStrategy`].
365/// A non-null error is heap-boxed and owned by the consumer: the wrapper
366/// copies the code, message, and payload, then releases the box with
367/// `{prefix}_error_free` exactly once.
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct AsyncProtocol<'a> {
370    /// The lowered async surface: launcher and callback typedef.
371    pub binding: &'a AsyncBinding,
372    /// Whether the launcher carries a `cancel_token` slot before
373    /// `callback`/`context`.
374    pub cancellable: bool,
375    /// How the callback's result slots are received, including the release
376    /// owed ([`RetPass::frees_bytes`]) or the destroy symbol an adopted object owes.
377    pub result: RetPass,
378    /// How the callback's `err` slot is interpreted.
379    pub error: ErrorStrategy,
380}
381
382impl AsyncBinding {
383    /// Build the full completion contract for this async function, resolving
384    /// the result plan against the declaring `module` and `prefix`.
385    pub fn protocol<'a>(&'a self, f: &FnBinding, prefix: &str) -> AsyncProtocol<'a> {
386        AsyncProtocol {
387            binding: self,
388            cancellable: f.cancellable,
389            result: ret_pass(f.ret.as_ref(), prefix),
390            error: f.error_strategy(),
391        }
392    }
393}
394
395/// The callback-interface contract every backend renders.
396///
397/// A callback interface is the consumer's side of the boundary: the consumer
398/// supplies an implementation, the producer calls it. The contract has four
399/// clauses:
400///
401/// 1. **One static vtable per interface.** The wrapper emits exactly one
402///    process-wide vtable value for the interface whose entries are
403///    trampolines from the C signature ([`CallbackMethodBinding::abi_params`])
404///    into the native implementation, plus a trailing `free` entry.
405/// 2. **Context is a handle-table key.** The wrapper stores the native
406///    implementation in a table keyed by an integer or pointer it passes as
407///    `ctx`, so the implementation stays alive as long as the producer holds
408///    the callback and garbage collectors never see a raw pointer.
409/// 3. **Arguments are received like returns.** Strings, bytes, and buffers
410///    arriving in a trampoline are borrowed for the call: the wrapper copies
411///    or decodes them before returning and frees nothing. Object arguments
412///    transfer one strong reference the wrapper adopts. Method returns are
413///    direct values written straight into the C return.
414/// 4. **Foreign failures trap.** When the native implementation raises, the
415///    trampoline calls `{prefix}_error_set(out_err, -4, message)` and returns
416///    a default value; it must never let an exception unwind through the C
417///    frame. The producer then aborts its call with `FOREIGN_ERROR_CODE`.
418///
419/// Trampolines may be invoked from any producer thread; the wrapper is
420/// responsible for whatever thread affinity its runtime demands (a GIL
421/// acquisition, a JNI attach, a threadsafe-function hop).
422///
423/// [`CallbackMethodBinding::abi_params`]: crate::model::CallbackMethodBinding::abi_params
424#[derive(Debug, Clone, PartialEq, Eq)]
425pub struct CallbackProtocol<'a> {
426    /// The lowered callback interface: vtable tag and methods.
427    pub binding: &'a CallbackInterfaceBinding,
428    /// How each method's parameters are received inside a trampoline, in
429    /// method order then parameter order.
430    pub method_args: Vec<Vec<RetPass>>,
431}
432
433impl CallbackInterfaceBinding {
434    /// Build the full contract for this callback interface, resolving each
435    /// parameter's receiving plan against the declaring `module` and
436    /// `prefix`.
437    pub fn protocol<'a>(&'a self, prefix: &str) -> CallbackProtocol<'a> {
438        CallbackProtocol {
439            binding: self,
440            method_args: self
441                .methods
442                .iter()
443                .map(|m| {
444                    m.params
445                        .iter()
446                        .map(|p| ret_pass(Some(&p.ty), prefix))
447                        .collect()
448                })
449                .collect(),
450        }
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn arg_pass_classifies_every_family() {
460        let pb = |name: &str, ty: Ty| ParamBinding::new(name, ty, None, "m");
461        assert!(matches!(
462            pb("x", Ty::I32).arg_pass(),
463            ArgPass::Direct { slot } if slot.name == "x"
464        ));
465        assert!(matches!(
466            pb("s", Ty::StringUtf8).arg_pass(),
467            ArgPass::String { ptr, .. } if ptr.name == "s_ptr"
468        ));
469        assert!(matches!(
470            pb("data", Ty::Bytes).arg_pass(),
471            ArgPass::Bytes { ptr, len } if ptr.name == "data_ptr" && len.name == "data_len"
472        ));
473        assert!(matches!(
474            pb("c", Ty::Record("Contact".into())).arg_pass(),
475            ArgPass::Buffer { ptr, len } if ptr.name == "c_ptr" && len.name == "c_len"
476        ));
477        assert!(matches!(
478            pb("o", Ty::Optional(Box::new(Ty::I32))).arg_pass(),
479            ArgPass::Buffer { .. }
480        ));
481        assert!(matches!(
482            pb("store", Ty::Interface("Store".into())).arg_pass(),
483            ArgPass::Object {
484                nullable: false,
485                ..
486            }
487        ));
488        assert!(matches!(
489            pb(
490                "store",
491                Ty::Optional(Box::new(Ty::Interface("Store".into())))
492            )
493            .arg_pass(),
494            ArgPass::Object { nullable: true, .. }
495        ));
496        assert!(matches!(
497            pb("l", Ty::CallbackInterface("Listener".into())).arg_pass(),
498            ArgPass::Callback { ctx, vtable } if ctx.name == "l_ctx" && vtable.name == "l_vtable"
499        ));
500    }
501
502    #[test]
503    fn ret_pass_distinguishes_copy_decode_and_adopt() {
504        assert_eq!(ret_pass(None, "weaveffi"), RetPass::Void);
505        assert_eq!(ret_pass(Some(&Ty::I64), "weaveffi"), RetPass::Direct);
506        assert_eq!(ret_pass(Some(&Ty::StringUtf8), "weaveffi"), RetPass::String);
507        assert_eq!(ret_pass(Some(&Ty::Bytes), "weaveffi"), RetPass::Bytes);
508        for ty in [
509            Ty::Record("Contact".into()),
510            Ty::RichEnum("Shape".into()),
511            Ty::List(Box::new(Ty::StringUtf8)),
512            Ty::List(Box::new(Ty::Interface("Store".into()))),
513            Ty::Optional(Box::new(Ty::I64)),
514        ] {
515            assert_eq!(ret_pass(Some(&ty), "weaveffi"), RetPass::Buffer, "{ty}");
516        }
517        assert_eq!(
518            ret_pass(Some(&Ty::Interface("kv.Store".into())), "weaveffi"),
519            RetPass::Object {
520                destroy_symbol: "weaveffi_kv_Store_destroy".into(),
521                clone_symbol: "weaveffi_kv_Store_clone".into(),
522                nullable: false,
523            }
524        );
525        assert_eq!(
526            ret_pass(
527                Some(&Ty::Optional(Box::new(Ty::Interface("kv.Store".into())))),
528                "weaveffi"
529            ),
530            RetPass::Object {
531                destroy_symbol: "weaveffi_kv_Store_destroy".into(),
532                clone_symbol: "weaveffi_kv_Store_clone".into(),
533                nullable: true,
534            }
535        );
536        assert!(RetPass::String.frees_bytes());
537        assert!(RetPass::Buffer.frees_bytes());
538        assert!(!RetPass::Direct.frees_bytes());
539    }
540}