Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

IDL Type Reference

WeaveFFI consumes a declarative IDL (Interface Definition Language) that describes modules, types, and functions. YAML, JSON, and TOML are all supported; this reference uses YAML throughout.

Editor autocomplete (JSON Schema)

WeaveFFI ships a JSON Schema for the IDL. To get autocomplete and validation in editors that support the YAML Language Server (VS Code, Neovim, Helix, …), add the following header comment to the top of your YAML file:

# yaml-language-server: $schema=./weaveffi.schema.json

The schema is generated by weaveffi schema --format json-schema and a copy is checked in at weaveffi.schema.json in the repository root.

Top-level structure

The shape of an IDL document, with placeholder ellipses for nested arrays and objects:

# yaml-language-server: $schema=./weaveffi.schema.json
version: "0.5.0"
package:
  name: my_app
  version: "1.0.0"
modules:
  - name: my_module
    structs: [...]
    enums: [...]
    interfaces: [...]
    functions: [...]
    callbacks: [...]
    listeners: [...]
    errors: { ... }
    modules: [...]
generators:
  swift:
    module_name: MyApp

A complete, validating example lives at the bottom of this page in the Complete example section.

FieldTypeRequiredDescription
versionstringyesSchema version; only the current version ("0.5.0") is accepted
packagePackagenoPublishable identity stamped into every generated manifest (see Package metadata)
modulesarray of ModuleyesOne or more modules
generatorsmap of string to objectnoPer-generator configuration (see generators section)

Module

FieldTypeRequiredDescription
namestringyesLowercase identifier (e.g. calculator)
functionsarray of FunctionnoFree functions exported by this module
interfacesarray of InterfacenoInterface (object) type definitions (see Interfaces)
structsarray of StructnoStruct type definitions
enumsarray of EnumnoEnum type definitions
callbacksarray of CallbacknoCallback type definitions
listenersarray of ListenernoListener (event subscription) definitions
errorsErrorDomainnoOptional error domain (see Error domain)
modulesarray of ModulenoNested sub-modules (see nested modules)

Function

FieldTypeRequiredDescription
namestringyesFunction identifier
paramsarray of ParamyesInput parameters (may be empty [])
returnTypeRefnoReturn type (omit for void functions)
docstringnoDocumentation string
throwsboolnoMark as fallible with a typed domain error (default false); requires an error domain in scope, on the same module or an ancestor (see Error domain)
asyncboolnoMark as asynchronous (default false)
cancellableboolnoAllow cancellation (only meaningful when async: true)
deprecatedstringnoDeprecation message shown to consumers
sincestringnoVersion when this function was introduced

Param

FieldTypeRequiredDescription
namestringyesParameter name
typeTypeRefyesParameter type
mutableboolnoMark as mutable (default false). Indicates the callee may modify the value in-place.
docstringnoDocumentation string (see Documentation comments)

Package metadata

