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

WeaveFFI

WeaveFFI is a toolkit for generating cross-language FFI bindings and language-specific packages from a concise API definition. It works with any native library that exposes a stable C ABI — whether written in Rust, C, C++, Zig, or another language. This book covers the concepts, setup, and end-to-end workflows.

  • Goals: strong safety model, clear memory ownership, ergonomic bindings.
  • Targets: C, Swift, Android (JNI), Node.js, and Web/WASM.
  • Implementation: define your API once; WeaveFFI generates the C ABI contract and idiomatic wrappers for each platform.

See the roadmap for high-level milestones and the getting started guide to try it.

WeaveFFI Roadmap

This roadmap tracks high-level goals for WeaveFFI. The project generates multi-language bindings from an API definition (YAML, JSON, or TOML), producing a stable C ABI contract consumed by language-specific wrappers.

Crate structure

CratePurpose
weaveffi-irIR model + YAML/JSON/TOML parsing via serde
weaveffi-abiC ABI runtime helpers (error struct, handles, memory free functions)
weaveffi-coreGenerator trait, Orchestrator, validation, shared utilities, template engine
weaveffi-gen-cC header generator
weaveffi-gen-cppC++ header + RAII wrapper + CMake scaffold generator
weaveffi-gen-swiftSwiftPM System Library + Swift wrapper generator
weaveffi-gen-androidKotlin/JNI wrapper + Gradle skeleton generator
weaveffi-gen-nodeN-API addon loader + TypeScript types generator
weaveffi-gen-wasmWASM loader + JS/TS wrapper generator
weaveffi-gen-pythonPython ctypes binding + .pyi stubs generator
weaveffi-gen-dotnet.NET P/Invoke binding generator
weaveffi-gen-dartDart dart:ffi binding + pubspec.yaml generator
weaveffi-gen-goGo CGo binding + go.mod generator
weaveffi-gen-rubyRuby FFI binding + gemspec generator
weaveffi-cliCLI binary (installed as weaveffi)
samples/calculatorEnd-to-end sample Rust library
samples/contactsContacts sample with structs, enums, and optionals
samples/inventoryMulti-module sample with cross-type features
samples/node-addonN-API addon for the calculator sample
samples/async-demoAsync demo with callback-based C ABI convention
samples/eventsEvents sample with callbacks, listeners, and iterators

What works today

  • CLI with commands: generate, new, validate, extract, lint, diff, doctor, completions, schema-version
  • IR parsing from YAML, JSON, and TOML with validation (name collisions, reserved keywords, unsupported shapes)
  • Code generators for C, C++, Swift, Android, Node.js, WASM, Python, .NET, Dart, Go, and Ruby targets
  • Rich type system: primitives, strings, bytes, handles, typed handles, structs, enums, optionals, lists, maps, iterators, callbacks
  • Annotated Rust extraction — derive/proc-macro input as an alternative to hand-written YAML
  • Incremental codegen with content-hash caching to skip unchanged files
  • Generator configuration via TOML config file with per-target options
  • Inline generator config via [generators.<target>] sections in IDL files
  • Template engine — Tera-based user-overridable code templates loaded from a templates/ directory
  • Pre/post hooks — run arbitrary scripts before and after code generation
  • Scaffolding--scaffold emits Rust extern "C" stubs for the API (sync and async)
  • Inline helpers — error types and memory management utilities generated into each package
  • Samples demonstrating end-to-end usage (calculator, contacts, inventory, async-demo, events)
  • C ABI layer with error struct, string/bytes free functions, error domains, typed handles, and callback convention
  • Validation warnings--warn and lint command for non-fatal diagnostics
  • Diff mode — compare generated output against existing files
  • Shell completionsweaveffi completions <shell> for bash, zsh, fish, PowerShell
  • Schema versioning — IR version field with schema-version for querying
  • Async IR model — async functions with completion callback convention and cancellation support
  • Advanced IR features — sub-modules, builder pattern, deprecated/since annotations, mutable params, default field values, borrowed types

Completed

  • Usable CLI that reads a YAML IR, validates it, and generates bindings for all five original targets (C, Swift, Android, Node, WASM)
  • Calculator sample and mdBook documentation site
  • C ABI layer with error struct, string/bytes free functions, and handle convention
  • Extended IR: structs, enums, optional types, arrays/slices, and maps
  • Richer string and byte-buffer handling
  • Packaging improvements (SwiftPM, Gradle, npm scaffolds)
  • Annotated Rust crate extraction (weaveffi extract) as an alternative to hand-written YAML
  • Improved diagnostics, validation warnings, and weaveffi lint command
  • Incremental codegen with content-hash caching
  • Generator configuration via TOML config file
  • DX polish: --dry-run, --quiet, --verbose, diff command, improved doctor
  • Python target (ctypes + .pyi type stubs + pip-installable package)
  • .NET target (P/Invoke + .csproj + .nuspec)
  • Inline generated helpers per target (error types, memory wrappers)
  • WASM generator rewritten to be API-driven (JS wrappers + .d.ts)
  • Inventory sample demonstrating multi-module and cross-type features
  • Publishing to crates.io with automated semantic-release pipeline
  • C++ target (RAII wrappers, std::string/std::vector/std::optional/std::unordered_map, CMakeLists.txt, exception-based errors, configurable namespace/header/standard)
  • Dart target (dart:ffi bindings, enum generation, pubspec.yaml, null-safe code, configurable package name)
  • Go target (CGo bindings, Go error pattern, go.mod, idiomatic naming, configurable module path)
  • Ruby target (FFI gem bindings, struct class wrappers, gemspec, enum modules, configurable module/gem namespace)
  • Template engine (Tera) with user-overridable templates and template directory discovery
  • Pre-generation and post-generation hook commands
  • Inline [generators.<target>] sections in IDL files for per-target configuration
  • IR schema version field
  • Shell auto-completions (weaveffi completions <shell> for bash, zsh, fish, PowerShell)
  • Improved weaveffi new with full project scaffold (Cargo.toml, lib.rs, IDL, README)
  • Typed handles (handle<Name>) replacing raw u64 for type-safe handle usage
  • Benchmarking infrastructure (criterion) for codegen throughput
  • Async IR model and C ABI async convention (completion callbacks with context pointers)
  • Callback and listener patterns in the IR (register/unregister function pairs)
  • Iterator type in the IR for streaming/sequence patterns
  • Nested sub-module support in the IR
  • Builder pattern support for struct construction
  • Versioned API evolution: deprecated/since annotations, default field values
  • Borrowed types (&str, &[u8]) for zero-copy parameter passing
  • Mutable parameter annotations for safer codegen
  • Async-demo and events samples demonstrating callbacks, listeners, and iterators
  • Cross-module type references (struct in one module used as param in another)
  • WASM generator aligned with the C ABI error model (out_err parameter handling in generated JS)
  • Node N-API addon stub bodies completed with functional glue
  • End-to-end integration tests for JSON and TOML input formats
  • Generator edge-case coverage (deeply nested optionals, maps of lists, enum-keyed maps)
  • Zero-copy string and byte-buffer passing (borrowed slices across the ABI boundary)
  • Arena/pool allocation patterns for batch handle creation and destruction
  • IR parsing and validation profiling and optimization
  • Generated code memory safety audit (double-free, use-after-free, null pointer paths)
  • Swift async/await mapping for async functions
  • Kotlin coroutine (suspend fun) mapping for async functions
  • Node.js Promise mapping for async functions
  • Python asyncio mapping for async functions
  • .NET Task<T> / async mapping for async functions
  • Cancellation token support for long-running async operations
  • Full cross-platform CI (Windows added to the test matrix)
  • Security audit of all generated code patterns (memory safety, input validation)

Future releases

Dart Flutter integration

  • Flutter plugin scaffold with platform channel integration

Documentation and benchmarks

  • Benchmark results published on documentation site
  • Per-target tutorials for C, C++, Dart, Go, Ruby, WASM, and .NET
  • Cookbook recipes for common integration patterns

Design principle: standalone generated packages

Generated packages should be fully self-contained and publishable to their native ecosystem (npm, CocoaPods, Maven Central, PyPI, NuGet, pub.dev, etc.) without requiring consumers to install WeaveFFI tooling, runtimes, or support packages. WeaveFFI is a build-time tool for library authors — end users should never need to know it exists. Any helper code (error types, memory management utilities) is generated inline into each package rather than pulled from a shared runtime dependency.

Non-goals (for now)

  • Full RPC / IPC framework: WeaveFFI generates in-process FFI bindings, not network protocols. gRPC, Cap’n Proto, or similar tools are better suited for cross-process communication.
  • Automatic Rust implementation: WeaveFFI generates the consumer side (bindings). The library author still writes the Rust (or C) implementation behind the ABI.
  • GUI framework bindings: Complex GUI toolkits with deep object hierarchies and inheritance are out of scope. WeaveFFI targets function-oriented APIs with flat or moderately nested data types.

Getting Started

This guide walks you through installing WeaveFFI, defining an API, generating multi-language bindings, implementing the Rust library, and calling it from C.

Prerequisites

You need the Rust toolchain (stable channel) installed. Verify with:

rustc --version
cargo --version

1) Install WeaveFFI

Install the CLI from crates.io:

cargo install weaveffi-cli

This puts the weaveffi binary on your PATH.

2) Create a new project

Scaffold a starter project:

weaveffi new my-project
cd my-project

This creates a my-project/ directory containing:

  • weaveffi.yml — an example API definition with add, mul, and echo functions
  • README.md — quick-start notes

3) Define your API

Open weaveffi.yml and replace its contents with an API that has a struct and a function:

version: "0.1.0"
modules:
  - name: math
    structs:
      - name: Point
        fields:
          - { name: x, type: f64 }
          - { name: y, type: f64 }
    functions:
      - name: add
        params:
          - { name: a, type: i32 }
          - { name: b, type: i32 }
        return: i32

The IDL supports primitives (i32, f64, bool, string, bytes, handle), optionals (string?), and lists ([i32]). See the IDL Schema reference for the full specification.

4) Generate bindings

Run the generator to produce bindings for all targets:

weaveffi generate weaveffi.yml -o generated --scaffold

The --scaffold flag also emits a scaffold.rs with Rust FFI stubs you can use as a starting point. The output tree looks like:

generated/
├── c/          # C header + convenience stubs
├── swift/      # SwiftPM package + Swift wrapper
├── android/    # Kotlin JNI wrapper + Gradle skeleton
├── node/       # N-API loader + TypeScript types
├── wasm/       # WASM loader stub
└── scaffold.rs # Rust FFI function stubs

5) Examine the generated output

C header (generated/c/weaveffi.h)

The C generator produces an opaque struct with lifecycle functions and getters, plus a module-level function. Every exported function takes an out_err parameter for error reporting:

typedef struct weaveffi_math_Point weaveffi_math_Point;

weaveffi_math_Point* weaveffi_math_Point_create(
    double x, double y, weaveffi_error* out_err);
void weaveffi_math_Point_destroy(weaveffi_math_Point* ptr);
double weaveffi_math_Point_get_x(const weaveffi_math_Point* ptr);
double weaveffi_math_Point_get_y(const weaveffi_math_Point* ptr);

int32_t weaveffi_math_add(int32_t a, int32_t b, weaveffi_error* out_err);

Swift wrapper (generated/swift/Sources/WeaveFFI/WeaveFFI.swift)

Structs become classes that own an OpaquePointer and free it on deinit. Module functions are grouped under a Swift enum namespace:

public class Point {
    let ptr: OpaquePointer
    deinit { weaveffi_math_Point_destroy(ptr) }

    public var x: Double { weaveffi_math_Point_get_x(ptr) }
    public var y: Double { weaveffi_math_Point_get_y(ptr) }
}

public enum Math {
    public static func add(a: Int32, b: Int32) throws -> Int32 { ... }
}

TypeScript types (generated/node/types.d.ts)

Structs become interfaces with mapped types. Functions use the IR name directly (no module prefix):

export interface Point {
  x: number;
  y: number;
}

// module math
export function add(a: number, b: number): number

6) Implement the Rust library

The generated scaffold.rs contains todo!() stubs for every function. Create a Rust library crate and fill in the implementations.

Cargo.toml:

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

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

[dependencies]
weaveffi-abi = { version = "0.1" }

src/lib.rs — implement the add function (struct lifecycle omitted for brevity):

#![allow(unused)]
#![allow(unsafe_code)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]

fn main() {
use std::os::raw::c_char;
use weaveffi_abi::{self as abi, weaveffi_error};

#[no_mangle]
pub extern "C" fn weaveffi_math_add(
    a: i32,
    b: i32,
    out_err: *mut weaveffi_error,
) -> i32 {
    abi::error_set_ok(out_err);
    a + b
}

#[no_mangle]
pub extern "C" fn weaveffi_free_string(ptr: *const c_char) {
    abi::free_string(ptr);
}

#[no_mangle]
pub extern "C" fn weaveffi_free_bytes(ptr: *mut u8, len: usize) {
    abi::free_bytes(ptr, len);
}

#[no_mangle]
pub extern "C" fn weaveffi_error_clear(err: *mut weaveffi_error) {
    abi::error_clear(err);
}
}

Key points:

  • Every exported function uses #[no_mangle] and extern "C".
  • out_err must always be cleared on success with abi::error_set_ok.
  • On error, call abi::error_set(out_err, code, message) and return a zero/null value.
  • The library must export weaveffi_free_string, weaveffi_free_bytes, and weaveffi_error_clear for the runtime.

Build with:

cargo build

This produces a shared library (libmy_math.dylib on macOS, libmy_math.so on Linux).

7) Build and test with C

Write a small C program that calls your library:

main.c:

#include <stdio.h>
#include "weaveffi.h"

int main(void) {
    struct weaveffi_error err = {0};

    int32_t sum = weaveffi_math_add(3, 4, &err);
    if (err.code) {
        printf("error: %s\n", err.message);
        weaveffi_error_clear(&err);
        return 1;
    }
    printf("add(3, 4) = %d\n", sum);

    return 0;
}

Compile, link, and run:

# macOS
cc -I generated/c main.c -L target/debug -lmy_math -o my_example
DYLD_LIBRARY_PATH=target/debug ./my_example

# Linux
cc -I generated/c main.c -L target/debug -lmy_math -o my_example
LD_LIBRARY_PATH=target/debug ./my_example

Expected output:

add(3, 4) = 7

Next steps

  • Run weaveffi doctor to check which platform toolchains are available.
  • See the Calculator tutorial for a full end-to-end walkthrough including Swift and Node.js.
  • Read the IDL Schema reference for all supported types and features.
  • Explore the Generators section for target-specific details.

Samples

This repo includes sample projects under samples/ that showcase end-to-end usage of the C ABI layer. Each sample contains a YAML API definition and a Rust crate that implements the corresponding weaveffi_* C ABI functions.

Calculator

Path: samples/calculator

The simplest sample — a single module with four functions that exercise primitive types (i32) and string passing. Good starting point for understanding the basic C ABI contract.

What it demonstrates:

  • Scalar parameters and return values (i32)
  • String parameters and return values (C string ownership)
  • Error propagation via weaveffi_error (e.g. division by zero)
  • The weaveffi_free_string / weaveffi_error_clear lifecycle helpers

Build and generate bindings:

cargo build -p calculator
weaveffi generate samples/calculator/calculator.yml -o generated

This produces target-specific output under generated/ (C headers, Swift wrapper, Android skeleton, Node addon loader, WASM stub). Runnable examples that consume the generated output are in examples/.

Contacts

Path: samples/contacts

A CRUD-style sample with a single module that exercises richer type-system features than the calculator.

What it demonstrates:

  • Enum definitions (ContactType with Personal, Work, Other)
  • Struct definitions (Contact with typed fields)
  • Optional fields (string? for the email)
  • List return types ([Contact])
  • Handle-based resource management (create_contact returns a handle)
  • Struct getter and setter functions
  • Enum conversion functions (from_i32 / to_i32)
  • Struct destroy and list-free lifecycle functions

Build and generate bindings:

cargo build -p contacts
weaveffi generate samples/contacts/contacts.yml -o generated

Inventory

Path: samples/inventory

A richer, multi-module sample with products and orders modules that exercises cross-module struct references and nested list types.

What it demonstrates:

  • Multiple modules in a single API definition
  • Enums (Category with Electronics, Clothing, Food, Books)
  • Structs with optional fields, list fields ([string] tags), and float types
  • List-returning search functions (search_products filtered by category)
  • Cross-module struct passing (add_product_to_order takes a Product)
  • Nested struct lists (Order.items is [OrderItem])
  • Full CRUD operations across both modules

Build and generate bindings:

cargo build -p inventory
weaveffi generate samples/inventory/inventory.yml -o generated

Async Demo

Path: samples/async-demo

Demonstrates the async function pattern using callback-based invocation. Async functions in the YAML definition get an _async suffix at the C ABI layer and accept a callback + context pointer instead of returning directly.

What it demonstrates:

  • Async function declarations (async: true in the YAML)
  • Callback-based C ABI pattern (weaveffi_tasks_run_task_async)
  • Callback type definitions (weaveffi_tasks_run_task_callback)
  • Batch async operations (run_batch processes a list of names sequentially)
  • Synchronous fallback functions (cancel_task is non-async in the same module)
  • Struct return types through callbacks (TaskResult delivered via callback)

Build and run tests:

cargo build -p async-demo
cargo test -p async-demo

Note: The validator currently rejects async: true in API definitions. This sample exists to exercise the planned async ABI pattern ahead of full validator support.

Events

Path: samples/events

Demonstrates callbacks, event listeners, and iterator-based return types.

What it demonstrates:

  • Callback type definitions (OnMessage callback)
  • Listener registration and unregistration (message_listener)
  • Event-driven patterns (sending a message triggers the registered callback)
  • Iterator return types (iter<string> in the YAML)
  • Iterator lifecycle (get_messages returns a MessageIterator, advanced with _next, freed with _destroy)

Build and run tests:

cargo build -p events
cargo test -p events

Node Addon

Path: samples/node-addon

An N-API addon crate that loads the calculator’s C ABI shared library at runtime via libloading and exposes the functions as JavaScript-friendly #[napi] exports. Used by the Node.js example in examples/.

What it demonstrates:

  • Dynamic loading of a weaveffi_* shared library from JavaScript
  • Mapping C ABI error structs to N-API errors
  • String ownership across the FFI boundary (CString in, CStr out, free)

Build (requires the calculator library first):

cargo build -p calculator
cargo build -p weaveffi-node-addon

Reference

IDL Type Reference

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

Top-level structure

version: "0.2.0"
modules:
  - name: my_module
    structs: [...]
    enums: [...]
    functions: [...]
    callbacks: [...]
    listeners: [...]
    errors: { ... }
    modules: [...]
generators:
  swift:
    module_name: MyApp
FieldTypeRequiredDescription
versionstringyesSchema version ("0.1.0" or "0.2.0")
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 FunctionyesFunctions exported by this module
structsarray of StructnoStruct type definitions
enumsarray of EnumnoEnum type definitions
callbacksarray of CallbacknoCallback type definitions
listenersarray of ListenernoListener (event subscription) definitions
errorsErrorDomainnoOptional 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
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.

Primitive types

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

TypeDescriptionExample value
i32Signed 32-bit integer-42
u32Unsigned 32-bit integer300
i64Signed 64-bit integer9000000000
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

Primitive examples

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. At the C ABI level, handle<T> is still a uint64_t.

functions:
  - name: create_session
    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

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, or lists — any valid TypeRef.


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

Enum example

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.


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

Optional example

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

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

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

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

functions:
  - name: scan_entries
    params:
      - { name: prefix, type: string }
    return: "iter<Contact>"
    doc: "Lazily iterates over matching contacts"

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

modules:
  - name: events
    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 }

    functions:
      - name: subscribe
        params:
          - { name: callback, type: on_data }

Callbacks are referenced by name in function parameters, similar to how structs and enums are referenced.


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

modules:
  - name: events
    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.2.0"
modules:
  - name: app
    functions:
      - name: init
        params: []

    modules:
      - name: auth
        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

Type references to structs and enums must resolve within the same module (including its parent chain). Cross-module references between sibling modules are not currently supported — define shared types in a common parent module or duplicate the definition.


Async and lifecycle annotations

Async functions

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

functions:
  - name: fetch_data
    params:
      - { name: url, type: string }
    return: string
    async: true

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

Async void functions (no return type) emit a validator warning since they are unusual.

Deprecated functions

