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
| Crate | Purpose |
|---|---|
weaveffi-ir | IR model + YAML/JSON/TOML parsing via serde |
weaveffi-abi | C ABI runtime helpers (error struct, handles, memory free functions) |
weaveffi-core | Generator trait, Orchestrator, validation, shared utilities, template engine |
weaveffi-gen-c | C header generator |
weaveffi-gen-cpp | C++ header + RAII wrapper + CMake scaffold generator |
weaveffi-gen-swift | SwiftPM System Library + Swift wrapper generator |
weaveffi-gen-android | Kotlin/JNI wrapper + Gradle skeleton generator |
weaveffi-gen-node | N-API addon loader + TypeScript types generator |
weaveffi-gen-wasm | WASM loader + JS/TS wrapper generator |
weaveffi-gen-python | Python ctypes binding + .pyi stubs generator |
weaveffi-gen-dotnet | .NET P/Invoke binding generator |
weaveffi-gen-dart | Dart dart:ffi binding + pubspec.yaml generator |
weaveffi-gen-go | Go CGo binding + go.mod generator |
weaveffi-gen-ruby | Ruby FFI binding + gemspec generator |
weaveffi-cli | CLI binary (installed as weaveffi) |
samples/calculator | End-to-end sample Rust library |
samples/contacts | Contacts sample with structs, enums, and optionals |
samples/inventory | Multi-module sample with cross-type features |
samples/node-addon | N-API addon for the calculator sample |
samples/async-demo | Async demo with callback-based C ABI convention |
samples/events | Events 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 —
--scaffoldemits Rustextern "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 —
--warnandlintcommand for non-fatal diagnostics - Diff mode — compare generated output against existing files
- Shell completions —
weaveffi completions <shell>for bash, zsh, fish, PowerShell - Schema versioning — IR version field with
schema-versionfor 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 lintcommand - Incremental codegen with content-hash caching
- Generator configuration via TOML config file
- DX polish:
--dry-run,--quiet,--verbose,diffcommand, improveddoctor - Python target (ctypes +
.pyitype 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:ffibindings, enum generation,pubspec.yaml, null-safe code, configurable package name) - Go target (CGo bindings, Go
errorpattern,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 newwith full project scaffold (Cargo.toml, lib.rs, IDL, README) - Typed handles (
handle<Name>) replacing rawu64for 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_errparameter 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/awaitmapping for async functions - Kotlin coroutine (
suspend fun) mapping for async functions - Node.js
Promisemapping for async functions - Python
asynciomapping for async functions - .NET
Task<T>/asyncmapping 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 withadd,mul, andechofunctionsREADME.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]andextern "C". out_errmust always be cleared on success withabi::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, andweaveffi_error_clearfor 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 doctorto 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_clearlifecycle 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 (
ContactTypewithPersonal,Work,Other) - Struct definitions (
Contactwith typed fields) - Optional fields (
string?for the email) - List return types (
[Contact]) - Handle-based resource management (
create_contactreturns 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 (
CategorywithElectronics,Clothing,Food,Books) - Structs with optional fields, list fields (
[string]tags), and float types - List-returning search functions (
search_productsfiltered by category) - Cross-module struct passing (
add_product_to_ordertakes aProduct) - Nested struct lists (
Order.itemsis[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: truein the YAML) - Callback-based C ABI pattern (
weaveffi_tasks_run_task_async) - Callback type definitions (
weaveffi_tasks_run_task_callback) - Batch async operations (
run_batchprocesses a list of names sequentially) - Synchronous fallback functions (
cancel_taskis non-async in the same module) - Struct return types through callbacks (
TaskResultdelivered via callback)
Build and run tests:
cargo build -p async-demo
cargo test -p async-demo
Note: The validator currently rejects
async: truein 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 (
OnMessagecallback) - 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_messagesreturns aMessageIterator, 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
| Field | Type | Required | Description |
|---|---|---|---|
version | string | yes | Schema version ("0.1.0" or "0.2.0") |
modules | array of Module | yes | One or more modules |
generators | map of string to object | no | Per-generator configuration (see generators section) |
Module
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Lowercase identifier (e.g. calculator) |
functions | array of Function | yes | Functions exported by this module |
structs | array of Struct | no | Struct type definitions |
enums | array of Enum | no | Enum type definitions |
callbacks | array of Callback | no | Callback type definitions |
listeners | array of Listener | no | Listener (event subscription) definitions |
errors | ErrorDomain | no | Optional error domain |
modules | array of Module | no | Nested sub-modules (see nested modules) |
Function
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Function identifier |
params | array of Param | yes | Input parameters (may be empty []) |
return | TypeRef | no | Return type (omit for void functions) |
doc | string | no | Documentation string |
async | bool | no | Mark as asynchronous (default false) |
cancellable | bool | no | Allow cancellation (only meaningful when async: true) |
deprecated | string | no | Deprecation message shown to consumers |
since | string | no | Version when this function was introduced |
Param
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Parameter name |
type | TypeRef | yes | Parameter type |
mutable | bool | no | Mark as mutable (default false). Indicates the callee may modify the value in-place. |
Primitive types
The following primitive types are supported. All primitives are valid in both parameters and return types.
| Type | Description | Example value |
|---|---|---|
i32 | Signed 32-bit integer | -42 |
u32 | Unsigned 32-bit integer | 300 |
i64 | Signed 64-bit integer | 9000000000 |
f64 | 64-bit floating point | 3.14 |
bool | Boolean | true |
string | UTF-8 string (owned copy) | "hello" |
bytes | Byte buffer (owned copy) | binary data |
handle | Opaque 64-bit identifier | resource id |
handle<T> | Typed handle scoped to type T | resource id |
&str | Borrowed string (zero-copy, param-only) | "hello" |
&[u8] | Borrowed byte slice (zero-copy, param-only) | binary data |
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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Struct name (e.g. Contact) |
doc | string | no | Documentation string |
fields | array of Field | yes | Must have at least one field |
builder | bool | no | Generate a builder class (default false) |
When builder: true, generators emit a builder class with with_* setter
methods and a build() method, enabling incremental construction of
complex structs.
Each field:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Field name |
type | TypeRef | yes | Field type |
doc | string | no | Documentation string |
default | value | no | Default value for this field |
Struct example
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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Enum name (e.g. Color) |
doc | string | no | Documentation string |
variants | array of Variant | yes | Must have at least one variant |
Each variant:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Variant name (e.g. Red) |
value | i32 | yes | Integer discriminant |
doc | string | no | Documentation string |
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.
| Syntax | Meaning |
|---|---|
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).
| Syntax | Meaning |
|---|---|
[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.
| Syntax | Meaning |
|---|---|
{string:i32} | Map from string to i32 |
{string:Contact} | Map from string to struct |
{i32:string} | Map from i32 to string |
{string:[i32]} | Map from string to list of i32 |
Map example
structs:
- name: Contact
fields:
- { name: id, type: i64 }
- { name: name, type: string }
- { name: email, type: "string?" }
functions:
- name: update_scores
params:
- { name: scores, type: "{string:i32}" }
return: bool
doc: "Update player scores by name"
- name: get_contacts
params: []
return: "{string:Contact}"
doc: "Returns a map of name to Contact"
- name: merge_tags
params:
- { name: current, type: "{string:string}" }
- { name: additions, type: "{string:string}" }
return: "{string:string}"
YAML note: Quote map types like
"{string:i32}"because YAML interprets bare{...}as an inline mapping.
C ABI convention
Maps are passed across the FFI boundary as parallel arrays of keys and
values, plus a shared length. A map parameter {K:V} named m expands to
three C parameters:
const K* m_keys, const V* m_values, size_t m_len
A map return value expands to out-parameters:
K* out_keys, V* out_values, size_t* out_len
For example, a function update_scores(scores: {string:i32}) generates:
void weaveffi_mymod_update_scores(
const char* const* scores_keys,
const int32_t* scores_values,
size_t scores_len,
weaveffi_error* out_err
);
Key type restrictions
Only primitive types (i32, u32, i64, f64, bool, string, bytes,
handle) and enum types are valid map keys. The validator rejects structs,
lists, and maps as key types.
Nested types
Optional and list modifiers compose freely:
| Syntax | Meaning |
|---|---|
[Contact?] | List of optional contacts (items may be null) |
[i32]? | Optional list of i32 (the entire list may be null) |
[string?] | List of optional strings |
{string:[i32]} | Map from string to list of i32 |
{string:i32}? | Optional map (the entire map may be null) |
Nested type example
functions:
- name: search
params:
- { name: query, type: string }
return: "[Contact?]"
doc: "Returns a list where some entries may be null (redacted)"
- name: get_scores
params:
- { name: user_id, type: i64 }
return: "[i32]?"
doc: "Returns null if user has no scores, otherwise a list"
- name: bulk_update
params:
- { name: emails, type: "[string?]" }
return: i32
The parser evaluates type syntax outside-in: [Contact?] is parsed as
List(Optional(Contact)), while [Contact]? is parsed as
Optional(List(Contact)).
Iterator types
Wrap a type in iter<T> to declare a lazy iterator over values of type T.
Unlike [T] (which materializes the full list), iterators yield elements
one at a time and are suitable for large or streaming result sets.
| Syntax | Meaning |
|---|---|
iter<i32> | Iterator over i32 values |
iter<string> | Iterator over strings |
iter<Contact> | Iterator over structs |
Iterator example
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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Callback name |
params | array of Param | yes | Parameters passed to the callback |
doc | string | no | Documentation string |
Callback example
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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Listener name |
event_callback | string | yes | Name of the callback this listener uses |
doc | string | no | Documentation string |
Listener example
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.
| Type | Params | Returns | Struct fields | Notes |
|---|---|---|---|---|
i32 | yes | yes | yes | |
u32 | yes | yes | yes | |
i64 | yes | yes | yes | |
f64 | yes | yes | yes | |
bool | yes | yes | yes | |
string | yes | yes | yes | |
bytes | yes | yes | yes | |
handle | yes | yes | yes | |
handle<T> | yes | yes | yes | Typed handle |
&str | yes | yes | yes | Borrowed, zero-copy |
&[u8] | yes | yes | yes | Borrowed, zero-copy |
StructName | yes | yes | yes | |
EnumName | yes | yes | yes | |
T? | yes | yes | yes | |
[T] | yes | yes | yes | |
[T?] | yes | yes | yes | |
[T]? | yes | yes | yes | |
{K:V} | yes | yes | yes | |
{K:V}? | yes | yes | yes | |
iter<T> | no | yes | no | Return-only |
Complete example
A full 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_callbackmust reference a callback in the same module. - Error domain names must not collide with function names.
ABI mapping
- Parameters map to C ABI types;
stringandbytesare passed as pointer + length. - Return values are direct scalars except:
string: returnsconst char*allocated by Rust; caller must free viaweaveffi_free_string.bytes: returnsconst uint8_t*and requires an extrasize_t* out_lenparam; caller frees withweaveffi_free_bytes.
- Each function takes a trailing
weaveffi_error* out_errfor error reporting.
Error domain
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_errparameter of typeweaveffi_error*. - On success:
out_err->code == 0andout_err->message == NULL. - On failure:
out_err->code != 0andout_err->messagepoints 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
WeaveFFIErrorand 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.nodeplaced 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)
- GitHub:
-
Use hyphenated slugs for subpackages and components, prefixed with the top-level slug:
- Examples:
weaveffi-core,weaveffi-ir,weaveheap-core
- Examples:
-
Planned package names (not yet published):
- crates.io:
weaveffi,weaveffi-core,weaveffi-ir, etc. - npm:
@weavefoundry/weaveffi - PyPI:
weaveffi - SPM (repo slug):
weaveffi
- crates.io:
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).
- Crates: hyphenated subcrates on crates.io (e.g.,
-
Swift / Apple platforms
- Package products and modules: UpperCamelCase (e.g.,
WeaveFFI,WeaveHeap). - Keep repo slug condensed; SPM product name provides the CamelCase surface.
- Package products and modules: UpperCamelCase (e.g.,
-
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).
- Group ID / package base: reverse-DNS, all lowercase (e.g.,
-
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
WeaveFFIin examples when using default exports or named namespaces.
- Package name: scope + condensed for top-level, hyphenated for subpackages (e.g.,
-
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).
- PyPI name: top-level condensed (e.g.,
-
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>).
- Target/library names: snake_case (e.g.,
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 asweaveffi, Swift moduleWeaveFFI). For subpackages, installweaveffi-core(import asweaveffi_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).
- Rust crates:
- Prefer condensed top-level slugs. Avoid hyphenated top-level slugs like
weave-ffi,weave-heapgoing forward.
Examples
-
Rust
- Crate:
weaveffi-core - Import:
use weaveffi_core::{WeaveFFI};
- Crate:
-
Swift (SPM)
- Repo:
weaveffi - Package product:
WeaveFFI - Import:
import WeaveFFI
- Repo:
-
Python (planned)
- Package:
weaveffi - Import:
import weaveffi as ffi
- Package:
-
Node (planned)
- Package:
@weavefoundry/weaveffi - Import:
import { WeaveFFI } from '@weavefoundry/weaveffi'
- Package:
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
WeaveFFIthat declaresexternal funs - JNI C shims that call into the generated C ABI
CMakeLists.txtfor building the shared library
Generated artifacts
generated/android/settings.gradlegenerated/android/build.gradlegenerated/android/src/main/kotlin/com/weaveffi/WeaveFFI.ktgenerated/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 type | Kotlin type (external) | Kotlin type (wrapper) | JNI C type |
|---|---|---|---|
i32 | Int | Int | jint |
u32 | Long | Long | jlong |
i64 | Long | Long | jlong |
f64 | Double | Double | jdouble |
bool | Boolean | Boolean | jboolean |
string | String | String | jstring |
bytes | ByteArray | ByteArray | jbyteArray |
handle | Long | Long | jlong |
StructName | Long | StructName | jlong |
EnumName | Int | EnumName | jint |
T? | T? | T? | jobject |
[i32] | IntArray | IntArray | jintArray |
[i64] | LongArray | LongArray | jlongArray |
[string] | Array<String> | Array<String> | jobjectArray |
Build steps
- Ensure Android SDK and NDK are installed (Android Studio recommended).
- 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
- Open
generated/androidin Android Studio. - Sync Gradle and build the
:weaveffiAAR. - 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.hgenerated/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:
| Kind | Pattern | Example |
|---|---|---|
| Function | weaveffi_{module}_{function} | weaveffi_contacts_create_contact |
| Struct type | weaveffi_{module}_{Struct} | weaveffi_contacts_Contact |
| Struct create | weaveffi_{module}_{Struct}_create | weaveffi_contacts_Contact_create |
| Struct destroy | weaveffi_{module}_{Struct}_destroy | weaveffi_contacts_Contact_destroy |
| Struct getter | weaveffi_{module}_{Struct}_get_{field} | weaveffi_contacts_Contact_get_name |
| Enum type | weaveffi_{module}_{Enum} | weaveffi_contacts_ContactType |
| Enum variant | weaveffi_{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 type | C parameter type | C return type |
|---|---|---|
i32 | int32_t | int32_t |
u32 | uint32_t | uint32_t |
i64 | int64_t | int64_t |
f64 | double | double |
bool | bool | bool |
string | const uint8_t* ptr, size_t len | const char* |
bytes | const uint8_t* ptr, size_t len | const uint8_t* + size_t* out_len |
handle | weaveffi_handle_t | weaveffi_handle_t |
Struct | const weaveffi_m_S* | weaveffi_m_S* |
Enum | weaveffi_m_E | weaveffi_m_E |
T? (value) | const T* (NULL = absent) | T* (NULL = absent) |
[T] | const T* items, size_t items_len | T* + 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.nodegenerated/node/types.d.ts— function signatures inferred from your IRgenerated/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 type | TypeScript type |
|---|---|
i32 | number |
u32 | number |
i64 | number |
f64 | number |
bool | boolean |
string | string |
bytes | Buffer |
handle | bigint |
StructName | StructName |
EnumName | EnumName |
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.swiftgenerated/swift/Sources/CWeaveFFI/module.modulemap— C module map pointing at the generated headergenerated/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 JSDocgenerated/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 type | WASM type | Convention |
|---|---|---|
i32 | i32 | Direct value |
u32 | i32 | Direct value (unsigned interpretation) |
i64 | i64 | Direct value |
f64 | f64 | Direct value |
bool | i32 | 0 = false, 1 = true |
string | i32+i32 | Pointer + length in linear memory |
bytes | i32+i32 | Pointer + length in linear memory |
handle | i64 | Opaque 64-bit identifier |
StructName | i64 | Opaque handle (pointer) |
EnumName | i32 | Integer discriminant |
T? | varies | _is_present flag or null pointer |
[T] | i32+i32 | Pointer + 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.
ctypesships 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
.pyfiles 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
| File | Purpose |
|---|---|
python/weaveffi/__init__.py | Re-exports everything from weaveffi.py |
python/weaveffi/weaveffi.py | ctypes bindings: library loader, wrapper functions, classes |
python/weaveffi/weaveffi.pyi | Type stub for IDE autocompletion and mypy |
python/pyproject.toml | PEP 621 project metadata |
python/setup.py | Fallback setuptools script |
python/README.md | Basic 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 type | Python type hint | ctypes type |
|---|---|---|
i32 | int | ctypes.c_int32 |
u32 | int | ctypes.c_uint32 |
i64 | int | ctypes.c_int64 |
f64 | float | ctypes.c_double |
bool | bool | ctypes.c_int32 |
string | str | ctypes.c_char_p |
bytes | bytes | ctypes.POINTER(ctypes.c_uint8) + ctypes.c_size_t |
handle | int | ctypes.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
strvalues 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_pvalues are decoded from UTF-8 via_bytes_to_string(). The Rust runtime owns the returned pointer; the preamble registersweaveffi_free_stringfor cleanup.
Bytes
- Passing bytes in: Python
bytesare 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_lenparameter. The wrapper copies the data into a Pythonbytesobject 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:
- A
_WeaveffiErrorStruct(mirroring the Cweaveffi_error) is allocated. - It is passed as the last argument to the C function via
ctypes.byref(). - After the call,
_check_error()inspects the struct. Ifcode != 0, it reads the message, callsweaveffi_error_clearto free the Rust-allocated string, and raisesWeaveffiError.
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 targetingnet8.0generated/dotnet/WeaveFFI.nuspec— NuGet package metadatagenerated/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 type | C# type | P/Invoke type |
|---|---|---|
i32 | int | int |
u32 | uint | uint |
i64 | long | long |
f64 | double | double |
bool | bool | int |
string | string | IntPtr |
handle | ulong | ulong |
bytes | byte[] | IntPtr |
StructName | StructName | IntPtr |
EnumName | EnumName | int |
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 CMakegenerated/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 type | C++ type | Passed as parameter |
|---|---|---|
i32 | int32_t | int32_t |
u32 | uint32_t | uint32_t |
i64 | int64_t | int64_t |
f64 | double | double |
bool | bool | bool |
string | std::string | const std::string& |
bytes | std::vector<uint8_t> | const std::vector<uint8_t>& |
handle | void* | void* |
StructName | StructName | const StructName& |
EnumName | EnumName (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:ffiships 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
| File | Purpose |
|---|---|
dart/lib/weaveffi.dart | dart:ffi bindings: library loader, typedefs, lookup bindings, wrapper functions, enum/struct classes |
dart/pubspec.yaml | Package metadata (name, SDK constraint, ffi dependency) |
dart/README.md | Basic usage instructions |
dart:ffi approach
All native calls go through a single DynamicLibrary instance. For each C
symbol, the generator emits:
- A native typedef describing the C function signature using FFI types
(
Int32,Pointer<Utf8>, etc.). - A Dart typedef describing the equivalent Dart signature (
int,Pointer<Utf8>, etc.). - A
lookupFunctioncall that resolves the symbol at load time. - 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 type | Dart type | Native FFI type | Dart FFI type |
|---|---|---|---|
i32 | int | Int32 | int |
u32 | int | Uint32 | int |
i64 | int | Int64 | int |
f64 | double | Double | double |
bool | bool | Int32 | int |
string | String | Pointer<Utf8> | Pointer<Utf8> |
bytes | List<int> | Pointer<Uint8> | Pointer<Uint8> |
handle | int | Int64 | int |
StructName | StructName | Pointer<Void> | Pointer<Void> |
EnumName | EnumName | Int32 | int |
T? | T? | same as inner type | same 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 againstnullptrbefore wrapping. If null, they returnnull:
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
WeaveffiExceptionvia 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
Stringvalues are converted to native UTF-8 viatoNativeUtf8()(frompackage:ffi). The resulting pointer is freed in afinallyblock viacalloc.free(). - Receiving strings back: Returned
Pointer<Utf8>values are decoded viatoDartString().
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
podspecto bundlelibweaveffi.dylib. - Android: Place
.sofiles underandroid/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
| File | Purpose |
|---|---|
go/weaveffi.go | CGo bindings: preamble, type wrappers, function wrappers |
go/go.mod | Go module descriptor (configurable module path) |
go/README.md | Prerequisites 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 type | Go type | C type (CGo) |
|---|---|---|
i32 | int32 | C.int32_t |
u32 | uint32 | C.uint32_t |
i64 | int64 | C.int64_t |
f64 | float64 | C.double |
bool | bool | C._Bool |
string | string | *C.char (via C.CString/C.GoString) |
bytes | []byte | *C.uint8_t + C.size_t |
handle | int64 | C.weaveffi_handle_t |
Struct | *StructName | *C.weaveffi_mod_Struct |
Enum | EnumName | C.weaveffi_mod_Enum |
T? | *T | pointer to scalar; nil-able pointer for strings/structs |
[T] | []T | pointer + C.size_t |
{K: V} | map[K]V | key/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:
- Declares a
C.weaveffi_errorvariable. - Passes its address as the last argument to the C function.
- Checks
cErr.code != 0after the call. - On error, extracts the message with
C.GoString, clears the C-side error withC.weaveffi_error_clear, and returns a Goerrorviafmt.Errorfalong 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. Adefer 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 callsC.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 callsC.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
ffigem 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
.rbfiles are plain Ruby — no Makefile, noextconf.rb, no build step beyondgem 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
| File | Purpose |
|---|---|
ruby/lib/weaveffi.rb | FFI bindings: library loader, attach_function declarations, wrapper classes |
ruby/weaveffi.gemspec | Gem specification with ffi ~> 1.15 dependency |
ruby/README.md | Prerequisites 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 type | Ruby type | FFI type |
|---|---|---|
i32 | Integer | :int32 |
u32 | Integer | :uint32 |
i64 | Integer | :int64 |
f64 | Float | :double |
bool | true/false | :int32 (0/1 conversion) |
string | String | :string (param) / :pointer (return) |
bytes | String (binary) | :pointer + :size_t |
handle | Integer | :uint64 |
Struct | StructName | :pointer |
Enum | Integer | :int32 |
T? | T or nil | :pointer for scalars; same pointer for strings/structs |
[T] | Array | :pointer + :size_t |
{K: V} | Hash | key/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
:stringparameters. FFI handles the encoding to null-terminated C strings automatically. - Receiving strings back: Returned
:pointervalues are read withread_string, then the Rust-allocated pointer is freed viaweaveffi_free_string. The wrapper copies the data into a Ruby string before freeing.
Bytes
- Passing bytes in: A
FFI::MemoryPointeris allocated, the byte data is copied in viaput_bytes, and the pointer is passed with a length parameter. - Receiving bytes back: The C function writes to an
out_lenparameter. The wrapper reads the data viaread_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:
_createallocates and returns a pointer. Caller owns it._destroyfrees the struct. Must be called exactly once._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
| Resource | Allocator | Free function | Notes |
|---|---|---|---|
| Returned string | Rust | weaveffi_free_string | Every const char* return |
| Returned bytes | Rust | weaveffi_free_bytes | Pass both pointer and length |
| Struct instance | Rust | *_destroy | Call exactly once |
| String from getter | Rust | weaveffi_free_string | Getter returns an owned copy |
| Error message | Rust | weaveffi_error_clear | Clears 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;
| Field | Type | Description |
|---|---|---|
code | int32_t | 0 = success, non-zero = error |
message | const 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 = 0is 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
codevalue. - No collision with functions. The error domain
namemust not match any function name in the same module. - Non-empty name. The error domain
namemust 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:
- Zero-initialize:
weaveffi_error err = {0, NULL}; - Call the function, passing
&erras the last argument. - Check
err.code— if non-zero, readerr.messageand clear withweaveffi_error_clear(&err). - 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()
}
}
| Helper | Effect |
|---|---|
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
| Language | Error mechanism | How errors surface |
|---|---|---|
| C | Check code field | Caller inspects err.code after every call |
| Swift | throws | WeaveFFIError thrown, caught with do/catch |
| Kotlin | Exception | RuntimeException thrown, caught with try/catch |
| Node.js | Thrown Error | Native addon throws JS Error |
| WASM | Return code | Caller 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"
| Field | Type | Default | Description |
|---|---|---|---|
async | bool | false | Mark the function as asynchronous |
cancellable | bool | false | Allow 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: trueis only meaningful whenasync: true. Settingcancellableon a synchronous function has no effect.
Best practices
- Prefer async for I/O-bound operations. Network requests, file I/O, and database queries are good candidates for async.
- Use cancellable for long-running operations. File uploads, streaming downloads, and batch processing should be cancellable.
- Avoid async for CPU-bound work. Short computations (math, parsing, validation) should remain synchronous.
- 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/&selfreceivers are ignored (only typed parameters are extracted).- The function body is irrelevant to extraction; only the signature matters.
- Doc comments (
///) on the function become thedocfield 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 type | WeaveFFI TypeRef | IDL string |
|---|---|---|
i32 | I32 | i32 |
u32 | U32 | u32 |
i64 | I64 | i64 |
f64 | F64 | f64 |
bool | Bool | bool |
String | StringUtf8 | string |
Vec<u8> | Bytes | bytes |
u64 | Handle | handle |
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 identifier | Struct(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>]
| Flag | Default | Description |
|---|---|---|
<INPUT> | required | Path to a .rs source file |
-o, --output | stdout | Write to a file instead of stdout |
-f, --format | yaml | Output 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 Structblocks 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>). -
selfreceivers.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
asyncfield is always set tofalse. The WeaveFFI validator rejectsasync: 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
| Key | Type | Default | Description |
|---|---|---|---|
swift_module_name | string | "WeaveFFI" | Name of the Swift module in Package.swift and the Sources/ directory. |
android_package | string | "com.weaveffi" | Java/Kotlin package declaration in the generated JNI wrapper. |
node_package_name | string | "weaveffi" | Package name in the generated Node.js N-API loader. |
wasm_module_name | string | "weaveffi_wasm" | Module name in the generated WASM JavaScript loader. |
c_prefix | string | "weaveffi" | Prefix prepended to every C ABI symbol ({prefix}_{module}_{function}). |
strip_module_prefix | bool | false | Strip 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 filegenerated/swift— SwiftPM System Library (CWeaveFFI) and Swift wrapper (WeaveFFI)generated/android— Kotlin wrapper + JNI shims + Gradle skeletongenerated/node— N-API addon loader +.d.tsgenerated/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/androidin Android Studio and build the:weaveffiAAR. - Build for WASM:
cargo build --target wasm32-unknown-unknown --releaseand load withgenerated/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
-
Create a new iOS App in Xcode (SwiftUI or UIKit).
-
Add the static library. Drag
MyGreeter.xcframework(or the.afile for a single architecture) into your project navigator. Ensure it appears under Build Phases > Link Binary With Libraries. -
Add the generated Swift package. In Xcode, go to File > Add Package Dependencies > Add Local… and select
generated/swift/. This adds theCWeaveFFI(C module map) andWeaveFFI(Swift wrapper) targets. -
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 findweaveffi.h. -
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/releasefor device builds). -
Add a bridging dependency. In your app target’s Build Phases > Dependencies, ensure
WeaveFFIis 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 aStringGreeter.greeting(_:_:)— returns aGreetingobject with.messageand.langpropertiesGreeting— a class wrapping the opaque Rust pointer, with automatic cleanup ondeinit
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
| Problem | Solution |
|---|---|
Undefined symbols for architecture arm64 | Check that the static library is linked and the library search path is correct. |
Module 'CWeaveFFI' not found | Ensure 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 Mac | Build with x86_64-apple-ios and create a universal binary with lipo. |
Next steps
- See the Swift generator reference for type mapping details.
- Read the Memory Ownership guide to understand struct lifecycle management.
- Explore the Calculator tutorial for a simpler end-to-end walkthrough.
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
-
Create a new Android project in Android Studio (Empty Activity, Kotlin, minimum SDK 21+).
-
Copy the generated android module. Copy the
generated/android/directory into your project as a Gradle module. In your rootsettings.gradle, add:include ':weaveffi' project(':weaveffi').projectDir = new File('generated/android') -
Add the module dependency. In your app’s
build.gradle:dependencies { implementation project(':weaveffi') } -
Place the Rust shared libraries. Copy each
.sointo the matchingjniLibsdirectory: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 -
Copy the C header. The JNI shims need
weaveffi.h. Ensure theCMakeLists.txtingenerated/android/src/main/cpp/has the correcttarget_include_directoriespointing togenerated/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): StringWeaveFFI.greeting(name: String, lang: String): Long— returns an opaque handle to aGreetingstruct
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
- Sync Gradle in Android Studio.
- Select an emulator or connected device.
- Press Run (Shift+F10). The app should display “Hello, Android!”.
Troubleshooting
| Problem | Solution |
|---|---|
UnsatisfiedLinkError: dlopen failed | The .so is missing from jniLibs/ or was built for the wrong ABI. |
java.lang.RuntimeException from JNI | A WeaveFFI error was raised. Check the exception message for details. |
Linker errors during cargo build | Ensure ANDROID_NDK_HOME is set and the NDK toolchain is on PATH. |
No implementation found for native method | The JNI function names must match the Kotlin package path exactly. |
Next steps
- See the Android generator reference for type mapping and JNI details.
- Read the Error Handling guide — JNI shims
convert C errors to
RuntimeExceptionautomatically. - Explore the Calculator tutorial for a simpler end-to-end walkthrough.
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:
| Platform | Output |
|---|---|
| macOS | target/release/libmygreeter.dylib |
| Linux | target/release/libmygreeter.so |
| Windows | target/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
| Problem | Solution |
|---|---|
OSError: dlopen ... not found | The shared library is not on the library search path. Set DYLD_LIBRARY_PATH or LD_LIBRARY_PATH. |
WeaveffiError at runtime | A Rust-side error was returned. Check the error code and message. |
ModuleNotFoundError: No module named 'weaveffi' | Run pip install . from generated/python/. |
| mypy type errors | Ensure weaveffi.pyi is in the package directory alongside weaveffi.py. |
Next steps
- See the Python generator reference for type mapping and memory management details.
- Read the Error Handling guide for the full error model.
- Explore the Calculator tutorial for a simpler end-to-end walkthrough.
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:
filesmust includeindex.node(the compiled N-API addon).osandcpufields 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
| Problem | Solution |
|---|---|
Error: Cannot find module './index.node' | The compiled addon is missing. Copy the built .dylib/.so as index.node. |
Error: dlopen ... not found | The Rust shared library is not on the library path. Set DYLD_LIBRARY_PATH or LD_LIBRARY_PATH. |
TypeError: weaveffi.hello is not a function | The 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
- See the Node generator reference for type
mapping details and the full
types.d.tsformat. - Read the Memory Ownership guide for struct lifecycle semantics.
- Explore the Calculator tutorial for a simpler end-to-end walkthrough.