The optional top-level package block is the single source of truth for the publishable identity stamped into every generated ecosystem manifest: package.json (Node/Wasm), pyproject.toml/setup.py (Python), *.gemspec (Ruby), *.csproj/*.nuspec (.NET), pubspec.yaml (Dart), Package.swift (Swift), go.mod (Go), settings.gradle (Android), and CMakeLists.txt (C++). Declaring it once keeps the name, version, and metadata consistent across all eleven targets instead of every generator hardcoding weaveffi / 0.1.0.

Package schema

FieldTypeRequiredDescription
namestringyesDistribution name (npm/PyPI/gem/NuGet/pub/…)
versionstringyesSemantic version stamped into each manifest
descriptionstringnoOne-line package description
licensestringnoSPDX license expression (e.g. MIT, Apache-2.0)
authorsarray of stringnoAuthor entries (Name <email>)
homepagestringnoProject homepage URL
repositorystringnoSource repository URL

Name and version resolution

Each target resolves its package name with the following precedence (first non-empty wins):

  1. an explicit per-target override (e.g. python.package_name, dart.package_name, ruby.gem_name),
  2. package.name,
  3. the IDL file stem (e.g. kvstore.ymlkvstore),
  4. the built-in default weaveffi.

The version resolves from package.version, falling back to 0.1.0. Names are normalized per ecosystem, e.g. a Python import package or Ruby require path lowercases and replaces non-alphanumerics with _ (my-kv.storemy_kv_store), while the published distribution name keeps the original spelling.

Code-level identity that has no manifest of its own still follows the package where it is unambiguous: the Swift module name defaults to the PascalCased package.name (async-demoAsyncDemo). The stable C ABI symbol prefix is not affected: it stays weaveffi (or your global c_prefix) so the generated bindings keep calling the symbols the producer exports.

Package example

version: "0.5.0"
package:
  name: kvstore
  version: "1.0.0"
  description: An embedded key-value store API.
  license: MIT
  authors:
    - WeaveFoundry <hello@weavefoundry.dev>
  homepage: https://github.com/weavefoundry/weaveffi
  repository: https://github.com/weavefoundry/weaveffi
modules:
  - name: kv
    functions:
      - name: count
        params: []
        return: i64

Primitive types

The following primitive types are supported. All primitives are valid in both parameters and return types.

TypeDescriptionExample value
i8Signed 8-bit integer-12
i16Signed 16-bit integer-1000
i32Signed 32-bit integer-42
i64Signed 64-bit integer9000000000
u8Unsigned 8-bit integer200
u16Unsigned 16-bit integer60000
u32Unsigned 32-bit integer300
u64Unsigned 64-bit integer18000000000
f3232-bit floating point1.5
f6464-bit floating point3.14
boolBooleantrue
stringUTF-8 string (owned copy)"hello"
bytesByte buffer (owned copy)binary data
handleOpaque 64-bit identifierresource id
handle<T>Typed handle scoped to type Tresource id
&strBorrowed string (zero-copy, param-only)"hello"
&[u8]Borrowed byte slice (zero-copy, param-only)binary data

Note on JavaScript/Wasm: 64-bit integers (i64, u64) surface as BigInt in the Node and WebAssembly backends; all narrower integers and the floats surface as number.

Primitive examples

version: "0.5.0"
modules:
  - name: primitives
    structs:
      - name: Session
        fields:
          - { name: id, type: i64 }
    functions:
      - name: add
        params:
          - { name: a, type: i32 }
          - { name: b, type: i32 }
        return: i32

      - name: scale
        params:
          - { name: value, type: f64 }
          - { name: factor, type: f64 }
        return: f64

      - name: count
        params:
          - { name: limit, type: u32 }
        return: u32

      - name: timestamp
        params: []
        return: i64

      - name: is_valid
        params:
          - { name: token, type: string }
        return: bool

      - name: echo
        params:
          - { name: message, type: string }
        return: string

      - name: compress
        params:
          - { name: data, type: bytes }
        return: bytes

      - name: open_resource
        params:
          - { name: path, type: string }
        return: handle

      - name: close_resource
        params:
          - { name: id, type: handle }

      - name: open_session
        params:
          - { name: config, type: string }
        return: "handle<Session>"
        doc: "Returns a typed handle scoped to Session"

      - name: write_fast
        params:
          - { name: data, type: "&str" }
        doc: "Borrowed string: no copy at the FFI boundary"

      - name: send_raw
        params:
          - { name: payload, type: "&[u8]" }
        doc: "Borrowed byte slice: no copy at the FFI boundary"

Typed handles

handle<T> is a typed variant of handle that associates the opaque identifier with a named type T. This gives generators type-safety information, for example, generating a distinct wrapper class per handle type. T must be a struct defined in the same module so the generator knows how to spell the handle’s type. At the C ABI level, handle<T> is still a uint64_t.

version: "0.5.0"
modules:
  - name: sessions
    structs:
      - name: Session
        fields:
          - { name: id, type: i64 }
    functions:
      - name: create_session
        params: []
        return: "handle<Session>"

      - name: close_session
        params:
          - { name: session, type: "handle<Session>" }

Borrowed types

&str and &[u8] are zero-copy borrowed variants of string and bytes. They indicate that the callee only reads the data for the duration of the call and does not take ownership. This avoids an allocation and copy at the FFI boundary.

YAML note: Quote borrowed types like "&str" and "&[u8]" because YAML interprets & as an anchor indicator.


Struct definitions

Structs define composite types with named, typed fields. Define structs under the structs key of a module, then reference them by name in function signatures and other type positions.

Struct schema

FieldTypeRequiredDescription
namestringyesStruct name (e.g. Contact)
docstringnoDocumentation string
fieldsarray of FieldyesMust have at least one field
builderboolnoGenerate a builder class (default false)

When builder: true, generators emit a builder class with with_* setter methods and a build() method, enabling incremental construction of complex structs.

Each field:

FieldTypeRequiredDescription
namestringyesField name
typeTypeRefyesField type
docstringnoDocumentation string
defaultvaluenoDefault value for this field

Struct example

version: "0.5.0"
modules:
  - name: geometry
    structs:
      - name: Point
        doc: "A 2D point in space"
        fields:
          - name: x
            type: f64
            doc: "X coordinate"
          - name: "y"
            type: f64
            doc: "Y coordinate"

      - name: Rect
        fields:
          - name: origin
            type: Point
          - name: width
            type: f64
          - name: height
            type: f64

      - name: Config
        builder: true
        fields:
          - name: timeout
            type: i32
            default: 30
          - name: retries
            type: i32
            default: 3
          - name: label
            type: "string?"

    functions:
      - name: distance
        params:
          - { name: a, type: Point }
          - { name: b, type: Point }
        return: f64

      - name: bounding_box
        params:
          - { name: points, type: "[Point]" }
        return: Rect

Struct fields may reference other structs, enums, optionals, lists, or maps. Interface references, borrowed types, and iterators are not valid field types (see Type compatibility).


Enum definitions

Enums define a fixed set of named integer variants. Each variant has an explicit value (i32). Define enums under the enums key.

Enum schema

FieldTypeRequiredDescription
namestringyesEnum name (e.g. Color)
docstringnoDocumentation string
variantsarray of VariantyesMust have at least one variant

Each variant:

FieldTypeRequiredDescription
namestringyesVariant name (e.g. Red)
valuei32yesInteger discriminant
docstringnoDocumentation string
fieldsarray of FieldnoAssociated data: makes the enum a sum type

Enum example

version: "0.5.0"
modules:
  - name: contacts
    enums:
      - name: ContactType
        doc: "Category of contact"
        variants:
          - name: Personal
            value: 0
            doc: "Friends and family"
          - name: Work
            value: 1
            doc: "Professional contacts"
          - name: Other
            value: 2

    functions:
      - name: count_by_type
        params:
          - { name: contact_type, type: ContactType }
        return: i32

Variant values must be unique within an enum, and variant names must be unique within an enum.

Rich (algebraic) enums: sum types

When one or more variants declare fields, the enum becomes a rich enum: an algebraic sum type whose variants carry associated data (like a Rust enum or a Swift enum with associated values). A unit variant (no fields) and a data variant may coexist in the same enum.

version: "0.5.0"
modules:
  - name: shapes
    enums:
      - name: Shape
        doc: "An algebraic shape"
        variants:
          - name: Empty
            value: 0
          - name: Circle
            value: 1
            fields:
              - { name: radius, type: f64 }
          - name: Rectangle
            value: 2
            fields:
              - { name: width, type: f32 }
              - { name: height, type: f32 }
    functions:
      - name: area
        params:
          - { name: shape, type: Shape }
        return: f64

Unlike a plain C-style enum (which crosses the ABI by value as an integer), a rich enum crosses as an opaque object pointer, exactly like a struct. The C ABI gains a tag reader, a per-variant constructor and field getters, and a destructor; each backend wraps these into an idiomatic owned type:

BackendSurface
C*_tag, *_{Variant}_new, *_{Variant}_get_{field}, *_destroy
C++RAII class with nested Tag, static factories, per-variant getters
Python/Rubyclass with a tag, per-variant factory + accessor methods
C#/Goowned class/struct with Tag, per-variant factories + accessors

A variant fields entry uses the same shape as a struct field (name, type, doc), but obeys the positional rules of a return-like slot: no borrowed (&str/&[u8]) types and no iterators. Field names must be unique within a variant.


Interfaces

An interface is a first-class object type: a stateful resource with identity and behavior, declared under the interfaces key of a module. Where a struct models a plain data record with fields, an interface models something you hold and operate on: a store, a session, a connection. The object lives behind the FFI boundary and crosses it as an opaque reference; consumers see a real class (or the target’s closest analogue) whose constructors, methods, and statics call back into the producer.

Interface schema

FieldTypeRequiredDescription
namestringyesInterface type name (e.g. Store)
docstringnoDocumentation string
constructorsarray of ConstructornoStatic functions returning a new instance
methodsarray of FunctionnoInstance methods
staticsarray of FunctionnoStatic functions namespaced under the interface

An interface must declare at least one member, and constructor, method, and static names share one namespace per interface (no duplicates across the three lists).

Each constructor:

FieldTypeRequiredDescription
namestringyesConstructor identifier (e.g. open)
paramsarray of ParamyesInput parameters (may be empty [])
docstringnoDocumentation string
throwsboolnoMark as fallible with a typed domain error (default false)

A constructor implicitly returns a new instance of its interface, so it declares no return field, and it may not be async (expose an async static factory returning the interface instead). A constructor named new becomes the canonical constructor where the target language has one (Swift init, Python __init__); every other constructor becomes a static factory method.

Methods and statics use the full Function schema, including throws, async, cancellable, deprecated, and since. A method receives the instance implicitly; it does not declare a self parameter. A static takes no instance at all.

Lifecycle

Every interface receives an implicit destructor: the C ABI gains a *_destroy symbol, and each generated wrapper releases the underlying object through its language’s natural disposal hook (RAII destructors, deinit, __del__, IDisposable, finalizers, close()). You never declare destroy in the IDL.

Ownership follows the reference direction: an interface passed as a parameter is borrowed for the duration of the call, while an interface returned from a call is a new owned reference the caller (or its wrapper) must eventually release.

Where interface types may appear

An interface name is a valid TypeRef in function and method parameters, in return types, and in optionals of those (Store, Store?). It is not valid as a struct field, a collection element ([Store]), a map key or value, or a callback parameter; the validator rejects those positions.

Interface example

A trimmed version of the kvstore sample’s Store interface:

version: "0.5.0"
modules:
  - name: kv
    errors:
      name: KvError
      codes:
        - { name: KeyNotFound, code: 1001, message: "key not found" }
        - { name: IoError, code: 1004, message: "I/O failure" }
    interfaces:
      - name: Store
        doc: An embedded key-value store owning its entries
        constructors:
          - name: open
            doc: Open (or create) a store backed by the given filesystem path
            params:
              - { name: path, type: string }
            throws: true
        methods:
          - name: get
            doc: Look up a value by key
            params:
              - { name: key, type: string }
            return: bytes
            throws: true
          - name: delete
            doc: Remove the entry for the given key, returning true if it existed
            params:
              - { name: key, type: string }
            return: bool
            throws: true
          - name: count
            doc: Return the number of live entries in the store
            params: []
            return: i64
        statics:
          - name: default_capacity
            doc: The largest number of live entries one store will hold
            params: []
            return: i64

C ABI lowering

An interface lowers to an opaque struct tag plus one C symbol per member. Constructors return an owned pointer; methods take a leading const {tag}* self argument before their declared parameters; statics take no self; and the implicit destructor releases the object:

typedef struct weaveffi_kv_Store weaveffi_kv_Store;

/* Constructor: returns a new owned instance. */
weaveffi_kv_Store* weaveffi_kv_Store_open(const char* path, weaveffi_error* out_err);

