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 Schema

An IDL document describes an API: its modules, types, and functions. YAML, JSON, and TOML are all accepted; this page uses YAML. It documents schema version 0.11.0, the only version the current tools accept. A Rust producer never writes one by hand (the macro embeds the API in the built library, and weaveffi extract prints it as an IDL), but the schema is the same either way.

Every YAML block on this page is a complete document that passes weaveffi validate, except blocks whose first line is # rejected:, which show what the validator refuses and the error it reports.

Editor support

The CLI prints a JSON Schema for the document with weaveffi schema --format json-schema, and a copy is checked in as weaveffi.schema.json. Editors using the YAML Language Server pick it up from a header comment:

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

Document structure

version: "0.11.0"
modules:
  - name: my_module
    doc: "..."
    structs: [...]
    enums: [...]
    interfaces: [...]
    callback_interfaces: [...]
    functions: [...]
    errors: { ... }
    modules: [...]

The document describes only the API. Package identity and generator options live in weaveffi.toml. Every object rejects unknown keys, so a misspelled field fails to parse instead of being ignored.

FieldTypeRequiredMeaning
versionstringyesMust be "0.11.0"
modulesarray of ModuleyesTop-level modules

Module

FieldTypeMeaning
namestring (required)Namespace segment and C symbol component (kv)
docstringModule documentation
functionsarray of FunctionFree functions
structsarray of StructRecords (value types)
enumsarray of EnumC-style or rich enums
interfacesarray of InterfaceReference-counted object types
callback_interfacesarray of CallbackInterfaceMethod sets the consumer implements
errorsErrorDomainThe module’s error domain
modulesarray of ModuleNested submodules

Function

The same shape describes free functions, interface constructors, methods, and statics, and callback-interface methods, with the restrictions noted in their sections.

FieldTypeMeaning
namestring (required)Function name
paramsarray of ParamOrdered parameters (default none)
returnTypeRefReturn type; omit for none
docstringDocumentation
throwsboolReport typed domain errors (default false)
asyncboolComplete asynchronously (default false)
cancellableboolAccept a cancel token; requires async: true
deprecatedstringDeprecation message propagated to every binding

A Param has a required name and type and an optional doc.

Types

A TypeRef is a string:

SyntaxMeaning
i8 i16 i32 i64 u8 u16 u32 u64Fixed-width integers
f32 f64IEEE 754 floats
boolBoolean
stringUTF-8 text
bytesByte string ([u8] is accepted and becomes bytes)
NameA struct, enum, interface, or callback interface, by its bare name
T?Optional
[T]List
{K:V}Map
iter<T>Lazy iterator (returns only)

A user type is always written as its bare name: type names are global (see Modules and names), so there’s no module-qualified spelling, and a dotted name such as kv.Store is rejected with QualifiedTypeRef. usize, isize, u128, i128, and char aren’t primitives; a reference to one is reported as UnsupportedPrimitive (use u64 or i64 for sizes and wide integers, and string for characters).

The parser reads outside in: [Contact?] is a list of optional contacts and [Contact]? an optional list. Composites nest to any depth. Quote anything YAML would otherwise interpret ("string?", "[i32]", "{string:i32}"). The Node and Wasm targets surface i64 and u64 as BigInt.

Map keys must be an integer, bool, string, or a C-style enum, so every target can use its native dictionary with exact equality. Floats, bytes, composites, and objects are rejected:

# rejected: InvalidMapKey
version: "0.11.0"
modules:
  - name: maps
    functions:
      - name: by_weight
        params:
          - { name: index, type: "{f64:i32}" }

Iterators are lazy: every binding pulls one element per step of its native iteration idiom and destroys the handle exactly once. iter<T> is valid only as the outermost return of a synchronous callable. It’s rejected in a parameter, a field, or nested in another type (IteratorInInvalidPosition), and as the return of an async function (AsyncIteratorReturn):

# rejected: IteratorInInvalidPosition
version: "0.11.0"
modules:
  - name: streaming
    functions:
      - name: consume
        params:
          - { name: items, type: "iter<i32>" }

Type positions

“Buffered field” means a struct field, rich-enum variant field, or error payload field.

TypeParamReturnBuffered fieldInside T? [T] {K:V}Map keyCallback paramCallback return
integers, floats, boolyesyesyesyesintegers, boolyesyes
stringyesyesyesyesyesyesyes
bytesyesyesyesyesnoyesyes
C-style enumyesyesyesyesyesyesyes
struct, rich enumyesyesyesyesnoyesyes
interfaceyesyesyesyesnoyesyes
callback interfaceyesnonoCb? as a top-level parameter onlynonono
T?, [T], {K:V}yesyesyesyesnoyesyes
iter<T>noyes (sync)nonononono

