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) and which
12//!   release it owes (`{prefix}_free_bytes` or the interface's `_destroy`).
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//! * **Iterators** ([`IteratorProtocol`]): the pull contract of `iter<T>`,
17//!   including the requirement that wrappers stay **lazy** (one producer
18//!   `next` per consumer step, never a hidden drain into a list).
19//! * **Async** ([`AsyncProtocol`]): the completion-callback contract,
20//!   including the rule that results and errors are owned by the consumer
21//!   and released through the runtime free symbols.
22//! * **Callback interfaces** ([`CallbackProtocol`]): the contract for the
23//!   consumer-implemented vtable the producer calls back into.
24//!
25//! Every classification here derives from [`Ty::family`] and every symbol
26//! comes from the [`Model`](crate::model::Model), so a backend that renders
27//! these plans in its own syntax cannot drift from the others on semantics;
28//! only the spelling differs.
29
30use crate::abi::AbiParam;
31use crate::model::{
32    AsyncBinding, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, IteratorBinding,
33    ParamBinding,
34};
35use crate::ty::{Family, Ty};
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    /// (the interface's
116    /// [`clone_symbol`](crate::model::InterfaceBinding::clone_symbol)).
117    Buffer {
118        /// The `const uint8_t*` data slot.
119        ptr: &'a AbiParam,
120        /// The `size_t` length slot.
121        len: &'a AbiParam,
122    },
123    /// A borrowed object pointer: the wrapper passes the wrapped object's
124    /// native handle and retains its own reference. When `nullable`, the IDL
125    /// type is `Interface?` and null means none.
126    Object {
127        /// The single object-pointer slot.
128        slot: &'a AbiParam,
129        /// `true` for `Interface?`: null is a legal "none" argument.
130        nullable: bool,
131    },
132    /// A callback interface: the wrapper registers its native implementation
133    /// in a handle table, passes the table key as `ctx` and the interface's
134    /// static vtable as `vtable`, and removes the entry when the producer
135    /// calls the vtable's `free` (see [`CallbackProtocol`]). When
136    /// `nullable`, the IDL type is `Cb?` and a null `vtable` means none.
137    Callback {
138        /// The `void*` context slot.
139        ctx: &'a AbiParam,
140        /// The `const {vtable}*` slot.
141        vtable: &'a AbiParam,
142        /// `true` for `Cb?`: a null vtable is a legal "none" argument.
143        nullable: bool,
144    },
145}
146
147impl ParamBinding {
148    /// The passing contract for this parameter.
149    ///
150    /// # Panics
151    ///
152    /// Panics if the parameter's precomputed ABI slots disagree with its
153    /// type's family, which would be a bug in the model construction, not a
154    /// user error.
155    pub fn arg_pass(&self) -> ArgPass<'_> {
156        let pair = || {
157            assert!(
158                self.abi.len() == 2,
159                "two-slot parameter '{}' must have exactly two ABI slots",
160                self.name
161            );
162            (&self.abi[0], &self.abi[1])
163        };
164        let single = || {
165            assert!(
166                self.abi.len() == 1,
167                "single-slot parameter '{}' must have exactly one ABI slot",
168                self.name
169            );
170            &self.abi[0]
171        };
172        match self.ty.family() {
173            Family::Direct => ArgPass::Direct { slot: single() },
174            Family::String => {
175                let (ptr, len) = pair();
176                ArgPass::String { ptr, len }
177            }
178            Family::Bytes => {
179                let (ptr, len) = pair();
180                ArgPass::Bytes { ptr, len }
181            }
182            Family::Buffer => {
183                let (ptr, len) = pair();
184                ArgPass::Buffer { ptr, len }
185            }
186            Family::Object { nullable } => ArgPass::Object {
187                slot: single(),
188                nullable,
189            },
190            Family::Callback { nullable } => {
191                let (ctx, vtable) = pair();
192                ArgPass::Callback {
193                    ctx,
194                    vtable,
195                    nullable,
196                }
197            }
198            Family::Iterator => unreachable!("iterators are never parameters"),
199        }
200    }
201}
202
203/// How a value produced by the producer crosses back to the wrapper: the
204/// receiving contract, including the decode step and the release obligation.
205///
206/// Sync returns, async results, and iterator elements all use this.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum RetPass {
209    /// No return value.
210    Void,
211    /// By-value return (scalar, bool, C-style enum): use directly, nothing to
212    /// free.
213    Direct,
214    /// Owned `(const uint8_t*, out_len)` UTF-8: decode into the native string,
215    /// then release with
216    /// `{prefix}_free_bytes`.
217    String,
218    /// Owned `(const uint8_t*, out_len)` raw bytes: copy, then release with
219    /// `{prefix}_free_bytes`.
220    Bytes,
221    /// Owned `(const uint8_t*, out_len)` value buffer: decode via the wire
222    /// format ([`Ty::wire`]), adopting any object tokens it carries, then
223    /// release with `{prefix}_free_bytes`.
224    Buffer,
225    /// One strong object reference the wrapper adopts into its disposal
226    /// idiom, eventually released with the interface's `_destroy` symbol
227    /// ([`InterfaceBinding::destroy_symbol`], from
228    /// [`Model::interface`](crate::model::Model::interface)). When
229    /// `nullable`, the IDL type is `Interface?` and a null return means
230    /// none.
231    ///
232    /// [`InterfaceBinding::destroy_symbol`]: crate::model::InterfaceBinding::destroy_symbol
233    Object {
234        /// `true` for `Interface?`: a null return is a legal "none" result.
235        nullable: bool,
236    },
237}
238
239impl RetPass {
240    /// The receiving contract for a value of type `ty` produced by a
241    /// callable. `None` (a void return) is [`RetPass::Void`].
242    ///
243    /// # Panics
244    ///
245    /// Panics on an iterator return, whose contract is [`IteratorProtocol`],
246    /// not a value-passing plan (backends dispatch on
247    /// [`CallShape`](crate::model::CallShape) before consulting this), and on
248    /// a callback interface, which validation never admits as a return.
249    pub fn of(ty: Option<&Ty>) -> RetPass {
250        let Some(ty) = ty else {
251            return RetPass::Void;
252        };
253        match ty.family() {
254            Family::Direct => RetPass::Direct,
255            Family::String => RetPass::String,
256            Family::Bytes => RetPass::Bytes,
257            Family::Buffer => RetPass::Buffer,
258            Family::Object { nullable } => RetPass::Object { nullable },
259            Family::Callback { .. } => panic!("callback interfaces are never returned"),
260            Family::Iterator => panic!("iterator returns follow IteratorProtocol, not a RetPass"),
261        }
262    }
263}
264
265/// The `iter<T>` pull contract every backend renders.
266///
267/// The producer returns an opaque iterator handle; the consumer then calls
268/// `next` once per element and `destroy` exactly once when done. The binding
269/// contract has three clauses every wrapper must satisfy:
270///
271/// 1. **Laziness.** The wrapper exposes the target's native lazy iteration
272///    idiom (a Python iterator, a Ruby `Enumerator`, a Go `iter.Seq2`, a C#
273///    `IEnumerable`, a Dart `Iterable`, a JS iterable, a Swift `Sequence`, a
274///    Kotlin `Iterator`) and issues **one producer `next` call per consumer
275///    step**. Draining the producer into a hidden list defeats the point of
276///    `iter<T>` (constant-memory streaming) and is a contract violation.
277/// 2. **Element ownership.** Each `next` writes an element the consumer now
278///    owns; the wrapper receives it exactly as it would a return of the same
279///    type ([`elem`](Self::elem)): copy and free a string or bytes, decode
280///    and free a buffer, adopt an object.
281/// 3. **Handle lifecycle.** `destroy` is called exactly once: eagerly on
282///    exhaustion, and from the wrapper's disposal idiom (RAII destructor,
283///    finalizer, `close()`, generator cleanup) when iteration is abandoned
284///    early.
285///
286/// Each `next` call also carries `out_err` and follows the owning function's
287/// [`ErrorStrategy`].
288#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct IteratorProtocol<'a> {
290    /// The lowered iterator surface: launcher, `next`, and destroy symbols.
291    pub binding: &'a IteratorBinding,
292    /// How each element written by `next` is received.
293    pub elem: RetPass,
294    /// How `out_err` reports from the launcher and each `next` call are
295    /// interpreted.
296    pub error: ErrorStrategy,
297}
298
299impl IteratorBinding {
300    /// Build the full pull contract for this iterator of `f`.
301    pub fn protocol<'a>(&'a self, f: &FnBinding) -> IteratorProtocol<'a> {
302        IteratorProtocol {
303            binding: self,
304            elem: RetPass::of(Some(&self.elem)),
305            error: f.error_strategy(),
306        }
307    }
308}
309
310/// The async completion contract every backend renders.
311///
312/// The launcher returns immediately; the producer later invokes the completion
313/// callback exactly once, from an arbitrary producer thread. The contract has
314/// three clauses:
315///
316/// 1. **Single completion.** The callback fires exactly once per launch; the
317///    wrapper resolves its native future idiom (a Python `asyncio` future, a
318///    JS `Promise`, a Swift continuation, a C# `TaskCompletionSource`, a Go
319///    channel) exactly once and then releases the registration.
320/// 2. **Owned results.** Everything passed to the callback is owned by the
321///    consumer. String results are released with `{prefix}_free_bytes`,
322///    byte and buffered-value results with `{prefix}_free_bytes`, and
323///    interface-object results transfer one strong reference (the wrapper
324///    adopts the pointer). This is what lets runtimes that defer callback
325///    bodies past the native return (Dart's `NativeCallable.listener`, for
326///    example) decode safely; a wrapper that processes results inline still
327///    copies or decodes first and then frees.
328/// 3. **Foreign-thread delivery.** The callback runs on a producer thread,
329///    so the wrapper must hop back to its native scheduler before touching
330///    consumer state (`call_soon_threadsafe`, a threadsafe function, a
331///    dispatched continuation) rather than resolving inline where the
332///    target's runtime forbids it.
333///
334/// The callback's `err` slot follows the owning function's [`ErrorStrategy`].
335/// A non-null error is heap-boxed and owned by the consumer: the wrapper
336/// copies the code, message, and payload, then releases the box with
337/// `{prefix}_error_free` exactly once.
338#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct AsyncProtocol<'a> {
340    /// The lowered async surface: launcher and callback typedef.
341    pub binding: &'a AsyncBinding,
342    /// Whether the launcher carries a `cancel_token` slot before
343    /// `callback`/`context`.
344    pub cancellable: bool,
345    /// How the callback's result slots are received, including the release
346    /// owed (`{prefix}_free_bytes` or the destroy symbol an adopted object
347    /// owes).
348    pub result: RetPass,
349    /// How the callback's `err` slot is interpreted.
350    pub error: ErrorStrategy,
351}
352
353impl AsyncBinding {
354    /// Build the full completion contract for this async function `f`.
355    pub fn protocol<'a>(&'a self, f: &FnBinding) -> AsyncProtocol<'a> {
356        AsyncProtocol {
357            binding: self,
358            cancellable: f.cancellable,
359            result: RetPass::of(f.ret.as_ref()),
360            error: f.error_strategy(),
361        }
362    }
363}
364
365/// The callback-interface contract every backend renders.
366///
367/// A callback interface is the consumer's side of the boundary: the consumer
368/// supplies an implementation, the producer calls it. The contract has five
369/// clauses:
370///
371/// 1. **One static vtable per interface.** The wrapper emits exactly one
372///    process-wide vtable value for the interface. It starts with the fixed
373///    header, `size` (`sizeof` the vtable as the wrapper compiled it),
374///    `flags` (`0`), and `free`, followed by one trampoline per method from
375///    the C signature ([`CallbackMethodBinding::abi_params`]) into the native
376///    implementation. The producer rejects a vtable smaller than the one it
377///    was built with, so a stale binding fails with `-3` instead of calling
378///    through a missing slot.
379/// 2. **Context is a handle-table key.** The wrapper stores the native
380///    implementation in a table keyed by an integer or pointer it passes as
381///    `ctx`, so the implementation stays alive as long as the producer holds
382///    the callback and garbage collectors never see a raw pointer. `free`
383///    may run on any producer thread.
384/// 3. **Arguments are received like returns.** Strings, bytes, and buffers
385///    arriving in a trampoline are borrowed for the call: the wrapper copies
386///    or decodes them before returning and frees nothing. Object arguments
387///    transfer one strong reference the wrapper adopts.
388/// 4. **Returns transfer to the producer** ([`method_returns`](Self::method_returns)).
389///    A direct value is the C return. An object is returned as one strong
390///    reference (a fresh `_clone` of the wrapper's handle). A string, bytes,
391///    or buffer is written to the trailing `out_ptr`/`out_len` slots as a run
392///    the wrapper allocates with `{prefix}_alloc`; the producer adopts it.
393/// 5. **Failures go through `out_err`.** When the native implementation
394///    raises, the trampoline calls `{prefix}_error_set(out_err, code,
395///    message)` and returns a zero value; it must never let an exception
396///    unwind through the C frame. A method whose
397///    [`method_errors`](Self::method_errors) entry is
398///    [`ErrorStrategy::Throws`] reports a declared domain error with its
399///    positive code and its payload fields (`{prefix}_error_set_payload`);
400///    every other failure uses `-4`, and the producer treats any code it
401///    can't attribute to the method's domain as `-4`.
402///
403/// Trampolines may be invoked from any producer thread; the wrapper is
404/// responsible for whatever thread affinity its runtime demands (a GIL
405/// acquisition, a JNI attach, a threadsafe-function hop).
406///
407/// [`CallbackMethodBinding::abi_params`]: crate::model::CallbackMethodBinding::abi_params
408#[derive(Debug, Clone, PartialEq, Eq)]
409pub struct CallbackProtocol<'a> {
410    /// The lowered callback interface: vtable tag and methods.
411    pub binding: &'a CallbackInterfaceBinding,
412    /// How each method's parameters are received inside a trampoline, in
413    /// method order then parameter order.
414    pub method_args: Vec<Vec<RetPass>>,
415    /// How each method's return value crosses back to the producer, in
416    /// method order. [`RetPass::String`], [`RetPass::Bytes`], and
417    /// [`RetPass::Buffer`] are written to the `out_ptr`/`out_len` slots as a
418    /// `{prefix}_alloc` run.
419    pub method_returns: Vec<RetPass>,
420    /// How each method may report failure, in method order.
421    pub method_errors: Vec<ErrorStrategy>,
422}
423
424impl CallbackMethodBinding {
425    /// The error strategy of this method: [`ErrorStrategy::Throws`] when it
426    /// declares `throws: true`, otherwise [`ErrorStrategy::Trap`] (any
427    /// failure reaches the producer as `-4`).
428    pub fn error_strategy(&self) -> ErrorStrategy {
429        if self.throws {
430            ErrorStrategy::Throws
431        } else {
432            ErrorStrategy::Trap
433        }
434    }
435}
436
437impl CallbackInterfaceBinding {
438    /// Build the full contract for this callback interface.
439    pub fn protocol(&self) -> CallbackProtocol<'_> {
440        CallbackProtocol {
441            binding: self,
442            method_args: self
443                .methods
444                .iter()
445                .map(|m| m.params.iter().map(|p| RetPass::of(Some(&p.ty))).collect())
446                .collect(),
447            method_returns: self
448                .methods
449                .iter()
450                .map(|m| RetPass::of(m.ret.as_ref()))
451                .collect(),
452            method_errors: self
453                .methods
454                .iter()
455                .map(CallbackMethodBinding::error_strategy)
456                .collect(),
457        }
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464    use crate::ir::{Api, Function, Param, TypeRef};
465    use crate::model::Model;
466    use crate::pkg::Identity;
467    use crate::ty::Prim;
468
469    fn param(name: &str, ty: &str) -> Param {
470        Param {
471            name: name.into(),
472            ty: crate::ir::parse_type_ref(ty).unwrap(),
473            doc: None,
474        }
475    }
476
477    /// A model with interface `Store` and callback interface `Listener` in
478    /// `kv`, a record `Contact`, and one function `f` taking `params`.
479    fn model(params: Vec<Param>) -> Model {
480        let yaml = r#"
481version: "0.11.0"
482modules:
483  - name: kv
484    structs: [{ name: Contact, fields: [{ name: id, type: i64 }] }]
485    interfaces: [{ name: Store, methods: [{ name: get }] }]
486    callback_interfaces: [{ name: Listener, methods: [{ name: on }] }]
487"#;
488        let mut api: Api = serde_yaml::from_str(yaml).unwrap();
489        api.modules[0].functions.push(Function {
490            name: "f".into(),
491            params,
492            returns: Some(TypeRef::Prim(Prim::I32)),
493            doc: None,
494            throws: false,
495            r#async: false,
496            cancellable: false,
497            deprecated: None,
498        });
499        Model::assume_valid(&api, Identity::named("weaveffi"))
500    }
501
502    #[test]
503    fn arg_pass_classifies_every_family() {
504        let m = model(vec![
505            param("x", "i32"),
506            param("s", "string"),
507            param("data", "bytes"),
508            param("c", "Contact"),
509            param("o", "i32?"),
510            param("store", "Store"),
511            param("maybe", "Store?"),
512            param("l", "Listener"),
513            param("ml", "Listener?"),
514        ]);
515        let p = &m.modules[0].functions[0].params;
516        assert!(matches!(p[0].arg_pass(), ArgPass::Direct { slot } if slot.name == "x"));
517        assert!(matches!(p[1].arg_pass(), ArgPass::String { ptr, .. } if ptr.name == "s_ptr"));
518        assert!(matches!(
519            p[2].arg_pass(),
520            ArgPass::Bytes { ptr, len } if ptr.name == "data_ptr" && len.name == "data_len"
521        ));
522        assert!(matches!(
523            p[3].arg_pass(),
524            ArgPass::Buffer { ptr, len } if ptr.name == "c_ptr" && len.name == "c_len"
525        ));
526        assert!(matches!(p[4].arg_pass(), ArgPass::Buffer { .. }));
527        assert!(matches!(
528            p[5].arg_pass(),
529            ArgPass::Object {
530                nullable: false,
531                ..
532            }
533        ));
534        assert!(matches!(
535            p[6].arg_pass(),
536            ArgPass::Object { nullable: true, .. }
537        ));
538        assert!(matches!(
539            p[7].arg_pass(),
540            ArgPass::Callback { ctx, vtable, nullable: false }
541                if ctx.name == "l_ctx" && vtable.name == "l_vtable"
542        ));
543        assert!(matches!(
544            p[8].arg_pass(),
545            ArgPass::Callback { nullable: true, .. }
546        ));
547    }
548
549    #[test]
550    fn ret_pass_distinguishes_copy_decode_and_adopt() {
551        assert_eq!(RetPass::of(None), RetPass::Void);
552        assert_eq!(RetPass::of(Some(&Ty::Prim(Prim::I64))), RetPass::Direct);
553        assert_eq!(RetPass::of(Some(&Ty::Prim(Prim::String))), RetPass::String);
554        assert_eq!(RetPass::of(Some(&Ty::Prim(Prim::Bytes))), RetPass::Bytes);
555        for ty in [
556            Ty::Record("Contact".into()),
557            Ty::RichEnum("Shape".into()),
558            Ty::List(Box::new(Ty::Prim(Prim::String))),
559            Ty::List(Box::new(Ty::Interface("Store".into()))),
560            Ty::Optional(Box::new(Ty::Prim(Prim::I64))),
561        ] {
562            assert_eq!(RetPass::of(Some(&ty)), RetPass::Buffer, "{ty}");
563        }
564        assert_eq!(
565            RetPass::of(Some(&Ty::Interface("Store".into()))),
566            RetPass::Object { nullable: false }
567        );
568        assert_eq!(
569            RetPass::of(Some(&Ty::Optional(Box::new(Ty::Interface("Store".into()))))),
570            RetPass::Object { nullable: true }
571        );
572    }
573}