/* Methods: an implicit leading self slot. */
bool weaveffi_kv_Store_delete(const weaveffi_kv_Store* self, const char* key,
                              weaveffi_error* out_err);
int64_t weaveffi_kv_Store_count(const weaveffi_kv_Store* self, weaveffi_error* out_err);

/* Statics: no self slot. */
int64_t weaveffi_kv_Store_default_capacity(weaveffi_error* out_err);

/* Implicit destructor: releases the object. */
void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self);

Async and iterator-returning members follow the same shapes as free functions (the async launcher and the iterator handle simply carry the self slot). Free functions and interface members share the module’s C symbol namespace, so a free function named Store_get would collide with a get method on an interface Store; the validator rejects the collision.


Optional types

Append ? to any type to make it optional (nullable). When a value is absent, the default is null.

SyntaxMeaning
string?Optional string
i32?Optional i32
Contact?Optional struct reference
Color?Optional enum reference
Store?Optional interface reference (params and returns only)

Optional example

version: "0.5.0"
modules:
  - name: contacts
    structs:
      - name: Contact
        fields:
          - { name: id, type: i64 }
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: nickname, type: "string?" }
    functions:
      - name: find_contact
        params:
          - { name: id, type: i64 }
        return: "Contact?"
        doc: "Returns null if no contact exists with the given id"

      - name: update_email
        params:
          - { name: id, type: i64 }
          - { name: email, type: "string?" }