How each family crosses the boundary is in the C ABI contract.

Structs

A struct is a record: a value type with named fields, crossing the ABI by value as a value buffer. Bindings map it to a data class, struct, or record type; no per-struct C functions exist.

FieldTypeMeaning
namestring (required)Type name
docstringDocumentation
deprecatedstringDeprecation message
fieldsarray of Field (required, non-empty)Each with name, type, optional doc
version: "0.11.0"
modules:
  - name: geometry
    structs:
      - name: Point
        doc: A point in the plane.
        fields:
          - { name: x, type: f64 }
          - { name: "y", type: f64 }
      - name: Polygon
        fields:
          - { name: points, type: "[Point]" }
          - { name: label, type: "string?" }
    functions:
      - name: area
        params:
          - { name: shape, type: Polygon }
        return: f64

Enums

An enum is a fixed set of variants, each with an explicit i32 value.

FieldTypeMeaning
namestring (required)Type name
doc, deprecatedstringDocumentation, deprecation message
variantsarray of Variant (required, non-empty)Each with name, value, optional doc and fields

When no variant has fields, the enum is C-style: it crosses as an int32_t and may be a map key. When any variant has fields, the enum is rich (a sum type): it crosses as a value buffer holding the variant’s value as a tag followed by its fields, and bindings map it to a Swift enum with associated values, a sealed class hierarchy, a tagged union, and so on. Unit and data variants may mix.

version: "0.11.0"
modules:
  - name: shapes
    enums:
      - name: Unit
        variants:
          - { name: Metric, value: 0 }
          - { name: Imperial, value: 1 }
      - name: Shape
        variants:
          - { name: Empty, value: 0 }
          - name: Circle
            value: 1
            fields:
              - { name: radius, type: f64 }
          - name: Rect
            value: 2
            fields:
              - { name: width, type: f32 }
              - { name: height, type: f32 }
    functions:
      - name: area
        params:
          - { name: shape, type: Shape }
          - { name: unit, type: Unit }
        return: f64

Variant names and values are unique within an enum, and a variant’s field names are unique within the variant.

Interfaces

An interface is an object type: state with identity and behavior that lives in the producer and crosses the ABI as an opaque, reference-counted pointer. Consumers see a class whose constructors, methods, and statics call into the producer.

FieldTypeMeaning
namestring (required)Type name
doc, deprecatedstringDocumentation, deprecation message
constructorsarray of FunctionReturn a new instance; no return, never async
methodsarray of FunctionTake the instance implicitly
staticsarray of FunctionNo instance

An interface needs at least one member, and constructor, method, and static names share one namespace. A constructor named new becomes the canonical constructor where the target has one (Swift init, Python __init__); others become static factories. For an async factory, declare an async static that returns the interface.

An interface may appear in every position except a map key: parameters, returns, optionals, lists, map values, record and variant fields, error payloads, iterator elements, async results, and callback parameters and returns. A map keyed by an interface reports both InvalidMapKey and InterfaceInInvalidPosition. Store? at the top level of a parameter or return is a nullable pointer. Every position transfers or borrows references by the rules in Errors and Memory.

version: "0.11.0"
modules:
  - name: kv
    errors:
      name: KvError
      codes:
        - { name: KeyNotFound, code: 1001, message: "key not found" }
    structs:
      - name: StoreInfo
        fields:
          - { name: label, type: string }
          - { name: store, type: Store }
    interfaces:
      - name: Store
        constructors:
          - name: open
            params:
              - { name: path, type: string }
            throws: true
        methods:
          - name: get
            params:
              - { name: key, type: string }
            return: bytes
            throws: true
          - name: share
            return: Store
          - name: larger
            params:
              - { name: other, type: "Store?" }
            return: "Store?"
          - name: describe
            params:
              - { name: label, type: string }
            return: StoreInfo
        statics:
          - name: open_many
            params:
              - { name: paths, type: "[string]" }
            return: "[Store]"
            throws: true
# rejected: InvalidMapKey, InterfaceInInvalidPosition
version: "0.11.0"
modules:
  - name: kv
    interfaces:
      - name: Store
        methods:
          - { name: count, return: i64 }
    functions:
      - name: index
        params:
          - { name: stores, type: "{Store:i32}" }

An interface lowers to an opaque type, one symbol per member (methods take a leading self), and implicit _clone and _destroy symbols you never declare. From the kitchen_sink test fixture (prefix kitchen_sink, module kitchen):