Mark a function as deprecated with a migration message:

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:

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.2.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
i32yesyesyes
u32yesyesyes
i64yesyesyes
f64yesyesyes
boolyesyesyes
stringyesyesyes
bytesyesyesyes
handleyesyesyes
handle<T>yesyesyesTyped handle
&stryesyesyesBorrowed, zero-copy
&[u8]yesyesyesBorrowed, zero-copy
StructNameyesyesyes
EnumNameyesyesyes
T?yesyesyes
[T]yesyesyes
[T?]yesyesyes
[T]?yesyesyes
{K:V}yesyesyes
{K:V}?yesyesyes
iter<T>noyesnoReturn-only

Complete example

A full API definition combining structs, enums, optionals, lists, and nested types:

version: "0.1.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 }

    functions:
      - name: create_contact
        params:
          - { name: first_name, type: string }
          - { name: last_name, type: string }
          - { name: email, type: "string?" }
          - { name: contact_type, type: ContactType }
        return: handle

      - name: get_contact
        params:
          - { name: id, type: handle }
        return: Contact

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

      - name: delete_contact
        params:
          - { name: id, type: handle }
        return: bool

      - name: count_contacts
        params: []
        return: i32

Validation rules

  • Module, function, parameter, struct, enum, 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.).
  • 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.
  • Enum variant values must be unique within their enum.
  • Type references to structs/enums must resolve to a definition in the same module.
  • Async functions are allowed. Async void functions (no return type) emit a warning.
  • Listener event_callback must reference a callback in the same module.
  • Error domain names must not collide with function names.

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

You can declare an optional error domain on a module to reserve symbolic names and numeric codes:

errors:
  name: ContactErrors
  codes:
    - { name: not_found, code: 1, message: "Contact not found" }
    - { name: duplicate, code: 2, message: "Contact already exists" }

Error codes must be non-zero and unique. Error domain names must not collide with function names in the same module.

Memory and Error Model

This section summarizes the C ABI conventions exposed by WeaveFFI and how to manage ownership across the FFI boundary.

Error handling

  • Every generated C function ends with an out_err parameter of type weaveffi_error*.
  • On success: out_err->code == 0 and out_err->message == NULL.
  • On failure: out_err->code != 0 and out_err->message points to a Rust-allocated NUL-terminated UTF-8 string that must be cleared.

Relevant declarations (from the generated header):

typedef struct weaveffi_error { int32_t code; const char* message; } weaveffi_error;
void weaveffi_error_clear(weaveffi_error* err);

Typical C usage:

struct weaveffi_error err = {0};
int32_t sum = weaveffi_calculator_add(3, 4, &err);
if (err.code) { fprintf(stderr, "%s\n", err.message ? err.message : ""); weaveffi_error_clear(&err); }

Notes:

  • The default unspecified error code used by the runtime is -1.
  • Future versions may map module error domains to well-known codes.

Strings and bytes

Returned strings are owned by Rust and must be freed by the caller:

const char* s = weaveffi_calculator_echo(msg, &err);
// ... use s ...
weaveffi_free_string(s);

Returned bytes include a separate out-length parameter and must be freed by the caller:

size_t out_len = 0;
const uint8_t* buf = weaveffi_module_fn(/* params ... */, &out_len, &err);
// ... copy data from buf ...
weaveffi_free_bytes((uint8_t*)buf, out_len);

Relevant declarations:

void weaveffi_free_string(const char* ptr);
void weaveffi_free_bytes(uint8_t* ptr, size_t len);

Handles

Opaque resources are represented as weaveffi_handle_t (64-bit). Treat them as tokens; their lifecycle APIs are defined by your module.

Language wrappers

  • Swift: the generated wrapper throws WeaveFFIError and automatically clears errors and frees returned strings.
  • Node: the provided N-API addon clears errors and frees returned strings; the generated JS loader expects a compiled addon index.node placed next to it.

C-string safety

When constructing C strings, interior NUL bytes are sanitized on the Rust side to maintain valid C semantics.

Naming and Package Conventions

Naming and Package Conventions

This guide standardizes how we name the Weave projects, repositories, packages, modules, and identifiers across ecosystems.

Human-facing brand names (prose)

  • Use condensed names in sentences and documentation:
    • WeaveFFI
    • WeaveHeap

Repository and package slugs (URLs and registries)

  • Use condensed lowercase slugs for top-level repositories:

    • GitHub: weaveffi, weaveheap (repos: weavefoundry/weaveffi, weavefoundry/weaveheap)
  • Use hyphenated slugs for subpackages and components, prefixed with the top-level slug:

    • Examples: weaveffi-core, weaveffi-ir, weaveheap-core
  • Planned package names (not yet published):

    • crates.io: weaveffi, weaveffi-core, weaveffi-ir, etc.
    • npm: @weavefoundry/weaveffi
    • PyPI: weaveffi
    • SPM (repo slug): weaveffi

Rationale: condensed top-level slugs unify handles across registries and are ergonomic to type; hyphenated subpackages remain idiomatic and map cleanly to ecosystems that normalize to underscores or CamelCase.

Code identifiers by ecosystem

  • Rust

    • Crates: hyphenated subcrates on crates.io (e.g., weaveffi-core), imported as underscores (e.g., weaveffi_core). Top-level crate (if any): weaveffi.
    • Modules/paths: snake_case.
    • Types/traits/enums: CamelCase (e.g., WeaveFFI).
  • Swift / Apple platforms

    • Package products and modules: UpperCamelCase (e.g., WeaveFFI, WeaveHeap).
    • Keep repo slug condensed; SPM product name provides the CamelCase surface.
  • Java / Kotlin (Android)

    • Group ID / package base: reverse-DNS, all lowercase (e.g., com.weavefoundry.weaveffi).
    • Artifact ID: top-level condensed (e.g., weaveffi); sub-artifacts hyphenated (e.g., weaveffi-android).
    • Class names: UpperCamelCase (e.g., WeaveFFI).
  • JavaScript / TypeScript (Node, bundlers)

    • Package name: scope + condensed for top-level, hyphenated for subpackages (e.g., @weavefoundry/weaveffi, @weavefoundry/weaveffi-core).
    • Import alias: flexible, prefer WeaveFFI in examples when using default exports or named namespaces.
  • Python

    • PyPI name: top-level condensed (e.g., weaveffi); subpackages hyphenated (e.g., weaveffi-core).
    • Import module: condensed for top-level (e.g., import weaveffi); underscores for hyphenated subpackages (e.g., import weaveffi_core).
  • C / CMake

    • Target/library names: snake_case (e.g., weaveffi, weaveffi_core).
    • Header guards / include dirs: snake_case or directory-based (e.g., #include <weaveffi/weaveffi.h>).

Writing guidelines

  • In prose, prefer the condensed brand names: “WeaveFFI”, “WeaveHeap”.
  • In code snippets, follow the host language conventions above.
  • For cross-language docs, show both the repo/package slug and the language-appropriate identifier on first mention, e.g., “Install weaveffi (import as weaveffi, Swift module WeaveFFI). For subpackages, install weaveffi-core (import as weaveffi_core).”

Migration guidance

  • New crates and packages should follow the condensed top-level + hyphenated subpackage pattern:
    • Rust crates: weaveffi-*, weaveheap-*.
    • npm packages (planned): @weavefoundry/weaveffi-*, @weavefoundry/weaveheap-*.
    • Swift products: UpperCamelCase (e.g., WeaveFFICore).
  • Prefer condensed top-level slugs. Avoid hyphenated top-level slugs like weave-ffi, weave-heap going forward.

Examples

  • Rust

    • Crate: weaveffi-core
    • Import: use weaveffi_core::{WeaveFFI};
  • Swift (SPM)

    • Repo: weaveffi
    • Package product: WeaveFFI
    • Import: import WeaveFFI
  • Python (planned)

    • Package: weaveffi
    • Import: import weaveffi as ffi
  • Node (planned)

    • Package: @weavefoundry/weaveffi
    • Import: import { WeaveFFI } from '@weavefoundry/weaveffi'

Generators

This section contains language-specific generators and guidance for using the artifacts they produce. Choose a target below to explore the details.

Android

The Android generator produces a Gradle android-library template with:

  • Kotlin wrapper WeaveFFI that declares external funs
  • JNI C shims that call into the generated C ABI
  • CMakeLists.txt for building the shared library

Generated artifacts

  • generated/android/settings.gradle
  • generated/android/build.gradle
  • generated/android/src/main/kotlin/com/weaveffi/WeaveFFI.kt
  • generated/android/src/main/cpp/{weaveffi_jni.c,CMakeLists.txt}

Generated code examples

Given this IDL definition:

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

    structs:
      - name: Contact
        fields:
          - { name: name, type: string }
          - { name: age, type: i32 }

    functions:
      - name: get_contact
        params:
          - { name: id, type: i32 }
        return: Contact

      - name: find_by_type
        params:
          - { name: contact_type, type: ContactType }
        return: "[Contact]"

Kotlin wrapper class

Functions are declared as @JvmStatic external fun inside a companion object. The native library is loaded in the init block:

package com.weaveffi

class WeaveFFI {
    companion object {
        init { System.loadLibrary("weaveffi") }

        @JvmStatic external fun get_contact(id: Int): Long
        @JvmStatic external fun find_by_type(contact_type: Int): LongArray
    }
}

Struct parameters and returns use Long (opaque handle). Enum parameters use Int.

Kotlin enum classes

Enums generate a Kotlin enum class with an integer value and a fromValue factory:

enum class ContactType(val value: Int) {
    Personal(0),
    Work(1),
    Other(2);

    companion object {
        fun fromValue(value: Int): ContactType = entries.first { it.value == value }
    }
}

Kotlin struct wrapper classes

Structs generate a Kotlin class that wraps a native handle (Long). The class implements Closeable for deterministic cleanup and provides property getters backed by JNI native methods:

class Contact internal constructor(private var handle: Long) : java.io.Closeable {
    companion object {
        init { System.loadLibrary("weaveffi") }

        @JvmStatic external fun nativeCreate(name: String, age: Int): Long
        @JvmStatic external fun nativeDestroy(handle: Long)
        @JvmStatic external fun nativeGetName(handle: Long): String
        @JvmStatic external fun nativeGetAge(handle: Long): Int

        fun create(name: String, age: Int): Contact = Contact(nativeCreate(name, age))
    }

    val name: String get() = nativeGetName(handle)
    val age: Int get() = nativeGetAge(handle)

    override fun close() {
        if (handle != 0L) {
            nativeDestroy(handle)
            handle = 0L
        }
    }

    protected fun finalize() {
        close()
    }
}

Usage:

Contact.create("Alice", 30).use { contact ->
    println("${contact.name}, age ${contact.age}")
}

JNI C shims

The JNI layer (weaveffi_jni.c) bridges Kotlin external declarations to the C ABI functions. Each JNI function acquires parameters from the JVM, calls the C ABI, checks for errors (throwing RuntimeException on failure), and releases JNI resources:

#include <jni.h>
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include "weaveffi.h"

JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_get_1contact(
    JNIEnv* env, jclass clazz, jint id) {
    weaveffi_error err = {0, NULL};
    weaveffi_contacts_Contact* rv = weaveffi_contacts_get_contact(
        (int32_t)id, &err);
    if (err.code != 0) {
        jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException");
        const char* msg = err.message ? err.message : "WeaveFFI error";
        (*env)->ThrowNew(env, exClass, msg);
        weaveffi_error_clear(&err);
        return 0;
    }
    return (jlong)(intptr_t)rv;
}

String parameters are converted via GetStringUTFChars/ReleaseStringUTFChars. Optional value types are unboxed from Java wrapper classes (Integer, Long, Double, Boolean).

CMake configuration

The CMake file links the JNI shim against the C header:

cmake_minimum_required(VERSION 3.22)
project(weaveffi)
add_library(weaveffi SHARED weaveffi_jni.c)
target_include_directories(weaveffi PRIVATE ../../../../c)

The target_include_directories path points at generated/c/ where weaveffi.h lives.

Type mapping reference

IDL typeKotlin type (external)Kotlin type (wrapper)JNI C type
i32IntIntjint
u32LongLongjlong
i64LongLongjlong
f64DoubleDoublejdouble
boolBooleanBooleanjboolean
stringStringStringjstring
bytesByteArrayByteArrayjbyteArray
handleLongLongjlong
StructNameLongStructNamejlong
EnumNameIntEnumNamejint
T?T?T?jobject
[i32]IntArrayIntArrayjintArray
[i64]LongArrayLongArrayjlongArray
[string]Array<String>Array<String>jobjectArray

Build steps

  1. Ensure Android SDK and NDK are installed (Android Studio recommended).
  2. Cross-compile the Rust library for your target architecture:

macOS (host) targeting Android

rustup target add aarch64-linux-android armv7-linux-androideabi
cargo build --target aarch64-linux-android --release

Linux (host) targeting Android

rustup target add aarch64-linux-android armv7-linux-androideabi
export ANDROID_NDK_HOME=/path/to/ndk
cargo build --target aarch64-linux-android --release
  1. Open generated/android in Android Studio.
  2. Sync Gradle and build the :weaveffi AAR.
  3. Integrate the AAR into your app module. Ensure your app loads the Rust-produced native library (e.g., libcalculator) at runtime on device/emulator.

The JNI shims convert strings/bytes and propagate errors by throwing RuntimeException.

C

The C generator emits a single header weaveffi.h containing function prototypes, error types, and memory helpers; it also includes an optional weaveffi.c placeholder for future convenience wrappers.

Generated artifacts

  • generated/c/weaveffi.h
  • generated/c/weaveffi.c

Generated code examples

Given this IDL definition:

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

    structs:
      - name: Contact
        fields:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }

    functions:
      - name: create_contact
        params:
          - { name: first_name, type: string }
          - { name: last_name, type: string }
        return: Contact

      - name: find_contact
        params:
          - { name: id, type: "i32?" }
        return: "Contact?"

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

      - name: count_contacts
        params: []
        return: i32

Header format

The generated header includes an include guard, standard C headers, a #ifdef __cplusplus guard, and the common error/memory types:

#ifndef WEAVEFFI_H
#define WEAVEFFI_H

#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef uint64_t weaveffi_handle_t;

typedef struct weaveffi_error {
    int32_t code;
    const char* message;
} weaveffi_error;

void weaveffi_error_clear(weaveffi_error* err);
void weaveffi_free_string(const char* ptr);
void weaveffi_free_bytes(uint8_t* ptr, size_t len);

// ... module declarations ...

#ifdef __cplusplus
}
#endif

#endif // WEAVEFFI_H

Opaque struct pattern

Structs use a forward-declared opaque typedef. Callers interact with structs exclusively through create/destroy/getter functions — they cannot inspect fields directly:

typedef struct weaveffi_contacts_Contact weaveffi_contacts_Contact;

weaveffi_contacts_Contact* weaveffi_contacts_Contact_create(
    const char* name,
    const char* email,
    int32_t age,
    weaveffi_error* out_err);

void weaveffi_contacts_Contact_destroy(weaveffi_contacts_Contact* ptr);

const char* weaveffi_contacts_Contact_get_name(
    const weaveffi_contacts_Contact* ptr);
const char* weaveffi_contacts_Contact_get_email(
    const weaveffi_contacts_Contact* ptr);
int32_t weaveffi_contacts_Contact_get_age(
    const weaveffi_contacts_Contact* ptr);

Naming conventions

All C ABI symbols follow a strict naming convention:

KindPatternExample
Functionweaveffi_{module}_{function}weaveffi_contacts_create_contact
Struct typeweaveffi_{module}_{Struct}weaveffi_contacts_Contact
Struct createweaveffi_{module}_{Struct}_createweaveffi_contacts_Contact_create
Struct destroyweaveffi_{module}_{Struct}_destroyweaveffi_contacts_Contact_destroy
Struct getterweaveffi_{module}_{Struct}_get_{field}weaveffi_contacts_Contact_get_name
Enum typeweaveffi_{module}_{Enum}weaveffi_contacts_ContactType
Enum variantweaveffi_{module}_{Enum}_{Variant}weaveffi_contacts_ContactType_Personal

Enum typedefs

Enums generate a C typedef enum with prefixed variant names:

typedef enum {
    weaveffi_contacts_ContactType_Personal = 0,
    weaveffi_contacts_ContactType_Work = 1,
    weaveffi_contacts_ContactType_Other = 2
} weaveffi_contacts_ContactType;

Optional parameters and returns

Optional value types are passed as const pointers; NULL means absent. Optional pointer types (string, struct) reuse the same pointer — NULL signals absence:

// Optional i32 parameter: const int32_t* (NULL = absent)
int32_t* weaveffi_store_find(const int32_t* id, weaveffi_error* out_err);

// Optional string return: const char* (NULL = absent)
const char* weaveffi_store_get_name(weaveffi_error* out_err);

// Optional struct return: pointer (NULL = absent)
weaveffi_contacts_Contact* weaveffi_contacts_find_contact(
    const int32_t* id, weaveffi_error* out_err);

List parameters and returns

Lists are passed as pointer + length. Return lists include an out_len output parameter:

// List parameter: pointer + length
void weaveffi_batch_process(
    const int32_t* items, size_t items_len,
    weaveffi_error* out_err);

// List return: pointer + out_len
int32_t* weaveffi_batch_get_ids(
    size_t* out_len,
    weaveffi_error* out_err);

// List of structs return
weaveffi_contacts_Contact** weaveffi_contacts_list_contacts(
    size_t* out_len,
    weaveffi_error* out_err);

Type mapping reference

IDL typeC parameter typeC return type
i32int32_tint32_t
u32uint32_tuint32_t
i64int64_tint64_t
f64doubledouble
boolboolbool
stringconst uint8_t* ptr, size_t lenconst char*
bytesconst uint8_t* ptr, size_t lenconst uint8_t* + size_t* out_len
handleweaveffi_handle_tweaveffi_handle_t
Structconst weaveffi_m_S*weaveffi_m_S*
Enumweaveffi_m_Eweaveffi_m_E
T? (value)const T* (NULL = absent)T* (NULL = absent)
[T]const T* items, size_t items_lenT* + size_t* out_len

Error handling

Every generated function takes a trailing weaveffi_error* out_err. On failure, out_err->code is set to a non-zero value and out_err->message points to a Rust-allocated string. Always check and clear:

weaveffi_error err = {0, NULL};
int32_t result = weaveffi_contacts_count_contacts(&err);
if (err.code != 0) {
    fprintf(stderr, "Error %d: %s\n", err.code, err.message);
    weaveffi_error_clear(&err);
    return 1;
}

Memory management

Rust-allocated strings and byte buffers must be freed by the caller:

const char* name = weaveffi_contacts_Contact_get_name(contact);
printf("Name: %s\n", name);
weaveffi_free_string(name);

size_t len;
const uint8_t* data = weaveffi_storage_get_data(&len, &err);
// ... use data ...
weaveffi_free_bytes((uint8_t*)data, len);

Build and run (calculator sample)

macOS

cargo build -p calculator

cd examples/c
cc -I ../../generated/c main.c -L ../../target/debug -lcalculator -o c_example
DYLD_LIBRARY_PATH=../../target/debug ./c_example

Linux

cargo build -p calculator

cd examples/c
cc -I ../../generated/c main.c -L ../../target/debug -lcalculator -o c_example
LD_LIBRARY_PATH=../../target/debug ./c_example

See examples/c/main.c for usage of errors and returned strings.

Node

The Node generator produces a CommonJS loader and .d.ts type definitions for your functions. The generated addon uses the N-API (Node-API) interface to load the C ABI symbols and expose JS-friendly functions.

Generated artifacts

  • generated/node/index.js — CommonJS loader that requires ./index.node
  • generated/node/types.d.ts — function signatures inferred from your IR
  • generated/node/package.json

Generated code examples

Given this IDL definition:

version: "0.1.0"
modules:
  - name: contacts
    enums:
      - name: Color
        variants:
          - { name: Red, value: 0 }
          - { name: Green, value: 1 }
          - { name: Blue, value: 2 }

    structs:
      - name: Contact
        fields:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: tags, type: "[string]" }

    functions:
      - name: get_contact
        params:
          - { name: id, type: i32 }
        return: "Contact?"

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

      - name: set_favorite_color
        params:
          - { name: contact_id, type: i32 }
          - { name: color, type: "Color?" }

      - name: get_tags
        params:
          - { name: contact_id, type: i32 }
        return: "[string]"

TypeScript interfaces

Structs map to TypeScript interfaces with typed fields:

export interface Contact {
  name: string;
  email: string | null;
  tags: string[];
}

Enums