YAML note: Quote optional types like "string?" and "Contact?" to prevent the YAML parser from treating ? as special syntax.


List types

Wrap a type in [T] brackets to declare a list (variable-length sequence).

SyntaxMeaning
[i32]List of i32
[string]List of strings
[Contact]List of structs
[Color]List of enums

List example

version: "0.5.0"
modules:
  - name: lists
    structs:
      - name: Contact
        fields:
          - { name: id, type: i64 }
    functions:
      - name: sum
        params:
          - { name: values, type: "[i32]" }
        return: i32

      - name: list_contacts
        params: []
        return: "[Contact]"

      - name: batch_delete
        params:
          - { name: ids, type: "[i64]" }
        return: i32

YAML note: Quote list types like "[i32]" and "[Contact]" because YAML interprets bare [...] as an inline array.


Map types

Wrap a key-value pair in {K:V} braces to declare a map (dictionary / associative array). Keys must be primitive types or enums; structs, lists, and maps are not valid key types. Values may be any valid TypeRef.

SyntaxMeaning
{string:i32}Map from string to i32
{string:Contact}Map from string to struct
{i32:string}Map from i32 to string
{string:[i32]}Map from string to list of i32

Map example

version: "0.5.0"
modules:
  - name: maps
    structs:
      - name: Contact
        fields:
          - { name: id, type: i64 }
          - { name: name, type: string }
          - { name: email, type: "string?" }
    functions:
      - name: update_scores
        params:
          - { name: scores, type: "{string:i32}" }
        return: bool
        doc: "Update player scores by name"

      - name: get_contacts
        params: []
        return: "{string:Contact}"
        doc: "Returns a map of name to Contact"

      - name: merge_tags
        params:
          - { name: current, type: "{string:string}" }
          - { name: additions, type: "{string:string}" }
        return: "{string:string}"