typedef struct kitchen_sink_kitchen_Gadget kitchen_sink_kitchen_Gadget;
kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_new(int64_t id, kitchen_sink_error* out_err);
const uint8_t* kitchen_sink_kitchen_Gadget_describe(const kitchen_sink_kitchen_Gadget* self, size_t* out_len, kitchen_sink_error* out_err);
kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_clone(const kitchen_sink_kitchen_Gadget* self);
void kitchen_sink_kitchen_Gadget_destroy(kitchen_sink_kitchen_Gadget* self);

Callback interfaces

A callback interface is the inverse of an interface: methods the consumer implements and the producer calls, any number of times and from any thread, for as long as it holds the implementation.

FieldTypeMeaning
namestring (required)Type name
doc, deprecatedstringDocumentation, deprecation message
methodsarray of Function (required, non-empty)In vtable order

Methods are restricted:

  • synchronous: no async, no cancellable (InvalidCallbackMethod);
  • return nothing or any type except an iterator (InvalidCallbackMethod) or a callback interface (CallbackInterfaceInInvalidPosition); a string, bytes, or buffered return is a run the consumer allocates with {prefix}_alloc and the producer adopts, and an object return transfers one reference to the producer;
  • parameters may be any type except an iterator (IteratorInInvalidPosition) or a callback interface (CallbackInterfaceInInvalidPosition); an object parameter transfers one reference to the consumer.

A method may set throws: true when an error domain is in scope for its module (otherwise it’s ThrowsWithoutErrorDomain). Its consumer may then fail with one of the domain’s codes, with the code’s fields (which can’t include objects), and the producer receives the typed error. Any other consumer failure, and every failure of a method without throws, reaches the producer as the runtime code -4.

A callback interface is valid only as a top-level parameter of a function, constructor, method, or static, passed bare (Listener) or optional (Listener?, where none is a null vtable). Anything else is CallbackInterfaceInInvalidPosition:

# rejected: CallbackInterfaceInInvalidPosition
version: "0.11.0"
modules:
  - name: events
    callback_interfaces:
      - name: Listener
        methods:
          - { name: on_event, params: [{ name: id, type: i64 }] }
    functions:
      - name: current_listener
        return: Listener
version: "0.11.0"
modules:
  - name: events
    enums:
      - name: Delivery
        variants:
          - { name: Accept, value: 0 }
          - { name: Skip, value: 1 }
    structs:
      - name: Message
        fields:
          - { name: topic, type: string }
          - { name: text, type: string }
    callback_interfaces:
      - name: Subscriber
        methods:
          - name: route
            params:
              - { name: topic, type: string }
            return: Delivery
          - name: on_message
            params:
              - { name: message, type: Message }
          - name: name
            return: string
    interfaces:
      - name: EventBus
        constructors:
          - name: new
        methods:
          - name: subscribe
            params:
              - { name: subscriber, type: Subscriber }
            return: i64

A callback interface lowers to a vtable type that starts with a header (the vtable’s size, reserved flags, and the free release hook) followed by one entry per method in declaration order; a parameter lowers to a ctx pointer plus the vtable. From the kitchen_sink fixture, whose label returns a string and throws, latest returns an Item?, and favorite returns a Gadget:

typedef struct kitchen_sink_kitchen_ReadyListener_vtable {
    uint32_t size;
    uint32_t flags;
    void (*free)(void* ctx);
    void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err);
    bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err);
    void (*label)(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err);
    void (*latest)(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err);
    kitchen_sink_kitchen_Gadget* (*favorite)(void* ctx, kitchen_sink_error* out_err);
} kitchen_sink_kitchen_ReadyListener_vtable;

int32_t kitchen_sink_kitchen_subscribe(void* listener_ctx, const kitchen_sink_kitchen_ReadyListener_vtable* listener_vtable, kitchen_sink_error* out_err);
bool kitchen_sink_kitchen_maybe_subscribe(void* listener_ctx, const kitchen_sink_kitchen_ReadyListener_vtable* listener_vtable, kitchen_sink_error* out_err);

Error domains

A module may declare one error domain: named, stable codes its throwing callables report. Each binding generates a typed error (an exception hierarchy, a Swift Error enum, a Go error type) so consumers match on the names you declared.

FieldTypeMeaning
namestring (required)Domain type name (KvError)
codesarray of Code (required)The codes

Each code has a required name (PascalCase by convention), positive code, and default message, plus an optional doc and optional fields: a structured payload with the same shape as struct fields, delivered as properties of the raised error.

