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

The Rust Producer Macro

If your producer is written in Rust, the most ergonomic workflow is to write a normal, safe Rust library, annotate it with the #[weaveffi::module] family of attributes, and let the weaveffi crate generate the #[no_mangle] extern "C" thunks that back the stable C ABI. The same annotated source is what weaveffi generate src/lib.rs reads to emit the IDL, the C header, and every language binding, so the producer you compile and the bindings you ship cannot drift: they are two views of one parse.

This is the “Rust as the source of truth” model. You never hand-write unsafe FFI glue, and there is no separate IDL file to keep in sync.

Setup

Add the single weaveffi facade crate and build a cdylib (plus an rlib if you also want to unit-test the safe functions in-crate):

[package]
name = "my-lib"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
weaveffi = "0.14"

A complete example

#![allow(unused)]
fn main() {
//! src/lib.rs

/// Arithmetic over 32-bit integers.
#[weaveffi::module]
pub mod calculator {
    /// The calculator's error domain: the codes its throwing functions report.
    #[weaveffi::error]
    #[derive(Debug)]
    pub enum CalcError {
        /// division by zero
        DivisionByZero = 1,
    }

    /// Add two integers.
    #[weaveffi::export]
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    /// Divide two integers, failing on a zero divisor.
    #[weaveffi::export]
    pub fn div(a: i32, b: i32) -> Result<i32, CalcError> {
        if b == 0 {
            return Err(CalcError::DivisionByZero);
        }
        Ok(a / b)
    }
}

// Emit the fixed runtime surface (memory, error, and cancel-token helpers)
// exactly once per cdylib.
weaveffi::export_runtime!();
}

That is the whole producer. Building it yields a shared library exporting weaveffi_calculator_add and weaveffi_calculator_div with the exact signatures the generated C header declares. A Result<T, E> return marks the function throws: true in the IDL: the return type is T, and Err is reported through the trailing out_err parameter with the code the #[weaveffi::error] enum declares, so every binding surfaces it as a typed domain error (see Error Handling). A throwing function needs an error domain in scope on its module or an ancestor.

Generate the bindings straight from the same file:

weaveffi generate src/lib.rs -o generated --target c,swift,python

The attributes

AttributeWhere it goesEffect
#[weaveffi::module]inline mod foo { ... }Marks an exported namespace and drives the codegen. Modules may nest.
#[weaveffi::export]fnExports a function. A Result<T, E> return is throws: true; () (and Result<(), E>) is a void return.
#[weaveffi::record]named-field structA by-value record. Generates create, destroy, and a getter per field.
#[weaveffi::interface]struct with an impl blockAn opaque object type. The impl block’s pub fns become constructors (those returning Self), methods (&self), and statics; a destroy symbol is implicit.
#[weaveffi::error]unit-variant enumDeclares the module’s error domain. Every variant needs an explicit = N discriminant; the doc comment is the code’s default message.
#[weaveffi::enumeration]#[repr(i32)] enumA C-style enum. Every variant needs an explicit = N discriminant.
#[weaveffi::callback]fnDeclares a callback signature (see roadmap).
#[weaveffi::listener(event = "Name")]fnDeclares an event listener bound to a callback (see roadmap).
#[weaveffi::cancellable]async fnMarks an async function as accepting a cancel token (see roadmap).
#[weaveffi::builder]#[weaveffi::record] structOpts the record into a fluent builder (see roadmap).

Only items carrying a marker are exported. Private helpers, use items, the module’s in-memory state, and free functions without #[weaveffi::export] are left untouched, so a module can freely mix its exported surface with its implementation. Doc comments (///) on items, fields, and variants flow into the generated IDL and every binding.

Call weaveffi::export_runtime!() exactly once in the crate (not per module). It emits the fixed C ABI runtime symbols (weaveffi_free_string, weaveffi_free_bytes, weaveffi_error_clear, the cancel-token helpers, and the arena) that every binding links against.

How values cross the boundary

The macro marshals each argument and result through the audited weaveffi::abi runtime, so every unsafe pointer operation lives in one reviewed place rather than in generated glue. You write ordinary Rust types; the macro picks the matching ABI shape:

Rust typeIDL typeC ABI shape
i8..i64, u8..u32, f32, f64, boolsamethe scalar
String, &strstringconst char*
Vec<u8>, &[u8]bytesconst uint8_t* ptr, size_t len
u64handleweaveffi_handle_t
*mut T, *const Thandle<T>opaque T*
a #[weaveffi::record] structthe recordopaque object pointer
a #[weaveffi::enumeration] enumthe enumint-sized discriminant
Option<T>T?nullable pointer or value slot
Vec<T>[T]ptr + len (object pointers for record lists)

A u64 parameter or return is an opaque handle. Reach for the IDL directly if you need a real 64-bit scalar. See Annotated Rust Extraction for the exhaustive table.

Records

A #[weaveffi::record] struct that crosses the boundary by value must derive Clone (the macro clones it out of the caller’s heap). The generated surface matches the canonical C ABI for a record: a create constructor over the fields, a destroy, and a getter per field.

#![allow(unused)]
fn main() {
#[weaveffi::record]
#[derive(Clone, Debug)]
pub struct Contact {
    pub id: i64,
    pub first_name: String,
    pub email: Option<String>,
    pub kind: ContactType,
}
}

Interfaces

A #[weaveffi::interface] struct is a first-class object type: the consumer holds an opaque pointer to a live instance rather than a copied value. The impl block defines the surface; the struct’s own fields (its state) never cross the boundary, so they need no annotations.

#![allow(unused)]
fn main() {
#[weaveffi::interface]
pub struct ContactBook {
    contacts: Mutex<Vec<Contact>>,
    next_id: AtomicI64,
}

impl ContactBook {
    /// Create an empty book.               // -> constructor
    pub fn new() -> Self { /* ... */ }

    /// Add a contact to the book.          // -> throwing method
    pub fn add(&self, first_name: String, /* ... */) -> Result<Contact, ContactsError> { /* ... */ }

    /// Number of contacts in the book.     // -> method
    pub fn count(&self) -> i32 { /* ... */ }
}
}

Every binding wraps the pointer in an idiomatic class (Swift class with deinit, Kotlin Closeable, Python __del__, Go Close(), and so on) and the implicit destroy symbol frees the instance exactly once. See samples/contacts and samples/inventory for complete producers.

Cross-module references

Modules can reference each other’s records and enums. Import the type with a normal use and pass it by value or by reference:

#![allow(unused)]
fn main() {
#[weaveffi::module]
pub mod products {
    #[weaveffi::record]
    #[derive(Clone)]
    pub struct Product { pub id: i64, pub price: f64 }
    // ...
}

#[weaveffi::module]
pub mod orders {
    use super::products::Product;

    /// Takes a `products::Product` across the module boundary.
    #[weaveffi::export]
    pub fn add_product(order_id: u64, product: Product) -> bool {
        // ... look up the order and append the product ...
        true
    }
}
}

Each module is expanded on its own, so the macro emits a pointer-passing thunk named for its own module while the CLI (which sees the whole crate) resolves the reference to products.Product in the IDL and header. Both spellings are the same opaque pointer at the ABI level, so the producer and the generated bindings agree. See samples/inventory for a complete two-module example.

Feature support

The proc-macro generates cdylib glue for the full IDL feature set. Every feature below is understood by the IDL, the validator, and every generator, and the macro emits the matching producer glue, so an annotated module compiles straight to a weaveffi_* cdylib with no hand-written extern "C" layer.

FeatureMacro codegenReference sample
Modules, nested modulesSupportedinventory, kvstore
Sync functions, Result errorsSupportedcalculator, contacts
Error domains (#[weaveffi::error])Supportedcalculator, contacts
Interfaces (constructors / methods / statics)Supportedcontacts, inventory
Records (create / destroy / getters)Supportedcontacts
C-style enumsSupportedcontacts, shapes
Scalars, string, bytes, handles, typed handlesSupportedkvstore
Optionals, lists (scalar / string / record), mapsSupportedinventory, kvstore
Async (and cancellable) functionsSupportedasync-demo, kvstore
Callbacks and event listenersSupportedevents, kvstore
Iterator returnsSupportedevents, kvstore
Rich (data-carrying) enumsSupportedshapes
Builder recordsSupportedkvstore

A few narrow shapes are still rejected at compile time with a clear message rather than emitting glue that disagrees with the header, notably iterator parameters (as opposed to iterator returns) and tuple-style rich-enum variants (use named fields instead). When the macro can’t express a producer it fails loudly, so it never drifts silently from the IDL. The generators deliver the full feature set on the consumer side regardless; the samples in the right-hand column are working references for each pattern.

See also