YAML note: Quote map types like "{string:i32}" because YAML interprets bare {...} as an inline mapping.

C ABI convention

Maps are passed across the FFI boundary as parallel arrays of keys and values, plus a shared length. A map parameter {K:V} named m expands to three C parameters:

const K* m_keys, const V* m_values, size_t m_len

A map return value expands to out-parameters:

K* out_keys, V* out_values, size_t* out_len

For example, a function update_scores(scores: {string:i32}) generates:

void weaveffi_mymod_update_scores(
    const char* const* scores_keys,
    const int32_t* scores_values,
    size_t scores_len,
    weaveffi_error* out_err
);

Key type restrictions

Only primitive types (i32, u32, i64, f64, bool, string, bytes, handle) and enum types are valid map keys. The validator rejects structs, lists, and maps as key types.


Nested types

Optional and list modifiers compose freely:

SyntaxMeaning
[Contact?]List of optional contacts (items may be null)
[i32]?Optional list of i32 (the entire list may be null)
[string?]List of optional strings
{string:[i32]}Map from string to list of i32
{string:i32}?Optional map (the entire map may be null)

Nested type example

version: "0.5.0"
modules:
  - name: nested
    structs:
      - name: Contact
        fields:
          - { name: id, type: i64 }
    functions:
      - name: search
        params:
          - { name: query, type: string }
        return: "[Contact?]"
        doc: "Returns a list where some entries may be null (redacted)"

      - name: get_scores
        params:
          - { name: user_id, type: i64 }
        return: "[i32]?"
        doc: "Returns null if user has no scores, otherwise a list"

      - name: bulk_update
        params:
          - { name: emails, type: "[string?]" }
        return: i32

The parser evaluates type syntax outside-in: [Contact?] is parsed as List(Optional(Contact)), while [Contact]? is parsed as Optional(List(Contact)).


Iterator types

Wrap a type in iter<T> to declare a lazy iterator over values of type T. Unlike [T] (which materializes the full list), iterators yield elements one at a time and are suitable for large or streaming result sets.

SyntaxMeaning
iter<i32>Iterator over i32 values
iter<string>Iterator over strings
iter<Contact>Iterator over structs

Iterator example

version: "0.5.0"
modules:
  - name: streaming
    structs:
      - name: Contact
        fields:
          - { name: id, type: i64 }
    functions:
      - name: scan_entries
        params:
          - { name: prefix, type: string }
        return: "iter<Contact>"
        doc: "Lazily iterates over matching contacts"

Laziness is part of the binding contract, not just the C ABI shape. Every generated wrapper surfaces iter<T> as its language’s native lazy iteration idiom: an iter.Seq in Go (iter.Seq2 when the function throws), a Sequence in Swift, an input-iterator range in C++, an iterator in Python, an Enumerator in Ruby, an IEnumerable<T> in C#, an Iterable<T> in Dart, an iterable in JavaScript, and an Iterator<T> with close() in Kotlin. The wrapper issues one producer next call per consumer step; no target drains the sequence into a hidden list. The underlying iterator handle is destroyed exactly once, eagerly on exhaustion or from the wrapper’s disposal idiom when iteration is abandoned early, and errors from the launch and from each next follow the function’s error strategy (see the Error Handling guide). The full pull contract is stated in weaveffi_core::plan::IteratorProtocol; element ownership is covered in the Memory Ownership guide.

Iterators are only valid as return types. The validator rejects iterators in parameter positions.


Callbacks

Callbacks define function signatures that can be passed from the host language into Rust. They enable event-driven patterns where Rust code invokes a caller-provided function.

Callback schema

FieldTypeRequiredDescription
namestringyesCallback name
paramsarray of ParamyesParameters passed to the callback
docstringnoDocumentation string

Callback example

version: "0.5.0"
modules:
  - name: events
    functions: []
    callbacks:
      - name: on_data
        params:
          - { name: payload, type: string }
        doc: "Fired when data arrives"

      - name: on_error
        params:
          - { name: code, type: i32 }
          - { name: message, type: string }