version: "0.11.0"
modules:
  - name: kv
    errors:
      name: KvError
      codes:
        - name: KeyNotFound
          code: 1001
          message: key not found
          fields:
            - { name: key, type: string }
        - { name: StoreFull, code: 1003, message: store has reached capacity }
    functions:
      - name: lookup
        params:
          - { name: key, type: string }
        return: bytes
        throws: true
      - name: count
        return: i64

Declaring a domain reserves the codes; a function, constructor, method, or static opts in with throws: true. A callable without throws (count above) has a plain signature and can’t report a domain error; a failure there is a producer bug and traps (see Errors and Memory). A domain is in scope for its module and every module nested in it, and throws: true with no domain in scope is rejected:

# rejected: ThrowsWithoutErrorDomain
version: "0.11.0"
modules:
  - name: contacts
    functions:
      - name: get_contact
        params:
          - { name: id, type: i64 }
        return: string
        throws: true

Codes must be positive (0 is success and negative codes belong to the runtime) and unique within their domain. Code names must be unique across every domain in the API, because several targets flatten them into one namespace; qualify one (OrderNotFound) when two domains need the same idea.

Async, cancellable, and deprecated

async: true makes a function, method, or static complete through a callback, surfaced as each language’s async idiom; cancellable: true adds a cancel token. Both are covered in Async and Cancellation. Constructors can’t be async, and cancellable requires async:

# rejected: CancellableNotAsync
version: "0.11.0"
modules:
  - name: net
    functions:
      - { name: ping, return: bool, cancellable: true }

deprecated: on a function, method, static, struct, enum, interface, or callback interface propagates its message to every binding’s native deprecation marker.

Modules and names

Modules nest to any depth. A nested module shares the validation rules of a top-level one, and an error domain declared on a parent serves the subtree.

Names are global. Modules group declarations and namespace their C symbols, but they don’t scope names:

  • Struct, enum, interface, callback interface, and error domain names are unique across the whole API (DuplicateTypeName).
  • Free-function names are unique across the whole API (DuplicateFunctionName), because several targets flatten every module’s functions into one namespace. A free function also can’t share its name with an error domain (NameCollisionWithErrorDomain).
  • Error code names are unique across every domain (DuplicateErrorCodeName).

Members are scoped to their owner: parameters to their callable, fields to their record or variant, variants to their enum, and constructors, methods, and statics to their interface. Sibling modules need distinct names (DuplicateModuleName), but modules in different subtrees may share one.

Because every type name identifies one declaration, a reference is always the bare name, wherever the type is declared. A reference to a name nothing declares is an UnknownTypeRef. C symbols join the declaring module’s path with underscores: a function get_record in module app.data is {prefix}_app_data_get_record, and a record Record declared there is {prefix}_app_data_Record.

version: "0.11.0"
modules:
  - name: kv
    interfaces:
      - name: Store
        constructors:
          - name: open
            params:
              - { name: path, type: string }
    modules:
      - name: stats
        structs:
          - name: Stats
            fields:
              - { name: total_entries, type: i64 }
        functions:
          - name: get_stats
            params:
              - { name: store, type: Store }
            return: Stats
# rejected: DuplicateFunctionName, QualifiedTypeRef
version: "0.11.0"
modules:
  - name: kv
    interfaces:
      - name: Store
        constructors:
          - { name: open, params: [{ name: path, type: string }] }
    functions:
      - { name: stats, params: [{ name: store, type: kv.Store }], return: i64 }
  - name: metrics
    functions:
      - { name: stats, return: i64 }

Documentation comments