Enums map to TypeScript enums with explicit integer values:

export enum Color {
  Red = 0,
  Green = 1,
  Blue = 2,
}

Nullable types

Optional types are expressed as union types with null:

// Optional parameter
color: Color | null

// Optional return
export function get_contact(id: number): Contact | null

Array types

List types map to TypeScript arrays. Lists of optionals use parenthesized union types:

// Simple array
export function get_tags(contact_id: number): string[]

// Array return of structs
export function list_contacts(): Contact[]

// Array of optionals (if defined)
(number | null)[]

Complete generated types.d.ts

For the IDL above, the full generated file looks like:

// Generated types for WeaveFFI functions
export interface Contact {
  name: string;
  email: string | null;
  tags: string[];
}
export enum Color {
  Red = 0,
  Green = 1,
  Blue = 2,
}
// module contacts
export function get_contact(id: number): Contact | null
export function list_contacts(): Contact[]
export function set_favorite_color(contact_id: number, color: Color | null): void
export function get_tags(contact_id: number): string[]

Type mapping reference

IDL typeTypeScript type
i32number
u32number
i64number
f64number
boolboolean
stringstring
bytesBuffer
handlebigint
StructNameStructName
EnumNameEnumName
T?T | null
[T]T[]

Running the example

macOS

cargo build -p calculator
cp target/debug/libindex.dylib generated/node/index.node

cd examples/node
DYLD_LIBRARY_PATH=../../target/debug npm start

Linux

cargo build -p calculator
cp target/debug/libindex.so generated/node/index.node

cd examples/node
LD_LIBRARY_PATH=../../target/debug npm start

Notes

  • The loader expects the compiled N-API addon next to it as index.node.
  • The N-API addon crate is in samples/node-addon.

Swift

The Swift generator emits a SwiftPM System Library (CWeaveFFI) that references the generated C header via a module.modulemap, and a thin Swift module (WeaveFFI) that wraps the C API with Swift types and throws-based error handling.

Generated artifacts

  • generated/swift/Package.swift
  • generated/swift/Sources/CWeaveFFI/module.modulemap — C module map pointing at the generated header
  • generated/swift/Sources/WeaveFFI/WeaveFFI.swift — thin Swift wrapper

Generated code examples

Given this IDL definition:

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

    structs:
      - name: Contact
        fields:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }

    functions:
      - name: create_contact
        params:
          - { name: name, type: string }
          - { name: age, type: i32 }
        return: Contact

      - name: find_contact
        params:
          - { name: id, type: i32 }
        return: "Contact?"

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

      - name: set_type
        params:
          - { name: id, type: i32 }
          - { name: contact_type, type: ContactType }

Enums

Enums map to Swift enums backed by Int32. Variant names are converted to lowerCamelCase:

public enum ContactType: Int32 {
    case personal = 0
    case work = 1
    case other = 2
}

Structs (opaque wrapper classes)

Structs are wrapped as Swift classes holding an OpaquePointer to the Rust-allocated data. The deinit calls the C ABI destroy function to free memory. Field access is through computed properties that call the C ABI getters:

public class Contact {
    let ptr: OpaquePointer

    init(ptr: OpaquePointer) {
        self.ptr = ptr
    }

    deinit {
        weaveffi_contacts_Contact_destroy(ptr)
    }

    public var name: String {
        let raw = weaveffi_contacts_Contact_get_name(ptr)
        guard let raw = raw else { return "" }
        defer { weaveffi_free_string(raw) }
        return String(cString: raw)
    }

    public var email: String {
        let raw = weaveffi_contacts_Contact_get_email(ptr)
        guard let raw = raw else { return "" }
        defer { weaveffi_free_string(raw) }
        return String(cString: raw)
    }

    public var age: Int32 {
        return weaveffi_contacts_Contact_get_age(ptr)
    }
}

Optional handling

Optional types map to Swift optionals (T?). For value returns, the generator dereferences via pointee. For string optionals, it guards against nil and frees the string. For struct optionals, it wraps the pointer:

// Optional value return: -> Int32?
return rv?.pointee

// Optional string return: -> String?
guard let rv = rv else { return nil }
defer { weaveffi_free_string(rv) }
return String(cString: rv)

// Optional struct return: -> Contact?
return rv.map { Contact(ptr: $0) }

Optional value parameters use withOptionalPointer to pass a nullable pointer to the C ABI:

@inline(__always)
func withOptionalPointer<T, R>(to value: T?, _ body: (UnsafePointer<T>?) throws -> R) rethrows -> R {
    guard let value = value else { return try body(nil) }
    return try withUnsafePointer(to: value) { try body($0) }
}

Array/List handling

List types map to Swift arrays ([T]). Parameters are passed using withUnsafeBufferPointer to provide pointer+length to the C ABI. Return values are converted from a C pointer+length pair:

// List parameter: [Int32]
ids.withUnsafeBufferPointer { ids_buf in
    let ids_ptr = ids_buf.baseAddress
    let ids_len = ids_buf.count
    // ... call C function with ids_ptr, ids_len ...
}

// List return: -> [Int32]
var outLen: Int = 0
let rv = weaveffi_batch_get_ids(&outLen, &err)
try check(&err)
guard let rv = rv else { return [] }
return Array(UnsafeBufferPointer(start: rv, count: outLen))

Functions

Module functions are generated as static methods on an enum namespace. Every function takes a trailing weaveffi_error* and the Swift wrapper calls try check(&err) to convert errors to Swift exceptions:

public enum Contacts {
    public static func create_contact(_ name: String, _ age: Int32) throws -> Contact {
        var err = weaveffi_error(code: 0, message: nil)
        // ... buffer setup for string params ...
        let rv = weaveffi_contacts_create_contact(name_ptr, name_len, age, &err)
        try check(&err)
        guard let rv = rv else {
            throw WeaveFFIError.error(code: -1, message: "null pointer")
        }
        return Contact(ptr: rv)
    }

    public static func find_contact(_ id: Int32) throws -> Contact? {
        var err = weaveffi_error(code: 0, message: nil)
        let rv = weaveffi_contacts_find_contact(id, &err)
        try check(&err)
        return rv.map { Contact(ptr: $0) }
    }

    public static func list_contacts() throws -> [Contact] {
        var err = weaveffi_error(code: 0, message: nil)
        var outLen: Int = 0
        let rv = weaveffi_contacts_list_contacts(&outLen, &err)
        try check(&err)
        guard let rv = rv else { return [] }
        return (0..<outLen).map { Contact(ptr: rv[$0]!) }
    }
}

Try the example app

macOS

cargo build -p calculator

cd examples/swift
swiftc \
  -I ../../generated/swift/Sources/CWeaveFFI \
  -L ../../target/debug -lcalculator \
  -Xlinker -rpath -Xlinker ../../target/debug \
  Sources/App/main.swift -o .build/debug/App

DYLD_LIBRARY_PATH=../../target/debug .build/debug/App

Linux

cargo build -p calculator

cd examples/swift
swiftc \
  -I ../../generated/swift/Sources/CWeaveFFI \
  -L ../../target/debug -lcalculator \
  -Xlinker -rpath -Xlinker ../../target/debug \
  Sources/App/main.swift -o .build/debug/App

LD_LIBRARY_PATH=../../target/debug .build/debug/App

Integration via SwiftPM

In a real app, add the System Library as a dependency and link it with your target. The CWeaveFFI module map provides header linkage; import WeaveFFI in your Swift code for the ergonomic wrapper.

WASM

The WASM generator produces a minimal JS loader and README to help get started with wasm32-unknown-unknown. Full ergonomics are planned for future releases.

Generated artifacts

  • generated/wasm/weaveffi_wasm.js — ES module loader with JSDoc
  • generated/wasm/README.md — quickstart and type conventions

Generated code examples

JS loader

The generated loader provides a loadWeaveFFI async function that instantiates a .wasm module and returns its exports:

export async function loadWeaveFFI(url) {
  const response = await fetch(url);
  const bytes = await response.arrayBuffer();
  const { instance } = await WebAssembly.instantiate(bytes, {});
  return instance.exports;
}

Usage:

const wasm = await loadWeaveFFI('lib.wasm');
const sum = wasm.weaveffi_math_add(1, 2);

Type conventions at the WASM boundary

WASM only supports numeric types natively (i32, i64, f32, f64). Complex types are encoded as follows:

Structs

Structs are passed as opaque handles (i64 pointers into linear memory). Use the generated C ABI accessor functions to read/write fields:

// Create a struct (returns i64 handle)
const handle = wasm.weaveffi_contacts_create();

// Read a field via accessor
const age = wasm.weaveffi_contacts_Contact_get_age(handle);

// Destroy when done
wasm.weaveffi_contacts_Contact_destroy(handle);

Enums

Enums are passed as i32 values corresponding to the variant’s integer discriminant:

// 0 = Red, 1 = Green, 2 = Blue
wasm.weaveffi_ui_set_color(0);

Optionals

Optional values use 0 / null to represent the absent case. For numeric optionals, a separate _is_present flag (i32: 0 or 1) is used. For handle-typed optionals, a null pointer (0) signals absence:

// Present optional: (is_present=1, value=5000)
wasm.weaveffi_config_set_timeout(1, 5000);

// Absent optional: (is_present=0, value=0)
wasm.weaveffi_config_set_timeout(0, 0);

Lists

Lists are passed as a pointer + length pair (i32 pointer, i32 length) referencing a contiguous region in linear memory. The caller is responsible for allocating and freeing the backing memory:

// Write data into WASM linear memory
const ptr = wasm.weaveffi_alloc(4 * items.length);
const view = new Int32Array(wasm.memory.buffer, ptr, items.length);
view.set(items);

// Pass pointer + length to the function
wasm.weaveffi_data_process(ptr, items.length);

// Free the memory
wasm.weaveffi_dealloc(ptr, 4 * items.length);

Type mapping reference

IDL typeWASM typeConvention
i32i32Direct value
u32i32Direct value (unsigned interpretation)
i64i64Direct value
f64f64Direct value
booli320 = false, 1 = true
stringi32+i32Pointer + length in linear memory
bytesi32+i32Pointer + length in linear memory
handlei64Opaque 64-bit identifier
StructNamei64Opaque handle (pointer)
EnumNamei32Integer discriminant
T?varies_is_present flag or null pointer
[T]i32+i32Pointer + length in linear memory

Build

macOS

rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release

Linux

rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release

The build commands are identical on both platforms since WASM is a cross-compilation target.

Serve the .wasm file and load it with the provided JS helper.

Python

The Python generator produces pure-Python ctypes bindings, type stubs, and packaging files. It uses Python’s built-in ctypes module to call the C ABI directly — no compilation step, no native extension modules, no third-party dependencies.

Why ctypes?

  • Zero dependencies. ctypes ships with every CPython install since Python 2.5.
  • Works with any Python 3.7+. No version-specific native extensions to maintain.
  • No build step. The generated .py files are plain Python — pip install . is enough.
  • Transparent. Developers can read and debug the generated code directly.

The trade-off is that ctypes calls are slower than compiled extensions (cffi, pybind11, PyO3). For most FFI workloads the overhead is negligible compared to the work done inside the Rust library.

Generated artifacts

FilePurpose
python/weaveffi/__init__.pyRe-exports everything from weaveffi.py
python/weaveffi/weaveffi.pyctypes bindings: library loader, wrapper functions, classes
python/weaveffi/weaveffi.pyiType stub for IDE autocompletion and mypy
python/pyproject.tomlPEP 621 project metadata
python/setup.pyFallback setuptools script
python/README.mdBasic usage instructions

Generated code examples

Given this IDL definition:

version: "0.1.0"
modules:
  - name: contacts
    enums:
      - name: ContactType
        doc: "Type of contact"
        variants:
          - { name: Personal, value: 0 }
          - { name: Work, value: 1 }
          - { name: Other, value: 2 }

    structs:
      - name: Contact
        doc: "A contact record"
        fields:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }

    functions:
      - name: create_contact
        params:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: contact_type, type: ContactType }
        return: handle

      - name: get_contact
        params:
          - { name: id, type: handle }
        return: Contact

      - name: count_contacts
        params: []
        return: i32

Library loader

The generated module auto-detects the platform and loads the shared library:

def _load_library() -> ctypes.CDLL:
    system = platform.system()
    if system == "Darwin":
        name = "libweaveffi.dylib"
    elif system == "Windows":
        name = "weaveffi.dll"
    else:
        name = "libweaveffi.so"
    return ctypes.CDLL(name)

_lib = _load_library()

Functions

Each IDL function becomes a Python function with full type hints. The wrapper declares ctypes argtypes/restype, converts arguments, calls the C symbol, checks for errors, and converts the return value:

def create_contact(name: str, email: Optional[str], contact_type: "ContactType") -> int:
    _fn = _lib.weaveffi_contacts_create_contact
    _fn.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int32, ctypes.POINTER(_WeaveffiErrorStruct)]
    _fn.restype = ctypes.c_uint64
    _email_c = _string_to_bytes(email)
    _err = _WeaveffiErrorStruct()
    _result = _fn(_string_to_bytes(name), _email_c, contact_type.value, ctypes.byref(_err))
    _check_error(_err)
    return _result

Enums

Enums map to Python IntEnum subclasses:

class ContactType(IntEnum):
    """Type of contact"""
    Personal = 0
    Work = 1
    Other = 2

Enum parameters are passed as .value (an int32); enum returns are wrapped back into the enum class.

Structs

Structs become Python classes backed by an opaque pointer. Fields are exposed as @property getters that call the corresponding C getter function:

class Contact:
    """A contact record"""

    def __init__(self, _ptr: int) -> None:
        self._ptr = _ptr

    def __del__(self) -> None:
        if self._ptr is not None:
            _lib.weaveffi_contacts_Contact_destroy.argtypes = [ctypes.c_void_p]
            _lib.weaveffi_contacts_Contact_destroy.restype = None
            _lib.weaveffi_contacts_Contact_destroy(self._ptr)
            self._ptr = None

    @property
    def name(self) -> str:
        _fn = _lib.weaveffi_contacts_Contact_get_name
        _fn.argtypes = [ctypes.c_void_p]
        _fn.restype = ctypes.c_char_p
        _result = _fn(self._ptr)
        return _bytes_to_string(_result) or ""

    @property
    def email(self) -> Optional[str]:
        _fn = _lib.weaveffi_contacts_Contact_get_email
        _fn.argtypes = [ctypes.c_void_p]
        _fn.restype = ctypes.c_char_p
        _result = _fn(self._ptr)
        return _bytes_to_string(_result)

    @property
    def age(self) -> int:
        _fn = _lib.weaveffi_contacts_Contact_get_age
        _fn.argtypes = [ctypes.c_void_p]
        _fn.restype = ctypes.c_int32
        _result = _fn(self._ptr)
        return _result

Type stubs (.pyi)

The generator also produces a .pyi stub file for IDE support and static analysis tools like mypy:

from enum import IntEnum
from typing import Dict, List, Optional

class ContactType(IntEnum):
    Personal: int
    Work: int
    Other: int

class Contact:
    @property
    def name(self) -> str: ...
    @property
    def email(self) -> Optional[str]: ...
    @property
    def age(self) -> int: ...

def create_contact(name: str, email: Optional[str], contact_type: "ContactType") -> int: ...
def get_contact(id: int) -> "Contact": ...
def count_contacts() -> int: ...

Type mapping reference

IDL typePython type hintctypes type
i32intctypes.c_int32
u32intctypes.c_uint32
i64intctypes.c_int64
f64floatctypes.c_double
boolboolctypes.c_int32
stringstrctypes.c_char_p
bytesbytesctypes.POINTER(ctypes.c_uint8) + ctypes.c_size_t
handleintctypes.c_uint64
Struct"StructName"ctypes.c_void_p
Enum"EnumName"ctypes.c_int32
T?Optional[T]ctypes.POINTER(scalar) for values; same pointer for strings/structs
[T]List[T]ctypes.POINTER(scalar) + ctypes.c_size_t
{K: V}Dict[K, V]key/value pointer arrays + ctypes.c_size_t

Booleans are transmitted as c_int32 (0/1) because C has no standard fixed-width boolean type across ABIs.

Build and install

1. Generate bindings

weaveffi generate --input api.yaml --output generated/ --target python

2. Build the Rust shared library

cargo build --release -p your_library

This produces libweaveffi.dylib (macOS), libweaveffi.so (Linux), or weaveffi.dll (Windows) in target/release/.

3. Install the Python package

cd generated/python
pip install .

Or for development:

pip install -e .

4. Make the shared library findable

The shared library must be on the system library search path at runtime:

macOS:

DYLD_LIBRARY_PATH=../../target/release python your_script.py

Linux:

LD_LIBRARY_PATH=../../target/release python your_script.py

Windows: Place weaveffi.dll in the same directory as your script, or add its directory to PATH.

5. Use the bindings

from weaveffi import ContactType, create_contact, get_contact, count_contacts

handle = create_contact("Alice", "alice@example.com", ContactType.Work)
contact = get_contact(handle)
print(f"{contact.name} ({contact.email})")
print(f"Total: {count_contacts()}")

Memory management

The generated Python wrappers handle memory ownership automatically:

Strings

  • Passing strings in: Python str values are encoded to UTF-8 bytes via _string_to_bytes() before crossing the FFI boundary. ctypes manages the lifetime of these temporary byte buffers.
  • Receiving strings back: Returned c_char_p values are decoded from UTF-8 via _bytes_to_string(). The Rust runtime owns the returned pointer; the preamble registers weaveffi_free_string for cleanup.

Bytes

  • Passing bytes in: Python bytes are copied into a ctypes array ((c_uint8 * len(data))(*data)) and passed with a length parameter.
  • Receiving bytes back: The C function writes to an out_len parameter. The wrapper copies the data into a Python bytes object via slicing (_result[:_out_len.value]), then the Rust side is responsible for the original buffer.

Structs (opaque pointers)

Struct wrappers hold an opaque c_void_p. The __del__ destructor calls the corresponding _destroy C function to free the Rust-side allocation:

def __del__(self) -> None:
    if self._ptr is not None:
        _lib.weaveffi_contacts_Contact_destroy(self._ptr)
        self._ptr = None

The _PointerGuard context manager is available for explicit lifetime control:

class _PointerGuard(contextlib.AbstractContextManager):
    def __init__(self, ptr, free_fn) -> None:
        self.ptr = ptr
        self._free_fn = free_fn

    def __exit__(self, *exc) -> bool:
        if self.ptr is not None:
            self._free_fn(self.ptr)
            self.ptr = None
        return False

Error handling

C-level errors are converted to Python exceptions automatically. The generated module defines a WeaveffiError exception class:

class WeaveffiError(Exception):
    def __init__(self, code: int, message: str) -> None:
        self.code = code
        self.message = message
        super().__init__(f"({code}) {message}")

Every function call follows this pattern:

  1. A _WeaveffiErrorStruct (mirroring the C weaveffi_error) is allocated.
  2. It is passed as the last argument to the C function via ctypes.byref().
  3. After the call, _check_error() inspects the struct. If code != 0, it reads the message, calls weaveffi_error_clear to free the Rust-allocated string, and raises WeaveffiError.
class _WeaveffiErrorStruct(ctypes.Structure):
    _fields_ = [
        ("code", ctypes.c_int32),
        ("message", ctypes.c_char_p),
    ]

def _check_error(err: _WeaveffiErrorStruct) -> None:
    if err.code != 0:
        code = err.code
        message = err.message.decode("utf-8") if err.message else ""
        _lib.weaveffi_error_clear(ctypes.byref(err))
        raise WeaveffiError(code, message)

Callers use standard Python try/except:

from weaveffi import create_contact, ContactType, WeaveffiError

try:
    handle = create_contact("Alice", None, ContactType.Personal)
except WeaveffiError as e:
    print(f"Error {e.code}: {e.message}")

.NET

The .NET generator emits a C# class library that wraps the C ABI using P/Invoke (DllImport). Structs are exposed as IDisposable classes with property getters, and errors are surfaced as .NET exceptions.

Generated artifacts

  • generated/dotnet/WeaveFFI.cs — C# bindings (P/Invoke declarations, wrapper classes, enums, structs)
  • generated/dotnet/WeaveFFI.csproj — SDK-style project targeting net8.0
  • generated/dotnet/WeaveFFI.nuspec — NuGet package metadata
  • generated/dotnet/README.md — build and pack instructions

P/Invoke approach