Callback names are not a valid TypeRef. Callbacks are wired up at the module level: declare them under callbacks:, reference them from a listeners: entry via event_callback, and emit asynchronous results from functions marked async: true.


Listeners

Listeners provide a higher-level abstraction over callbacks for event subscription patterns. A listener combines an event callback with subscribe/unsubscribe lifecycle management.

Listener schema

FieldTypeRequiredDescription
namestringyesListener name
event_callbackstringyesName of the callback this listener uses
docstringnoDocumentation string

Listener example

version: "0.5.0"
modules:
  - name: events
    functions: []
    callbacks:
      - name: on_data
        params:
          - { name: payload, type: string }

    listeners:
      - name: data_stream
        event_callback: on_data
        doc: "Subscribe to data events"

The event_callback must reference a callback defined in the same module.


Nested modules

Modules can contain sub-modules, enabling hierarchical organization of large APIs. Nested modules share the same validation rules as top-level modules.

Nested module example

version: "0.5.0"
modules:
  - name: app
    functions:
      - name: init
        params: []

    modules:
      - name: auth
        structs:
          - name: Session
            fields:
              - { name: id, type: i64 }
        functions:
          - name: login
            params:
              - { name: username, type: string }
              - { name: password, type: string }
            return: "handle<Session>"

      - name: data
        structs:
          - name: Record
            fields:
              - { name: id, type: i64 }
              - { name: value, type: string }

        functions:
          - name: get_record
            params:
              - { name: id, type: i64 }
            return: Record

C ABI symbols for nested modules use underscores to join the path: weaveffi_app_auth_login, weaveffi_app_data_get_record.

Cross-module type references

Struct, enum, interface, and error domain names are unique across the whole API (the validator rejects two types sharing a bare name, wherever they are declared). A bare type name therefore resolves unambiguously from any module: reference the type by name and the toolchain qualifies the reference to its owning module internally.

For example, a nested stats module can take the parent kv module’s Store interface as a parameter:

version: "0.5.0"
modules:
  - name: kv
    errors:
      name: KvError
      codes:
        - { name: IoError, code: 1004, message: "I/O failure" }
    interfaces:
      - name: Store
        constructors:
          - name: open
            params:
              - { name: path, type: string }
            throws: true
        methods:
          - name: count
            params: []
            return: i64
    modules:
      - name: stats
        structs:
          - name: Stats
            fields:
              - { name: total_entries, type: i64 }
        functions:
          - name: get_stats
            doc: Snapshot the current store statistics
            params:
              - { name: store, type: Store }
            return: Stats
            throws: true

The generated bindings spell a cross-module reference with the owning module’s qualification where the target needs one.


Async and lifecycle annotations

Async functions

Functions can be marked as asynchronous. See the Async Functions guide for detailed per-target behaviour.

version: "0.5.0"
modules:
  - name: net
    errors:
      name: NetError
      codes:
        - { name: Unreachable, code: 1, message: "host unreachable" }
    functions:
      - name: fetch_data
        params:
          - { name: url, type: string }
        return: string
        async: true
        throws: true

      - name: upload_file
        params:
          - { name: path, type: string }
        return: bool
        async: true
        cancellable: true

async composes with throws: an async function that also declares throws: true delivers its failure as the module’s typed domain error through the target’s async idiom (an async throws function in Swift, a rejected promise carrying the typed error in JavaScript, and so on). An async function without throws gets a plain async shape, and a failure can only be a producer bug.

Async void functions (no return type) emit a validator warning since they are unusual. An async function cannot return an iterator (iter<T>); return a list instead or make the function synchronous.

Deprecated functions

Mark a function as deprecated with a migration message:

version: "0.5.0"
modules:
  - name: legacy
    functions:
      - name: add_old
        params:
          - { name: a, type: i32 }
          - { name: b, type: i32 }
        return: i32
        deprecated: "Use add_v2 instead"
        since: "0.1.0"

Generators propagate the deprecation message to the target language (e.g. @available(*, deprecated) in Swift, @Deprecated in Kotlin, warn in Ruby).

Mutable parameters

Mark a parameter as mutable when the callee may modify it in-place:

version: "0.5.0"
modules:
  - name: buffers
    functions:
      - name: fill_buffer
        params:
          - { name: buf, type: bytes, mutable: true }

This affects the C ABI signature (non-const pointer) and may influence generated wrapper code in target languages.


Generators section

The top-level generators key provides per-generator configuration directly in the IDL file. This is an alternative to using a separate TOML configuration file with --config.

version: "0.5.0"
modules:
  - name: math
    functions:
      - name: add
        params:
          - { name: a, type: i32 }
          - { name: b, type: i32 }
        return: i32

