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

Annotated Rust Extraction

Overview

One way to drive WeaveFFI is to make annotated Rust your source of truth. The #[weaveffi::module] proc-macro reads that annotated source to generate the producer’s C ABI glue (see The Rust Producer Macro), and the CLI reads the same annotations to derive the IDL and bindings. Both paths call into one shared extractor (weaveffi-bridge), so the IDL the CLI emits and the symbols the macro produces cannot drift.

You can point weaveffi generate and weaveffi extract straight at a .rs file. generate lowers the source to the IR in memory and runs the generators; extract writes the derived IDL to disk (handy for review, for committing a canonical IDL alongside the source, or for piping into another command).

When to use

Reach for a .rs input when:

  • You want the Rust implementation to be the single source of truth, with no separate IDL to maintain.
  • You want the IDL to track signature changes automatically: edit the Rust, re-run.

Author an IDL document (YAML/JSON/TOML) directly when:

  • You want to design the API before any Rust exists.
  • You need a feature the extractor cannot infer from Rust syntax, such as struct field defaults, package and per-generator configuration, or since: without an accompanying #[deprecated]. See Pitfalls.

Step-by-step

1. Annotate the Rust source

Mark an inline module with #[weaveffi::module] and tag the items you want to export. The attributes come from the weaveffi crate; the same crate’s macro generates the producer glue when you compile the library.

#![allow(unused)]
fn main() {
/// Catalog operations.
#[weaveffi::module]
pub mod inventory {
    /// A product in the catalog.
    #[weaveffi::record]
    #[derive(Clone)]
    pub struct Product {
        /// Stable identifier.
        pub id: i32,
        pub name: String,
        pub price: f64,
        pub tags: Vec<String>,
    }

    /// Product availability.
    #[weaveffi::enumeration]
    #[repr(i32)]
    #[derive(Clone, Copy)]
    pub enum Availability {
        InStock = 0,
        OutOfStock = 1,
        Preorder = 2,
    }

    /// Look up a product by ID.
    #[weaveffi::export]
    pub fn get_product(id: i32) -> Option<Product> {
        todo!()
    }

    /// Replaced by `search_v2` in 0.3.0.
    #[weaveffi::export]
    #[deprecated(since = "0.2.0", note = "use search_v2 instead")]
    pub fn search(query: String, limit: i32) -> Vec<Product> {
        todo!()
    }

    /// A nested namespace.
    #[weaveffi::module]
    pub mod nested {
        /// Lives inside `inventory::nested`.
        #[weaveffi::export]
        pub fn helper() -> i32 {
            0
        }
    }
}
}

2. Run weaveffi extract

weaveffi extract src/lib.rs                    # YAML to stdout
weaveffi extract src/lib.rs -o api.yml         # YAML to file
weaveffi extract src/lib.rs -f json -o api.json  # JSON to file
weaveffi extract src/lib.rs | weaveffi generate -o generated

The extracted IDL is validated automatically and extraction fails loudly if the result would not generate, for example a handle<T> whose target type the source never declares, a duplicate name, or a listener pointing at a missing callback. Pass --warn to downgrade those errors to a warning: line on stderr and emit the IDL anyway, which is useful when bootstrapping from source that references types you have not declared yet:

weaveffi extract src/lib.rs --warn          # best-effort, errors as warnings

3. Generate directly, or validate and generate the IDL

Skip the intermediate file and generate from the source:

weaveffi generate src/lib.rs -o generated/

Or commit the derived IDL and feed that to the rest of the toolchain:

weaveffi extract src/lib.rs -o api.yml
weaveffi validate api.yml
weaveffi generate api.yml -o generated/

Reference

CLI command

weaveffi extract <INPUT> [--output <PATH>] [--format <FORMAT>] [--warn]
FlagDefaultDescription
<INPUT>requiredPath to a .rs source file
-o, --outputstdoutWrite to a file instead of stdout
-f, --formatyamlOutput format: yaml, json, or toml
--warnoffDowngrade validation errors to warnings and emit the IDL anyway

Attribute reference