All native calls go through a single internal NativeMethods class that declares [DllImport] entries with CallingConvention.Cdecl. The library name defaults to "weaveffi" — at runtime the .NET host resolves this to the platform-specific shared library (libweaveffi.dylib, libweaveffi.so, or weaveffi.dll).

Each P/Invoke declaration maps 1:1 to a C ABI symbol using the weaveffi_{module}_{function} naming convention. Every function takes a trailing ref WeaveffiError err parameter so the wrapper can convert native errors into managed exceptions.

Generated code examples

Given this IDL definition:

version: "0.1.0"
modules:
  - name: contacts
    enums:
      - name: ContactType
        doc: Type of contact
        variants:
          - { name: Personal, value: 0 }
          - { name: Work, value: 1 }
          - { name: Other, value: 2 }

    structs:
      - name: Contact
        doc: A contact record
        fields:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }
          - { name: active, type: bool }
          - { name: contact_type, type: ContactType }

    functions:
      - name: create_contact
        params:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }
        return: handle

      - name: get_contact
        params:
          - { name: id, type: handle }
        return: Contact

      - name: find_contact
        params:
          - { name: id, type: i32 }
        return: "Contact?"

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

      - name: count_contacts
        params: []
        return: i32

Enums

Enums map to C# enums with explicit integer values:

/// <summary>Type of contact</summary>
public enum ContactType
{
    Personal = 0,
    Work = 1,
    Other = 2,
}

Structs (IDisposable wrapper classes)

Structs are wrapped as C# classes implementing IDisposable. The class holds an IntPtr handle to the Rust-allocated data. Dispose() calls the C ABI destroy function, and a finalizer provides a safety net for unmanaged cleanup:

/// <summary>A contact record</summary>
public class Contact : IDisposable
{
    private IntPtr _handle;
    private bool _disposed;

    internal Contact(IntPtr handle)
    {
        _handle = handle;
    }

    internal IntPtr Handle => _handle;

    public string Name
    {
        get
        {
            var ptr = NativeMethods.weaveffi_contacts_Contact_get_name(_handle);
            var str = WeaveFFIHelpers.PtrToString(ptr);
            NativeMethods.weaveffi_free_string(ptr);
            return str;
        }
    }

    public string? Email
    {
        get
        {
            var ptr = NativeMethods.weaveffi_contacts_Contact_get_email(_handle);
            if (ptr == IntPtr.Zero) return null;
            var str = WeaveFFIHelpers.PtrToString(ptr);
            NativeMethods.weaveffi_free_string(ptr);
            return str;
        }
    }

    public int Age
    {
        get
        {
            return NativeMethods.weaveffi_contacts_Contact_get_age(_handle);
        }
    }

    public bool Active
    {
        get
        {
            return NativeMethods.weaveffi_contacts_Contact_get_active(_handle) != 0;
        }
    }

    public ContactType ContactType
    {
        get
        {
            return (ContactType)NativeMethods.weaveffi_contacts_Contact_get_contact_type(_handle);
        }
    }

    public void Dispose()
    {
        if (!_disposed)
        {
            NativeMethods.weaveffi_contacts_Contact_destroy(_handle);
            _disposed = true;
        }
        GC.SuppressFinalize(this);
    }

    ~Contact()
    {
        Dispose();
    }
}

Functions

Module functions are generated as static methods on a wrapper class named after the module (e.g. Contacts). String parameters are marshalled to UTF-8 via Marshal.StringToCoTaskMemUTF8 and freed in a finally block. Every call checks the error struct and throws WeaveffiException on failure:

public static class Contacts
{
    public static ulong CreateContact(string name, string? email, int age)
    {
        var err = new WeaveffiError();
        var namePtr = Marshal.StringToCoTaskMemUTF8(name);
        var emailPtr = email != null ? Marshal.StringToCoTaskMemUTF8(email) : IntPtr.Zero;
        try
        {
            var result = NativeMethods.weaveffi_contacts_create_contact(
                namePtr, emailPtr, age, ref err);
            WeaveffiError.Check(err);
            return result;
        }
        finally
        {
            Marshal.FreeCoTaskMem(namePtr);
            if (emailPtr != IntPtr.Zero) Marshal.FreeCoTaskMem(emailPtr);
        }
    }

    public static Contact GetContact(ulong id)
    {
        var err = new WeaveffiError();
        var result = NativeMethods.weaveffi_contacts_get_contact(id, ref err);
        WeaveffiError.Check(err);
        return new Contact(result);
    }

    public static Contact? FindContact(int id)
    {
        var err = new WeaveffiError();
        var result = NativeMethods.weaveffi_contacts_find_contact(id, ref err);
        WeaveffiError.Check(err);
        return result == IntPtr.Zero ? null : new Contact(result);
    }

    public static Contact[] ListContacts()
    {
        var err = new WeaveffiError();
        var result = NativeMethods.weaveffi_contacts_list_contacts(out var outLen, ref err);
        WeaveffiError.Check(err);
        if (result == IntPtr.Zero) return Array.Empty<Contact>();
        var arr = new Contact[(int)outLen];
        for (int i = 0; i < (int)outLen; i++)
        {
            arr[i] = new Contact(Marshal.ReadIntPtr(result, i * IntPtr.Size));
        }
        return arr;
    }

    public static int CountContacts()
    {
        var err = new WeaveffiError();
        var result = NativeMethods.weaveffi_contacts_count_contacts(ref err);
        WeaveffiError.Check(err);
        return result;
    }
}

P/Invoke declarations

The internal NativeMethods class contains the raw DllImport bindings:

internal static class NativeMethods
{
    private const string LibName = "weaveffi";

    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
    internal static extern void weaveffi_free_string(IntPtr ptr);

    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
    internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len);

    [DllImport(LibName, EntryPoint = "weaveffi_contacts_create_contact",
               CallingConvention = CallingConvention.Cdecl)]
    internal static extern ulong weaveffi_contacts_create_contact(
        IntPtr name, IntPtr email, int age, ref WeaveffiError err);

    [DllImport(LibName, EntryPoint = "weaveffi_contacts_get_contact",
               CallingConvention = CallingConvention.Cdecl)]
    internal static extern IntPtr weaveffi_contacts_get_contact(
        ulong id, ref WeaveffiError err);

    // ... additional declarations for each function and struct accessor
}

Type mapping reference

IDL typeC# typeP/Invoke type
i32intint
u32uintuint
i64longlong
f64doubledouble
boolboolint
stringstringIntPtr
handleulongulong
bytesbyte[]IntPtr
StructNameStructNameIntPtr
EnumNameEnumNameint
T?T? (nullable)IntPtr
[T]T[]IntPtr
{K: V}Dictionary<K, V>IntPtr

Memory management via IDisposable

Each generated struct class implements IDisposable. Calling Dispose() invokes the C ABI _destroy function to free the Rust-allocated memory. A C# finalizer (~ClassName()) acts as a safety net in case Dispose() is not called explicitly.

Use the using statement for deterministic cleanup:

using (var contact = Contacts.GetContact(id))
{
    Console.WriteLine(contact.Name);
    Console.WriteLine(contact.Email ?? "(none)");
}

Strings returned from getters are copied into managed memory and the native pointer is freed immediately via weaveffi_free_string, so string properties do not require manual disposal.

Error handling via exceptions

Native errors are propagated through a WeaveffiError struct (LayoutKind.Sequential) containing an integer code and a message pointer. After every P/Invoke call the wrapper invokes WeaveffiError.Check(err), which throws a WeaveffiException when the code is non-zero:

public class WeaveffiException : Exception
{
    public int Code { get; }

    public WeaveffiException(int code, string message) : base(message)
    {
        Code = code;
    }
}

Catch errors in consumer code:

try
{
    var contact = Contacts.GetContact(42);
}
catch (WeaveffiException ex)
{
    Console.WriteLine($"Error {ex.Code}: {ex.Message}");
}

Building

dotnet build

The .csproj targets net8.0 and enables AllowUnsafeBlocks. Place the native shared library where the .NET runtime can find it (e.g. next to the built DLL, or set LD_LIBRARY_PATH / DYLD_LIBRARY_PATH).

NuGet packaging

dotnet pack

The resulting .nupkg will be in bin/Debug/ (or bin/Release/ with -c Release). The generated .nuspec pre-fills package metadata (id, version, license, description). For production use, bundle the native shared library in the NuGet package under runtimes/{rid}/native/.

C++

The C++ generator emits a single header-only library weaveffi.hpp that wraps the C ABI with idiomatic C++ types. Structs use RAII classes with move semantics, errors become exceptions, and async functions return std::future. A CMakeLists.txt is included for easy integration.

Generated artifacts

  • generated/cpp/weaveffi.hpp — header-only C++ bindings (extern “C” declarations, RAII wrapper classes, enum classes, inline function wrappers)
  • generated/cpp/CMakeLists.txt — INTERFACE library target for CMake
  • generated/cpp/README.md — build instructions

RAII approach

All structs are wrapped as C++ classes that own a void* handle to Rust-allocated data. The destructor calls the C ABI _destroy function, so resources are freed automatically when the object goes out of scope — no manual cleanup required.

Copy construction and copy assignment are deleted to prevent double-free bugs. Move construction and move assignment are supported, transferring ownership by nulling out the source handle.

This design means you can use standard C++ patterns like returning structs from functions, storing them in containers via std::move, and relying on stack unwinding for cleanup during exceptions.

Generated code examples

Given this IDL definition:

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

    structs:
      - name: Contact
        fields:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }
          - { name: contact_type, type: ContactType }

    functions:
      - name: create_contact
        params:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }
        return: Contact

      - name: find_contact
        params:
          - { name: id, type: i32 }
        return: "Contact?"

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

      - name: count_contacts
        params: []
        return: i32

      - name: fetch_contact
        async: true
        params:
          - { name: id, type: i32 }
        return: Contact

Enums

Enums map to C++ enum class backed by int32_t:

enum class ContactType : int32_t {
    Personal = 0,
    Work = 1,
    Other = 2
};

Structs (RAII wrapper classes)

Structs are wrapped as C++ classes holding a void* handle to the Rust-allocated data. The destructor calls the C ABI destroy function. Field access is through getter methods that call the C ABI getters:

class Contact {
    void* handle_;

public:
    explicit Contact(void* h) : handle_(h) {}

    ~Contact() {
        if (handle_) weaveffi_contacts_Contact_destroy(
            static_cast<weaveffi_contacts_Contact*>(handle_));
    }

    Contact(const Contact&) = delete;
    Contact& operator=(const Contact&) = delete;

    Contact(Contact&& other) noexcept : handle_(other.handle_) {
        other.handle_ = nullptr;
    }

    Contact& operator=(Contact&& other) noexcept {
        if (this != &other) {
            if (handle_) weaveffi_contacts_Contact_destroy(
                static_cast<weaveffi_contacts_Contact*>(handle_));
            handle_ = other.handle_;
            other.handle_ = nullptr;
        }
        return *this;
    }

    void* handle() const { return handle_; }

    std::string name() const {
        const char* raw = weaveffi_contacts_Contact_get_name(
            static_cast<const weaveffi_contacts_Contact*>(handle_));
        std::string ret(raw);
        weaveffi_free_string(raw);
        return ret;
    }

    std::optional<std::string> email() const {
        auto* raw = weaveffi_contacts_Contact_get_email(
            static_cast<const weaveffi_contacts_Contact*>(handle_));
        if (!raw) return std::nullopt;
        std::string ret(raw);
        weaveffi_free_string(raw);
        return ret;
    }

    int32_t age() const {
        return weaveffi_contacts_Contact_get_age(
            static_cast<const weaveffi_contacts_Contact*>(handle_));
    }

    ContactType contact_type() const {
        return static_cast<ContactType>(
            weaveffi_contacts_Contact_get_contact_type(
                static_cast<const weaveffi_contacts_Contact*>(handle_)));
    }
};

Functions

Module functions are generated as inline free functions in the weaveffi namespace. Every function checks the error struct after calling the C ABI and throws WeaveFFIError on failure:

namespace weaveffi {

inline Contact contacts_create_contact(
    const std::string& name,
    const std::optional<std::string>& email,
    int32_t age)
{
    weaveffi_error err{};
    auto result = weaveffi_contacts_create_contact(
        name.c_str(),
        email.has_value() ? email.value().c_str() : nullptr,
        age, &err);
    if (err.code != 0) {
        std::string msg(err.message ? err.message : "unknown error");
        int32_t code = err.code;
        weaveffi_error_clear(&err);
        throw WeaveFFIError(code, msg);
    }
    return Contact(result);
}

inline std::optional<Contact> contacts_find_contact(int32_t id) {
    weaveffi_error err{};
    auto result = weaveffi_contacts_find_contact(id, &err);
    if (err.code != 0) {
        std::string msg(err.message ? err.message : "unknown error");
        int32_t code = err.code;
        weaveffi_error_clear(&err);
        throw WeaveFFIError(code, msg);
    }
    if (!result) return std::nullopt;
    return Contact(result);
}

inline std::vector<Contact> contacts_list_contacts() {
    size_t out_len = 0;
    weaveffi_error err{};
    auto result = weaveffi_contacts_list_contacts(&out_len, &err);
    if (err.code != 0) { /* ... throw ... */ }
    std::vector<Contact> ret;
    ret.reserve(out_len);
    for (size_t i = 0; i < out_len; ++i)
        ret.emplace_back(Contact(result[i]));
    return ret;
}

inline int32_t contacts_count_contacts() {
    weaveffi_error err{};
    auto result = weaveffi_contacts_count_contacts(&err);
    if (err.code != 0) { /* ... throw ... */ }
    return result;
}

} // namespace weaveffi

Type mapping reference

IDL typeC++ typePassed as parameter
i32int32_tint32_t
u32uint32_tuint32_t
i64int64_tint64_t
f64doubledouble
boolboolbool
stringstd::stringconst std::string&
bytesstd::vector<uint8_t>const std::vector<uint8_t>&
handlevoid*void*
StructNameStructNameconst StructName&
EnumNameEnumName (enum class)EnumName
T?std::optional<T>const std::optional<T>&
[T]std::vector<T>const std::vector<T>&
{K: V}std::unordered_map<K, V>const std::unordered_map<K, V>&

Error handling via exceptions

Native errors are surfaced through a WeaveFFIError class that extends std::runtime_error. Every generated function checks the C ABI error struct after each call and throws on non-zero error codes:

class WeaveFFIError : public std::runtime_error {
    int32_t code_;

public:
    WeaveFFIError(int32_t code, const std::string& msg)
        : std::runtime_error(msg), code_(code) {}
    int32_t code() const { return code_; }
};

When the IDL defines custom error codes, the generator also emits specific exception subclasses (e.g. NotFoundError, ValidationError) that inherit from WeaveFFIError, and the error-checking logic uses a switch statement to throw the most specific type.

Catch errors in consumer code:

try {
    auto contact = weaveffi::contacts_find_contact(42);
} catch (const weaveffi::WeaveFFIError& e) {
    std::cerr << "Error " << e.code() << ": " << e.what() << std::endl;
}

Async support via std::future

Async IDL functions generate wrappers that return std::future<T>. Under the hood, the wrapper creates a std::promise, passes a callback to the C ABI _async function, and resolves (or rejects) the promise when the callback fires:

inline std::future<Contact> contacts_fetch_contact(int32_t id) {
    auto* promise_ptr = new std::promise<Contact>();
    auto future = promise_ptr->get_future();
    weaveffi_contacts_fetch_contact_async(id,
        [](void* context, weaveffi_error* err,
           weaveffi_contacts_Contact* result) {
            auto* p = static_cast<std::promise<Contact>*>(context);
            if (err && err->code != 0) {
                std::string msg(err->message ? err->message : "unknown error");
                p->set_exception(std::make_exception_ptr(
                    WeaveFFIError(err->code, msg)));
            } else {
                p->set_value(Contact(result));
            }
            delete p;
        }, static_cast<void*>(promise_ptr));
    return future;
}

Use it with .get() (blocking) or integrate with your application’s event loop:

auto future = weaveffi::contacts_fetch_contact(42);
auto contact = future.get();
std::cout << contact.name() << std::endl;

Cancellable async functions accept an optional weaveffi_cancel_token* parameter.

CMake integration

The generated CMakeLists.txt defines an INTERFACE library target called weaveffi_cpp:

cmake_minimum_required(VERSION 3.14)
project(weaveffi_cpp)
add_library(weaveffi_cpp INTERFACE)
target_include_directories(weaveffi_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(weaveffi_cpp INTERFACE weaveffi)
target_compile_features(weaveffi_cpp INTERFACE cxx_std_17)

To use in your project, add the generated directory as a subdirectory and link your target:

add_subdirectory(path/to/generated/cpp)
add_executable(myapp main.cpp)
target_link_libraries(myapp weaveffi_cpp)

This automatically adds the header include path, links the weaveffi native library, and requires C++17. Then include the header:

#include "weaveffi.hpp"

Make sure the Rust-built shared library (libweaveffi.dylib, libweaveffi.so, or weaveffi.dll) is discoverable at link and run time.

Dart

The Dart generator produces a pure-Dart FFI package that wraps the C ABI using dart:ffi. It uses DynamicLibrary.open to load the shared library at runtime and lookupFunction to resolve each C symbol. No native compilation step or code generation tooling (e.g. ffigen) is required — the generated .dart file is ready to use.

Why dart:ffi?

  • Built into the Dart SDK. dart:ffi ships with Dart since 2.6 and is the official mechanism for calling native code.
  • Works with Flutter. The same bindings work in Flutter apps on iOS, Android, macOS, Linux, and Windows.
  • No build step. The generated Dart file is plain Dart — add the package as a dependency and import it.
  • Null-safe. Generated code uses Dart’s sound null-safety throughout.

Generated artifacts

FilePurpose
dart/lib/weaveffi.dartdart:ffi bindings: library loader, typedefs, lookup bindings, wrapper functions, enum/struct classes
dart/pubspec.yamlPackage metadata (name, SDK constraint, ffi dependency)
dart/README.mdBasic usage instructions

dart:ffi approach

All native calls go through a single DynamicLibrary instance. For each C symbol, the generator emits:

  1. A native typedef describing the C function signature using FFI types (Int32, Pointer<Utf8>, etc.).
  2. A Dart typedef describing the equivalent Dart signature (int, Pointer<Utf8>, etc.).
  3. A lookupFunction call that resolves the symbol at load time.
  4. A wrapper function with idiomatic Dart types (String, bool, enum classes, struct classes) that handles marshalling, calls the looked-up function, checks for errors, and converts the result.

Every C function takes a trailing Pointer<_WeaveffiError> parameter. The wrapper allocates this struct via calloc, passes it to the native call, and calls _checkError afterward to convert non-zero error codes into a WeaveffiException.

Generated code examples

Given this IDL definition:

version: "0.1.0"
modules:
  - name: contacts
    enums:
      - name: ContactType
        doc: Type of contact
        variants:
          - { name: Personal, value: 0 }
          - { name: Work, value: 1 }
          - { name: Other, value: 2 }

    structs:
      - name: Contact
        doc: A contact record
        fields:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }
          - { name: contact_type, type: ContactType }

    functions:
      - name: create_contact
        params:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: contact_type, type: ContactType }
        return: handle

      - name: get_contact
        params:
          - { name: id, type: handle }
        return: Contact

      - name: find_contact
        params:
          - { name: id, type: i32 }
        return: "Contact?"

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

      - name: count_contacts
        params: []
        return: i32

Library loader

The generated module auto-detects the platform and loads the shared library:

DynamicLibrary _openLibrary() {
  if (Platform.isMacOS) return DynamicLibrary.open('libweaveffi.dylib');
  if (Platform.isLinux) return DynamicLibrary.open('libweaveffi.so');
  if (Platform.isWindows) return DynamicLibrary.open('weaveffi.dll');
  throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}');
}

final DynamicLibrary _lib = _openLibrary();

Enums

Enums map to Dart enhanced enums with an int value field. Variant names are converted to lowerCamelCase:

/// Type of contact
enum ContactType {
  personal(0),
  work(1),
  other(2),
  ;
  const ContactType(this.value);
  final int value;

  static ContactType fromValue(int value) =>
      ContactType.values.firstWhere((e) => e.value == value);
}

Enum parameters are passed as .value (an int mapped to Int32); enum returns are converted back via fromValue.

Structs (opaque wrapper classes)

Structs are wrapped as Dart classes holding a Pointer<Void> to the Rust-allocated data. A dispose() method calls the C ABI destroy function. Field access is through getters that call the C ABI getter functions:

/// A contact record
class Contact {
  final Pointer<Void> _handle;
  Contact._(this._handle);