generators:
  swift:
    module_name: MyMathLib
  android:
    package: com.example.math
  ruby:
    module_name: MathBindings
    gem_name: math_bindings
  go:
    module_path: github.com/myorg/mathlib

Each key under generators is the target name (matching the --target flag). The value is a target-specific configuration object. See the Generator Configuration guide for the full list of options.


Type compatibility

All types are valid in both parameter and return positions unless noted.

TypeParamsReturnsStruct fieldsNotes
i8yesyesyes
i16yesyesyes
i32yesyesyes
i64yesyesyes
u8yesyesyes
u16yesyesyes
u32yesyesyes
u64yesyesyesBigInt in JS/Wasm
f32yesyesyes
f64yesyesyes
boolyesyesyes
stringyesyesyes
bytesyesyesyes
handleyesyesyes
handle<T>yesyesyesTyped handle
&stryesyesyesBorrowed, zero-copy
&[u8]yesyesyesBorrowed, zero-copy
StructNameyesyesyes
EnumNameyesyesyes
InterfaceNameyesyesnoAlso InterfaceName?; not in collections
T?yesyesyes
[T]yesyesyes
[T?]yesyesyes
[T]?yesyesyes
{K:V}yesyesyes
{K:V}?yesyesyes
iter<T>noyesnoReturn-only

Complete example

A full IDL combining structs, enums, optionals, lists, an interface, and a typed error domain (a trimmed version of the contacts sample):

version: "0.5.0"
modules:
  - name: contacts
    enums:
      - name: ContactType
        variants:
          - { name: Personal, value: 0 }
          - { name: Work, value: 1 }
          - { name: Other, value: 2 }

    structs:
      - name: Contact
        doc: "A contact record"
        fields:
          - { name: id, type: i64 }
          - { name: first_name, type: string }
          - { name: last_name, type: string }
          - { name: email, type: "string?" }
          - { name: contact_type, type: ContactType }

    errors:
      name: ContactsError
      codes:
        - { name: InvalidName, code: 1, message: "name must not be empty" }
        - { name: NotFound, code: 2, message: "contact not found" }

    interfaces:
      - name: ContactBook
        doc: "An in-memory address book owning its contacts"
        constructors:
          - name: new
            params: []
        methods:
          - name: add
            doc: "Add a contact, returning the stored record with its assigned id"
            params:
              - { name: first_name, type: string }
              - { name: last_name, type: string }
              - { name: email, type: "string?" }
              - { name: contact_type, type: ContactType }
            return: Contact
            throws: true

          - name: get
            doc: "Look up a contact by id"
            params:
              - { name: id, type: i64 }
            return: Contact
            throws: true

          - name: list
            params: []
            return: "[Contact]"

          - name: remove
            params:
              - { name: id, type: i64 }
            return: bool

          - name: count
            params: []
            return: i32

Validation rules

  • Module, function, parameter, struct, enum, interface, field, and variant names must be valid identifiers (start with a letter or _, contain only alphanumeric characters and _).
  • Names must be unique within their scope (no duplicate module names, no duplicate function names within a module, etc.).
  • Struct, enum, interface, and error domain names must be unique across the whole API, not just their module: generators emit flat per-language type names, and bare-name references resolve API-wide.
  • Reserved keywords are rejected: if, else, for, while, loop, match, type, return, async, await, break, continue, fn, struct, enum, mod, use.
  • Structs must have at least one field. Enums must have at least one variant. Interfaces must have at least one constructor, method, or static.
  • Enum variant values must be unique within their enum.
  • Type references to structs, enums, and interfaces must resolve to a definition somewhere in the API (see Cross-module type references).
  • Interface members (constructors, methods, and statics) share one namespace per interface; a constructor may not declare a return type and may not be async.
  • Interface types are valid only as parameters, return types, and optionals of those; not as struct fields, collection elements, map keys or values, or callback parameters.
  • Free functions and interface members share the module’s C symbol namespace; two declarations lowering to the same symbol are rejected.
  • throws: true requires an error domain in scope: on the same module or an ancestor module.
  • Error codes must be non-zero (and not -2, which is reserved for producer panics) and unique within their domain. Error code names must be unique within their domain and across every domain in the API.
  • Error domain names must not collide with function names in the same module.
  • Async functions are allowed. Async void functions (no return type) emit a warning. An async function cannot return an iterator.
  • Listener event_callback must reference a callback in the same module.

