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.
| Field | Type | Required | Description |
|---|---|---|---|
version | string | yes | Schema version; only the current version ("0.5.0") is accepted |
package | Package | no | Publishable identity stamped into every generated manifest (see Package metadata) |
modules | array of Module | yes | One or more modules |
generators | map of string to object | no | Per-generator configuration (see generators section) |
Module
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Lowercase identifier (e.g. calculator) |
functions | array of Function | no | Free functions exported by this module |
interfaces | array of Interface | no | Interface (object) type definitions (see Interfaces) |
structs | array of Struct | no | Struct type definitions |
enums | array of Enum | no | Enum type definitions |
callbacks | array of Callback | no | Callback type definitions |
listeners | array of Listener | no | Listener (event subscription) definitions |
errors | ErrorDomain | no | Optional error domain (see Error domain) |
modules | array of Module | no | Nested sub-modules (see nested modules) |
Function
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Function identifier |
params | array of Param | yes | Input parameters (may be empty []) |
return | TypeRef | no | Return type (omit for void functions) |
doc | string | no | Documentation string |
throws | bool | no | Mark 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) |
async | bool | no | Mark as asynchronous (default false) |
cancellable | bool | no | Allow cancellation (only meaningful when async: true) |
deprecated | string | no | Deprecation message shown to consumers |
since | string | no | Version when this function was introduced |
Param
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Parameter name |
type | TypeRef | yes | Parameter type |
mutable | bool | no | Mark as mutable (default false). Indicates the callee may modify the value in-place. |
doc | string | no | Documentation 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Distribution name (npm/PyPI/gem/NuGet/pub/…) |
version | string | yes | Semantic version stamped into each manifest |
description | string | no | One-line package description |
license | string | no | SPDX license expression (e.g. MIT, Apache-2.0) |
authors | array of string | no | Author entries (Name <email>) |
homepage | string | no | Project homepage URL |
repository | string | no | Source repository URL |
Name and version resolution
Each target resolves its package name with the following precedence (first non-empty wins):
- an explicit per-target override (e.g.
python.package_name,dart.package_name,ruby.gem_name), package.name,- the IDL file stem (e.g.
kvstore.yml→kvstore), - 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.store →
my_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-demo→AsyncDemo). The stable C ABI symbol prefix is not affected: it staysweaveffi(or your globalc_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.
| Type | Description | Example value |
|---|---|---|
i8 | Signed 8-bit integer | -12 |
i16 | Signed 16-bit integer | -1000 |
i32 | Signed 32-bit integer | -42 |
i64 | Signed 64-bit integer | 9000000000 |
u8 | Unsigned 8-bit integer | 200 |
u16 | Unsigned 16-bit integer | 60000 |
u32 | Unsigned 32-bit integer | 300 |
u64 | Unsigned 64-bit integer | 18000000000 |
f32 | 32-bit floating point | 1.5 |
f64 | 64-bit floating point | 3.14 |
bool | Boolean | true |
string | UTF-8 string (owned copy) | "hello" |
bytes | Byte buffer (owned copy) | binary data |
handle | Opaque 64-bit identifier | resource id |
handle<T> | Typed handle scoped to type T | resource id |
&str | Borrowed 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 asBigIntin the Node and WebAssembly backends; all narrower integers and the floats surface asnumber.
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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Struct name (e.g. Contact) |
doc | string | no | Documentation string |
fields | array of Field | yes | Must have at least one field |
builder | bool | no | Generate 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:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Field name |
type | TypeRef | yes | Field type |
doc | string | no | Documentation string |
default | value | no | Default 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Enum name (e.g. Color) |
doc | string | no | Documentation string |
variants | array of Variant | yes | Must have at least one variant |
Each variant:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Variant name (e.g. Red) |
value | i32 | yes | Integer discriminant |
doc | string | no | Documentation string |
fields | array of Field | no | Associated 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:
| Backend | Surface |
|---|---|
| C | *_tag, *_{Variant}_new, *_{Variant}_get_{field}, *_destroy |
| C++ | RAII class with nested Tag, static factories, per-variant getters |
| Python/Ruby | class with a tag, per-variant factory + accessor methods |
| C#/Go | owned 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Interface type name (e.g. Store) |
doc | string | no | Documentation string |
constructors | array of Constructor | no | Static functions returning a new instance |
methods | array of Function | no | Instance methods |
statics | array of Function | no | Static 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:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Constructor identifier (e.g. open) |
params | array of Param | yes | Input parameters (may be empty []) |
doc | string | no | Documentation string |
throws | bool | no | Mark 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.
| Syntax | Meaning |
|---|---|
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).
| Syntax | Meaning |
|---|---|
[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.
| Syntax | Meaning |
|---|---|
{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:
| Syntax | Meaning |
|---|---|
[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.
| Syntax | Meaning |
|---|---|
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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Callback name |
params | array of Param | yes | Parameters passed to the callback |
doc | string | no | Documentation 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Listener name |
event_callback | string | yes | Name of the callback this listener uses |
doc | string | no | Documentation 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.
| Type | Params | Returns | Struct fields | Notes |
|---|---|---|---|---|
i8 | yes | yes | yes | |
i16 | yes | yes | yes | |
i32 | yes | yes | yes | |
i64 | yes | yes | yes | |
u8 | yes | yes | yes | |
u16 | yes | yes | yes | |
u32 | yes | yes | yes | |
u64 | yes | yes | yes | BigInt in JS/Wasm |
f32 | yes | yes | yes | |
f64 | yes | yes | yes | |
bool | yes | yes | yes | |
string | yes | yes | yes | |
bytes | yes | yes | yes | |
handle | yes | yes | yes | |
handle<T> | yes | yes | yes | Typed handle |
&str | yes | yes | yes | Borrowed, zero-copy |
&[u8] | yes | yes | yes | Borrowed, zero-copy |
StructName | yes | yes | yes | |
EnumName | yes | yes | yes | |
InterfaceName | yes | yes | no | Also InterfaceName?; not in collections |
T? | yes | yes | yes | |
[T] | yes | yes | yes | |
[T?] | yes | yes | yes | |
[T]? | yes | yes | yes | |
{K:V} | yes | yes | yes | |
{K:V}? | yes | yes | yes | |
iter<T> | no | yes | no | Return-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
returntype and may not beasync. - 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: truerequires 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_callbackmust reference a callback in the same module.
ABI mapping
- Parameters map to C ABI types;
stringandbytesare passed as pointer + length. - Return values are direct scalars except:
string: returnsconst char*allocated by Rust; caller must free viaweaveffi_free_string.bytes: returnsconst uint8_t*and requires an extrasize_t* out_lenparam; caller frees withweaveffi_free_bytes.
- Each function takes a trailing
weaveffi_error* out_errfor 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Domain name, used to name the generated error type (e.g. KvError) |
codes | array of Code | yes | The named codes belonging to this domain |
Each code:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Code name, lowered to a case or subclass on the generated error type (e.g. KeyNotFound) |
code | i32 | yes | Stable numeric value carried across the C ABI |
message | string | yes | Default human-readable message |
doc | string | no | Documentation 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 (
0means 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
NotFoundwould 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.docInterfaceDef.doc, plus each of its constructors, methods, and statics (interface members use the function schema, so they carryFunction.docandParam.doc)StructDef.doc,StructField.docEnumDef.doc,EnumVariant.docCallbackDef.doc,ListenerDef.docErrorCode.doc
Per-target syntax:
| Target | Comment syntax | Param docs |
|---|---|---|
| C / C++ | /** ... */ directly above the declaration | not 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 binds | NumPy-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 convention | trailing // 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.