  void dispose() {
    _weaveffiContactsContactDestroy(_handle);
  }

  String get name {
    final err = calloc<_WeaveffiError>();
    try {
      final result = _weaveffiContactsContactGetName(_handle, err);
      _checkError(err);
      return result.toDartString();
    } finally {
      calloc.free(err);
    }
  }

  String? get email {
    final err = calloc<_WeaveffiError>();
    try {
      final result = _weaveffiContactsContactGetEmail(_handle, err);
      _checkError(err);
      if (result == nullptr) return null;
      return result.toDartString();
    } finally {
      calloc.free(err);
    }
  }

  int get age {
    final err = calloc<_WeaveffiError>();
    try {
      final result = _weaveffiContactsContactGetAge(_handle, err);
      _checkError(err);
      return result;
    } finally {
      calloc.free(err);
    }
  }

  ContactType get contactType {
    final err = calloc<_WeaveffiError>();
    try {
      final result = _weaveffiContactsContactGetContactType(_handle, err);
      _checkError(err);
      return ContactType.fromValue(result);
    } finally {
      calloc.free(err);
    }
  }
}

Functions

Each IDL function produces a set of typedefs, a lookupFunction binding, and a top-level wrapper function. String parameters are marshalled to native UTF-8 via toNativeUtf8() and freed in a finally block:

typedef _NativeWeaveffiContactsCreateContact =
    Int64 Function(Pointer<Utf8>, Pointer<Utf8>, Int32, Pointer<_WeaveffiError>);
typedef _DartWeaveffiContactsCreateContact =
    int Function(Pointer<Utf8>, Pointer<Utf8>, int, Pointer<_WeaveffiError>);
final _weaveffiContactsCreateContact = _lib.lookupFunction<
    _NativeWeaveffiContactsCreateContact,
    _DartWeaveffiContactsCreateContact>('weaveffi_contacts_create_contact');

int createContact(String name, String? email, ContactType contactType) {
  final err = calloc<_WeaveffiError>();
  final namePtr = name.toNativeUtf8();
  try {
    final result = _weaveffiContactsCreateContact(
        namePtr, email, contactType.value, err);
    _checkError(err);
    return result;
  } finally {
    calloc.free(namePtr);
    calloc.free(err);
  }
}

Contact getContact(int id) {
  final err = calloc<_WeaveffiError>();
  try {
    final result = _weaveffiContactsGetContact(id, err);
    _checkError(err);
    return Contact._(result);
  } finally {
    calloc.free(err);
  }
}

Contact? findContact(int id) {
  final err = calloc<_WeaveffiError>();
  try {
    final result = _weaveffiContactsFindContact(id, err);
    _checkError(err);
    if (result == nullptr) return null;
    return Contact._(result);
  } finally {
    calloc.free(err);
  }
}

Type mapping reference

IDL typeDart typeNative FFI typeDart FFI type
i32intInt32int
u32intUint32int
i64intInt64int
f64doubleDoubledouble
boolboolInt32int
stringStringPointer<Utf8>Pointer<Utf8>
bytesList<int>Pointer<Uint8>Pointer<Uint8>
handleintInt64int
StructNameStructNamePointer<Void>Pointer<Void>
EnumNameEnumNameInt32int
T?T?same as inner typesame as inner type
[T]List<T>Pointer<Void>Pointer<Void>
{K: V}Map<K, V>Pointer<Void>Pointer<Void>

Booleans are transmitted as Int32 (0/1) because C has no standard fixed-width boolean type across ABIs. The wrapper converts with flag ? 1 : 0 for parameters and result != 0 for returns.

Null-safety

Generated code uses Dart’s sound null-safety:

  • Optional return types (T?) check the native pointer against nullptr before wrapping. If null, they return null:
Contact? findContact(int id) {
  // ...
  if (result == nullptr) return null;
  return Contact._(result);
}
  • Optional struct fields (e.g. string?) produce nullable getters (String?) that guard against null pointers:
String? get email {
  // ...
  if (result == nullptr) return null;
  return result.toDartString();
}
  • Non-optional types are always non-nullable in the generated API. A non-optional struct return that receives a null pointer from the C layer will surface as a WeaveffiException via the error-checking mechanism.

Async support

Functions marked async: true in the IDL produce both a synchronous helper (prefixed with _) and a public Future-returning wrapper that runs the FFI call on a separate Isolate via Isolate.run:

String _fetchData(int id) {
  final err = calloc<_WeaveffiError>();
  try {
    final result = _weaveffiMathFetchData(id, err);
    _checkError(err);
    return result.toDartString();
  } finally {
    calloc.free(err);
  }
}

Future<String> fetchData(int id) async {
  return await Isolate.run(() => _fetchData(id));
}

This keeps the main isolate’s event loop responsive while the Rust function executes. The dart:isolate import is only included when the API contains at least one async function.

Error handling

Native errors are propagated through a _WeaveffiError FFI struct containing an integer code and a UTF-8 message pointer. After every native call, _checkError inspects the struct and throws WeaveffiException when the code is non-zero:

final class _WeaveffiError extends Struct {
  @Int32()
  external int code;
  external Pointer<Utf8> message;
}

class WeaveffiException implements Exception {
  final int code;
  final String message;
  WeaveffiException(this.code, this.message);
  @override
  String toString() => 'WeaveffiException($code): $message';
}

void _checkError(Pointer<_WeaveffiError> err) {
  if (err.ref.code != 0) {
    final msg = err.ref.message.toDartString();
    _weaveffiErrorClear(err);
    throw WeaveffiException(err.ref.code, msg);
  }
}

Catch errors in consumer code:

try {
  final contact = getContact(42);
  print(contact.name);
} on WeaveffiException catch (e) {
  print('Error ${e.code}: ${e.message}');
}

Memory management

Strings

  • Passing strings in: Dart String values are converted to native UTF-8 via toNativeUtf8() (from package:ffi). The resulting pointer is freed in a finally block via calloc.free().
  • Receiving strings back: Returned Pointer<Utf8> values are decoded via toDartString().

Structs (opaque pointers)

Struct wrappers hold a Pointer<Void>. The dispose() method calls the corresponding C ABI _destroy function. Callers are responsible for calling dispose() when done:

final contact = getContact(id);
try {
  print(contact.name);
  print(contact.email ?? '(none)');
} finally {
  contact.dispose();
}

Using in a Flutter project

1. Generate bindings

weaveffi generate --input api.yaml --output generated/ --target dart

2. Build the Rust shared library

Cross-compile for each Flutter target platform:

# iOS
cargo build --target aarch64-apple-ios --release

# Android
cargo build --target aarch64-linux-android --release

# macOS
cargo build --target aarch64-apple-darwin --release

# Linux
cargo build --target x86_64-unknown-linux-gnu --release

3. Add the generated package

Reference the generated package from your Flutter app’s pubspec.yaml:

dependencies:
  weaveffi:
    path: ../generated/dart

4. Bundle the shared library

Place the compiled shared library where Flutter can find it at runtime:

  • iOS/macOS: Add as a framework or use a podspec to bundle libweaveffi.dylib.
  • Android: Place .so files under android/src/main/jniLibs/{abi}/.
  • Linux/Windows: Place next to the executable or on the library search path.

5. Import and use

import 'package:weaveffi/weaveffi.dart';

void main() {
  final handle = createContact('Alice', 'alice@example.com', ContactType.work);
  final contact = getContact(handle);
  print('${contact.name} (${contact.email})');
  print('Total: ${countContacts()}');
  contact.dispose();
}

Build and test (standalone Dart)

1. Generate bindings

weaveffi generate --input api.yaml --output generated/ --target dart

2. Build the Rust shared library

cargo build --release -p your_library

3. Make the shared library findable

macOS:

DYLD_LIBRARY_PATH=../../target/release dart run example/main.dart

Linux:

LD_LIBRARY_PATH=../../target/release dart run example/main.dart

Windows: Place weaveffi.dll in the same directory as your script, or add its directory to PATH.

Go

The Go generator produces idiomatic Go bindings that use CGo to call the C ABI directly. It generates a single Go source file and a go.mod module descriptor, ready to be imported by any Go project.

Why CGo?

  • Standard toolchain. CGo is part of the Go distribution — no third-party tools or custom build steps needed.
  • Direct C interop. Go can call C functions through the import "C" pseudo-package with minimal overhead.
  • Stable ABI. The generated code links against the same stable C ABI shared library used by all other language targets.

The trade-off is that CGo builds are slower than pure Go and require a C compiler (gcc or clang) to be available. For FFI workloads the overhead is negligible compared to the work done inside the Rust library.

Generated artifacts

FilePurpose
go/weaveffi.goCGo bindings: preamble, type wrappers, function wrappers
go/go.modGo module descriptor (configurable module path)
go/README.mdPrerequisites and build instructions

The CGo approach

The generated weaveffi.go file opens with a CGo preamble comment block that tells the Go toolchain how to link the shared library and which headers to include:

package weaveffi

/*
#cgo LDFLAGS: -lweaveffi
#include "weaveffi.h"
#include <stdlib.h>
*/
import "C"

import (
	"fmt"
	"unsafe"
)

The #cgo LDFLAGS directive links against libweaveffi. At build time, CGo compiles the preamble with a C compiler and generates the glue code that lets Go call C functions. The unsafe package is imported only when the API includes string, bytes, or collection types that require pointer manipulation.

Generated code examples

Given this IDL definition:

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

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

    functions:
      - name: create_contact
        params:
          - { name: first_name, type: string }
          - { name: last_name, type: string }
          - { name: email, type: "string?" }
          - { name: contact_type, type: ContactType }
        return: handle

      - name: get_contact
        params:
          - { name: id, type: handle }
        return: Contact

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

      - name: delete_contact
        params:
          - { name: id, type: handle }
        return: bool

      - name: count_contacts
        params: []
        return: i32

Enums

Enums map to Go int32 type aliases with named constants:

type ContactType int32

const (
	ContactTypePersonal ContactType = 0
	ContactTypeWork     ContactType = 1
	ContactTypeOther    ContactType = 2
)

Structs (opaque wrapper types)

Structs are represented as Go structs holding a pointer to the Rust-allocated opaque C type. Field access is through getter methods that call the C ABI getter functions. A Close() method calls the C ABI destroy function to free the underlying memory:

type Contact struct {
	ptr *C.weaveffi_contacts_Contact
}

func (s *Contact) Id() int64 {
	return int64(C.weaveffi_contacts_Contact_get_id(s.ptr))
}

func (s *Contact) FirstName() string {
	return C.GoString(C.weaveffi_contacts_Contact_get_first_name(s.ptr))
}

func (s *Contact) Email() *string {
	cStr := C.weaveffi_contacts_Contact_get_email(s.ptr)
	if cStr == nil {
		return nil
	}
	v := C.GoString(cStr)
	return &v
}

func (s *Contact) ContactType() ContactType {
	return ContactType(C.weaveffi_contacts_Contact_get_contact_type(s.ptr))
}

func (s *Contact) Close() {
	if s.ptr != nil {
		C.weaveffi_contacts_Contact_destroy(s.ptr)
		s.ptr = nil
	}
}

Functions

Each IDL function becomes a Go function with PascalCase naming (module_function becomes ModuleFunction). Every function returns an error as its last return value. The wrapper marshals Go types to C types, calls the C ABI function, checks for errors, and converts the result back:

func ContactsCreateContact(firstName string, lastName string, email *string, contactType ContactType) (int64, error) {
	cFirstName := C.CString(firstName)
	defer C.free(unsafe.Pointer(cFirstName))
	cLastName := C.CString(lastName)
	defer C.free(unsafe.Pointer(cLastName))
	var cEmail *C.char
	if email != nil {
		cEmail = C.CString(*email)
		defer C.free(unsafe.Pointer(cEmail))
	}
	var cErr C.weaveffi_error
	result := C.weaveffi_contacts_create_contact(cFirstName, cLastName, cEmail, C.weaveffi_contacts_ContactType(contactType), &cErr)
	if cErr.code != 0 {
		goErr := fmt.Errorf("weaveffi: %s (code %d)", C.GoString(cErr.message), int(cErr.code))
		C.weaveffi_error_clear(&cErr)
		return 0, goErr
	}
	return int64(result), nil
}

func ContactsGetContact(id int64) (*Contact, error) {
	var cErr C.weaveffi_error
	result := C.weaveffi_contacts_get_contact(C.weaveffi_handle_t(id), &cErr)
	if cErr.code != 0 {
		goErr := fmt.Errorf("weaveffi: %s (code %d)", C.GoString(cErr.message), int(cErr.code))
		C.weaveffi_error_clear(&cErr)
		return nil, goErr
	}
	return &Contact{ptr: result}, nil
}

func ContactsCountContacts() (int32, error) {
	var cErr C.weaveffi_error
	result := C.weaveffi_contacts_count_contacts(&cErr)
	if cErr.code != 0 {
		goErr := fmt.Errorf("weaveffi: %s (code %d)", C.GoString(cErr.message), int(cErr.code))
		C.weaveffi_error_clear(&cErr)
		return 0, goErr
	}
	return int32(result), nil
}

Void functions return only error:

func SystemReset() error {
	var cErr C.weaveffi_error
	C.weaveffi_system_reset(&cErr)
	if cErr.code != 0 {
		goErr := fmt.Errorf("weaveffi: %s (code %d)", C.GoString(cErr.message), int(cErr.code))
		C.weaveffi_error_clear(&cErr)
		return goErr
	}
	return nil
}

Optional handling

Optional types map to Go pointer types (*T) for scalars and strings. Struct and collection optionals use their natural nil-able representations (pointers and slices are already nil-able in Go):

// Optional scalar parameter: *int32
var cId *C.int32_t
if id != nil {
	tmp := C.int32_t(*id)
	cId = &tmp
}

// Optional string parameter: *string
var cEmail *C.char
if email != nil {
	cEmail = C.CString(*email)
	defer C.free(unsafe.Pointer(cEmail))
}

// Optional struct return: *Contact
if result == nil {
	return nil, nil
}
return &Contact{ptr: result}, nil

List/Array handling

List types map to Go slices ([]T). Parameters are passed as pointer+length pairs to the C ABI. Return values are converted from a C pointer+length pair using unsafe.Slice:

// List return: []int32
var cOutLen C.size_t
result := C.weaveffi_store_list_ids(&cOutLen, &cErr)
// ... error check ...
count := int(cOutLen)
if count == 0 || result == nil {
	return nil, nil
}
goResult := make([]int32, count)
cSlice := unsafe.Slice((*C.int32_t)(unsafe.Pointer(result)), count)
for i, v := range cSlice {
	goResult[i] = int32(v)
}
return goResult, nil

Type mapping reference

IDL typeGo typeC type (CGo)
i32int32C.int32_t
u32uint32C.uint32_t
i64int64C.int64_t
f64float64C.double
boolboolC._Bool
stringstring*C.char (via C.CString/C.GoString)
bytes[]byte*C.uint8_t + C.size_t
handleint64C.weaveffi_handle_t
Struct*StructName*C.weaveffi_mod_Struct
EnumEnumNameC.weaveffi_mod_Enum
T?*Tpointer to scalar; nil-able pointer for strings/structs
[T][]Tpointer + C.size_t
{K: V}map[K]Vkey/value arrays + C.size_t

Booleans use C._Bool rather than an integer type, matching the CGo mapping of C’s _Bool.

Error handling

Every generated Go function returns error as its last return value, following Go’s idiomatic error-handling convention. The C ABI uses a weaveffi_error struct (with code and message fields) as an out parameter on every function call.

The generated wrapper:

  1. Declares a C.weaveffi_error variable.
  2. Passes its address as the last argument to the C function.
  3. Checks cErr.code != 0 after the call.
  4. On error, extracts the message with C.GoString, clears the C-side error with C.weaveffi_error_clear, and returns a Go error via fmt.Errorf along with the zero value for the return type.
var cErr C.weaveffi_error
result := C.weaveffi_calculator_add(C.int32_t(a), C.int32_t(b), &cErr)
if cErr.code != 0 {
	goErr := fmt.Errorf("weaveffi: %s (code %d)", C.GoString(cErr.message), int(cErr.code))
	C.weaveffi_error_clear(&cErr)
	return 0, goErr
}
return int32(result), nil

Callers use standard Go error checking:

sum, err := weaveffi.CalculatorAdd(2, 3)
if err != nil {
	log.Fatalf("add failed: %v", err)
}
fmt.Println(sum)

Memory management

Strings

  • Passing strings in: Go strings are converted to C strings via C.CString(), which allocates a copy in C memory. A defer C.free() ensures the copy is freed after the C call returns.
  • Receiving strings back: Returned C strings are converted to Go strings via C.GoString(), which copies the data into Go-managed memory. The wrapper then calls C.weaveffi_free_string() to free the Rust-allocated original.

Bytes

  • Passing bytes in: A pointer to the first element of the byte slice is passed with a length parameter. The slice data is valid for the duration of the C call (no copy needed).
  • Receiving bytes back: The wrapper uses C.GoBytes() to copy the data into a Go byte slice, then calls C.weaveffi_free_bytes() to free the Rust-allocated buffer.

Structs (opaque pointers)

Struct wrappers hold a typed C pointer (*C.weaveffi_mod_Struct). The Close() method calls the corresponding _destroy C function to free the Rust-side allocation and sets the pointer to nil to prevent double-free:

func (s *Contact) Close() {
	if s.ptr != nil {
		C.weaveffi_contacts_Contact_destroy(s.ptr)
		s.ptr = nil
	}
}

Unlike Swift (which uses deinit) or Python (which uses __del__), Go does not have deterministic destructors. Callers must explicitly call Close() when done with a struct, or use defer:

contact, err := weaveffi.ContactsGetContact(id)
if err != nil {
	log.Fatal(err)
}
defer contact.Close()
fmt.Println(contact.FirstName())

Boolean helpers

When the API uses boolean types, the generator includes helper functions to convert between Go bool and CGo C._Bool:

func boolToC(b bool) C._Bool {
	if b {
		return 1
	}
	return 0
}

func cToBool(b C._Bool) bool {
	return b != 0
}

These helpers are only emitted when the API actually uses booleans, keeping the generated code minimal.

Build and usage

1. Generate bindings

weaveffi generate --input api.yaml --output generated/ --target go

2. Build the Rust shared library

cargo build --release -p your_library

3. Set up CGo environment

Point CGo at the header and shared library:

export CGO_CFLAGS="-I/path/to/headers"
export CGO_LDFLAGS="-L/path/to/lib -lweaveffi"

4. Use in your Go project

package main

import (
	"fmt"
	"log"

	"weaveffi"
)

func main() {
	sum, err := weaveffi.CalculatorAdd(2, 3)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("2 + 3 = %d\n", sum)
}

Configuration

The go.mod module path defaults to weaveffi but can be customized via generator configuration:

generators:
  go:
    module_path: "github.com/myorg/mylib"

This produces a go.mod with module github.com/myorg/mylib instead of the default.

Ruby

The Ruby generator produces pure-Ruby FFI bindings using the ffi gem to call the C ABI directly. No compilation step, no native extensions — just require 'ffi' and go.

Why the FFI gem?

  • Minimal dependency. The ffi gem is the standard Ruby library for calling native code. It is battle-tested in projects like GRPC, Sass, and libsodium bindings.
  • No C compiler required. The generated .rb files are plain Ruby — no Makefile, no extconf.rb, no build step beyond gem install ffi.
  • Transparent. Developers can read and debug the generated code directly.

The trade-off is that FFI gem calls are slower than hand-written C extensions. For most FFI workloads the overhead is negligible compared to the work done inside the Rust library.

Generated artifacts

FilePurpose
ruby/lib/weaveffi.rbFFI bindings: library loader, attach_function declarations, wrapper classes
ruby/weaveffi.gemspecGem specification with ffi ~> 1.15 dependency
ruby/README.mdPrerequisites and usage instructions

The FFI gem approach

The generated module extends FFI::Library and uses attach_function to bind each C ABI symbol. Platform detection selects the correct shared library name at load time:

require 'ffi'

module WeaveFFI
  extend FFI::Library

  case FFI::Platform::OS
  when /darwin/
    ffi_lib 'libweaveffi.dylib'
  when /mswin|mingw/
    ffi_lib 'weaveffi.dll'
  else
    ffi_lib 'libweaveffi.so'
  end
end

Every C ABI function is declared with attach_function, mapping parameter types and return types to FFI type symbols (:int32, :pointer, etc.). A thin Ruby method then wraps each raw call with argument conversion, error checking, and return-value marshalling.