The extractor matches a marker by its final path segment, so both the namespaced form (#[weaveffi::record]) and a bare form (#[record]) resolve identically. Prefer the namespaced form: it is what the #[weaveffi::module] macro consumes, and it reads unambiguously.

AttributeWhere it goesEffect
#[weaveffi::module]inline modMarks an exported namespace. Required: only modules carrying it are extracted. Modules may nest.
#[weaveffi::export]free fnEmits a Function in the enclosing module. async fn sets async: true; a Result<T, E> return sets throws: true (the IDL return type is T).
#[weaveffi::record]named-field structEmits a StructDef.
#[weaveffi::interface]struct with an impl blockEmits an InterfaceDef. The impl block’s pub fns become constructors (those returning Self), methods (&self receivers), and statics.
#[weaveffi::error]unit-variant enumEmits the module’s error domain. Every variant needs an explicit = N discriminant; the first doc line is the code’s message.
#[weaveffi::builder]struct (with #[weaveffi::record])Sets builder: true on the emitted struct.
#[weaveffi::enumeration] + #[repr(i32)]enumEmits an EnumDef. Every variant must have an explicit = N discriminant.
#[weaveffi::cancellable]exported async fnSets cancellable: true.
#[weaveffi::callback]free fnEmits a module-level CallbackDef using the function’s name and parameters.
#[weaveffi::listener(event = "Name")]free fnEmits a ListenerDef referencing the named callback (the legacy event_callback key is also accepted).
#[deprecated(since = "...", note = "...")]exported fnPopulates since and deprecated. Bare #[deprecated] sets deprecated = "deprecated".

Doc comments (///) on items, fields, and enum variants become the doc field in the IR.

Macro versus extraction. Both the CLI extractor and the #[weaveffi::module] proc-macro understand the full annotation surface above, including interfaces, error domains, async, callbacks, listeners, iterators, rich enums, maps, and builders. A hand-authored IDL can additionally carry metadata that source can’t yet express (package and per-generator configuration, struct field defaults, and standalone since tags), which is why the advanced samples keep a committed YAML IDL for generation. See Feature support for the macro’s current matrix.

Type mapping

Rust typeWeaveFFI TypeRefIDL string
i8I8i8
i16I16i16
i32I32i32
i64I64i64
u8U8u8
u16U16u16
u32U32u32
f32F32f32
f64F64f64
boolBoolbool
StringStringUtf8string
Vec<u8>Bytesbytes
u64Handlehandle
&strBorrowedStr&str
&[u8]BorrowedBytes&[u8]
*mut T / *const TTypedHandle("T")handle<T>
Vec<T>List(T)[T]
Option<T>Optional(T)T?
weaveffi::Iter<T>Iterator(T)iter<T>
HashMap<K, V>Map(K, V){K:V}
BTreeMap<K, V>Map(K, V){K:V}
&T (other)inner typeT
&mut T (other)inner type, mutableT
Any other identifierStruct(name)name

Compositions work recursively: Option<Vec<i32>> becomes [i32]? and Vec<Option<String>> becomes [string?].

&mut T parameters are reduced to T and the surrounding Param record gets mutable: true. &T for any non-str/[u8] type is also reduced to T with mutable: false.

Round-trip integrity

The roundtrip_kitchen_sink integration test in crates/weaveffi-cli/tests/extract_roundtrip.rs proves that the hand-annotated form of the kitchen-sink IDL round-trips through weaveffi extract and matches the original IR for every supported feature: modules, nested modules, structs (including builders), enums, interfaces, error domains, per-function throws, callbacks, listeners, every primitive type, borrowed types, typed handles, optional/list/map composites, async, cancellable, and deprecated/since.

Pitfalls

The extractor parses syntax, not semantics. The items below cannot be inferred from Rust source alone and either must be added to the generated IDL by hand or are documented as round-trip gaps.

  • Package and per-generator configuration. The package: and generators: blocks have no source-level spelling; add them to the extracted IDL by hand (this is why the advanced samples commit a YAML).
  • Struct field default values. The IDL’s default: field cannot be derived from Rust syntax (Rust struct fields have no default expressions).
  • Standalone since: without #[deprecated]. since is only recovered when paired with #[deprecated(since = "...")]. To set since on a non-deprecated function, edit the YAML manually.
  • An error code’s doc: separate from its message:. In Rust the first doc line on a #[weaveffi::error] variant is the code’s message; the IDL can carry both, so a distinct doc: is dropped on round-trip.
  • Doc comments on parameters. Rust accepts /// on fn parameters but most formatters strip them; when present, the extractor preserves them, but plan for Param.doc to be lossy.
  • Generics, trait impl blocks, and macros. The extractor never resolves generics or expands macros, and it only reads the inherent impl block of a #[weaveffi::interface] type. Items produced by proc-macros and declarative macros are invisible.
  • External mod foo; declarations. Only inline modules (mod foo { ... }) are processed; declarations that point to other files are skipped.
  • Tuple and unit structs. Only structs with named fields work with #[weaveffi::record].
  • Enum discriminants are mandatory. C-style enums need #[repr(i32)] with explicit = N discriminants, and rich (payload-carrying) enum variants must use named fields; tuple-style variants are rejected.