ABI mapping

  • Parameters map to C ABI types; string and bytes are passed as pointer + length.
  • Return values are direct scalars except:
    • string: returns const char* allocated by Rust; caller must free via weaveffi_free_string.
    • bytes: returns const uint8_t* and requires an extra size_t* out_len param; caller frees with weaveffi_free_bytes.
  • Each function takes a trailing weaveffi_error* out_err for error reporting.

Error domain

A module can declare one error domain: a named set of symbolic error codes its fallible functions report. The domain generates a typed error construct in every target language (an error enum in Swift, an exception class hierarchy in Python, sealed exception subclasses in Kotlin, and so on), so consumers catch and match on the codes you declared rather than on raw integers.

Error domain schema

FieldTypeRequiredDescription
namestringyesDomain name, used to name the generated error type (e.g. KvError)
codesarray of CodeyesThe named codes belonging to this domain

Each code:

FieldTypeRequiredDescription
namestringyesCode name, lowered to a case or subclass on the generated error type (e.g. KeyNotFound)
codei32yesStable numeric value carried across the C ABI
messagestringyesDefault human-readable message
docstringnoDocumentation string

PascalCase code names (KeyNotFound, StoreFull) are the convention: each generator re-cases them into its own idiom.

Opting in with throws

Declaring a domain reserves the codes; a function, method, or constructor joins the typed error path by declaring throws: true:

version: "0.5.0"
modules:
  - name: contacts
    errors:
      name: ContactsError
      codes:
        - { name: InvalidName, code: 1, message: "name must not be empty" }
        - { name: NotFound, code: 2, message: "contact not found" }
    functions:
      - name: get_contact
        params:
          - { name: id, type: i64 }
        return: string
        throws: true

      - name: count_contacts
        params: []
        return: i32

A throwing callable surfaces in each target’s error idiom (throws in Swift, raise in Python, (T, error) in Go, exceptions elsewhere), and the error it delivers is the domain type: the numeric code maps to the matching declared case. A callable without throws (like count_contacts above) has a plain signature at the idiomatic surface; it cannot report a domain error, and a failure there can only be a producer bug, which surfaces as a generic branded error or a trap rather than a typed error. See the Error Handling guide for the full model, including panic behavior.

The domain is in scope for its module and every module nested inside it, so a domain declared on a parent module serves the whole subtree. Declaring throws: true with no domain in scope is a validation error.

Validation

  • Codes must be non-zero (0 means success) and must not be -2 (reserved for producer panics).
  • Numeric codes must be unique within a domain.
  • Code names must be unique within a domain and across every domain in the API: backends with flat namespaces derive one error class or constant per code, so two domains both declaring NotFound would collide. Qualify one of them (e.g. OrderNotFound).
  • The domain name must not collide with a function name in the same module, and it shares the API-wide type namespace with struct, enum, and interface names.

Documentation comments

Every IR element accepts an optional doc: field. WeaveFFI propagates that text into the generated bindings using each language’s native doc-comment syntax. Multi-line strings (use YAML’s | block form) are preserved across the boundary; single-line strings collapse to a one-liner where the target syntax allows.

Supported sites:

  • Function.doc, Param.doc
  • InterfaceDef.doc, plus each of its constructors, methods, and statics (interface members use the function schema, so they carry Function.doc and Param.doc)
  • StructDef.doc, StructField.doc
  • EnumDef.doc, EnumVariant.doc
  • CallbackDef.doc, ListenerDef.doc
  • ErrorCode.doc

Per-target syntax:

TargetComment syntaxParam docs
C / C++/** ... */ directly above the declarationnot emitted
Swift/// ... per line/// - Parameter name: ...
Kotlin / Android/** ... */ KDoc block@param name ... inside the KDoc block
TypeScript (Node)/** ... */ JSDoc@param name ...
TypeScript (Wasm)/** ... */ JSDoc@param name ...
Python"""...""" first statement; # ... above C ABI bindsNumPy-style Parameters section in the wrapper docstring
.NET (C#)/// <summary>...</summary> XML doc/// <param name="name">...</param>
Dart/// ...not emitted
Go// SymbolName ... per Go’s conventiontrailing // Parameters: block
Ruby# ... lines above the def# @param name [Object] ...

Example IDL:

version: "0.5.0"
modules:
  - name: docs
    structs:
      - name: Document
        doc: |
          Represents a single document tracked by the system.

          Documents are persisted to disk and exposed via the public API.
        fields:
          - { name: id, type: i64, doc: Stable opaque identifier }
          - { name: title, type: string, doc: Human-readable title }
    functions:
      - name: create_document
        doc: Create a brand new document
        params:
          - { name: title, type: string, doc: Human-readable title }
        return: Document

All eleven generators emit emit_doc calls before every documented declaration; absent or empty doc: fields produce no extra output, so the feature is fully opt-in.