Generated code examples

Given this IDL definition:

version: "0.1.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 }

    functions:
      - name: create_contact
        params:
          - { name: first_name, type: string }
          - { name: last_name, type: string }
          - { name: email, type: "string?" }
          - { name: contact_type, type: ContactType }
        return: handle

      - name: get_contact
        params:
          - { name: id, type: handle }
        return: Contact

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

      - name: count_contacts
        params: []
        return: i32

Enums

Enums map to Ruby modules with SHOUTY_SNAKE_CASE constants:

module ContactType
  PERSONAL = 0
  WORK = 1
  OTHER = 2
end

Enum values are plain integers and are passed directly to the C ABI.

Structs (AutoPointer wrapper classes)

Structs become Ruby classes with an FFI::AutoPointer handle. The AutoPointer ensures the C ABI _destroy function is called when the object is garbage collected, preventing memory leaks:

class ContactPtr < FFI::AutoPointer
  def self.release(ptr)
    WeaveFFI.weaveffi_contacts_Contact_destroy(ptr)
  end
end

class Contact
  attr_reader :handle

  def initialize(handle)
    @handle = ContactPtr.new(handle)
  end

  def self.create(handle)
    new(handle)
  end

  def destroy
    return if @handle.nil?
    @handle.free
    @handle = nil
  end

  def id
    result = WeaveFFI.weaveffi_contacts_Contact_get_id(@handle)
    result
  end

  def first_name
    result = WeaveFFI.weaveffi_contacts_Contact_get_first_name(@handle)
    return '' if result.null?
    str = result.read_string
    WeaveFFI.weaveffi_free_string(result)
    str
  end

  def email
    result = WeaveFFI.weaveffi_contacts_Contact_get_email(@handle)
    return nil if result.null?
    str = result.read_string
    WeaveFFI.weaveffi_free_string(result)
    str
  end

  def contact_type
    result = WeaveFFI.weaveffi_contacts_Contact_get_contact_type(@handle)
    result
  end
end

Functions

Each IDL function becomes a class method on the module. The wrapper creates an ErrorStruct, calls the C symbol, checks for errors, and converts the return value:

def self.create_contact(first_name, last_name, email, contact_type)
  err = ErrorStruct.new
  result = weaveffi_contacts_create_contact(
    first_name, last_name, email, contact_type, err)
  check_error!(err)
  result
end

def self.get_contact(id)
  err = ErrorStruct.new
  result = weaveffi_contacts_get_contact(id, err)
  check_error!(err)
  raise Error.new(-1, 'null pointer') if result.null?
  Contact.new(result)
end

def self.list_contacts
  err = ErrorStruct.new
  out_len = FFI::MemoryPointer.new(:size_t)
  result = weaveffi_contacts_list_contacts(out_len, err)
  check_error!(err)
  return [] if result.null?
  len = out_len.read(:size_t)
  result.read_array_of_pointer(len).map { |p| Contact.new(p) }
end

def self.count_contacts
  err = ErrorStruct.new
  result = weaveffi_contacts_count_contacts(err)
  check_error!(err)
  result
end

Error handling

The generated module defines an ErrorStruct (mirroring the C weaveffi_error) and an Error exception class. Every function call follows this pattern:

class ErrorStruct < FFI::Struct
  layout :code, :int32,
         :message, :pointer
end

class Error < StandardError
  attr_reader :code

  def initialize(code, message)
    @code = code
    super(message)
  end
end

def self.check_error!(err)
  return if err[:code].zero?
  code = err[:code]
  msg_ptr = err[:message]
  msg = msg_ptr.null? ? '' : msg_ptr.read_string
  weaveffi_error_clear(err.to_ptr)
  raise Error.new(code, msg)
end

Callers use standard Ruby begin/rescue:

require 'weaveffi'

begin
  handle = WeaveFFI.create_contact("Alice", "Smith", nil, ContactType::WORK)
rescue WeaveFFI::Error => e
  puts "Error #{e.code}: #{e.message}"
end

Type mapping reference

IDL typeRuby typeFFI type
i32Integer:int32
u32Integer:uint32
i64Integer:int64
f64Float:double
booltrue/false:int32 (0/1 conversion)
stringString:string (param) / :pointer (return)
bytesString (binary):pointer + :size_t
handleInteger:uint64
StructStructName:pointer
EnumInteger:int32
T?T or nil:pointer for scalars; same pointer for strings/structs
[T]Array:pointer + :size_t
{K: V}Hashkey/value pointer arrays + :size_t

Booleans are transmitted as :int32 (0/1). The wrapper converts true/false to integers on input and back to booleans on output.

Gem packaging

1. Generate bindings

weaveffi generate --input api.yaml --output generated/ --target ruby

2. Build the Rust shared library

cargo build --release -p your_library

This produces libweaveffi.dylib (macOS), libweaveffi.so (Linux), or weaveffi.dll (Windows) in target/release/.

3. Build and install the gem

cd generated/ruby
gem build weaveffi.gemspec
gem install weaveffi-0.1.0.gem

The generated gemspec declares ffi ~> 1.15 as its only runtime dependency.

4. Make the shared library findable

The shared library must be on the system library search path at runtime:

macOS:

DYLD_LIBRARY_PATH=../../target/release ruby your_script.rb

Linux:

LD_LIBRARY_PATH=../../target/release ruby your_script.rb

Windows: Place weaveffi.dll in the same directory as your script, or add its directory to PATH.

5. Use the bindings

require 'weaveffi'

handle = WeaveFFI.create_contact("Alice", "Smith", "alice@example.com",
                                  WeaveFFI::ContactType::WORK)
contact = WeaveFFI.get_contact(handle)
puts "#{contact.first_name} #{contact.last_name}"
puts "Email: #{contact.email || '(none)'}"
puts "Total: #{WeaveFFI.count_contacts}"

Memory management

The generated Ruby wrappers handle memory ownership automatically via FFI::AutoPointer and explicit free calls.

Strings

  • Passing strings in: Ruby strings are passed as :string parameters. FFI handles the encoding to null-terminated C strings automatically.
  • Receiving strings back: Returned :pointer values are read with read_string, then the Rust-allocated pointer is freed via weaveffi_free_string. The wrapper copies the data into a Ruby string before freeing.

Bytes

  • Passing bytes in: A FFI::MemoryPointer is allocated, the byte data is copied in via put_bytes, and the pointer is passed with a length parameter.
  • Receiving bytes back: The C function writes to an out_len parameter. The wrapper reads the data via read_string(len), then the Rust side is responsible for the original buffer.

Structs (AutoPointer release callbacks)

Each struct class uses FFI::AutoPointer to ensure automatic cleanup. AutoPointer calls the release class method when the Ruby object is garbage collected, which invokes the C ABI _destroy function:

class ContactPtr < FFI::AutoPointer
  def self.release(ptr)
    WeaveFFI.weaveffi_contacts_Contact_destroy(ptr)
  end
end

For explicit lifetime control, call destroy to free immediately:

contact = WeaveFFI.get_contact(handle)
puts contact.first_name
contact.destroy

Maps

Maps are passed across the FFI boundary as parallel arrays of keys and values plus a length. The wrapper builds FFI::MemoryPointer buffers for keys and values, and reconstructs a Ruby Hash from the returned arrays using each_with_object.

Configuration

The Ruby module name and gem name can be customized via generator configuration:

ruby_module_name = "MyBindings"
ruby_gem_name = "my_bindings"

This changes the generated module MyBindings declaration and the gemspec s.name field.

API

Reference documentation for the WeaveFFI Rust crates.

API docs are generated from source via cargo doc:

cargo doc --workspace --all-features --no-deps --open

When the documentation site is deployed, API docs are available under the API section.

Rust API (cargo doc)

Generate and view the Rust API docs locally:

cargo doc --workspace --all-features --no-deps --open

When the documentation site is deployed, API docs are available at weavefoundry.github.io/weaveffi/api/rust/weaveffi_core/.

Guides

Practical guides for working with WeaveFFI-generated bindings across languages.

  • Memory Ownership — allocation rules, freeing strings, bytes, structs, and errors across the FFI boundary.
  • Error Handling — the uniform error model and how each target language surfaces failures.
  • Annotated Rust Extraction — extract an API definition from annotated Rust source instead of writing YAML by hand.
  • Generator Configuration — customise Swift module names, Android packages, C prefixes, and other generator options via weaveffi.toml.

Memory Ownership Guide

WeaveFFI exposes Rust functionality through a stable C ABI. Because Rust and C (and Swift, Kotlin, etc.) have fundamentally different memory models, every allocation that crosses the FFI boundary follows strict ownership rules.

Golden rule: whoever allocates the memory owns it, and ownership must be explicitly transferred back for deallocation. Rust allocates; the caller frees through the designated weaveffi_free_* functions.

String ownership

Rust-returned strings are NUL-terminated, UTF-8, heap-allocated C strings created via CString::into_raw. The caller must free them with weaveffi_free_string after use.

C

weaveffi_error err = {0, NULL};
const char* echoed = weaveffi_calculator_echo(
    (const uint8_t*)"hello", 5, &err);
if (err.code) {
    fprintf(stderr, "%s\n", err.message);
    weaveffi_error_clear(&err);
    return 1;
}

printf("result: %s\n", echoed);
weaveffi_free_string(echoed);   // REQUIRED — Rust allocated this

Swift

var err = weaveffi_error(code: 0, message: nil)
let raw = weaveffi_calculator_echo(
    Array("hello".utf8), 5, &err)
// ... check err ...

if let raw = raw {
    let result = String(cString: raw)
    weaveffi_free_string(raw)   // REQUIRED — Rust allocated this
    print(result)
}

The generated Swift wrapper handles this automatically with defer:

let raw = weaveffi_calculator_echo(...)
defer { weaveffi_free_string(raw) }
return String(cString: raw!)

Common mistakes

// BUG: use-after-free — reading string after freeing it
const char* name = weaveffi_contacts_Contact_get_name(contact);
weaveffi_free_string(name);
printf("name: %s\n", name);    // UNDEFINED BEHAVIOR

// BUG: double-free — freeing the same pointer twice
const char* s = weaveffi_calculator_echo((const uint8_t*)"hi", 2, &err);
weaveffi_free_string(s);
weaveffi_free_string(s);       // UNDEFINED BEHAVIOR

// BUG: memory leak — forgetting to free
const char* s = weaveffi_calculator_echo((const uint8_t*)"hi", 2, &err);
printf("%s\n", s);
// missing weaveffi_free_string(s) — memory leaked

Byte buffer ownership

Byte buffers (bytes type) are returned as a const uint8_t* with a separate size_t* out_len output parameter. The caller must free them with weaveffi_free_bytes(ptr, len).

C

size_t out_len = 0;
const uint8_t* buf = weaveffi_module_get_data(&out_len, &err);
if (err.code) {
    weaveffi_error_clear(&err);
    return 1;
}

// Copy what you need before freeing
process_data(buf, out_len);
weaveffi_free_bytes((uint8_t*)buf, out_len);  // REQUIRED

Swift

var outLen: Int = 0
let raw = weaveffi_module_get_data(&outLen, &err)
guard let raw = raw else { return Data() }
defer { weaveffi_free_bytes(UnsafeMutablePointer(mutating: raw), outLen) }
let data = Data(bytes: raw, count: outLen)

Common mistakes

// BUG: wrong length — passing incorrect length to free_bytes
size_t len = 0;
const uint8_t* buf = weaveffi_module_get_data(&len, &err);
weaveffi_free_bytes((uint8_t*)buf, 0);    // WRONG length — undefined behavior

// BUG: forgetting to free
size_t len = 0;
const uint8_t* buf = weaveffi_module_get_data(&len, &err);
// missing weaveffi_free_bytes — memory leaked

Struct lifecycle

Structs are opaque on the C side. Their lifecycle follows a strict pattern:

  1. _create allocates and returns a pointer. Caller owns it.
  2. _destroy frees the struct. Must be called exactly once.
  3. _get_* getters read fields. Primitive getters (i32, f64, bool) return values directly — no memory management needed. String and bytes getters return new owned copies that the caller must free separately.

C

weaveffi_error err = {0, NULL};

// 1. Create — caller now owns the struct
weaveffi_contacts_Contact* contact = weaveffi_contacts_Contact_create(
    (const uint8_t*)"Alice", 5,
    (const uint8_t*)"alice@example.com", 17,
    30,
    &err);
if (err.code) {
    weaveffi_error_clear(&err);
    return 1;
}

// 2. Read fields — primitive getter, no free needed
int32_t age = weaveffi_contacts_Contact_get_age(contact);
printf("age: %d\n", age);

// 3. Read fields — string getter returns owned copy, must free
const char* name = weaveffi_contacts_Contact_get_name(contact);
printf("name: %s\n", name);
weaveffi_free_string(name);   // free the getter's returned string

// 4. Destroy — frees the struct itself
weaveffi_contacts_Contact_destroy(contact);

Swift

The generated Swift wrapper wraps the opaque pointer in a class whose deinit calls _destroy automatically:

public class Contact {
    let ptr: OpaquePointer

    init(ptr: OpaquePointer) { self.ptr = ptr }

    deinit { weaveffi_contacts_Contact_destroy(ptr) }

    public var name: String {
        let raw = weaveffi_contacts_Contact_get_name(ptr)
        guard let raw = raw else { return "" }
        defer { weaveffi_free_string(raw) }
        return String(cString: raw)
    }

    public var age: Int32 {
        return weaveffi_contacts_Contact_get_age(ptr)
    }
}

Swift’s ARC ensures deinit runs when the last reference is dropped. String getters use defer { weaveffi_free_string(...) } to free after copying into a Swift String.

Common mistakes

// BUG: use-after-free — accessing struct after destroying it
weaveffi_contacts_Contact_destroy(contact);
int32_t age = weaveffi_contacts_Contact_get_age(contact);  // UNDEFINED BEHAVIOR

// BUG: double-free — destroying twice
weaveffi_contacts_Contact_destroy(contact);
weaveffi_contacts_Contact_destroy(contact);  // UNDEFINED BEHAVIOR

// BUG: leaking getter string — getter returns owned copy
const char* name = weaveffi_contacts_Contact_get_name(contact);
// missing weaveffi_free_string(name) — leaked

// BUG: memory leak — forgetting to destroy
weaveffi_contacts_Contact* c = weaveffi_contacts_Contact_create(...);
// missing weaveffi_contacts_Contact_destroy(c) — struct leaked

Error struct lifecycle

Every FFI function takes a trailing weaveffi_error* out_err. On failure, Rust writes into out_err->code (non-zero) and out_err->message (a Rust-allocated C string). Clearing the error frees the message.

C

weaveffi_error err = {0, NULL};  // stack-allocated, zero-initialized

int32_t result = weaveffi_calculator_div(10, 0, &err);
if (err.code) {
    fprintf(stderr, "error %d: %s\n", err.code, err.message);
    weaveffi_error_clear(&err);  // frees err.message, zeroes fields
}

// err is now safe to reuse for the next call
result = weaveffi_calculator_add(1, 2, &err);

Swift

The generated Swift wrapper provides a check helper that copies the error message, clears the C error, and throws a Swift error:

var err = weaveffi_error(code: 0, message: nil)
let result = weaveffi_calculator_div(10, 0, &err)
try check(&err)  // throws WeaveFFIError, calls weaveffi_error_clear internally

Common mistakes

// BUG: leaking error message — forgetting to clear
weaveffi_error err = {0, NULL};
weaveffi_calculator_div(1, 0, &err);
if (err.code) {
    fprintf(stderr, "error: %s\n", err.message);
    // missing weaveffi_error_clear(&err) — err.message leaked
}

// BUG: use-after-free — reading message after clearing
weaveffi_error err = {0, NULL};
weaveffi_calculator_div(1, 0, &err);
if (err.code) {
    weaveffi_error_clear(&err);
    printf("%s\n", err.message);  // UNDEFINED BEHAVIOR — message was freed
}

Thread safety

All WeaveFFI-generated FFI functions are expected to be called from a single thread unless the module documentation explicitly states otherwise.

Concurrent calls from multiple threads into the same module may cause data races and undefined behavior. If you need multi-threaded access, synchronize externally (e.g., with a mutex or serial dispatch queue) on the calling side.

// CORRECT — all calls on the main thread
int32_t a = weaveffi_calculator_add(1, 2, &err);
int32_t b = weaveffi_calculator_mul(3, 4, &err);
// CORRECT — serialize access through a serial queue
let queue = DispatchQueue(label: "com.app.weaveffi")
queue.sync {
    let result = try? Calculator.add(a: 1, b: 2)
}

Summary

ResourceAllocatorFree functionNotes
Returned stringRustweaveffi_free_stringEvery const char* return
Returned bytesRustweaveffi_free_bytesPass both pointer and length
Struct instanceRust*_destroyCall exactly once
String from getterRustweaveffi_free_stringGetter returns an owned copy
Error messageRustweaveffi_error_clearClears code and frees message

Error Handling Guide

WeaveFFI uses a uniform error model across the FFI boundary. Every generated function carries an out-error parameter that reports success or failure through an integer code and an optional message string. Each target language maps this convention to its own idiomatic error mechanism.

The weaveffi_error struct

At the C ABI level, errors are represented by a simple struct:

typedef struct weaveffi_error {
    int32_t     code;
    const char* message;
} weaveffi_error;
FieldTypeDescription
codeint32_t0 = success, non-zero = error
messageconst char*NULL on success; Rust-allocated string on error

Every generated C function accepts a trailing weaveffi_error* out_err parameter. On success the runtime sets code = 0 and message = NULL. On failure it sets a non-zero code and writes a human-readable message.

Key rule: code = 0 always means success. Any non-zero value is an error. The runtime uses -1 as the default unspecified error code when no domain code applies.

After reading the error message you must call weaveffi_error_clear to free the Rust-allocated string. See the Memory Ownership Guide for details.

Defining error domains in the IDL

You can declare an error domain on a module to assign symbolic names and stable numeric codes to expected failure conditions. Error domains are optional — if omitted, errors still work but use the default code -1.

YAML syntax

version: "0.1.0"
modules:
  - name: contacts
    errors:
      name: ContactErrors
      codes:
        - name: not_found
          code: 1
          message: "Contact not found"
        - name: duplicate
          code: 2
          message: "Contact already exists"
        - name: invalid_email
          code: 3
          message: "Email address is invalid"

    functions:
      - name: create_contact
        params:
          - { name: name, type: string }
          - { name: email, type: string }
        return: handle

      - name: get_contact
        params:
          - { name: id, type: handle }
        return: string

Validation rules

The validator enforces these constraints on error domains:

  • Non-zero codes. code = 0 is reserved for success and will be rejected.
  • Unique names. No two error codes may share the same name.
  • Unique numeric codes. No two error codes may share the same code value.
  • No collision with functions. The error domain name must not match any function name in the same module.
  • Non-empty name. The error domain name must not be blank.

How each language maps errors

C

In C, the caller allocates a weaveffi_error on the stack, passes its address, and checks code after the call.

weaveffi_error err = {0, NULL};

const char* contact = weaveffi_contacts_get_contact(id, &err);
if (err.code) {
    fprintf(stderr, "error %d: %s\n", err.code,
            err.message ? err.message : "unknown");
    weaveffi_error_clear(&err);
    return 1;
}

printf("contact: %s\n", contact);
weaveffi_free_string(contact);

The pattern is always the same:

  1. Zero-initialize: weaveffi_error err = {0, NULL};
  2. Call the function, passing &err as the last argument.
  3. Check err.code — if non-zero, read err.message and clear with weaveffi_error_clear(&err).
  4. The error struct can be reused for subsequent calls.

Swift

The generated Swift wrapper defines a WeaveFFIError enum and a check helper. Functions are marked throws and raise a Swift error automatically when the C-level code is non-zero.

public enum WeaveFFIError: Error, CustomStringConvertible {
    case error(code: Int32, message: String)
    public var description: String {
        switch self {
        case let .error(code, message):
            return "(\(code)) \(message)"
        }
    }
}

Callers use standard do/catch:

do {
    let contact = try Contacts.getContact(id: handle)
    print(contact)
} catch let e as WeaveFFIError {
    print("Failed: \(e)")
}

Behind the scenes the generated code initializes a C error, calls the FFI function, and invokes check(&err) which throws if code != 0:

var err = weaveffi_error(code: 0, message: nil)
let raw = weaveffi_contacts_get_contact(id, &err)
try check(&err)  // throws WeaveFFIError, calls weaveffi_error_clear internally