doc: is accepted on modules, functions, parameters, structs, fields, enums, variants, interfaces and their members, callback interfaces and their methods, and error codes. Every generator emits it in the target’s native doc-comment syntax (///, /** */, docstrings, XML docs); multi-line YAML block strings (|) are preserved. An absent doc: emits nothing.

Validation

weaveffi validate (and every generating command) runs every rule and reports every violation in one pass, each with a message, a suggestion, and a source span located within the enclosing declaration when one can be found. --format json emits the same diagnostics, each with a code field naming the variant alongside the variant’s own fields (name, module, and so on); --warn adds advisory warnings.

The rules in brief:

  • Names are identifiers (a letter or _, then ASCII letters, digits, and _) and not one of if, else, for, while, loop, match, type, return, async, await, break, continue, fn, struct, enum, mod, use. Generators escape other languages’ keywords themselves.
  • Type names, free-function names, and error code names are unique across the whole API; every other name is unique in its scope (see Modules and names).
  • Structs, enums, interfaces, and callback interfaces aren’t empty.
  • Types are bare names that resolve, and appear only in the positions in the table above.
  • throws needs a domain in scope; cancellable needs async.
  • Codes are positive and their values unique within a domain; no free function shares a domain’s name.
  • No two declarations lower to the same C symbol. Every symbol is the prefix plus the underscore-joined module path plus the declaration name, so a function x_y in module m collides with a function y in module m.x or m_x; so do a free function Store_get and a get method on interface Store, a function named Store_clone and Store’s implicit _clone, and a function named foo_callback and the completion type of an async foo. Symbols that fall in a family the C value-buffer helpers reserve ({prefix}_list_*, {prefix}_map_*, and so on, which covers everything in a top-level module named list) collide too. The full symbol table is in the C ABI contract.

A document with an unsupported version reports only UnsupportedSchemaVersion.

Error catalog

CodeReported when
UnsupportedSchemaVersionversion isn’t 0.11.0
NoModuleNamea module has no name
InvalidModuleNamea module name isn’t an identifier or is reserved
DuplicateModuleNametwo sibling modules share a name
InvalidIdentifiera name isn’t a valid identifier
ReservedKeyworda name is a reserved word
DuplicateFunctionNametwo free functions anywhere in the API share a name
DuplicateParamNametwo parameters of a callable share a name
NameCollisionWithErrorDomaina free function and an error domain share a name
DuplicateStructFieldtwo fields of a struct share a name
EmptyStructa struct has no fields
EmptyEnuman enum has no variants
DuplicateEnumVarianttwo variants of an enum share a name
DuplicateEnumValuetwo variants of an enum share a value
DuplicateEnumVariantFieldtwo fields of a rich-enum variant share a name
DuplicateInterfaceMembertwo members of an interface share a name
EmptyInterfacean interface has no members
ConstructorHasReturna constructor declares return
AsyncConstructora constructor is async
InterfaceInInvalidPositionan interface is used as a map key
EmptyCallbackInterfacea callback interface has no methods
DuplicateCallbackMethodtwo methods of a callback interface share a name
InvalidCallbackMethoda callback method is async or cancellable, or returns an iterator
CallbackInterfaceInInvalidPositiona callback interface appears other than as a top-level parameter (bare or Cb?)
DuplicateTypeNametwo types (including error domains) anywhere in the API share a name
UnknownTypeRefa reference names no declaration
QualifiedTypeRefa reference is a dotted, module-qualified name
UnsupportedPrimitivea reference names usize, isize, u128, i128, or char
InvalidMapKeya map key isn’t an integer, bool, string, or C-style enum
IteratorInInvalidPositioniter<T> appears other than as an outermost return
AsyncIteratorReturnan async function returns iter<T>
CancellableNotAsynca synchronous callable is cancellable
ThrowsWithoutErrorDomainthrows: true on a callable or callback method with no error domain in scope
ErrorDomainMissingNamean error domain has no name
DuplicateErrorCodetwo codes in a domain share a value
DuplicateErrorCodeNametwo codes anywhere in the API share a name
InvalidErrorCodea code is zero or negative
SymbolCollisiontwo declarations lower to the same C identifier, or one falls in a reserved helper family

Warnings

Warnings never fail validation.

CodeFires when
LargeEnumVariantCountan enum has more than 100 variants
DeepNestinga type nests composites more than three levels deep
EmptyModuleDoca module with functions has no doc: on itself or any function
AsyncVoidFunctionan async free function returns nothing
DeprecatedFunctiona free function is deprecated (informational)

Complete example

An address book combining an enum, a record, an error domain, an interface, and a nested module that uses its parent’s types:

version: "0.11.0"
modules:
  - name: contacts
    doc: An in-memory address book.
    errors:
      name: ContactsError
      codes:
        - { name: InvalidName, code: 1, message: name must not be empty }
        - { name: NotFound, code: 2, message: contact not found }
    enums:
      - name: ContactType
        variants:
          - { name: Personal, value: 0 }
          - { name: Work, value: 1 }
    structs:
      - name: Contact
        fields:
          - { name: id, type: i64 }
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: contact_type, type: ContactType }
    interfaces:
      - name: ContactBook
        constructors:
          - name: new
        methods:
          - name: add
            params:
              - { name: name, type: string }
              - { name: email, type: "string?" }
              - { name: contact_type, type: ContactType }
            return: Contact
            throws: true
          - name: get
            params:
              - { name: id, type: i64 }
            return: Contact
            throws: true
          - name: list
            return: "[Contact]"
    modules:
      - name: groups
        functions:
          - name: count_of_type
            params:
              - { name: book, type: ContactBook }
              - { name: contact_type, type: ContactType }
            return: i32