Kotlin / Android

The generated JNI bridge checks the error after each C call and throws a RuntimeException with the error message when code != 0.

try {
    val contact = Contacts.getContact(id)
    println(contact)
} catch (e: RuntimeException) {
    println("Failed: ${e.message}")
}

In the generated JNI C code:

weaveffi_error err = {0, NULL};
// ... call the C function with &err ...
if (err.code != 0) {
    jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException");
    const char* msg = err.message ? err.message : "WeaveFFI error";
    (*env)->ThrowNew(env, exClass, msg);
    weaveffi_error_clear(&err);
    return;
}

Node.js

The Node.js generator emits TypeScript type declarations and a loader for a native N-API addon. Error handling is performed by the addon at runtime — when the C-level code != 0, the addon throws a JavaScript Error with the message.

import { Contacts } from "./generated";

try {
    const contact = Contacts.getContact(id);
    console.log(contact);
} catch (e) {
    console.error("Failed:", (e as Error).message);
}

WASM

The WASM generator produces a JavaScript loader that checks the return value after each call. Error handling at the WASM boundary uses numeric return codes; the caller inspects the result to determine success or failure.

const result = instance.exports.weaveffi_contacts_get_contact(id);
if (result === 0) {
    console.error("call failed — check error output");
}

Note: The WASM error surface is still evolving. Future versions will provide richer error propagation.

Setting errors from Rust implementations

When implementing a module in Rust, use the helpers from weaveffi_abi to report success or failure through the out-error pointer:

#![allow(unused)]
fn main() {
use weaveffi_abi::{self as abi, weaveffi_error};

#[no_mangle]
pub extern "C" fn weaveffi_contacts_get_contact(
    id: u64,
    out_err: *mut weaveffi_error,
) -> *const std::ffi::c_char {
    // Success path — clear the error and return a value
    abi::error_set_ok(out_err);

    // Failure path — set a non-zero code and message
    abi::error_set(out_err, 1, "Contact not found");
    std::ptr::null()
}
}
HelperEffect
error_set_ok(out_err)Sets code = 0, frees any prior message
error_set(out_err, code, msg)Sets a non-zero code and allocates message
result_to_out_err(result, out_err)Maps Result<T, E>Ok clears, Err sets code -1

Use error domain codes from your IDL to give callers stable, actionable values. For example, if your IDL defines not_found = 1, call error_set(out_err, 1, "Contact not found").

Summary

LanguageError mechanismHow errors surface
CCheck code fieldCaller inspects err.code after every call
SwiftthrowsWeaveFFIError thrown, caught with do/catch
KotlinExceptionRuntimeException thrown, caught with try/catch
Node.jsThrown ErrorNative addon throws JS Error
WASMReturn codeCaller checks return value

Async Functions

WeaveFFI supports marking functions as asynchronous using the async: true field in the IDL. Async functions represent operations that execute off the calling thread and deliver their result via a callback or language-native async mechanism.

IDL declaration

functions:
  - name: fetch_data
    params:
      - { name: url, type: string }
    return: string
    async: true
    doc: "Fetches data from the given URL"

  - name: upload_file
    params:
      - { name: path, type: string }
      - { name: data, type: bytes }
    return: bool
    async: true
    cancellable: true
    doc: "Uploads a file, can be cancelled"
FieldTypeDefaultDescription
asyncboolfalseMark the function as asynchronous
cancellableboolfalseAllow the async operation to be cancelled

How async works across the C ABI

Async functions use a callback-based pattern at the C ABI layer. Instead of returning a value directly, the C function accepts a callback pointer and a user-data pointer. When the operation completes, Rust invokes the callback with the result (or error) on a background thread.

typedef void (*weaveffi_callback_string)(
    const char* result,
    const weaveffi_error* err,
    void* user_data
);

void weaveffi_mymod_fetch_data(
    const uint8_t* url, size_t url_len,
    weaveffi_callback_string on_complete,
    void* user_data
);

For cancellable functions, the C ABI additionally returns a cancel handle:

uint64_t weaveffi_mymod_upload_file(
    const uint8_t* path, size_t path_len,
    const uint8_t* data, size_t data_len,
    weaveffi_callback_bool on_complete,
    void* user_data
);

void weaveffi_cancel(uint64_t cancel_handle);

Target language patterns

Each generator maps the callback-based C ABI to the target language’s native async idiom.

Swift

Async functions generate Swift async throws methods. The wrapper bridges from the C callback to Swift’s structured concurrency using withCheckedThrowingContinuation:

public static func fetchData(_ url: String) async throws -> String {
    return try await withCheckedThrowingContinuation { continuation in
        // ... marshal params, call C ABI with callback ...
    }
}

For cancellable functions, the generated code uses withTaskCancellationHandler to wire Swift task cancellation to the C ABI cancel function.

Kotlin/Android

Async functions generate Kotlin suspend functions. The wrapper uses suspendCancellableCoroutine to bridge from the JNI callback to Kotlin coroutines:

suspend fun fetchData(url: String): String =
    suspendCancellableCoroutine { cont ->
        // ... call JNI native method with callback ...
    }

Cancellable functions register an invokeOnCancellation handler that calls the C ABI cancel function.

Node.js

Async functions return a Promise. The wrapper creates a Promise and passes resolve/reject callbacks through the N-API bridge:

export function fetchData(url: string): Promise<string>

WASM

Async functions return a Promise in the WASM JavaScript bindings:

export function fetchData(url: string): Promise<string>

Python

Async functions generate async def wrappers that bridge from the C callback to Python’s asyncio event loop using loop.create_future():

async def fetch_data(url: str) -> str:
    loop = asyncio.get_running_loop()
    future = loop.create_future()
    # ... call C ABI with callback that resolves the future ...
    return await future

.NET

Async functions generate Task<T>-returning methods. The wrapper uses a TaskCompletionSource to bridge from the native callback:

public static Task<string> FetchDataAsync(string url)
{
    var tcs = new TaskCompletionSource<string>();
    // ... P/Invoke with callback that sets tcs result ...
    return tcs.Task;
}

Go

Async functions generate Go functions that accept a callback parameter or return a channel, matching Go’s concurrency idioms:

func MymodFetchData(url string, callback func(string, error))

Ruby

Async functions are currently skipped by the Ruby generator. The generated Ruby module only includes synchronous function wrappers.

Dart

Async functions generate Dart Future<T>-returning methods using Completer:

Future<String> fetchData(String url) {
  final completer = Completer<String>();
  // ... call FFI with callback that completes the future ...
  return completer.future;
}

C / C++

The C and C++ generators emit the raw callback-based interface directly, since C and C++ do not have a standard async runtime. The caller is responsible for managing threading and callback lifetime.

Validator behaviour

  • Async functions with no return type emit a warning (async void is unusual and may indicate a missing return type).
  • Async functions with a return type pass validation normally.
  • cancellable: true is only meaningful when async: true. Setting cancellable on a synchronous function has no effect.

Best practices

  1. Prefer async for I/O-bound operations. Network requests, file I/O, and database queries are good candidates for async.
  2. Use cancellable for long-running operations. File uploads, streaming downloads, and batch processing should be cancellable.
  3. Avoid async for CPU-bound work. Short computations (math, parsing, validation) should remain synchronous.
  4. Always specify a return type. Async void functions are valid but unusual — the validator will warn you.

Annotated Rust Extraction

Instead of hand-writing a YAML, JSON, or TOML API definition, you can annotate your Rust source code with WeaveFFI attributes and extract an equivalent IDL file automatically. This keeps your API definition co-located with your implementation and eliminates drift between the two.

Attributes

WeaveFFI recognises three marker attributes. They are checked by name only — no proc-macro crate is required. You can define them as no-op attribute macros, or simply allow unknown attributes in the annotated module.

#[weaveffi_export]

Marks a free function for export. The function signature (name, parameters, return type) is extracted into the functions list of the enclosing module.

#![allow(unused)]
fn main() {
mod math {
    #[weaveffi_export]
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}
}
  • self / &self receivers are ignored (only typed parameters are extracted).
  • The function body is irrelevant to extraction; only the signature matters.
  • Doc comments (///) on the function become the doc field in the IR.

#[weaveffi_struct]

Marks a struct for export. Only structs with named fields are supported.

#![allow(unused)]
fn main() {
mod shapes {
    /// A 2D point.
    #[weaveffi_struct]
    struct Point {
        x: f64,
        /// The vertical coordinate.
        y: f64,
    }
}
}
  • Tuple structs and unit structs are not supported.
  • Doc comments on the struct and individual fields are preserved.

#[weaveffi_enum]

Marks an enum for export. The enum must have #[repr(i32)] and every variant must have an explicit integer discriminant.

#![allow(unused)]
fn main() {
mod status {
    /// Traffic-light colors.
    #[weaveffi_enum]
    #[repr(i32)]
    enum Color {
        Red = 0,
        Green = 1,
        Blue = 2,
    }
}
}
  • Negative discriminants are supported (e.g. Neg = -1).
  • Variants without explicit values cause an extraction error.
  • Enums without #[repr(i32)] cause an extraction error.

Type mapping rules

The extractor maps Rust types to WeaveFFI TypeRef values according to these rules:

Rust typeWeaveFFI TypeRefIDL string
i32I32i32
u32U32u32
i64I64i64
f64F64f64
boolBoolbool
StringStringUtf8string
Vec<u8>Bytesbytes
u64Handlehandle
Vec<T>List(T)[T]
Option<T>Optional(T)T?
HashMap<K, V>Map(K, V){K:V}
BTreeMap<K, V>Map(K, V){K:V}
Any other identifierStruct(name)name

Types compose recursively — Option<Vec<i32>> becomes [i32]? and Vec<Option<String>> becomes [string?].

Complete example

Given the following annotated Rust module:

#![allow(unused)]
fn main() {
mod inventory {
    /// A product in the catalog.
    #[weaveffi_struct]
    struct Product {
        id: i32,
        name: String,
        price: f64,
        tags: Vec<String>,
    }

    /// Product availability.
    #[weaveffi_enum]
    #[repr(i32)]
    enum Availability {
        InStock = 0,
        OutOfStock = 1,
        Preorder = 2,
    }

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

    /// List all products matching a query.
    #[weaveffi_export]
    fn search(query: String, limit: i32) -> Vec<Product> {
        todo!()
    }
}
}

Running weaveffi extract lib.rs produces:

version: '0.1.0'
modules:
- name: inventory
  functions:
  - name: get_product
    params:
    - name: id
      type: i32
    return: Product?
    doc: Look up a product by ID.
    async: false
  - name: search
    params:
    - name: query
      type: string
    - name: limit
      type: i32
    return: '[Product]'
    doc: List all products matching a query.
    async: false
  structs:
  - name: Product
    doc: A product in the catalog.
    fields:
    - name: id
      type: i32
    - name: name
      type: string
    - name: price
      type: f64
    - name: tags
      type: '[string]'
  enums:
  - name: Availability
    doc: Product availability.
    variants:
    - name: InStock
      value: 0
    - name: OutOfStock
      value: 1
    - name: Preorder
      value: 2

This YAML can then be fed directly to weaveffi generate to produce bindings.

CLI command

weaveffi extract <INPUT> [--output <PATH>] [--format <FORMAT>]
FlagDefaultDescription
<INPUT>requiredPath to a .rs source file
-o, --outputstdoutWrite to a file instead of stdout
-f, --formatyamlOutput format: yaml, json, or toml

Examples

# Print YAML to stdout
weaveffi extract src/api.rs

# Write JSON to a file
weaveffi extract src/api.rs --format json --output api.json

# Pipe into generate
weaveffi extract src/api.rs -o api.yml && weaveffi generate api.yml -o generated

The extracted API is validated after extraction. Validation warnings are printed to stderr but do not prevent output.

Limitations and unsupported patterns

The extractor uses syn to parse Rust source at the syntax level. It does not perform type resolution, trait solving, or macro expansion. The following patterns are not supported:

  • Trait implementations. Methods inside impl Trait for Struct blocks are not scanned. Only free functions annotated with #[weaveffi_export] are extracted.

  • Generic functions. Functions with type parameters (fn foo<T>(...)) are not supported. All parameter and return types must be concrete.

  • Lifetime annotations. References (&str, &[u8]) and lifetime parameters ('a) are not supported. Use owned types (String, Vec<u8>).

  • self receivers. fn method(&self, ...) parameters are silently skipped. Only typed parameters are extracted.

  • External modules. mod foo; declarations (without an inline body) are skipped. The extractor only processes modules with inline content (mod foo { ... }).

  • Tuple and unit structs. Only structs with named fields are supported by #[weaveffi_struct].

  • Enums without #[repr(i32)]. The extractor requires #[repr(i32)] and explicit discriminants on every variant. Rust-style enums with data payloads are not supported.

  • Macro-generated items. Items produced by procedural or declarative macros are invisible to the extractor since it operates on unexpanded source.

  • Async functions. The async field is always set to false. The WeaveFFI validator rejects async: true.

Generator Configuration

WeaveFFI reads an optional TOML configuration file that lets you customise names, packages, and prefixes used by each code generator. When no configuration file is provided, every option falls back to a sensible default.

Passing the configuration file

Use the --config flag on the generate command:

weaveffi generate api.yml -o generated --config weaveffi.toml

When --config is omitted, all options use their default values.

File format

The configuration file is plain TOML. All keys are top-level — there are no nested tables. Every key is optional; omit a key to keep its default.

Minimal example

An empty file (or no file at all) is valid — defaults apply to everything:

# weaveffi.toml — all defaults

Full example

# weaveffi.toml

# Swift module name used in Package.swift and the Sources/ directory.
swift_module_name = "MyApp"

# Java/Kotlin package name for the Android JNI wrapper.
android_package = "com.example.myapp"

# npm package name emitted in the Node.js loader.
node_package_name = "@myorg/myapp"

# WASM module name used in the JavaScript loader.
wasm_module_name = "myapp_wasm"

# Prefix for C ABI symbol names (e.g. myapp_math_add instead of weaveffi_math_add).
c_prefix = "myapp"

# When true, strip the module name from generated identifiers where applicable.
strip_module_prefix = true

Configuration options

KeyTypeDefaultDescription
swift_module_namestring"WeaveFFI"Name of the Swift module in Package.swift and the Sources/ directory.
android_packagestring"com.weaveffi"Java/Kotlin package declaration in the generated JNI wrapper.
node_package_namestring"weaveffi"Package name in the generated Node.js N-API loader.
wasm_module_namestring"weaveffi_wasm"Module name in the generated WASM JavaScript loader.
c_prefixstring"weaveffi"Prefix prepended to every C ABI symbol ({prefix}_{module}_{function}).
strip_module_prefixboolfalseStrip the module name from generated identifiers where applicable.

swift_module_name

Controls the Swift package and module name. The generated Package.swift references this name, and the source directory is created as Sources/{swift_module_name}/.

swift_module_name = "CoolLib"

Produces Sources/CoolLib/CoolLib.swift and a matching Package.swift:

// Package.swift
let package = Package(
    name: "CoolLib",
    ...
    targets: [
        .target(name: "CoolLib", path: "Sources/CoolLib"),
    ]
)

android_package

Sets the Java/Kotlin package for the generated JNI bridge. The package determines the directory structure and the package declaration at the top of the generated .kt file.

android_package = "com.example.myapp"

Produces:

package com.example.myapp

node_package_name

Sets the package name used in the generated Node.js loader. This is the name consumers use in require() or import statements.

node_package_name = "@myorg/cool-lib"

wasm_module_name

Sets the module name used in the generated WASM JavaScript loader and TypeScript declarations.

wasm_module_name = "coolapp_wasm"

c_prefix

Replaces the default weaveffi prefix on all C ABI symbol names. This affects every generated header and every language binding that references C symbols.

c_prefix = "myapp"

With an API module named math containing a function add, the exported C symbol becomes myapp_math_add instead of the default weaveffi_math_add.

strip_module_prefix

When set to true, generated identifiers omit the module name where the target language supports namespacing natively.

strip_module_prefix = true

Common recipes

iOS/macOS project

swift_module_name = "MyAppFFI"
c_prefix = "myapp"
weaveffi generate api.yml -o generated -t swift,c --config weaveffi.toml

Android project

android_package = "com.example.myapp.ffi"
c_prefix = "myapp"
weaveffi generate api.yml -o generated -t android --config weaveffi.toml

Node.js package

node_package_name = "@myorg/myapp-native"
weaveffi generate api.yml -o generated -t node --config weaveffi.toml

All targets with custom prefix

swift_module_name = "MyAppFFI"
android_package = "com.example.myapp"
node_package_name = "@example/myapp"
wasm_module_name = "myapp_wasm"
c_prefix = "myapp"
weaveffi generate api.yml -o generated --config weaveffi.toml

Tutorials

Tutorial: Calculator end-to-end

This tutorial uses the included samples/calculator crate and shows how to generate artifacts and run platform examples.

1) Generate artifacts

weaveffi generate samples/calculator/calculator.yml -o generated

This writes headers and templates under generated/:

  • generated/c — C header and convenience C file
  • generated/swift — SwiftPM System Library (CWeaveFFI) and Swift wrapper (WeaveFFI)
  • generated/android — Kotlin wrapper + JNI shims + Gradle skeleton
  • generated/node — N-API addon loader + .d.ts
  • generated/wasm — minimal loader stub

2) Build the Rust sample

cargo build -p calculator

This produces a shared library:

  • macOS: target/debug/libcalculator.dylib
  • Linux: target/debug/libcalculator.so

3) Run the C example

macOS

cd examples/c
cc -I ../../generated/c main.c -L ../../target/debug -lcalculator -o c_example
DYLD_LIBRARY_PATH=../../target/debug ./c_example

Linux

cd examples/c
cc -I ../../generated/c main.c -L ../../target/debug -lcalculator -o c_example
LD_LIBRARY_PATH=../../target/debug ./c_example

4) Run the Node example

macOS

cp target/debug/libindex.dylib generated/node/index.node
cd examples/node
DYLD_LIBRARY_PATH=../../target/debug npm start

Linux

cp target/debug/libindex.so generated/node/index.node
cd examples/node
LD_LIBRARY_PATH=../../target/debug npm start

5) Try Swift (macOS)

cargo build -p calculator
cd examples/swift
swiftc \
  -I ../../generated/swift/Sources/CWeaveFFI \
  -L ../../target/debug -lcalculator \
  -Xlinker -rpath -Xlinker ../../target/debug \
  Sources/App/main.swift -o .build/debug/App
DYLD_LIBRARY_PATH=../../target/debug .build/debug/App

On Linux, replace DYLD_LIBRARY_PATH with LD_LIBRARY_PATH.

6) Android and WASM

  • Open generated/android in Android Studio and build the :weaveffi AAR.
  • Build for WASM: cargo build --target wasm32-unknown-unknown --release and load with generated/wasm/weaveffi_wasm.js.

Tutorial: Swift iOS App

This tutorial walks through building a Rust library, generating Swift bindings with WeaveFFI, and integrating everything into an Xcode iOS project.

Prerequisites

  • Rust toolchain (stable channel)
  • Xcode 15+ with iOS SDK
  • WeaveFFI CLI installed (cargo install weaveffi-cli)
  • iOS Rust targets:
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios

1) Define your API

Create a file called greeter.yml:

version: "0.1.0"
modules:
  - name: greeter
    structs:
      - name: Greeting
        fields:
          - { name: message, type: string }
          - { name: lang, type: string }
    functions:
      - name: hello
        params:
          - { name: name, type: string }
        return: string
      - name: greeting
        params:
          - { name: name, type: string }
          - { name: lang, type: string }
        return: Greeting

2) Generate bindings

weaveffi generate greeter.yml -o generated --scaffold

This produces:

generated/
├── c/
│   └── weaveffi.h
├── swift/
│   ├── Package.swift
│   └── Sources/
│       ├── CWeaveFFI/
│       │   └── module.modulemap
│       └── WeaveFFI/
│           └── WeaveFFI.swift
└── scaffold.rs

3) Create the Rust library

Create a new Cargo project for the native library:

cargo init --lib mygreeter

mygreeter/Cargo.toml:

[package]
name = "mygreeter"
version = "0.1.0"
edition = "2021"

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

[dependencies]
weaveffi-abi = { version = "0.1" }

Use staticlib for iOS — Xcode links static libraries into the app bundle. cdylib is included for desktop testing.

mygreeter/src/lib.rs:

#![allow(unused)]
#![allow(unsafe_code)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]

fn main() {
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use weaveffi_abi::{self as abi, weaveffi_error};

#[no_mangle]
pub extern "C" fn weaveffi_greeter_hello(
    name_ptr: *const c_char,
    _name_len: usize,
    out_err: *mut weaveffi_error,
) -> *const c_char {
    abi::error_set_ok(out_err);
    let name = unsafe { CStr::from_ptr(name_ptr) }.to_str().unwrap_or("world");
    let msg = format!("Hello, {name}!");
    CString::new(msg).unwrap().into_raw() as *const c_char
}

#[no_mangle]
pub extern "C" fn weaveffi_free_string(ptr: *const c_char) {
    abi::free_string(ptr);
}

#[no_mangle]
pub extern "C" fn weaveffi_free_bytes(ptr: *mut u8, len: usize) {
    abi::free_bytes(ptr, len);
}

#[no_mangle]
pub extern "C" fn weaveffi_error_clear(err: *mut weaveffi_error) {
    abi::error_clear(err);
}
}

Fill in the remaining functions (weaveffi_greeter_greeting, weaveffi_greeter_Greeting_destroy, getters, etc.) using the generated scaffold.rs as a guide.

4) Build for iOS

Build the static library for each iOS target:

# Physical devices (arm64)
cargo build -p mygreeter --target aarch64-apple-ios --release

# Simulator (arm64 Apple Silicon)
cargo build -p mygreeter --target aarch64-apple-ios-sim --release

# Simulator (x86_64 Intel Mac)
cargo build -p mygreeter --target x86_64-apple-ios --release

Create a universal simulator library with lipo:

mkdir -p target/universal-ios-sim/release

lipo -create \
  target/aarch64-apple-ios-sim/release/libmygreeter.a \
  target/x86_64-apple-ios/release/libmygreeter.a \
  -output target/universal-ios-sim/release/libmygreeter.a

Optionally, create an XCFramework that bundles both device and simulator slices:

xcodebuild -create-xcframework \
  -library target/aarch64-apple-ios/release/libmygreeter.a \
  -headers generated/c/ \
  -library target/universal-ios-sim/release/libmygreeter.a \
  -headers generated/c/ \
  -output MyGreeter.xcframework

5) Set up the Xcode project

  1. Create a new iOS App in Xcode (SwiftUI or UIKit).

  2. Add the static library. Drag MyGreeter.xcframework (or the .a file for a single architecture) into your project navigator. Ensure it appears under Build Phases > Link Binary With Libraries.

  3. Add the generated Swift package. In Xcode, go to File > Add Package Dependencies > Add Local… and select generated/swift/. This adds the CWeaveFFI (C module map) and WeaveFFI (Swift wrapper) targets.

  4. Set the Header Search Path. Under Build Settings > Header Search Paths, add the path to generated/c/ (e.g. $(SRCROOT)/../generated/c). This lets the module map find weaveffi.h.

  5. Set the Library Search Path. Under Build Settings > Library Search Paths, add the path to the Rust static library (e.g. $(SRCROOT)/../target/aarch64-apple-ios/release for device builds).

  6. Add a bridging dependency. In your app target’s Build Phases > Dependencies, ensure WeaveFFI is listed.

6) Call from Swift

import WeaveFFI

struct ContentView: View {
    @State private var greeting = ""

    var body: some View {
        VStack {
            Text(greeting)
            Button("Greet") {
                do {
                    greeting = try Greeter.hello("Swift")
                } catch {
                    greeting = "Error: \(error)"
                }
            }
        }
        .padding()
    }
}

The generated WeaveFFI module exposes:

  • Greeter.hello(_:) — returns a String
  • Greeter.greeting(_:_:) — returns a Greeting object with .message and .lang properties
  • Greeting — a class wrapping the opaque Rust pointer, with automatic cleanup on deinit

7) Build and run

Select an iOS Simulator target in Xcode and press Cmd+R. The app should display “Hello, Swift!” when you tap the button.

For a physical device, ensure you built for aarch64-apple-ios and that the correct library search path is set.

Troubleshooting

ProblemSolution
Undefined symbols for architecture arm64Check that the static library is linked and the library search path is correct.
Module 'CWeaveFFI' not foundEnsure the header search path points to generated/c/.
No such module 'WeaveFFI'Add the generated/swift/ local package to your Xcode project.
Simulator crash on Intel MacBuild with x86_64-apple-ios and create a universal binary with lipo.

Next steps

Tutorial: Android App

This tutorial walks through building a Rust library, generating Kotlin bindings with WeaveFFI, and integrating everything into an Android Studio project.

Prerequisites

  • Rust toolchain (stable channel)
  • Android Studio with NDK installed (via SDK Manager)
  • WeaveFFI CLI installed (cargo install weaveffi-cli)
  • Android Rust targets:
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android

1) Define your API

Create a file called greeter.yml:

version: "0.1.0"
modules:
  - name: greeter
    structs:
      - name: Greeting
        fields:
          - { name: message, type: string }
          - { name: lang, type: string }
    functions:
      - name: hello
        params:
          - { name: name, type: string }
        return: string
      - name: greeting
        params:
          - { name: name, type: string }
          - { name: lang, type: string }
        return: Greeting

2) Generate bindings

weaveffi generate greeter.yml -o generated --scaffold

This produces (among other targets):

generated/
├── c/
│   └── weaveffi.h
├── android/
│   ├── settings.gradle
│   ├── build.gradle
│   └── src/main/
│       ├── kotlin/com/weaveffi/WeaveFFI.kt
│       └── cpp/
│           ├── weaveffi_jni.c
│           └── CMakeLists.txt
└── scaffold.rs

3) Create the Rust library

cargo init --lib mygreeter

mygreeter/Cargo.toml:

[package]
name = "mygreeter"
version = "0.1.0"
edition = "2021"

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

[dependencies]
weaveffi-abi = { version = "0.1" }

mygreeter/src/lib.rs:

#![allow(unused)]
#![allow(unsafe_code)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]

fn main() {
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use weaveffi_abi::{self as abi, weaveffi_error};

#[no_mangle]
pub extern "C" fn weaveffi_greeter_hello(
    name_ptr: *const c_char,
    _name_len: usize,
    out_err: *mut weaveffi_error,
) -> *const c_char {
    abi::error_set_ok(out_err);
    let name = unsafe { CStr::from_ptr(name_ptr) }.to_str().unwrap_or("world");
    let msg = format!("Hello, {name}!");
    CString::new(msg).unwrap().into_raw() as *const c_char
}

#[no_mangle]
pub extern "C" fn weaveffi_free_string(ptr: *const c_char) {
    abi::free_string(ptr);
}

#[no_mangle]
pub extern "C" fn weaveffi_free_bytes(ptr: *mut u8, len: usize) {
    abi::free_bytes(ptr, len);
}

#[no_mangle]
pub extern "C" fn weaveffi_error_clear(err: *mut weaveffi_error) {
    abi::error_clear(err);
}
}

Fill in the remaining functions using scaffold.rs as a guide.

4) Configure the Android NDK toolchain

Set the ANDROID_NDK_HOME environment variable to the NDK path. On macOS with Android Studio’s default install location:

export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/$(ls $HOME/Library/Android/sdk/ndk | sort -V | tail -1)"

Create a .cargo/config.toml in your project to point Cargo at the NDK linkers:

[target.aarch64-linux-android]
linker = "aarch64-linux-android21-clang"

[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi21-clang"

[target.x86_64-linux-android]
linker = "x86_64-linux-android21-clang"

Add the NDK toolchain to your PATH:

export PATH="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH"

Replace darwin-x86_64 with linux-x86_64 on Linux.

5) Cross-compile for Android

cargo build -p mygreeter --target aarch64-linux-android --release
cargo build -p mygreeter --target armv7-linux-androideabi --release
cargo build -p mygreeter --target x86_64-linux-android --release

This produces shared libraries:

target/aarch64-linux-android/release/libmygreeter.so
target/armv7-linux-androideabi/release/libmygreeter.so
target/x86_64-linux-android/release/libmygreeter.so

6) Set up the Android Studio project

  1. Create a new Android project in Android Studio (Empty Activity, Kotlin, minimum SDK 21+).

  2. Copy the generated android module. Copy the generated/android/ directory into your project as a Gradle module. In your root settings.gradle, add:

    include ':weaveffi'
    project(':weaveffi').projectDir = new File('generated/android')
    
  3. Add the module dependency. In your app’s build.gradle:

    dependencies {
        implementation project(':weaveffi')
    }
    
  4. Place the Rust shared libraries. Copy each .so into the matching jniLibs directory:

    mkdir -p app/src/main/jniLibs/arm64-v8a
    mkdir -p app/src/main/jniLibs/armeabi-v7a
    mkdir -p app/src/main/jniLibs/x86_64
    
    cp target/aarch64-linux-android/release/libmygreeter.so \
      app/src/main/jniLibs/arm64-v8a/libmygreeter.so
    
    cp target/armv7-linux-androideabi/release/libmygreeter.so \
      app/src/main/jniLibs/armeabi-v7a/libmygreeter.so
    
    cp target/x86_64-linux-android/release/libmygreeter.so \
      app/src/main/jniLibs/x86_64/libmygreeter.so
    
  5. Copy the C header. The JNI shims need weaveffi.h. Ensure the CMakeLists.txt in generated/android/src/main/cpp/ has the correct target_include_directories pointing to generated/c/.

7) Call from Kotlin

import com.weaveffi.WeaveFFI

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val message = WeaveFFI.hello("Android")
        findViewById<TextView>(R.id.textView).text = message
    }
}

The generated WeaveFFI companion object loads the native library automatically and exposes:

  • WeaveFFI.hello(name: String): String
  • WeaveFFI.greeting(name: String, lang: String): Long — returns an opaque handle to a Greeting struct

Struct wrappers (like Greeting) implement Closeable for deterministic cleanup:

import com.weaveffi.Greeting

Greeting.create("Hi", "en").use { g ->
    println("${g.message} (${g.lang})")
}

8) Build and run

  1. Sync Gradle in Android Studio.
  2. Select an emulator or connected device.
  3. Press Run (Shift+F10). The app should display “Hello, Android!”.

Troubleshooting

ProblemSolution
UnsatisfiedLinkError: dlopen failedThe .so is missing from jniLibs/ or was built for the wrong ABI.
java.lang.RuntimeException from JNIA WeaveFFI error was raised. Check the exception message for details.
Linker errors during cargo buildEnsure ANDROID_NDK_HOME is set and the NDK toolchain is on PATH.
No implementation found for native methodThe JNI function names must match the Kotlin package path exactly.

Next steps

Tutorial: Python Package

This tutorial walks through building a Rust library, generating Python ctypes bindings with WeaveFFI, and packaging it for pip install.

Prerequisites

  • Rust toolchain (stable channel)
  • Python 3.7+
  • WeaveFFI CLI installed (cargo install weaveffi-cli)

1) Define your API

Create a file called greeter.yml:

version: "0.1.0"
modules:
  - name: greeter
    structs:
      - name: Greeting
        fields:
          - { name: message, type: string }
          - { name: lang, type: string }
    functions:
      - name: hello
        params:
          - { name: name, type: string }
        return: string
      - name: greeting
        params:
          - { name: name, type: string }
          - { name: lang, type: string }
        return: Greeting

2) Generate bindings

weaveffi generate greeter.yml -o generated --scaffold

This produces (among other targets):

generated/
├── c/
│   └── weaveffi.h
├── python/
│   ├── pyproject.toml
│   ├── setup.py
│   ├── README.md
│   └── weaveffi/
│       ├── __init__.py
│       ├── weaveffi.py
│       └── weaveffi.pyi
└── scaffold.rs

The generated Python package uses ctypes — no native extension compilation is needed on the Python side.

3) Create the Rust library

cargo init --lib mygreeter

mygreeter/Cargo.toml:

[package]
name = "mygreeter"
version = "0.1.0"
edition = "2021"

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

[dependencies]
weaveffi-abi = { version = "0.1" }

mygreeter/src/lib.rs:

#![allow(unused)]
#![allow(unsafe_code)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]

fn main() {
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use weaveffi_abi::{self as abi, weaveffi_error};

#[no_mangle]
pub extern "C" fn weaveffi_greeter_hello(
    name_ptr: *const c_char,
    _name_len: usize,
    out_err: *mut weaveffi_error,
) -> *const c_char {
    abi::error_set_ok(out_err);
    let name = unsafe { CStr::from_ptr(name_ptr) }.to_str().unwrap_or("world");
    let msg = format!("Hello, {name}!");
    CString::new(msg).unwrap().into_raw() as *const c_char
}

#[no_mangle]
pub extern "C" fn weaveffi_free_string(ptr: *const c_char) {
    abi::free_string(ptr);
}

#[no_mangle]
pub extern "C" fn weaveffi_free_bytes(ptr: *mut u8, len: usize) {
    abi::free_bytes(ptr, len);
}

#[no_mangle]
pub extern "C" fn weaveffi_error_clear(err: *mut weaveffi_error) {
    abi::error_clear(err);
}
}

Fill in the remaining functions using scaffold.rs as a guide.

4) Build the shared library

cargo build -p mygreeter --release

This produces the shared library:

PlatformOutput
macOStarget/release/libmygreeter.dylib
Linuxtarget/release/libmygreeter.so
Windowstarget/release/mygreeter.dll

5) Install the Python package

cd generated/python
pip install .

For development iteration, use an editable install:

pip install -e .

6) Make the shared library findable

The generated ctypes loader looks for libweaveffi.dylib (macOS), libweaveffi.so (Linux), or weaveffi.dll (Windows) on the system library search path.

Rename or symlink your library to match the expected name, then set the library path:

macOS:

cp target/release/libmygreeter.dylib target/release/libweaveffi.dylib
DYLD_LIBRARY_PATH=target/release python your_script.py

Linux:

cp target/release/libmygreeter.so target/release/libweaveffi.so
LD_LIBRARY_PATH=target/release python your_script.py

Windows:

Place weaveffi.dll in the same directory as your script or add its directory to PATH.

Alternatively, for production, copy the shared library into the Python package directory and adjust the loader path in weaveffi.py.

7) Use the bindings

Create a script called demo.py:

from weaveffi import hello, greeting

msg = hello("Python")
print(msg)  # "Hello, Python!"

g = greeting("Python", "en")
print(f"{g.message} ({g.lang})")

Run it:

DYLD_LIBRARY_PATH=target/release python demo.py   # macOS
LD_LIBRARY_PATH=target/release python demo.py      # Linux

Error handling

WeaveFFI errors are raised as WeaveffiError exceptions:

from weaveffi import WeaveffiError

try:
    result = hello("")
except WeaveffiError as e:
    print(f"Error {e.code}: {e.message}")

Struct lifecycle

Struct wrappers automatically free Rust memory when garbage collected. For explicit control, delete the reference:

g = greeting("Python", "en")
print(g.message)
del g  # immediately calls the Rust destroy function

8) Type stubs and IDE support

The generated weaveffi.pyi stub file provides type information for editors and mypy:

mypy demo.py

IDEs like VS Code and PyCharm will show autocomplete for all generated functions, classes, and properties.

Troubleshooting

ProblemSolution
OSError: dlopen ... not foundThe shared library is not on the library search path. Set DYLD_LIBRARY_PATH or LD_LIBRARY_PATH.
WeaveffiError at runtimeA Rust-side error was returned. Check the error code and message.
ModuleNotFoundError: No module named 'weaveffi'Run pip install . from generated/python/.
mypy type errorsEnsure weaveffi.pyi is in the package directory alongside weaveffi.py.

Next steps

Tutorial: Node.js npm Package

This tutorial walks through building a Rust library, generating Node.js N-API bindings with WeaveFFI, and publishing as an npm package.

Prerequisites

  • Rust toolchain (stable channel)
  • Node.js 16+ and npm
  • WeaveFFI CLI installed (cargo install weaveffi-cli)

1) Define your API

Create a file called greeter.yml:

version: "0.1.0"
modules:
  - name: greeter
    structs:
      - name: Greeting
        fields:
          - { name: message, type: string }
          - { name: lang, type: string }
    functions:
      - name: hello
        params:
          - { name: name, type: string }
        return: string
      - name: greeting
        params:
          - { name: name, type: string }
          - { name: lang, type: string }
        return: Greeting

2) Generate bindings

weaveffi generate greeter.yml -o generated --scaffold

This produces (among other targets):

generated/
├── c/
│   └── weaveffi.h
├── node/
│   ├── index.js
│   ├── types.d.ts
│   └── package.json
└── scaffold.rs

3) Create the Rust library

cargo init --lib mygreeter

mygreeter/Cargo.toml:

[package]
name = "mygreeter"
version = "0.1.0"
edition = "2021"

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

[dependencies]
weaveffi-abi = { version = "0.1" }

mygreeter/src/lib.rs:

#![allow(unused)]
#![allow(unsafe_code)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]

fn main() {
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use weaveffi_abi::{self as abi, weaveffi_error};

#[no_mangle]
pub extern "C" fn weaveffi_greeter_hello(
    name_ptr: *const c_char,
    _name_len: usize,
    out_err: *mut weaveffi_error,
) -> *const c_char {
    abi::error_set_ok(out_err);
    let name = unsafe { CStr::from_ptr(name_ptr) }.to_str().unwrap_or("world");
    let msg = format!("Hello, {name}!");
    CString::new(msg).unwrap().into_raw() as *const c_char
}

#[no_mangle]
pub extern "C" fn weaveffi_free_string(ptr: *const c_char) {
    abi::free_string(ptr);
}

#[no_mangle]
pub extern "C" fn weaveffi_free_bytes(ptr: *mut u8, len: usize) {
    abi::free_bytes(ptr, len);
}

#[no_mangle]
pub extern "C" fn weaveffi_error_clear(err: *mut weaveffi_error) {
    abi::error_clear(err);
}
}

Fill in the remaining functions using scaffold.rs as a guide.

You also need an N-API addon crate that bridges Node’s JavaScript runtime to the C ABI. See samples/node-addon in the WeaveFFI repository for a working example.

4) Build the N-API addon

Build the Rust library:

cargo build -p mygreeter --release

Build the N-API addon (which links against your library and the C ABI):

cargo build -p node-addon --release

Copy the compiled addon into the generated node package:

macOS:

cp target/release/libindex.dylib generated/node/index.node

Linux:

cp target/release/libindex.so generated/node/index.node

The file must be named index.node — the generated index.js loader requires it at that path.

5) Test locally

Create a test script demo.js in the generated/node/ directory:

const weaveffi = require("./index");

const msg = weaveffi.hello("Node");
console.log(msg); // "Hello, Node!"

Run it:

macOS:

cd generated/node
DYLD_LIBRARY_PATH=../../target/release node demo.js

Linux:

cd generated/node
LD_LIBRARY_PATH=../../target/release node demo.js

TypeScript support

The generated types.d.ts provides full type definitions. In a TypeScript project:

import * as weaveffi from "./index";

const msg: string = weaveffi.hello("TypeScript");
console.log(msg);

const g: weaveffi.Greeting = weaveffi.greeting("TS", "en");
console.log(`${g.message} (${g.lang})`);

6) Prepare for npm publish

Edit generated/node/package.json to set your package metadata:

{
  "name": "@myorg/greeter",
  "version": "0.1.0",
  "main": "index.js",
  "types": "types.d.ts",
  "files": [
    "index.js",
    "index.node",
    "types.d.ts"
  ],
  "os": ["darwin", "linux"],
  "cpu": ["x64", "arm64"]
}

Key points:

  • files must include index.node (the compiled N-API addon).
  • os and cpu fields document supported platforms.
  • For cross-platform packages, consider publishing platform-specific optional dependencies (e.g. @myorg/greeter-darwin-arm64) and using an install script to select the right binary.

7) Publish

cd generated/node
npm pack    # creates a .tgz for inspection
npm publish # publishes to the npm registry

For scoped packages, use npm publish --access public.

Consuming the published package

npm install @myorg/greeter
const { hello } = require("@myorg/greeter");
console.log(hello("npm")); // "Hello, npm!"

Troubleshooting

ProblemSolution
Error: Cannot find module './index.node'The compiled addon is missing. Copy the built .dylib/.so as index.node.
Error: dlopen ... not foundThe Rust shared library is not on the library path. Set DYLD_LIBRARY_PATH or LD_LIBRARY_PATH.
TypeError: weaveffi.hello is not a functionThe N-API addon did not export the expected symbols. Check that the addon registers all functions.
Crashes on require()The addon was built for a different Node.js version or architecture. Rebuild with the correct target.

Next steps