Introduction
WeaveFFI generates type-safe bindings for 11 languages from a single IDL — no hand-written JNI, no duplicate implementations, no unsafe boilerplate.
Define your API once in YAML, JSON, or TOML; ship idiomatic packages for C, C++, Swift, Kotlin/Android, Node.js, WebAssembly, Python, .NET, Dart, Go, and Ruby that all talk to the same stable C ABI.
WeaveFFI works with any native library that exposes a stable C ABI —
whether it’s written in Rust, C, C++, Zig, or another language. Rust gets
first-class scaffolding via weaveffi generate --scaffold; other backends
implement the symbols declared in the generated C header directly.
Why WeaveFFI?
- One IDL, eleven languages. Describe your API once and ship packages to npm, SwiftPM, Maven, PyPI, NuGet, pub.dev, RubyGems, and Go modules.
- Stable C ABI underneath. Every target speaks to the same
extern "C"contract, so adding a new platform later is a code-gen change, not a rewrite. - Idiomatic per-target output. No lowest-common-denominator surface
area. Swift gets
async/awaitandthrows, Kotlin getssuspendand JNI glue, Python gets typed.pyistubs, TypeScript getsPromises, Dart getsdart:ffi— all from the same definition.
Design principle: standalone generated packages
Generated packages are fully self-contained and publishable to their native ecosystem (npm, CocoaPods, Maven Central, PyPI, NuGet, pub.dev, RubyGems, etc.) without requiring consumers to install WeaveFFI tooling or runtime dependencies. WeaveFFI is a build-time tool for library authors — consumers should never need to know it exists. Helper code (error types, memory management utilities) is generated inline into each package rather than pulled from a shared runtime dependency.
Where to next
- Getting Started — install → IDL → generate → call from C.
- Comparison — feature matrix vs UniFFI, cbindgen, diplomat, SWIG, autocxx, and an honest “when to choose WeaveFFI” guide.
- FAQ — runtime cost, customization, Windows support, distribution, licensing.
- Samples — the kitchen-sink
kvstorereference plus calculator/contacts/inventory walkthroughs. - Generators — per-target reference for each of the eleven languages.
- Guides — memory ownership, error handling, async, configuration.
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 IDL withadd,mul, andechofunctionsREADME.md— quick-start notes
3) Define your IDL
Open weaveffi.yml and replace its contents with an IDL that has a struct and
a function:
version: "0.3.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 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
}
// Emit the fixed WeaveFFI C ABI runtime surface (free_string, free_bytes,
// error_clear, cancel_token_*) in one line. Call this exactly once per
// cdylib.
abi::export_runtime!();
}
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 the WeaveFFI runtime symbols — invoke
weaveffi_abi::export_runtime!()to emit all of them in one line instead of writing each#[no_mangle]thunk by hand.
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.
Checking a single target
weaveffi doctor runs every toolchain check it knows about. To narrow it
down to a single target, pass --target {name}:
weaveffi doctor --target dart
weaveffi doctor --target cpp
weaveffi doctor --target go
weaveffi doctor --target ruby
weaveffi doctor --target dotnet
weaveffi doctor --target python
weaveffi doctor --target swift
weaveffi doctor --target android
weaveffi doctor --target node
weaveffi doctor --target wasm
Only checks whose applies_to set contains the chosen target (plus the
required Rust toolchain, which always runs) are executed. When --target
is set the command exits with a non-zero status if any of those checks
failed, making it scriptable in CI:
if ! weaveffi doctor --target dart; then
echo "Dart toolchain not ready" >&2
exit 1
fi
For machine-readable output (handy for piping into jq or aggregating
results across CI matrices), use --format json:
weaveffi doctor --target ruby --format json | jq '.[] | select(.ok == false)'
Each entry has id, name, ok, version, hint, and applies_to fields.
Architecture
This page is the canonical reference for how WeaveFFI works internally. It is the document new generator authors and contributors should read before making non-trivial changes; all other documentation is consumer- or library-author-facing.
High-level pipeline
Every weaveffi generate invocation flows through the same five
stages, in this order:
IDL file (YAML/JSON/TOML)
│
▼
Parse ── weaveffi-ir::parse — produces an `Api` IR
│
▼
Validate ── weaveffi-core::validate — rejects errors, collects warnings
│
▼
Resolve ── weaveffi-core::config — merges --config TOML and inline
│ generators: section into a single GeneratorConfig
▼
Generate ── weaveffi-core::codegen::Orchestrator — dispatches every
│ selected target generator in parallel via rayon
▼
Output ── Each generator writes its files under {out_dir}/{target}/
and updates {out_dir}/.weaveffi-cache/{target}.hash
Subcommands like validate, lint, diff, format, upgrade, and
watch re-use the parse and validate stages; generate, diff, and
watch additionally exercise resolve and generate.
Crate layout
The workspace is structured as a small set of stable, focused crates. The dependency graph is acyclic and shallow:
weaveffi-cli ──► weaveffi-core ──► weaveffi-ir
│
├──► weaveffi-gen-c
├──► weaveffi-gen-cpp
├──► weaveffi-gen-swift
├──► weaveffi-gen-android
├──► weaveffi-gen-node
├──► weaveffi-gen-wasm
├──► weaveffi-gen-python
├──► weaveffi-gen-dotnet
├──► weaveffi-gen-dart
├──► weaveffi-gen-go
└──► weaveffi-gen-ruby
weaveffi-abi ──► (stand-alone — linked at run time by every cdylib that
exposes the WeaveFFI C ABI)
weaveffi-fuzz ──► weaveffi-ir, weaveffi-core (workspace-private; unpublished)
| Crate | What it owns |
|---|---|
weaveffi-ir | The IR types (Api, Module, Function, TypeRef, …), the parse_api_str parser, the parse_type_ref mini-grammar, and CURRENT_SCHEMA_VERSION. |
weaveffi-abi | Stable C ABI runtime symbols: weaveffi_error, weaveffi_error_clear, weaveffi_free_string, weaveffi_free_bytes, the arena, cancel tokens. |
weaveffi-core | The Generator trait, the Orchestrator, validation rules, generator config resolution, and the per-generator hash cache. |
weaveffi-gen-* | Eleven generator crates. Each implements Generator and produces target-specific output (header, wrapper, package metadata). |
weaveffi-cli | The weaveffi binary. Parses the IDL, applies validation, instantiates every generator, and dispatches the Orchestrator. |
weaveffi-fuzz | cargo-fuzz harnesses for the parsers, the validator, and parse_type_ref. Workspace-private (not published to crates.io). |
Crates that contain unsafe code (weaveffi-abi, every samples/*
cdylib, weaveffi-fuzz, and the scaffold output emitted by
weaveffi generate --scaffold) opt in with
#![allow(unsafe_code)] at the top of their main source file. The
workspace-wide unsafe_code = deny lint forbids it everywhere else.
The IR
weaveffi_ir::ir defines a small algebraic type system. The shapes
that matter most:
Api { version, modules, generators }— root node.Module { name, functions, structs, enums, callbacks, listeners, errors, modules }— modules can nest.Function { name, params, returns, doc, async, cancellable, deprecated, since }.TypeRef— enumerates every supported type reference: primitives (I32,U32,I64,F64,Bool,StringUtf8,Bytes,Handle,BorrowedStr,BorrowedBytes), user types (Struct(String),Enum(String),TypedHandle(String)), and the four composite shapes (Optional,List,Map,Iterator).
Every IR type derives Debug, Clone, PartialEq, Serialize, and
Deserialize. Eq is derived where possible — a few types (Api,
Module, StructDef, StructField) intentionally omit Eq because
they transitively contain f64 (in default values) or
serde_yaml::Value.
TypeRef (de)serializes as a string with custom syntax (i32,
handle<T>, [T], {K:V}, T?, &str, &[u8]). The parser is
weaveffi_ir::ir::parse_type_ref; both human-written IDL and the
JSON Schema export rely on it.
Schema versioning
CURRENT_SCHEMA_VERSION (currently "0.3.0") lives in
crates/weaveffi-ir/src/ir.rs. SUPPORTED_VERSIONS lists
every version the upgrader can read (currently ["0.1.0", "0.2.0", "0.3.0"]). When you change the schema:
- Bump
CURRENT_SCHEMA_VERSIONand append the new version toSUPPORTED_VERSIONS. - Add migration code in
cmd_upgrade(weaveffi-cli/src/main.rs). - Update every sample IDL, the
weaveffi newtemplate, the README quickstart, and the Getting Started doc.
The stability page is the external contract; this section is the implementation note.
Validation
weaveffi_core::validate::validate_api is the single entry point.
It returns a Vec<ValidationError> (errors that must be fixed before
generation) and a separate Vec<ValidationWarning> (advisory; the
lint subcommand surfaces these).
Errors enforced today:
- Identifier well-formedness (
is_valid_identifier). - Reserved keyword rejection (
if,else,for,while,loop,match,type,return,async,await,break,continue,fn,struct,enum,mod,use). - Uniqueness of module/function/parameter/struct/enum/field/variant names within their respective scopes.
- Structs must have at least one field; enums at least one variant.
- Enum discriminant uniqueness within an enum.
- Type references resolve within the enclosing module chain (cross-sibling references are rejected; see Cross-module references).
- Iterator return types are valid in return position only.
- Map keys must be a primitive or enum type.
event_callbackon a listener must reference a callback in the same module.- Error domain name must not collide with a function name in the same module; codes must be non-zero and unique.
Warnings emitted today:
LargeEnumVariantCount(>100 variants).DeepNesting(composite types nested deeper than 3 levels).EmptyModuleDoc(nodoc:on any function in the module).AsyncVoidFunction(async without a return type).MutableOnValueType(mutable: trueon a non-pointer parameter).DeprecatedFunction(informational).
Async functions, cancellable functions, listeners, callbacks,
iterators (iter<T>), typed handles (handle<T>), borrowed types
(&str, &[u8]), nested modules, and cross-module type references are
all first-class. They pass validation and every generator handles
them. Do not re-add validator rejections for these features.
Generator configuration resolution
weaveffi_core::config::GeneratorConfig is the merged-and-resolved
configuration object every generator receives. It is built from three
sources (later wins):
- Defaults baked into
GeneratorConfig::default(). - The
--config <file.toml>external file passed togenerate. - The inline
generators:section of the IDL.
The IDL section is the project-local source of truth and overrides any machine-local TOML; see the Generator Configuration guide.
Orchestrator
weaveffi_core::codegen::Orchestrator coordinates the generator stage:
- If
--forceis set, every cache entry under{out_dir}/.weaveffi-cache/{target}.hashis invalidated. - For each registered generator, the orchestrator hashes
(api, generator.name())and compares against the persisted hash. - If
pre_generateis set inGeneratorConfig, the orchestrator shells out to it (cmd on Windows, sh elsewhere) and aborts on non-zero exit. - The pending generators run in parallel via
rayon::par_iter. Generators must therefore beSend + Sync. post_generateruns once after every generator has succeeded.- Each successful generator’s hash is persisted.
This per-generator caching is what lets weaveffi generate skip every
target whose IR has not changed since the last run; see the
Generator Configuration guide.
The Generator trait
Every target implements the Generator trait
(weaveffi_core::codegen::Generator):
pub trait Generator: Send + Sync {
fn name(&self) -> &'static str;
fn generate(&self, api: &Api, out_dir: &Utf8Path) -> Result<()>;
fn generate_with_config(
&self,
api: &Api,
out_dir: &Utf8Path,
_config: &GeneratorConfig,
) -> Result<()> {
self.generate(api, out_dir)
}
fn output_files(&self, _api: &Api, _out_dir: &Utf8Path) -> Vec<String> {
vec![]
}
fn output_files_with_config(
&self,
api: &Api,
out_dir: &Utf8Path,
_config: &GeneratorConfig,
) -> Vec<String> {
self.output_files(api, out_dir)
}
}
Generators emit code by direct string construction; there is no template-engine layer. Earlier prototypes wired in Tera with the intent of supporting user template overrides, but no generator ever read from it and the abstraction was removed in 0.4.0.
The signature reference above uses Result<T> from anyhow/color-eyre
and the IR types from weaveffi_ir; consult those crates for the
precise import set.
Implementation notes:
- Always implement
name()(returns the--targetflag value, e.g."swift"). - Implement the highest-level
generate_*method your generator needs and let the defaults forward through. Generators that do not read configuration can stop atgenerate. output_files_with_configis queried by--dry-runand the diff workflow. Override it whenever your generator’s file list depends on the IR or config (most do).- All file writes go inside
out_dir; do not write outside the passed directory or you will break the per-generator cache. - Generators run in parallel — share no mutable state across calls.
C ABI naming convention
Every emitted C symbol follows
{c_prefix}_{module}_{function} (default c_prefix = "weaveffi").
The c_prefix configuration is honored end-to-end: when set, the
generated C output uses it consistently, including references to
weaveffi-abi runtime symbols ({c_prefix}_error,
{c_prefix}_error_clear, {c_prefix}_free_string,
{c_prefix}_free_bytes).
Struct lifecycle, enum constants, and getter symbols follow the patterns in the C generator reference.
Determinism
Regenerating with the same WeaveFFI version on the same IDL produces byte-identical output.
The contract is enforced by determinism tests in the snapshot suite.
Internally, every HashMap iteration that contributes to generated
output has been replaced with BTreeMap or an explicit sort, and the
serde_json-backed cache key uses canonical ordering.
If you need to iterate a map inside a generator, use BTreeMap or
collect to a Vec and sort_by_key. Never rely on HashMap
iteration order for output; CI snapshot tests will fail
non-deterministically on different platforms or insta orderings.
Snapshot tests
crates/weaveffi-cli/tests/snapshots.rs runs every generator across an
eight-fixture corpus (the calculator, contacts, inventory, async-demo,
and events samples plus a kitchen-sink IDL). Output is diffed via
cargo-insta. When a snapshot diff is intentional:
cargo install cargo-insta --locked
cargo test -p weaveffi-cli --test snapshots
cargo insta review
Press a to accept, r to reject, s to skip. Commit accepted
.snap files in the same commit as the code change that produced
them — never commit .snap.new. CI rejects pending snapshots.
Adding a new generator
A condensed checklist (the long version lives in
CONTRIBUTING.md):
- Create
crates/weaveffi-gen-<lang>/mirroring the layout ofweaveffi-gen-c. Add it tomembersin the rootCargo.tomland depend onweaveffi-coreandweaveffi-ir. - Implement
Generator(start withgenerate; overridegenerate_with_configonce you accept config; overrideoutput_files_with_configso--dry-runandweaveffi diffwork). - Wire the generator into
crates/weaveffi-cli/src/main.rsso--target <name>accepts it (add a&LangGeneratorto theOrchestratorand an entry to the--targetparser). - Add snapshot fixtures in
crates/weaveffi-cli/tests/snapshots.rscovering at minimum the calculator, contacts, inventory, async-demo, and events sample IDLs. - Document the generator under
docs/src/generators/<lang>.mdand link it fromdocs/src/SUMMARY.md. - Add a consumer example under
examples/<lang>/and wire it intoexamples/run_all.sh. - Add
scripts/publish-crates.shto the dependency-ordered publish list (only when the crate is ready to be released).
Where to read next
- IDL Schema — the type system and validation rules from a user’s perspective.
- Generator Configuration — every option a consumer can set.
- Stability and Versioning — what counts as a breaking change once we hit 1.0.
- Memory Ownership — the per-target memory rules every generator must enforce.
- Async Functions — the per-target async invariants every async-capable generator implements.
Comparison
WeaveFFI sits in a crowded ecosystem of FFI tooling. This page is an honest, side-by-side look at how it compares to the projects you are most likely to evaluate against it: UniFFI, cbindgen, diplomat, SWIG, and autocxx.
All comparisons reflect the public state of each project at the time of writing. If something here is out of date, please open a PR.
At a glance
| WeaveFFI | UniFFI | cbindgen | diplomat | SWIG | autocxx | |
|---|---|---|---|---|---|---|
| Source language | Rust / C / C++ / Zig (anything with a C ABI) | Rust | Rust | Rust | C / C++ | C++ |
| Input format | YAML / JSON / TOML IDL | UDL or proc-macro on Rust | Rust source (annotated) | Rust source (annotated) | C/C++ headers + .i interface | C++ headers |
| Languages | ||||||
| C | ✓ | — | ✓ | ✓ | ✓ | — |
| C++ | ✓ (RAII, std::optional/vector/unordered_map) | — | ✓ (header) | ✓ | ✓ | ✓ (its purpose) |
| Swift | ✓ (SwiftPM, async/await, throws) | ✓ | — | ✓ | — | — |
| Kotlin / Android (JNI) | ✓ (Kotlin + JNI shim + Gradle) | ✓ | — | — | ✓ (Java via JNI) | — |
| Node.js | ✓ (N-API + .d.ts) | community add-on | — | — | ✓ (JavaScriptCore/V8) | — |
| WebAssembly | ✓ (loader + .d.ts) | — | — | ✓ (JS via WASM) | — | — |
| Python | ✓ (ctypes + .pyi) | ✓ | — | — | ✓ | — |
| .NET / C# | ✓ (P/Invoke + .csproj) | ✓ (community) | — | — | ✓ | — |
| Dart / Flutter | ✓ (dart:ffi) | community | — | ✓ | — | — |
| Go | ✓ (CGo) | community | — | — | ✓ | — |
| Ruby | ✓ (FFI gem) | — | — | — | ✓ | — |
| Type system | ||||||
Primitives + string | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
bytes / byte slices | ✓ | ✓ | ✓ (raw) | ✓ | partial | ✓ |
| Structs | ✓ (opaque + getters) | ✓ (records & objects) | ✓ (#[repr(C)]) | ✓ (opaque) | ✓ | ✓ |
| Enums w/ explicit discriminants | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Optionals | ✓ (T?) | ✓ | partial | ✓ | partial | ✓ |
| Lists | ✓ ([T]) | ✓ | partial | ✓ | ✓ | ✓ |
| Maps | ✓ ({K:V}) | ✓ | — | ✓ | partial | partial |
Typed handles (handle<T>) | ✓ | ✓ (objects) | — | ✓ (opaque) | partial | — |
Borrowed types (&str, &[u8]) | ✓ | partial | ✓ | ✓ | — | ✓ |
Iterators (iter<T>) | ✓ | ✓ (callbacks) | — | partial | partial | — |
| Async functions | ✓ (callback ABI + async/await/Promise/suspend/Task<T>) | ✓ | — | partial | — | — |
| Cancellable futures | ✓ (weaveffi_cancel_token) | partial | — | — | — | — |
| Callbacks / event listeners | ✓ (module-level) | ✓ | — (raw fn ptrs) | partial | partial | partial |
| Cross-module type references | ✓ | ✓ | n/a | ✓ | ✓ | ✓ |
| Nested modules | ✓ | partial | n/a | ✓ | ✓ | ✓ |
| Workflow | ||||||
| Single-binary CLI install | ✓ (cargo install weaveffi-cli) | ✓ | ✓ | ✓ | system package | ✓ |
| Standalone publishable packages | ✓ (npm, SwiftPM, pub.dev, NuGet, gem, etc.) | partial | n/a | partial | partial | n/a |
| JSON Schema for IDL editor support | ✓ | — | n/a | n/a | — | n/a |
extract from annotated source | ✓ (Rust) | ✓ (proc-macro) | ✓ (Rust) | ✓ (Rust) | n/a | ✓ (C++) |
watch mode | ✓ | — | ✓ (--watch) | — | — | partial |
format IDL canonicalizer | ✓ | — | n/a | n/a | — | n/a |
Schema migrations (upgrade) | ✓ | — | n/a | n/a | — | n/a |
| Custom template overrides | — | partial (Mako) | — | partial | ✓ (%typemap) | partial |
| Snapshot-tested generator output | ✓ | ✓ | ✓ | ✓ | partial | ✓ |
| Maturity | pre-1.0 | 1.0+ in Mozilla shipping products | 1.0+ widely deployed | pre-1.0 | 30+ years, ubiquitous | pre-1.0 |
| License | MIT OR Apache-2.0 | MPL-2.0 | MPL-2.0 | BSD-3-Clause | GPL with FOSS exception | MIT OR Apache-2.0 |
Legend: ✓ = first-class support; partial = supported with caveats or via extensions; — = not supported; n/a = not applicable to that tool’s scope.
Where competitors are stronger
We try hard to be honest about the trade-offs. Pick the right tool for the job:
- UniFFI is more mature. It ships in production at Mozilla (Firefox Sync, Glean, Nimbus) and has years of battle-testing across iOS, Android, and desktop. If you only need Swift, Kotlin, and Python today and you are comfortable with a UDL-or-proc-macro workflow, UniFFI is the safer choice.
- cbindgen is simpler if all you want is a C header. WeaveFFI generates a C header and ten other targets — if you only consume the C surface from C/C++ code, cbindgen has less ceremony, no IDL file, and a smaller dependency footprint.
- diplomat has a more polished C++ story. Its C++ output uses richer
templates and integrates more cleanly with existing C++ codebases. WeaveFFI’s
C++ output is RAII-based and includes a
CMakeLists.txt, but it’s optimized for greenfield projects, not for slotting into a 20-year-old C++ build system. - SWIG covers languages WeaveFFI doesn’t. Lua, Tcl, R, Octave, Perl, PHP — if your target is exotic, SWIG probably has a generator. SWIG also natively understands C and C++ headers, so you don’t need to author an IDL at all.
- autocxx is unmatched for “wrap an existing C++ library.” It reads your C++ headers directly and uses bindgen + cxx under the hood. WeaveFFI does not parse C++; you describe the surface area you want to expose, and WeaveFFI generates the contract.
- No IDE plugin yet. The other tools listed have community VSCode/JetBrains
extensions of varying quality. WeaveFFI ships a JSON Schema for editor
autocompletion and a
formatcommand, but no first-party IDE plugin. - No formal stability guarantee yet. WeaveFFI is pre-1.0; the IDL,
generated output, and runtime symbol names can shift in minor releases
(always with a
weaveffi upgradepath). UniFFI, cbindgen, and SWIG offer stronger compatibility commitments today.
When to choose WeaveFFI
WeaveFFI is the right pick when you want:
- One source of truth for many languages. If your library has to land in npm and SwiftPM and PyPI and NuGet and pub.dev and RubyGems and a Go module and a Gradle artifact — that’s the WeaveFFI sweet spot. UniFFI covers a smaller subset out of the box; cbindgen and autocxx don’t try.
- Standalone, publishable consumer packages. Generated packages are
self-contained: a Swift consumer adds your
.xcframework+ a SwiftPM manifest and is done. No “install WeaveFFI” step on the consumer side. - A native library that isn’t (only) Rust. WeaveFFI works against
anything that exposes a stable C ABI — Rust (with
--scaffoldconvenience), C, C++, Zig, etc. UniFFI and diplomat assume Rust; autocxx assumes C++. - Idiomatic per-target output, not a lowest-common-denominator API.
Async functions become
async/awaitin Swift,Promises in Node,suspend funin Kotlin,async defin Python, andTask<T>in C# — all from the sameasync: trueflag in the IDL. - A CLI workflow with
validate,lint,diff,watch,format, andupgrade. WeaveFFI is built for monorepos and CI: every sub-command has a--format jsonoutput mode, anddiff --checkandformat --checkare designed to drop into pre-commit and CI gates. - Honest pre-1.0 churn that’s mechanically migratable. Every breaking
IDL change ships with a
weaveffi upgrademigration. You don’t get stuck on an old version because the migration path is missing.
When to choose something else
- You only need Swift + Kotlin + Python and want maximum stability — use UniFFI.
- You only need a C header for a Rust crate — use cbindgen.
- You’re wrapping a large existing C++ codebase — use autocxx (or cxx + bindgen directly).
- Your target language is Lua, Tcl, R, Octave, Perl, or PHP — use SWIG.
- You need a battle-tested C++ binding generator with rich template support — use diplomat or SWIG.
Migrating to / from WeaveFFI
WeaveFFI’s IDL is intentionally close to UniFFI’s UDL surface area, which
makes hand-porting straightforward in either direction. There is no
automatic UDL → WeaveFFI converter today, but weaveffi extract can read
annotated Rust source and produce a starting IDL, which is often the
fastest path off any Rust-only generator. See the
extract guide for details.
FAQ
The top ten questions we hear about WeaveFFI. For broader context see the introduction, the comparison page, and the per-target generator docs.
1. Why not UniFFI?
UniFFI is excellent, ships in production at Mozilla, and is the right choice if you only need Swift, Kotlin, and Python. We built WeaveFFI because we needed:
- More targets out of the box. WeaveFFI ships first-class generators for C, C++, Swift, Kotlin/Android, Node.js, WASM, Python, .NET, Dart, Go, and Ruby — eleven in total. UniFFI’s first-party language list is shorter and the rest live as community extensions of varying maturity.
- A standalone CLI workflow. WeaveFFI is a single binary
(
cargo install weaveffi-cli) withvalidate,lint,diff,watch,format,upgrade, andextractsubcommands designed to drop into CI. UniFFI is a build-script integration first. - A non-Rust-only story. WeaveFFI’s IR is language-agnostic — any backend that can expose a stable C ABI (Rust, C, C++, Zig, …) can be driven from the same IDL. UniFFI is Rust-first.
- A YAML/JSON/TOML IDL with a JSON Schema. WeaveFFI ships
weaveffi.schema.jsonfor editor autocompletion. UniFFI’s UDL is custom-syntax and proc-macro is Rust-only.
If your matrix is only Swift+Kotlin+Python and you want maximum maturity today, UniFFI is the safer pick. See the comparison page for the full table.
2. Can I use it with C++ codebases?
Two distinct cases:
- Generating C++ bindings for consumers. Yes —
--target cppemits a header-only RAII C++ API (weaveffi.hpp) withstd::optional,std::vector,std::unordered_map, exception-based errors, move semantics, and aCMakeLists.txt. See the C++ generator docs. - Wrapping an existing C++ library. WeaveFFI does not parse C++ headers — you describe the surface area you want to expose in the IDL and the C++ implementation provides the stable C ABI symbols. If you want to start from C++ headers and auto-generate, look at autocxx or SWIG.
3. Does it support generics?
Yes, with a curated set of built-in generic shapes rather than open user-defined generics:
handle<T>— typed opaque pointers (compile-time-checked handle types per resource).iter<T>— lazy streaming sequences with_next/_destroyABI.[T]— homogeneous lists.{K:V}— homogeneous maps (passed as parallel key/value arrays at the C ABI).T?— optionals.&str,&[u8]— borrowed views (no copy at the boundary).
We deliberately do not support arbitrary user-defined generics
(e.g. Result<MyType, MyError> parameterized at the IDL level).
Cross-language generic monomorphization is a rabbit hole — the
built-in shapes cover ~95% of real-world FFI surface area without
requiring every target generator to implement type-erasure logic.
4. What’s the runtime overhead?
WeaveFFI itself adds no runtime beyond the small weaveffi-abi
crate (a few hundred lines: error helpers, string/byte-slice
allocators, cancel tokens). Per-call overhead is the cost of:
- Marshalling arguments across the C ABI (string→
const char*, list→*ptr + len, etc.). Borrowed types (&str,&[u8]) avoid copies. - The single
extern "C"function call. - Marshalling the return value back.
For primitive arguments and return types, this is roughly the cost of a normal function call plus an out-pointer write for the error. For larger structs, lists, and maps, it’s dominated by the underlying allocation/copy cost — not by anything WeaveFFI inserts.
Async functions add a callback indirection (the C ABI is callback-based) plus whatever runtime your backend uses. There is no scheduler imposed by WeaveFFI; the implementation chooses how to spawn work.
5. How are errors propagated?
Every generated function takes 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 a
heap-allocated UTF-8 message that the caller frees via
weaveffi_error_clear.
Each target language maps this to its native error story:
- C — direct
weaveffi_errorstruct. - C++ — exceptions (
weaveffi::Exception). - Swift —
throws+WeaveFFIError. - Kotlin — checked exceptions (
WeaveFFIException). - Node.js / TypeScript — thrown
Errorobjects (orPromise.rejectforasync). - WASM/JS — thrown
Error. - Python — raised
WeaveFFIError. - .NET — thrown
WeaveFFIException. - Dart — thrown
WeaveffiException. - Go — second
errorreturn value. - Ruby — raised
WeaveFFIError.
You can also declare named error domains in the IDL (per module) to assign stable numeric codes to expected failures. See the Error Handling guide.
6. Can I customize the generated code?
Yes, via two escape hatches in increasing order of power:
- Generator config (
--config cfg.tomlor inlinegenerators:table in the IDL). Controls Swift module names, Android package, C prefix, C++ namespace, Dart/Go/Ruby package names, and other per-target knobs. See the Generator Configuration guide. - Hook commands (
pre_generate/post_generatein the config). Run arbitrary shell commands before and after generation — useful forprettier,swiftformat,gofmt, etc.
If you need to change the C ABI shape itself, that’s a generator
contribution — see CONTRIBUTING.md.
7. Does it work with Flutter?
Yes — --target dart emits dart:ffi bindings plus a pubspec.yaml
that’s drop-in compatible with both Flutter and pure Dart projects.
You ship the generated package alongside the cdylib for each
platform Flutter targets (iOS framework, Android .so per ABI, macOS
.dylib, Linux .so, Windows .dll).
The generated Dart code uses the standard package:ffi helpers, so
it works on every Flutter platform that supports dart:ffi (i.e.
everything except Web today — for the browser, use --target wasm
and load the bindings via JS interop). See the
Dart generator docs.
8. Is it Windows-friendly?
Yes — WeaveFFI itself builds and runs on Windows (the CLI is plain Rust, no platform-specific dependencies). Generated outputs target Windows correctly:
- C / C++ — emitted headers are compiler-agnostic (MSVC, clang, gcc).
- .NET — P/Invoke uses
DllImportwith the right calling conventions and looks upweaveffi.dll. - Node.js — the N-API addon builds with
node-gypon Windows. - Python —
ctypesloadsweaveffi.dll. - Dart — looks up
weaveffi.dllviaPlatform.isWindows. - Go / Ruby — load the appropriate Windows shared library.
CI runs the Python end-to-end consumer test on Windows on every PR to keep the platform honest. The other targets are exercised on macOS and Linux only — if you hit a Windows-specific issue, please open an issue.
9. How do I distribute the cdylib?
You build a platform-specific shared library per target triple and ship it alongside the generated package. Three common patterns:
- Per-platform npm/PyPI/gem packages — publish one package per
(os, arch)and use a small loader in the consumer that picks the right binary at install or runtime. WeaveFFI generates the TypeScript/Python/Ruby loader, you supply the binaries. xcframeworkfor Swift — bundle iOS device, iOS simulator, and macOS slices into a single.xcframeworkthat SwiftPM can consume. The generatedPackage.swiftreferences it as a.binaryTarget..aarfor Android — package the JNI shim + per-ABI.sofiles into an Android Archive that Gradle resolves like any other dependency. The generatedbuild.gradleskeleton is compatible with this layout.
There is no opinionated “weaveffi publish” command today — you use each ecosystem’s normal publish flow. The generator-specific docs cover the recommended build matrix per language.
10. What’s the licensing?
WeaveFFI is dual-licensed under MIT OR Apache-2.0 at your option — the same dual-license used by the Rust project itself.
You can use WeaveFFI in commercial, closed-source, or open-source
projects without restriction. Generated code carries no license header
of its own — it’s yours to license however you like. Contributions
to the WeaveFFI repo are accepted under the same MIT-or-Apache-2.0
dual license; see CONTRIBUTING.md.
Stability and Versioning
WeaveFFI follows Semantic Versioning once it reaches 1.0.0. Until then it is in active pre-1.0 development and any surface area may change between minor versions. This page documents exactly what is — and isn’t — covered, what the deprecation policy will look like post-1.0, and how to bind your CI to a stable WeaveFFI workflow today.
What semver covers (post-1.0)
After the 1.0.0 release, the following surfaces will be governed by SemVer:
- CLI flags and subcommands. Every documented
weaveffi <subcommand>, every flag, every exit code, and every documented stdout/stderr format (--format jsonpayloads in particular). Adding a new optional flag is a minor bump; removing or renaming one is a breaking change. - IDL schema. The set of accepted top-level keys, type-reference syntax
(
handle<T>,iter<T>,[T],{K:V},T?,&str,&[u8], primitives, user-defined struct/enum names),versionsemantics, and the JSON Schema exported byweaveffi schema --format json-schema. - Generated code shape. The exported symbol names, function signatures, type names, package layouts, and ABI conventions of every generator’s output. A patch release will not change the bytes of the generated output; a minor release may add new symbols but will not remove or rename existing ones; a major release may break.
- Public Rust API of every published crate. That is
weaveffi-ir,weaveffi-abi,weaveffi-core,weaveffi-gen-c,weaveffi-gen-cpp,weaveffi-gen-swift,weaveffi-gen-android,weaveffi-gen-node,weaveffi-gen-wasm,weaveffi-gen-python,weaveffi-gen-dotnet,weaveffi-gen-dart,weaveffi-gen-go,weaveffi-gen-ruby, andweaveffi-cli. TheGeneratortrait, theOrchestrator, the IR types, and the C ABI runtime symbols exported fromweaveffi-abiare all public contracts.
What is NOT covered pre-1.0
While the workspace is at 0.x, everything above may change without
warning. In practice we try to keep breaking changes batched (one batch per
PRD, with a schema-version bump and a weaveffi upgrade migration), but the
contract is “no contract.” Things that have already changed during 0.x:
- IR type-reference syntax (
callbackwas removed in0.3.0). - The
Generatortrait gainedgenerate_with_configin0.3.0. A prototype Tera template hook (generate_with_templates,--templates,template_dir) was added and then removed in0.4.0because no generator ever consumed it. - The C ABI runtime added
weaveffi_arena_*andweaveffi_cancel_token_*families. weaveffi doctorgained--targetand--format json.
Pin the WeaveFFI version in CI (cargo install weaveffi-cli --version =0.3.0) and vendor the generated output in your repository so that
upgrades are an explicit, reviewable event.
Post-1.0 deprecation policy
Once we reach 1.0.0, breaking changes will follow this path:
- The feature is marked deprecated in a minor release. The CLI prints a
--warn-style diagnostic (weaveffi: warning: <name> is deprecated; <suggested replacement>) on every invocation that touches it. The generators emit a native deprecation marker where the target language supports one (#[deprecated]in Rust,@Deprecatedin Kotlin/Java,@available(*, deprecated:)in Swift,[Obsolete]in .NET, JSDoc@deprecatedin TypeScript, and so on — driven by the existing IDLdeprecated:field). - The deprecated feature continues to work for at least one full minor version.
- Removal lands in the next major release with a migration note in
CHANGELOG.md.
In short: nothing disappears in a patch release, nothing disappears without at least one minor release of warnings, and every removal ships with a documented replacement.
IR schema migration policy
The IR schema version is independent of the workspace version, but it is
tied to weaveffi-ir’s minor version: each weaveffi-ir minor bump
corresponds to at most one schema version bump.
CURRENT_SCHEMA_VERSION
in crates/weaveffi-ir/src/ir.rs is the source of truth. The supported
versions list (SUPPORTED_VERSIONS) names every schema version the parser
can read.
The migration guarantee:
weaveffi upgrade <file>always supports N-1 → N. If you skip a release, runweaveffi upgraderepeatedly (or pin to the intermediate version once, upgrade, then upgrade again) — the upgrader chains migrations in order through every version inSUPPORTED_VERSIONS.- The upgrader is idempotent. Running it on an already-current file prints
Already up to date (version X.Y.Z).and exits 0. - The upgrader exits non-zero in
--checkmode if migrations would rewrite the file, so you can wire it into CI:
weaveffi upgrade idl/api.yml --check
Schema version bumps are documented in CHANGELOG.md with a “Migration”
section explaining what the upgrader rewrote.
Generated-code stability (determinism)
Regenerating with the same WeaveFFI version on the same IDL produces byte-identical output.
This is enforced by the determinism tests added in PRD-v4 Phase
6: every generator’s output is hashed and re-hashed on the
kitchen-sink fixture, and any deviation fails CI. Internally, every
HashMap iteration that contributes to generated output has been replaced
by BTreeMap or an explicit sort. The serde_json-backed cache key uses a
canonical key ordering.
Practical consequences:
- Vendoring the generated
bindings/directory in your repository is safe. A reviewer will only see a diff when the IDL or the generator itself changes. weaveffi diff --check(see below) is a reliable CI gate.- Cross-platform regeneration (Linux vs macOS vs Windows) produces the same bytes for the same WeaveFFI version.
If you ever observe non-determinism, please file an issue with the IDL that triggers it — it’s a bug, not a quirk.
The weaveffi diff --check workflow for downstream CI
The single recommended way to guard a downstream repository against
“forgot to regenerate” mistakes is weaveffi diff --check:
weaveffi diff path/to/api.yml --out generated/ --check
diff --check regenerates into a temporary directory, compares against
--out, and exits:
- 0 when the on-disk output matches what regeneration would produce,
- 2 when at least one file differs (modified content),
- 3 when files are missing or extra (a target was added/removed).
It prints only the summary + N added, - M removed, ~ K modified —
suitable for CI logs without flooding the output.
A typical GitHub Actions step:
- name: Verify generated bindings are up to date
run: |
cargo install weaveffi-cli --locked --version =0.3.0
weaveffi diff idl/api.yml --out generated/ --check
Combine it with weaveffi format --check idl/api.yml (canonical IDL) and
weaveffi validate idl/api.yml (schema correctness) for a complete CI
guard.
See also
- Roadmap — what’s shipped, what’s planned for 1.0, what’s beyond.
- IDL Schema — the type system the schema version governs.
- Getting Started — installation and the basic
workflow
diff --checkplugs into.
Performance
WeaveFFI is designed to disappear in the build. Code generation should finish in under a second on every project from the calculator sample to a fully featured kitchen-sink API, leaving a budget for the surrounding build steps.
This page lists the explicit performance targets the project commits to,
the methodology used to measure them, the latest measurements taken on
commodity hardware, and the locations of the workflow artifacts that the
CI system uploads on every push to main.
Targets
The values below are hard targets enforced via the criterion benchmarks
in crates/weaveffi-core/benches/codegen_bench.rs and
crates/weaveffi-cli/benches/generate_bench.rs. The first
two benchmarks measure single-purpose pipeline stages; the latter two
measure the full code-generation surface (all 11 generators) end-to-end.
| Benchmark | Target | Inputs |
|---|---|---|
validate_kitchen_sink | < 5 ms | crates/weaveffi-cli/tests/fixtures/06_kitchen_sink.yml |
hash_kitchen_sink | < 1 ms | Same fixture, post-validation |
full_codegen_calculator | < 500 ms | samples/calculator/calculator.yml, all 11 generators |
full_codegen_kitchen_sink | < 2000 ms | Kitchen-sink fixture, all 11 generators |
A regression that pushes any of these benchmarks past its target is a
release blocker; the CI workflow uploads benchmark output as an artifact
on every push to main so reviewers can spot drift before it ships.
Methodology
The benchmarks use criterion.rs in its default sampling mode (100 samples, ~3 s measurement, statistical analysis). Each benchmark builds a fresh temporary directory per iteration so I/O is included in the measurement; this matches what users observe at the command line.
cargo bench --workspace -- --noplot
Profile a generator end-to-end with a flame graph:
cargo flamegraph -p weaveffi-cli --bench generate_bench
On macOS, the equivalent invocation uses cargo-instruments:
cargo instruments -t Time -p weaveffi-cli --bench generate_bench
Reference hardware for the numbers below: Apple M-series laptop, release
build (--release, lto = false), no other heavy processes running.
Latest measurements
These numbers were captured on the most recent baseline run after the hot-path optimizations described below. Each row is the criterion median; the parentheses show the headroom relative to the documented target.
| Benchmark | Median | Headroom vs target |
|---|---|---|
validate_kitchen_sink | 7.45 µs | ~670× under |
hash_kitchen_sink | 37.5 µs | ~27× under |
full_codegen_calculator | 6.92 ms | ~72× under |
full_codegen_kitchen_sink | 7.27 ms | ~275× under |
generate_c_large_api | 904 µs | — |
generate_swift_large_api | 1.93 ms | — |
generate_all_large_api | 24.1 ms | — |
generate_all_kitchen_sink | 7.27 ms | — |
The *_large_api benchmarks operate on a synthetic 10-module × 50-function
API (500 functions total) that does not have a documented ceiling; they
exist as a regression signal for the per-function cost of each generator.
Optimized hot paths
Profiling revealed three meaningful hot paths in the code-generation pipeline. Each one was tightened in this iteration; the optimizations delivered the cumulative ~7-10 % wall-clock improvement visible in the table above.
-
Pre-allocate output buffers. Both
render_c_headerandrender_swift_wrapperstarted fromString::new()and let the buffer grow by doubling, copying the entire string on each re-allocation. They now estimate the final output size from the number of modules, functions, structs, and callbacks in the API and pre-allocate accordingly viaString::with_capacity. -
write!instead ofpush_str(&format!(...))in the per-function hot loop ofrender_module_header(C generator) and the function wrappers in the Swift generator. Each replacement eliminates the intermediateStringthatformat!allocates before the result is appended to the output buffer. -
Drop the
Vec<String>+join(", ")pattern when emitting parameter signatures. The Swift generator now writes the comma-separated parameter list directly into the output buffer via thewrite_swift_params_sighelper; the C generator routes through awrite_params_intohelper that takes string slices, eliminating the per-parameter allocation loop and the joined intermediate.
These three categories are the ones explicitly called out as candidates in the original performance plan, in order of impact.
Things explicitly not optimized
serde_yamlparsing is the dominant cost of theweaveffi generatehappy path on disk because parsing happens before the benchmarks above run. The kitchen-sink fixture takes ~50 µs to parse on reference hardware, well below the validate/hash targets, andserde_yamldoes not expose a streaming API that is materially faster for our schemas. We accept it as the dominant CLI startup cost and document it here.
CI artifacts
The bench.yml workflow runs cargo bench on every
push to main and uploads the captured criterion output as a
bench-results artifact (retained for 90 days). To inspect the most
recent run:
- Open the bench workflow runs on GitHub.
- Pick the latest run that succeeded.
- Download the
bench-resultsartifact and extractbench.txt; it contains the full criterion output (medians, ranges, outlier counts) for the entire workspace.
The workflow does not gate merges on absolute thresholds today; instead it serves as the authoritative trail when a PR claims to improve or preserve benchmark numbers.
Roadmap
This page tracks where WeaveFFI is, where it’s going next, and what lives beyond 1.0. The shape and stability of each surface is documented in the stability page; this page is the feature timeline.
Completed
Everything shipped in the 0.x line. The CLI is feature-complete for
eleven targets and ~900 tests pass on Linux, macOS, and Windows.
Core CLI and IR
- Subcommands:
generate,new,validate,extract,lint,diff,doctor,completions,schema-version,upgrade,watch,format,schema. - IDL parsing from YAML, JSON, and TOML with span-aware
miette-rendered diagnostics. - Validation rejecting name collisions, reserved keywords, and
unsupported shapes; non-fatal diagnostics behind
--warnand the dedicatedlintcommand. - Inline
[generators.<target>]configuration in IDL files plus external TOML configs; everyGeneratorConfigfield is reachable from both. - IR schema versioning with
CURRENT_SCHEMA_VERSION = "0.3.0"and aweaveffi upgrademigrator that handles every supported source version. - JSON Schema export (
weaveffi schema --format json-schema) and a checked-inweaveffi.schema.jsonfor editor autocomplete. - File-watch regeneration (
weaveffi watch) with debounced events. - Canonical IDL formatter (
weaveffi format/--check). - Determinism: every generator’s output is byte-identical across runs on the same WeaveFFI version, enforced by tests.
- Snapshot tests via
cargo-instafor every generator across an eight-fixture corpus including a kitchen-sink IDL. - Fuzzing harnesses (
cargo-fuzz) for the YAML/JSON/TOML parsers, the validator, andparse_type_ref, with a 5-minute CI smoke run on every PR. - Parallel orchestrator with per-generator cache invalidation.
Type system
- Primitives,
string,bytes,&str,&[u8]borrowed views. - Structs (with
builder: true, default field values, doc strings). - Enums with explicit discriminants and per-variant docs.
- Optionals (
T?), lists ([T]), maps ({K:V}). - Typed handles (
handle<T>) and the legacy untypedhandlealias. - Iterators (
iter<T>) for streaming sequences. - Module-level
callbacks:andlisteners:for event patterns. async: trueandcancellable: truefunctions.- Cross-module type references and nested sub-modules.
deprecated:,since:, andmutable:annotations.
Generators (eleven targets)
- C — header with error struct, free helpers, typed handles,
configurable
c_prefixpropagated through every emitted symbol. - C++ — RAII header (
std::optional,std::vector,std::unordered_map,std::future), exception-based errors,CMakeLists.txt, configurable namespace and standard. - Swift — SwiftPM System Library + Swift wrapper with
async/awaitandthrows. - Android (Kotlin/JNI) — Kotlin wrapper, JNI shim, Gradle
scaffold,
suspend funfor async. - Node.js — N-API addon loader and
.d.tstypes withPromisefor async. - WASM — JS loader and
.d.tstypes aligned with the C ABI error model. - Python —
ctypesbinding,.pyistubs,asynciofor async. - .NET — P/Invoke binding,
.csproj/.nuspec,Task<T>for async. - Dart —
dart:ffibinding,pubspec.yaml,Future<T>for async. - Go — CGo binding,
go.mod, idiomaticerrorreturns. - Ruby —
ffigem binding, gemspec, struct class wrappers.
Quality and infrastructure
cargo deny,cargo audit,cargo machete, rustdoc lints, and coverage in CI on every PR.- Cross-platform CI matrix (Linux, macOS, Windows) including an
android-ndkjob, aswift-spmjob, and awindows-e2e-extendedjob. - End-to-end consumer programs in
examples/for every target, exercised in CI against the calculator and contacts cdylibs viaexamples/run_all.sh. - Async stress tests (1000 concurrent calls per target) verifying no callback/handle leaks.
- Doc strings flow through to native doc-comment syntax in every target.
- Standard prelude header (
// Generated by WeaveFFI X.Y.Z … DO NOT EDIT) on every generated file. - Templates engine (Tera) with user-overridable templates.
- Pre/post generation hooks.
- Benchmarking infrastructure (
criterion). - Automated publishing to crates.io via semantic-release.
- Governance:
CONTRIBUTING.mdat the repo root and the canonical internal architecture reference underdocs/src/.
Samples
calculator,contacts,inventory,async-demo,events,node-addon, plus the production-qualitykvstorereference that exercises every IDL feature in one place.
v1.0 candidate
Every PRD-v4 phase is complete. The workspace has graduated from “feature-complete prototype” to “1.0 release candidate” pending the format-canonicalization polish noted in the README “Status” section.
- Phase 1 — Eliminate
TypeRef::Callbackdead code across the workspace. - Phase 2 — Implement
weaveffi upgradeand bump schema to0.3.0. - Phase 3 — Wire every
GeneratorConfigoption through inline IDL configs. - Phase 4 — Source-span-aware diagnostics with
miette. - Phase 5 — Snapshot testing every generator with
insta. - Phase 6 — Deterministic generator output (sort all map iteration).
- Phase 7 —
weaveffi watch,weaveffi format, JSON Schema export. - Phase 8 — Real-world end-to-end consumer tests in CI for every target.
- Phase 9 — Quality infrastructure: deny, audit, machete, doc lints, coverage.
- Phase 10 — Fuzzing harness for parser, validator, and
parse_type_ref. - Phase 11 — Doc strings everywhere:
doc:→ native doc comments. - Phase 12 —
c_prefixaudit: full propagation through C, C++, scaffold. - Phase 13 — Build a non-trivial real-world sample:
kvstore. - Phase 14 — Parallel codegen with rayon and per-generator caching.
- Phase 15 — Cross-platform CI hardening: Windows path/hook fixes, NDK glue, Swift/iOS smoke.
- Phase 16 —
doctor --json, target-specific checks, richer hints. - Phase 17 —
--checkand--format jsonfor CI integration. - Phase 18 — Add CONTRIBUTING / SECURITY / CODE_OF_CONDUCT / ARCHITECTURE.
- Phase 19 — Async robustness: GC pinning, cancellation, leak tests.
- Phase 20 — Generator output polish: deterministic order, named-after-fields, prelude.
- Phase 21 — Beautiful README, comparison page, marketing polish.
- Phase 22 — Versioning policy, stability docs, roadmap to 1.0.
- Phase 23 — Performance: run benchmarks, profile, optimize hot paths, set targets.
- Phase 24 — Honesty pass on docs and full SUMMARY audit.
- Phase 25 — Add
weaveffi extractenhancements and round-trip integrity. - Phase 26 — Final quality pass and release-readiness checklist.
Post-1.0
Stretch goals that are out of scope for 1.0 but on our radar for the 1.x line:
- LSP server for IDL files. Schema-driven completion, hover docs, go-to-definition for cross-module type references, and validation diagnostics inside any LSP-aware editor.
- Official VS Code extension. Bundles the LSP server, adds syntax
highlighting for
.weaveffi.ymlfiles, runsweaveffi watchintegrated with the editor’s task system, and surfacesweaveffi doctorresults in the status bar. - Bazel and Buck rules. First-class build-system integration so
weaveffi_library(name = ..., idl = ...)produces all eleven target outputs as cacheable, reproducible Bazel/Buck targets. - Plugin SDK for third-party generators. A stable
Generatortrait surface plus aweaveffi-plugin-sdkcrate so generators for additional targets (e.g. Lua, Erlang, OCaml, R) can ship as independent crates and be loaded dynamically by the CLI. weaveffi publishautomation. A subcommand that drives the per-target package publishers —npm publish,pod trunk push,mvn deploy,gem push,dotnet nuget push,pub publish,go mod proxywarmup,pip upload— from a single config file, with dry-run support and a CI-friendly--checkmode.
If any of these would unblock your use case sooner, please open an issue describing the workflow you want — community demand drives the order.
See also
- Stability and Versioning — what’s covered by SemVer and how the deprecation policy works.
- Comparison — how WeaveFFI stacks up against UniFFI, cbindgen, diplomat, SWIG, and autocxx.
- Architecture — the canonical “how WeaveFFI works internally” reference for contributors.
Samples
This repo includes sample projects under samples/ that showcase end-to-end
usage of the C ABI layer. Each sample contains a YAML IDL and a Rust crate
that implements the corresponding weaveffi_* C ABI functions.
Kvstore (kitchen-sink reference)
Path: samples/kvstore
A production-quality, in-memory key/value store that exercises every IDL feature WeaveFFI supports in a single sample. Use this as the canonical reference when learning the IDL surface or when you need to copy/paste a real-world pattern for a new generator.
What it demonstrates:
- Typed handles (
handle<Store>) for opaque resource lifecycle - A struct (
Entry) with every primitive —i64,string,bytes, optional field (expires_at: i64?), list field (tags: [string]), and map field (metadata: {string:string}) — plus per-field doc strings andbuilder: true - A documented enum (
EntryKindwithVolatile,Persistent,Encrypted) - A documented error domain (
KvErrorwithKEY_NOT_FOUND,EXPIRED,STORE_FULL,IO_ERROR) - A module-level callback (
OnEvict) and listener (eviction_listener) - A streaming iterator return (
list_keys -> iter<string>) with prefix filter - A cancellable async function (
compact_async,async: true, cancellable: true) that respects aweaveffi_cancel_tokenwhile reclaiming bytes on a worker thread - A deprecated function (
legacy_put) andsince: "0.3.0"on every other function - A nested sub-module (
kv.stats) with its own struct (Stats) and a function that takes a cross-modulehandle<Store> - Inline
generators:overrides forswift.module_name,cpp.namespace,dotnet.namespace,dart.package_name,go.module_path, andruby.module_name
Build, generate bindings, and run the C ABI tests:
cargo build -p kvstore
cargo test -p kvstore
weaveffi generate samples/kvstore/kvstore.yml -o generated
Every consumer language under examples/ ships with a kvstore smoke test
(open -> put -> get -> delete -> close) that runs against the generated
bindings and the produced libkvstore cdylib; see examples/run_all.sh.
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 IDL
- 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: Async functions are fully supported by the validator. This sample focuses on the C ABI callback pattern; see the Async Functions guide for the per-target async/await story.
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
End-to-end testing
Every consumer language under examples/ ships with an executable
test that loads the calculator and contacts cdylibs at runtime and
asserts a representative slice of the C ABI (basic add, contact
create/list/cleanup). The examples/run_all.sh orchestrator builds
and runs each one in turn:
cargo build -p calculator -p contacts
WEAVEFFI_LIB=target/debug/libcalculator.dylib \
bash examples/run_all.sh
It prints [OK] {target} for each example that succeeds and exits
non-zero on the first failure. Use ONLY=python,ruby to run a
subset, or SKIP=android,go to omit individual targets. CI runs the
full matrix on Linux, most targets on macOS, and the Python path on
Windows. See the comment block at the top of examples/run_all.sh
for the full list of env vars and per-target prerequisites.
Reference
IDL Type Reference
WeaveFFI consumes a declarative IDL (Interface Definition Language) that describes modules, types, and functions. YAML, JSON, and TOML are all supported; this reference uses YAML throughout.
Editor autocomplete (JSON Schema)
WeaveFFI ships a JSON Schema for the IDL. To get autocomplete and validation in editors that support the YAML Language Server (VS Code, Neovim, Helix, …), add the following header comment to the top of your YAML file:
# yaml-language-server: $schema=./weaveffi.schema.json
The schema is generated by weaveffi schema --format json-schema and a copy
is checked in at weaveffi.schema.json
in the repository root.
Top-level structure
The shape of an IDL document, with placeholder ellipses for nested arrays and objects:
# yaml-language-server: $schema=./weaveffi.schema.json
version: "0.3.0"
modules:
- name: my_module
structs: [...]
enums: [...]
functions: [...]
callbacks: [...]
listeners: [...]
errors: { ... }
modules: [...]
generators:
swift:
module_name: MyApp
A complete, validating example lives at the bottom of this page in the Complete example section.
| Field | Type | Required | Description |
|---|---|---|---|
version | string | yes | Schema version ("0.1.0", "0.2.0", or "0.3.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. |
doc | string | no | Documentation string (see Documentation comments) |
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
version: "0.3.0"
modules:
- name: primitives
structs:
- name: Session
fields:
- { name: id, type: i64 }
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. T must be a struct defined in the same module so the generator
knows how to spell the handle’s type. At the C ABI level, handle<T> is
still a uint64_t.
version: "0.3.0"
modules:
- name: sessions
structs:
- name: Session
fields:
- { name: id, type: i64 }
functions:
- name: create_session
params: []
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
version: "0.3.0"
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
version: "0.3.0"
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
version: "0.3.0"
modules:
- name: contacts
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
version: "0.3.0"
modules:
- name: lists
structs:
- name: Contact
fields:
- { name: id, type: i64 }
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
version: "0.3.0"
modules:
- name: maps
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
version: "0.3.0"
modules:
- name: nested
structs:
- name: Contact
fields:
- { name: id, type: i64 }
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
version: "0.3.0"
modules:
- name: streaming
structs:
- name: Contact
fields:
- { name: id, type: i64 }
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
version: "0.3.0"
modules:
- name: events
functions: []
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 }
Callback names are not a valid TypeRef. Callbacks are wired up at the
module level: declare them under callbacks:, reference them from a
listeners: entry via event_callback, and emit asynchronous results
from functions marked async: true.
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
version: "0.3.0"
modules:
- name: events
functions: []
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.3.0"
modules:
- name: app
functions:
- name: init
params: []
modules:
- name: auth
structs:
- name: Session
fields:
- { name: id, type: i64 }
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.
version: "0.3.0"
modules:
- name: net
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:
version: "0.3.0"
modules:
- name: legacy
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:
version: "0.3.0"
modules:
- name: buffers
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.3.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 IDL combining structs, enums, optionals, lists, and nested types:
version: "0.3.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:
version: "0.3.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" }
functions:
- name: get_contact
params:
- { name: id, type: handle }
return: handle
Error codes must be non-zero and unique. Error domain names must not collide with function names in the same module.
Documentation comments
Every IR element accepts an optional doc: field. WeaveFFI propagates that
text into the generated bindings using each language’s native doc-comment
syntax. Multi-line strings (use YAML’s | block form) are preserved across
the boundary; single-line strings collapse to a one-liner where the target
syntax allows.
Supported sites:
Function.doc,Param.docStructDef.doc,StructField.docEnumDef.doc,EnumVariant.docCallbackDef.doc,ListenerDef.docErrorCode.doc
Per-target syntax:
| Target | Comment syntax | Param docs |
|---|---|---|
| C / C++ | /** ... */ directly above the declaration | not emitted |
| Swift | /// ... per line | /// - Parameter name: ... |
| Kotlin / Android | /** ... */ KDoc block | @param name ... inside the KDoc block |
| TypeScript (Node) | /** ... */ JSDoc | @param name ... |
| TypeScript (WASM) | /** ... */ JSDoc | @param name ... |
| Python | """...""" first statement; # ... above C ABI binds | NumPy-style Parameters section in the wrapper docstring |
| .NET (C#) | /// <summary>...</summary> XML doc | /// <param name="name">...</param> |
| Dart | /// ... | not emitted |
| Go | // SymbolName ... per Go’s convention | trailing // Parameters: block |
| Ruby | # ... lines above the def | # @param name [Object] ... |
Example IDL:
version: "0.3.0"
modules:
- name: docs
structs:
- name: Document
doc: |
Represents a single document tracked by the system.
Documents are persisted to disk and exposed via the public API.
fields:
- { name: id, type: i64, doc: Stable opaque identifier }
- { name: title, type: string, doc: Human-readable title }
functions:
- name: create_document
doc: Create a brand new document
params:
- { name: title, type: string, doc: Human-readable title }
return: Document
All eleven generators emit emit_doc calls before every documented
declaration; absent or empty doc: fields produce no extra output, so the
feature is fully opt-in.
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
Overview
The Android target produces a Gradle android-library template that
combines a Kotlin wrapper, JNI C shims, and a CMake build for the JNI
shared library. The wrapper exposes idiomatic Kotlin types while the JNI
layer bridges them to the C ABI.
What gets generated
| File | Purpose |
|---|---|
generated/android/settings.gradle | Gradle settings for the library module |
generated/android/build.gradle | android-library plugin, NDK config |
generated/android/src/main/kotlin/com/weaveffi/WeaveFFI.kt | Kotlin wrapper (enums, struct classes, namespaced functions) |
generated/android/src/main/cpp/weaveffi_jni.c | JNI shims that call the C ABI and throw Java exceptions |
generated/android/src/main/cpp/CMakeLists.txt | NDK CMake build for the JNI shared library |
Type mapping
| 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 |
Example IDL → generated code
version: "0.3.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]"
The Kotlin wrapper declares external fun entries inside a companion
object and loads the JNI library on first use:
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
}
}
Enums become Kotlin enum class with 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 }
}
}
Structs are wrapped in a Kotlin class implementing Closeable:
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
}
}
}
The JNI shims (weaveffi_jni.c) bridge each Kotlin external fun into
the C ABI, throwing RuntimeException on error:
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;
}
The CMake file links the JNI shim against the generated C header:
cmake_minimum_required(VERSION 3.22)
project(weaveffi)
add_library(weaveffi SHARED weaveffi_jni.c)
target_include_directories(weaveffi PRIVATE ../../../../c)
Build instructions
-
Install Android Studio (Giraffe or newer) plus the NDK.
-
Cross-compile the Rust cdylib for every Android ABI you support:
rustup target add aarch64-linux-android armv7-linux-androideabi \ x86_64-linux-android i686-linux-android export ANDROID_NDK_HOME=/path/to/ndk cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -t x86 \ build --release -p your_library -
Open
generated/androidin Android Studio, sync Gradle, and build the AAR (./gradlew :weaveffi:assemble). -
Add the resulting AAR as a dependency in your app module and ensure your
jniLibs/directory contains the Rust-built cdylib for each supported ABI.
Memory and ownership
- Struct wrappers implement
Closeable; either call.close()explicitly or useuse { ... }. Thefinalize()safety net runs during GC but is not a substitute for deterministic cleanup. - Strings returned from JNI are fresh Java strings; the JNI shim frees
the underlying Rust pointer with
weaveffi_free_stringbefore returning. - Byte arrays returned from JNI are copied with
SetByteArrayRegionbefore the Rust buffer is freed. - Optional values are passed as boxed wrappers (
Integer,Long,Double,Boolean); the JNI shim unboxes and forwards them to the C ABI.
Async support
Async IDL functions are exposed as Kotlin suspend fun declarations
that bridge the C ABI callback into a CompletableDeferred and
await() the result. The JNI shim retains the deferred via a global
reference, invokes it from the C callback, and releases the reference:
companion object {
@JvmStatic external fun fetchContactAsync(id: Int, deferred: Long): Unit
}
suspend fun fetchContact(id: Int): Contact {
val deferred = CompletableDeferred<Contact>()
val ref = JNIDeferred.retain(deferred)
try {
WeaveFFI.fetchContactAsync(id, ref)
return deferred.await()
} finally {
JNIDeferred.release(ref)
}
}
When the IDL marks the function cancel: true, the generated wrapper
hooks into Kotlin CoroutineContext cancellation and invokes the
underlying weaveffi_cancel_token.
Troubleshooting
UnsatisfiedLinkError: Couldn't find libweaveffi.so— the Rust-built cdylib was not packaged inside the AAR. Place it undersrc/main/jniLibs/<abi>/and rebuild.UnsatisfiedLinkErrorfor the JNI symbol itself — Kotlin external function names must match the JNI signature, including the_1escape for underscores. Re-runweaveffi generateif you hand-edited either side.- Crashes when releasing strings — the JNI shim is responsible for
calling
ReleaseStringUTFCharson everyGetStringUTFChars. If you edit the shim, keep the pairing intact. - R8/ProGuard removes
WeaveFFIsymbols — keep the wrapper class with-keep class com.weaveffi.** { *; }in your ProGuard rules.
C
Overview
The C target emits the canonical C header and a thin reference C file that every other WeaveFFI target ultimately speaks to. All cross-language bindings sit on top of these symbols, so the C output is also the easiest way to inspect what the IDL compiles to.
What gets generated
| File | Purpose |
|---|---|
generated/c/weaveffi.h | Public header: opaque types, enums, function prototypes, error/memory helpers |
generated/c/weaveffi.c | Empty placeholder for future convenience wrappers (kept so projects can link a single TU if desired) |
Type mapping
| 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 |
C ABI symbol naming follows a strict 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 |
When the IDL sets c_prefix, every symbol — including the runtime
helpers — is rewritten with the new prefix.
Example IDL → generated code
version: "0.3.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
The header opens with an include guard, standard headers, an
extern "C" block, and the shared error/memory helpers:
#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);
Structs become forward-declared opaque typedefs reached via create/destroy/getter functions:
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);
Enums turn into typed enum declarations with prefixed variants:
typedef enum {
weaveffi_contacts_ContactType_Personal = 0,
weaveffi_contacts_ContactType_Work = 1,
weaveffi_contacts_ContactType_Other = 2
} weaveffi_contacts_ContactType;
Optionals and lists use pointer-with-sentinel and pointer+length pairs:
int32_t* weaveffi_store_find(const int32_t* id, weaveffi_error* out_err);
weaveffi_contacts_Contact** weaveffi_contacts_list_contacts(
size_t* out_len,
weaveffi_error* out_err);
Every function takes a trailing weaveffi_error* out_err. On failure
out_err->code is non-zero and out_err->message points at a
Rust-allocated string the consumer must clear:
weaveffi_error err = {0, NULL};
int32_t total = 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;
}
Build instructions
The runnable example uses the calculator sample crate.
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
Windows:
cargo build -p calculator
cd examples\c
cl /I ..\..\generated\c main.c /link calculator.lib
.\main.exe
See examples/c/main.c for end-to-end usage.
Memory and ownership
Rust always owns memory it allocates. Strings and byte buffers returned across the boundary must be freed by the consumer with the matching helper:
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);
weaveffi_free_bytes((uint8_t*)data, len);
For struct handles, call the matching _destroy symbol when the
consumer is done. Borrowed parameters (const T*, string/bytes
inputs) remain owned by the caller for the duration of the call only.
Async support
Async functions (async: true) generate a callback-based variant with
the suffix _async. The wrapper accepts a function pointer whose
signature mirrors the synchronous return and an opaque void* context.
WeaveFFI invokes the callback once with either a result or an error.
typedef void (*weaveffi_demo_fetch_cb)(
void* context,
weaveffi_error* err,
const char* result);
void weaveffi_demo_fetch_async(
int32_t id,
weaveffi_demo_fetch_cb callback,
void* context);
Cancellable functions also accept a weaveffi_cancel_token*. See
Async functions for the full pattern.
Troubleshooting
undefined reference to weaveffi_*— make sure the linker sees the cdylib (-L target/debug -l<your-crate>). The header alone is not enough.- Crashes inside
weaveffi_free_string— the pointer was not Rust-allocated. Only free pointers returned from a generated getter or function. error: unknown type weaveffi_handle_t— the consumer included the header without<stdint.h>. Include order matters; the generated header pulls in the standard integer typedefs explicitly.weaveffi.cis empty — that file is intentionally a placeholder. All declarations live inweaveffi.h.
Node.js
Overview
The Node.js target produces a CommonJS loader plus TypeScript type
definitions. The actual native bridging happens in an N-API addon
(samples/node-addon for the in-tree examples) which the loader picks
up as index.node. The generator focuses on the consumer-facing surface
so that downstream projects can ship the same .node file with typed JS
bindings.
What gets generated
| File | Purpose |
|---|---|
generated/node/index.js | CommonJS loader that requires ./index.node |
generated/node/types.d.ts | TypeScript declarations for the public surface |
generated/node/package.json | npm package metadata (main, types) |
Type mapping
| 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[] |
Example IDL → generated code
version: "0.3.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]"
Structs become TypeScript interfaces and enums become explicit numeric TypeScript enums:
export interface Contact {
name: string;
email: string | null;
tags: string[];
}
export enum Color {
Red = 0,
Green = 1,
Blue = 2,
}
Optional return and parameter types use | null; arrays use T[]:
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[]
Build instructions
The runnable example uses the calculator sample.
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
Windows: copy target\debug\index.dll to generated\node\index.node
and run npm start from examples\node.
For your own project, build an N-API addon (see samples/node-addon),
copy the resulting platform-specific binary in as index.node, and
publish the generated directory as a private npm package or ship it
inside your app.
Memory and ownership
- The N-API addon is responsible for all conversions between JS values and C ABI types. Strings and byte buffers are copied into JS-managed storage, so consumers never need to think about freeing memory.
- Struct handles surface as opaque numeric IDs (
bigint). The addon exposes_destroyhelpers that tear down the underlying Rust state; use them intry/finallyblocks for deterministic cleanup. - Errors from the C ABI are converted into JavaScript
Errorinstances by the addon before bubbling up to the caller.
Async support
Async IDL functions are exposed as JS functions that return a Promise.
The N-API addon implements them with napi_create_async_work so that
the JS event loop stays responsive while the Rust function runs:
export function fetch_contact(id: number): Promise<Contact>;
When the IDL marks the function cancel: true, the addon also accepts
an AbortSignal parameter and forwards aborts to the underlying
weaveffi_cancel_token.
Troubleshooting
Error: Cannot find module 'index.node'— the addon binary is missing. Build the N-API addon for your platform and copy it intogenerated/node/asindex.node.dlopen: ... image not found— the addon links against the Rust cdylib at runtime; setDYLD_LIBRARY_PATH/LD_LIBRARY_PATHor copy the cdylib next toindex.node.BigInterrors withhandle— handles are 64-bit; pass them asbigint, notnumber.- TypeScript complains about missing types — point
tsconfig’spathsatgenerated/node/types.d.tsor include the generated package incompilerOptions.types.
Swift
Overview
The Swift target emits a SwiftPM System Library (CWeaveFFI) that
references the generated C header via a module.modulemap, plus a thin
Swift module (WeaveFFI) that wraps the C ABI in idiomatic Swift with
throws-based error handling and Swift-native types.
What gets generated
| File | Purpose |
|---|---|
generated/swift/Package.swift | SwiftPM manifest declaring CWeaveFFI (system library) and WeaveFFI (Swift wrapper) |
generated/swift/Sources/CWeaveFFI/module.modulemap | C module map pointing at the generated header |
generated/swift/Sources/WeaveFFI/WeaveFFI.swift | Swift wrapper: enums, struct classes, namespaced module functions |
Type mapping
| IDL type | Swift type | Notes |
|---|---|---|
i32 | Int32 | Direct value |
u32 | UInt32 | Direct value |
i64 | Int64 | Direct value |
f64 | Double | Direct value |
bool | Bool | Mapped to Int32 0/1 at the ABI |
string | String | UTF-8 buffers + length |
bytes | Data / [UInt8] | Pointer + length |
handle | UInt64 | Direct value |
StructName | StructName (class) | Wraps OpaquePointer |
EnumName | EnumName (enum) | Backed by Int32 |
T? | T? | Optional pointer / sentinel |
[T] | [T] | Pointer + length |
Example IDL → generated code
version: "0.3.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 become Swift enums with lowerCamelCase cases backed by Int32:
public enum ContactType: Int32 {
case personal = 0
case work = 1
case other = 2
}
Structs are wrapper classes around an OpaquePointer. The deinit calls
the C destructor; computed properties call the C 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)
}
}
Module functions live as static methods on a namespace enum and try
into Swift errors:
public enum Contacts {
public static func create_contact(_ name: String, _ age: Int32) throws -> Contact {
var err = weaveffi_error(code: 0, message: nil)
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)
}
}
Optionals and lists use withOptionalPointer and
withUnsafeBufferPointer helpers:
@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) }
}
ids.withUnsafeBufferPointer { buf in
let ids_ptr = buf.baseAddress
let ids_len = buf.count
}
Build instructions
The runnable example uses the calculator sample.
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
In a real SwiftPM application, add the generated package as a path
dependency, link CWeaveFFI and WeaveFFI, and ship the cdylib as part
of an XCFramework or bundled .dylib/.so.
Memory and ownership
- Struct classes own an
OpaquePointer. The classdeinitcalls the matching C destructor. - Returned strings are copied into Swift
Stringand the raw pointer is freed viaweaveffi_free_stringimmediately. withUnsafeBufferPointerandwithOptionalPointerkeep input buffers alive only for the duration of the C call — there is no copy.- For
bytesparameters, the wrapper useswithUnsafeBytesso Swift retains ownership.
Async support
Async IDL functions are exposed as async throws methods that bridge
the C ABI callback into Swift’s structured concurrency via
withCheckedThrowingContinuation:
public static func fetch_contact(_ id: Int32) async throws -> Contact {
return try await withCheckedThrowingContinuation { cont in
weaveffi_contacts_fetch_contact_async(id, { ctx, err, result in
let cont = Unmanaged<ContWrapper>.fromOpaque(ctx!)
.takeRetainedValue().cont
if let err = err, err.pointee.code != 0 {
cont.resume(throwing: WeaveFFIError.from(err.pointee))
} else if let result = result {
cont.resume(returning: Contact(ptr: result))
} else {
cont.resume(throwing: WeaveFFIError.error(code: -1,
message: "null result"))
}
}, Unmanaged.passRetained(ContWrapper(cont: cont)).toOpaque())
}
}
When the IDL marks the function cancel: true, the generated wrapper
exposes a Swift Task cancellation handler that forwards to the
underlying weaveffi_cancel_token.
Troubleshooting
module 'CWeaveFFI' not found— Xcode/SwiftPM did not pick up the generatedmodule.modulemap. Make sureSources/CWeaveFFI/module.modulemapis on disk and the package declaressystemLibrary(name: "CWeaveFFI").Library not loaded: libweaveffi.dylib— setDYLD_LIBRARY_PATHfor development or embed the dylib in your application bundle for distribution.- Crashes after
deinit— never reuse anOpaquePointerafter the owning Swift wrapper goes out of scope. The C side has already freed it. - Optional struct ends up
nileven when present — the C function is allowed to return a null pointer to indicate absence; double-check the Rust implementation actually returnsSome(_)for the case you expect.
WASM
Overview
The WASM target produces a minimal ES module loader plus a README to
help instantiate wasm32-unknown-unknown builds of WeaveFFI cdylibs.
Higher-level ergonomics (struct wrappers, async helpers) are intentionally
kept out of the generator and live in your application code so that the
WASM module stays as small as possible.
What gets generated
| File | Purpose |
|---|---|
generated/wasm/weaveffi_wasm.js | ES module loader with JSDoc |
generated/wasm/README.md | Quickstart and type conventions |
Type mapping
| 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 |
Example IDL → generated code
The loader exports a single async function that fetches and instantiates
a .wasm module:
export async function loadWeaveFFI(url) {
const response = await fetch(url);
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, {});
return instance.exports;
}
Use it directly:
const wasm = await loadWeaveFFI('lib.wasm');
const sum = wasm.weaveffi_math_add(1, 2);
Structs are passed across the boundary as opaque i64 handles:
const handle = wasm.weaveffi_contacts_create();
const age = wasm.weaveffi_contacts_Contact_get_age(handle);
wasm.weaveffi_contacts_Contact_destroy(handle);
Lists and strings cross via pointer+length pairs in linear memory:
const ptr = wasm.weaveffi_alloc(4 * items.length);
const view = new Int32Array(wasm.memory.buffer, ptr, items.length);
view.set(items);
wasm.weaveffi_data_process(ptr, items.length);
wasm.weaveffi_dealloc(ptr, 4 * items.length);
Build instructions
macOS / Linux / Windows (cross-compilation, all hosts):
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release -p your_library
The resulting .wasm is in target/wasm32-unknown-unknown/release/.
Serve it over HTTP and load it with the generated helper:
<script type="module">
import { loadWeaveFFI } from './weaveffi_wasm.js';
const wasm = await loadWeaveFFI('/your_library.wasm');
</script>
Memory and ownership
- WASM linear memory is owned by the module. Use the exported
weaveffi_alloc/weaveffi_dealloc(or__wbindgen_*helpers if you bundlewasm-bindgen) to manage buffers passed to the WASM module — every alloc must be paired with a dealloc. - Strings and byte buffers crossing into the WASM module require copying their contents into linear memory before the call.
- Struct handles must be paired with their generated
_destroyfunction to free the Rust-side allocation. - The host JS side is responsible for keeping references alive while the WASM call runs; the WASM module cannot reach back into JS-owned memory.
Async support
Native async is not yet emitted by the WASM target. Async IDL
functions still produce their synchronous C ABI counterparts and a
JavaScript shim that wraps the call in a Promise.resolve(...). For
true async (e.g. Promise-returning JS functions backed by Rust
futures) bundle the cdylib with wasm-bindgen or wasm-pack and
expose the resulting .wasm to the loader.
Troubleshooting
LinkError: import object field 'env' is not a Function— the loader instantiates with an empty imports object. If your Rust crate imports host functions, extendloadWeaveFFIto pass them in.- Out-of-memory after many calls — every pointer returned from the
WASM module must be deallocated. Wrap calls in helper functions that
always dealloc on
finally. - Wrong endianness or struct layout — WASM is little-endian and
uses
wasm32pointers. Always read with the matchingTypedArrayview (Int32Array,Uint8Array, …). - The
.wasmfile fails to instantiate — the build artifact must bewasm32-unknown-unknown.wasm32-wasimodules require WASI imports and cannot run in the browser without a polyfill.
Python
Overview
The Python target produces pure-Python ctypes bindings, type stubs, and
packaging files. Calls go through Python’s built-in ctypes module so
there is no compilation step, no native extension, and no third-party
runtime dependency. The generated package works on any Python 3.7+
interpreter that can dlopen the shared library.
The trade-off is that ctypes calls are slower than compiled extensions
(cffi, pybind11, PyO3). For typical FFI workloads the overhead is
negligible compared to the work done inside the Rust library.
What gets generated
| File | Purpose |
|---|---|
python/weaveffi/__init__.py | Re-exports the public API from weaveffi.py |
python/weaveffi/weaveffi.py | ctypes bindings: library loader, wrappers, 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 |
Type mapping
| 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 cross the boundary as c_int32 (0/1) because C has no
standard fixed-width boolean type across ABIs.
Example IDL → generated code
version: "0.3.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
The generated module loads the platform-specific 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 become Python functions with full type hints; ctypes
argtypes/restype are set up at the call site:
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
_err = _WeaveffiErrorStruct()
_result = _fn(_string_to_bytes(name), _string_to_bytes(email),
contact_type.value, ctypes.byref(_err))
_check_error(_err)
return _result
Enums become IntEnum subclasses:
class ContactType(IntEnum):
"""Type of contact"""
Personal = 0
Work = 1
Other = 2
Structs become Python classes that wrap a void pointer and expose
@property getters; __del__ calls the C destructor:
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
return _bytes_to_string(_fn(self._ptr)) or ""
The accompanying .pyi stub mirrors the public surface for IDE/mypy:
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: ...
Build instructions
-
Generate the bindings:
weaveffi generate --input api.yaml --output generated/ --target python -
Build the Rust shared library:
cargo build --release -p your_library -
Install the package (editable install for development):
cd generated/python pip install -e . -
Make the shared library findable at runtime:
- macOS:
export DYLD_LIBRARY_PATH=$PWD/../../target/release - Linux:
export LD_LIBRARY_PATH=$PWD/../../target/release - Windows: place
weaveffi.dllnext to your script or add its directory toPATH.
- macOS:
-
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 and ownership
-
Strings in: Python
stris encoded to UTF-8 by_string_to_bytesbefore crossing the boundary. ctypes manages the lifetime of the temporary buffer. -
Strings out: Returned
c_char_pis decoded via_bytes_to_string. The Rust runtime owns the original pointer; the preamble registersweaveffi_free_stringfor cleanup. -
Bytes: copied in via a ctypes array, copied out via slicing (
_result[:_out_len.value]). Rust frees the original buffer. -
Structs: wrappers hold an opaque
c_void_p.__del__calls the matching_destroyC function. For deterministic cleanup, use the_PointerGuardcontext manager:with _PointerGuard(handle, _lib.weaveffi_contacts_Contact_destroy): ...
Async support
Async IDL functions are exposed as async def wrappers that schedule
the C ABI callback onto the running asyncio event loop using
loop.call_soon_threadsafe and a Future. The wrapper captures the
loop, hands the C ABI a callback that resolves the future, and awaits
it:
async def fetch_contact(id: int) -> Contact:
loop = asyncio.get_running_loop()
fut: asyncio.Future[Contact] = loop.create_future()
_ctx_id = _retain_ctx((loop, fut))
_lib.weaveffi_contacts_fetch_contact_async(id, _async_trampoline, _ctx_id)
return await fut
When the IDL marks the function cancel: true, the wrapper hooks the
asyncio cancellation into a weaveffi_cancel_token.
Troubleshooting
OSError: cannot find ...— the loader could not locate the shared library. SetDYLD_LIBRARY_PATH/LD_LIBRARY_PATHor copy the library next to your script.WeaveffiError: ...— the Rust side returned a non-zero error code. CatchWeaveffiErrorand inspect.code/.message.AttributeError: ... has no attribute 'argtypes'— the wrapper setsargtypes/restypeat the call site; ensure you’re calling the generated function, not reaching into_libdirectly.- Garbage-collected struct still referenced from Rust — keep a
Python reference until you’re done; Python will call
__del__only after the last reference is dropped.
.NET
Overview
The .NET target emits a C# class library that wraps the C ABI through
P/Invoke.
Structs are exposed as IDisposable classes with property getters,
errors become managed exceptions, and the project targets net8.0.
What gets generated
| File | Purpose |
|---|---|
generated/dotnet/WeaveFFI.cs | C# bindings: P/Invoke declarations, wrapper classes, enums, exceptions |
generated/dotnet/WeaveFFI.csproj | SDK-style project (net8.0, AllowUnsafeBlocks) |
generated/dotnet/WeaveFFI.nuspec | NuGet package metadata |
generated/dotnet/README.md | Build and pack instructions |
Type mapping
| 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 |
Example IDL → generated code
version: "0.3.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: age, type: i32 }
return: handle
- name: get_contact
params:
- { name: id, type: handle }
return: Contact
- name: list_contacts
params: []
return: "[Contact]"
Enums become C# enums with explicit values:
/// <summary>Type of contact</summary>
public enum ContactType
{
Personal = 0,
Work = 1,
Other = 2,
}
Structs are wrapped in IDisposable classes with a finalizer safety
net:
public class Contact : IDisposable
{
private IntPtr _handle;
private bool _disposed;
internal Contact(IntPtr handle) { _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 void Dispose() {
if (!_disposed) {
NativeMethods.weaveffi_contacts_Contact_destroy(_handle);
_disposed = true;
}
GC.SuppressFinalize(this);
}
~Contact() { Dispose(); }
}
Functions live as static methods on a class named after the module
and throw 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);
}
}
}
P/Invoke entries live in an internal NativeMethods class:
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, 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);
}
Build instructions
-
Generate the bindings:
weaveffi generate --input api.yaml --output generated/ --target dotnet -
Build:
cd generated/dotnet dotnet build -
Pack as NuGet:
dotnet pack -c ReleaseThe resulting
.nupkglives inbin/Release/. For production packages, bundle the native cdylib inside the package underruntimes/{rid}/native/. -
Make the cdylib findable at runtime — place it next to the built DLL, set
LD_LIBRARY_PATH/DYLD_LIBRARY_PATH, or include it in the NuGet package as above.
Memory and ownership
- Each struct class implements
IDisposable; useusingfor deterministic cleanup. The finalizer is a safety net only and runs on a non-deterministic schedule. - Strings returned from getters are copied into managed memory and the
raw pointer is freed via
weaveffi_free_stringimmediately, so string properties do not require any disposal. - Strings passed as parameters are marshalled with
Marshal.StringToCoTaskMemUTF8and freed in afinallyblock. - Optional struct returns surface as
IntPtr.Zerofrom the C ABI and becomenullin C#.
Async support
Async IDL functions are exposed as async Task<T> methods. The
generator emits a static dispatcher that wires the C ABI callback into
a TaskCompletionSource<T>:
public static Task<Contact> FetchContactAsync(int id, CancellationToken ct = default)
{
var tcs = new TaskCompletionSource<Contact>();
var handle = GCHandle.Alloc(tcs);
NativeMethods.weaveffi_contacts_fetch_contact_async(
id, _asyncCallback, GCHandle.ToIntPtr(handle));
if (ct.CanBeCanceled) {
ct.Register(() => NativeMethods.weaveffi_cancel(/* token */));
}
return tcs.Task;
}
When the IDL marks the function cancel: true, the generated wrapper
forwards CancellationToken cancellation to the underlying
weaveffi_cancel_token.
Troubleshooting
DllNotFoundException: Unable to load DLL 'weaveffi'— the runtime cannot find the shared library. Place it in the application directory or setLD_LIBRARY_PATH/DYLD_LIBRARY_PATH.AccessViolationExceptionon dispose — the struct has been disposed twice. Wrap usage inusingand avoid passing handles around once disposed.- Strings returned with garbage characters — make sure your
binding is targeting
UTF8(Marshal.PtrToStringUTF8,StringToCoTaskMemUTF8); the generated helpers do this for you. - NuGet consumers cannot find the cdylib — ship it inside the
package under
runtimes/{rid}/native/so the .NET runtime resolves it automatically.
C++
Overview
The C++ target emits a header-only library weaveffi.hpp that wraps the
C ABI in idiomatic C++17. Structs become RAII classes with deleted copies
and movable handles, errors map to exceptions, and async functions return
std::future. A CMakeLists.txt is included so the generated directory
can be dropped into any CMake build.
What gets generated
| File | Purpose |
|---|---|
generated/cpp/weaveffi.hpp | Header-only bindings: extern “C” declarations, RAII wrappers, enum classes, inline function wrappers |
generated/cpp/CMakeLists.txt | INTERFACE library target (weaveffi_cpp) |
generated/cpp/README.md | Build instructions |
Type mapping
| 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>& |
Example IDL → generated code
version: "0.3.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 become enum class:
enum class ContactType : int32_t {
Personal = 0,
Work = 1,
Other = 2
};
Structs become RAII handle wrappers with deleted copy and noexcept move:
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&& o) noexcept : handle_(o.handle_) { o.handle_ = nullptr; }
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;
}
};
Free functions live in the weaveffi namespace and throw 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);
}
} // namespace weaveffi
WeaveFFIError extends std::runtime_error. When the IDL declares
custom error codes the generator also emits typed subclasses that the
exception dispatcher uses to throw the most specific exception:
try {
auto contact = weaveffi::contacts_find_contact(42);
} catch (const weaveffi::WeaveFFIError& e) {
std::cerr << "Error " << e.code() << ": " << e.what() << '\n';
}
Build instructions
The generated CMakeLists.txt defines an INTERFACE library:
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)
Consume it from your project:
add_subdirectory(path/to/generated/cpp)
add_executable(myapp main.cpp)
target_link_libraries(myapp weaveffi_cpp)
Then #include "weaveffi.hpp" and link against the Rust shared library
(libweaveffi.dylib, libweaveffi.so, or weaveffi.dll).
Memory and ownership
- Struct wrappers own a single
void*handle. The destructor calls the C_destroyfunction. Copies are deleted; moves transfer ownership by nulling the source handle. - Strings returned from getters are copied into
std::stringand the raw pointer is freed viaweaveffi_free_stringbefore returning. - Optional fields use
std::optional<T>; anullptrfrom the C layer becomesstd::nullopt. std::vector<T>returns own their contents. List parameters borrow the underlying buffer for the duration of the call.
Async support
Async IDL functions return std::future<T>. The wrapper allocates a
heap-owned std::promise, hands the C ABI a callback that resolves
(or rejects) the promise, and returns the corresponding future:
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 compose with your event loop. When
the IDL marks the function cancel: true, the generated wrapper
forwards an additional weaveffi_cancel_token*.
Troubleshooting
undefined reference to weaveffi_*— link against the Rust cdylib. The header alone is not enough.- Double-free crashes — RAII wrappers delete copy operators on
purpose. If you see double-frees, somewhere you have a manual copy or
a raw
void*shared between wrappers. - Exceptions not caught across DLL boundaries on MSVC — build the
consumer and the dynamically loaded library with the same
_HAS_EXCEPTIONSsetting and CRT. std::optionalis missing — the header requires C++17. Addtarget_compile_features(... cxx_std_17)to your CMake target.
Dart
Overview
The Dart target produces a pure-Dart FFI package that wraps the C ABI
using dart:ffi. It opens the
shared library with DynamicLibrary.open and resolves each symbol via
lookupFunction. There is no native compilation step or ffigen run
required — the generated .dart file is ready to import.
What gets generated
| File | Purpose |
|---|---|
dart/lib/weaveffi.dart | dart:ffi bindings: loader, typedefs, lookups, wrappers, struct/enum classes |
dart/pubspec.yaml | Package metadata and package:ffi dependency |
dart/README.md | Basic usage instructions |
Type mapping
| 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 cross as Int32 (0/1) and the wrapper converts both ways.
Example IDL → generated code
version: "0.3.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: find_contact
params:
- { name: id, type: i32 }
return: "Contact?"
The loader auto-detects the platform:
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 become Dart enhanced enums:
/// 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);
}
Structs are wrapped in classes with a dispose() method and getter
methods that call the C accessors:
/// 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);
}
}
}
Each function emits a native typedef, Dart typedef, lookup, and top-level wrapper:
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);
}
}
Build instructions
Standalone Dart:
-
Generate the bindings:
weaveffi generate --input api.yaml --output generated/ --target dart -
Build the Rust shared library:
cargo build --release -p your_library -
Make the cdylib findable at runtime:
- macOS:
DYLD_LIBRARY_PATH=$PWD/../../target/release dart run example/main.dart - Linux:
LD_LIBRARY_PATH=$PWD/../../target/release dart run example/main.dart - Windows: place
weaveffi.dllnext to the script or add its directory toPATH.
- macOS:
Flutter:
-
Generate the bindings as above.
-
Cross-compile the Rust cdylib for every Flutter target you support (
aarch64-apple-ios,aarch64-linux-android,x86_64-apple-darwin, etc.). -
Reference the generated package from your app’s
pubspec.yaml:dependencies: weaveffi: path: ../generated/dart -
Bundle the cdylib per platform:
- iOS / macOS: ship a Framework or use a
podspec. - Android: place
.sofiles underandroid/src/main/jniLibs/{abi}/. - Linux / Windows: place next to the executable or on the library search path.
- iOS / macOS: ship a Framework or use a
Memory and ownership
-
Strings: Dart
Stringvalues are converted withtoNativeUtf8(). The wrapper frees the resulting pointer in afinallyblock. Returned UTF-8 pointers are decoded withtoDartString(). -
Structs: wrappers hold a
Pointer<Void>. Thedispose()method calls the corresponding_destroyC function. Always wrap usage intry/finally:final contact = getContact(id); try { print(contact.name); } finally { contact.dispose(); } -
Optionals:
T?returns check the native pointer againstnullptrbefore wrapping; absent struct optionals becomenull.
Async support
Functions marked async: true produce a synchronous helper plus 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));
}
The dart:isolate import is only included when the IDL contains at
least one async function. When the IDL marks the function
cancel: true, the wrapper forwards Dart cancellation tokens to the
underlying weaveffi_cancel_token.
Troubleshooting
Invalid argument(s): Failed to load dynamic library— the cdylib is not on the search path. SetDYLD_LIBRARY_PATH/LD_LIBRARY_PATHor copy the library next to your executable.UnsupportedError: Unsupported platform— the loader maps todarwin,linux, andwindows. Other platforms (Android, iOS) use the Flutter integration where the framework opens the library.MissingPluginExceptionin Flutter — that error is unrelated to WeaveFFI; double-check that you depend on the generated package and haven’t shadowed it with a differentweaveffidependency.- Strings appear truncated — Rust strings are not nul-terminated;
make sure
toDartString()is reading the pointer returned from a generated getter, not a raw pointer.
Go
Overview
The Go target produces idiomatic Go bindings that use CGo to call the C
ABI. The generator emits one Go source file (weaveffi.go) plus a
go.mod so the result can be imported by any Go module. Functions
return (value, error) to match Go conventions; struct wrappers expose
methods plus an explicit Close().
What gets generated
| 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 |
Type mapping
| 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 map to C._Bool, matching CGo’s representation of _Bool.
Example IDL → generated code
version: "0.3.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: email, type: "string?" }
- { name: contact_type, type: ContactType }
functions:
- name: create_contact
params:
- { name: first_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
The generated weaveffi.go opens with the CGo preamble:
package weaveffi
/*
#cgo LDFLAGS: -lweaveffi
#include "weaveffi.h"
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"unsafe"
)
Enums become typed integer aliases:
type ContactType int32
const (
ContactTypePersonal ContactType = 0
ContactTypeWork ContactType = 1
ContactTypeOther ContactType = 2
)
Structs hold a typed C pointer and expose getters plus Close():
type Contact struct {
ptr *C.weaveffi_contacts_Contact
}
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) Close() {
if s.ptr != nil {
C.weaveffi_contacts_Contact_destroy(s.ptr)
s.ptr = nil
}
}
Functions return (value, error):
func ContactsCreateContact(firstName string, email *string, contactType ContactType) (int64, error) {
cFirstName := C.CString(firstName)
defer C.free(unsafe.Pointer(cFirstName))
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, 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
}
Lists round-trip through unsafe.Slice:
var cOutLen C.size_t
result := C.weaveffi_store_list_ids(&cOutLen, &cErr)
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) }
The Go module path defaults to weaveffi; override it via the
generator config:
version: "0.3.0"
modules:
- name: math
functions:
- name: add
params:
- { name: a, type: i32 }
- { name: b, type: i32 }
return: i32
generators:
go:
module_path: "github.com/myorg/mylib"
Build instructions
-
Generate the bindings:
weaveffi generate --input api.yaml --output generated/ --target go -
Build the Rust shared library:
cargo build --release -p your_library -
Point CGo at the header and library:
export CGO_CFLAGS="-I$PWD/generated/c" export CGO_LDFLAGS="-L$PWD/target/release -lweaveffi" -
Build and run a Go consumer:
cd generated/go go build ./...
CGo requires a C compiler (gcc or clang) on the host; on Windows
use a MinGW-w64 toolchain or the MSVC build provided by go env.
Memory and ownership
- Strings in:
C.CStringallocates a copy in C memory; the generated wrapper pairs everyCStringwith adefer C.free(...). - Strings out:
C.GoStringcopies the C string into Go-owned memory, then the wrapper callsweaveffi_free_stringto release the Rust allocation. - Bytes: input slices are passed by pointer for the duration of
the call (no copy); returned bytes are copied with
C.GoBytesand thenweaveffi_free_bytesis called. - Structs: wrappers hold a typed C pointer. Always pair with
defer s.Close()because Go has no deterministic destructors. - Optionals: scalar optionals are
*T; struct/string optionals rely on a nil pointer to indicate absence.
Async support
Async IDL functions are exposed as Go functions that return a typed
channel and an error. The wrapper allocates a Go-side struct,
registers it with the CGo handle table, hands the C ABI a callback,
and resolves the channel when the callback fires:
func ContactsFetchContact(id int32) (<-chan ContactsFetchContactResult, error) {
ch := make(chan ContactsFetchContactResult, 1)
handle := cgo.NewHandle(ch)
var cErr C.weaveffi_error
C.weaveffi_contacts_fetch_contact_async(C.int32_t(id),
C.weaveffi_callback(C.weaveffi_go_async_trampoline),
unsafe.Pointer(&handle), &cErr)
if cErr.code != 0 {
handle.Delete()
msg := C.GoString(cErr.message)
C.weaveffi_error_clear(&cErr)
return nil, fmt.Errorf("weaveffi: %s (code %d)", msg, int(cErr.code))
}
return ch, nil
}
When the IDL marks the function cancel: true, the wrapper accepts a
context.Context and forwards cancellation to the underlying
weaveffi_cancel_token.
Troubleshooting
undefined reference to weaveffi_*—CGO_LDFLAGSis missing the-lflag or-Ldirectory. Recheck the environment exports.could not determine kind of namein CGo — ensureCGO_CFLAGSpoints at the directory containingweaveffi.h.- Crashes after struct goes out of scope — Go does not call
Close()for you. Eitherdefer s.Close()or wrap usage in a helper that takes a closure. go: cannot find module providing package weaveffi— change the generator config sogo.moddeclares the module path you actually import, e.g.github.com/myorg/mylib.
Ruby
Overview
The Ruby target produces pure-Ruby FFI bindings using the
ffi gem to call the C ABI directly. There
is no native extension to compile — gem install ffi is the only
prerequisite. The generator emits a single .rb file plus a gemspec
ready for gem build and gem install.
The trade-off is that FFI gem calls are slower than a hand-written C extension. For typical FFI workloads the overhead is negligible compared to the work done inside the Rust library.
What gets generated
| 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 |
Type mapping
| 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 cross as :int32 (0/1); the wrapper converts both
directions.
Example IDL → generated code
version: "0.3.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: email, type: "string?" }
- { name: contact_type, type: ContactType }
functions:
- name: create_contact
params:
- { name: first_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]"
The generated module extends FFI::Library and selects the right
shared library at load time:
require 'ffi'
module WeaveFFI
extend FFI::Library
case FFI::Platform::OS
when /darwin/ then ffi_lib 'libweaveffi.dylib'
when /mswin|mingw/ then ffi_lib 'weaveffi.dll'
else ffi_lib 'libweaveffi.so'
end
end
Enums become Ruby modules with constants:
module ContactType
PERSONAL = 0
WORK = 1
OTHER = 2
end
Structs become classes wrapping an FFI::AutoPointer so the C
destructor is called when Ruby garbage-collects the wrapper:
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 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
end
Functions are class methods on the module and raise on failure:
def self.create_contact(first_name, email, contact_type)
err = ErrorStruct.new
result = weaveffi_contacts_create_contact(
first_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
The shared error machinery:
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
Catch errors with standard begin/rescue:
require 'weaveffi'
begin
handle = WeaveFFI.create_contact("Alice", nil, WeaveFFI::ContactType::WORK)
rescue WeaveFFI::Error => e
puts "Error #{e.code}: #{e.message}"
end
Build instructions
-
Generate the bindings:
weaveffi generate --input api.yaml --output generated/ --target ruby -
Build the Rust shared library:
cargo build --release -p your_library -
Build and install the gem:
cd generated/ruby gem build weaveffi.gemspec gem install weaveffi-0.1.0.gem -
Make the cdylib findable at runtime:
- macOS:
DYLD_LIBRARY_PATH=$PWD/../../target/release ruby your_script.rb - Linux:
LD_LIBRARY_PATH=$PWD/../../target/release ruby your_script.rb - Windows: place
weaveffi.dllnext to the script or add its directory toPATH.
- macOS:
The Ruby module name and gem name can be customised via generator configuration:
[ruby]
module_name = "MyBindings"
gem_name = "my_bindings"
Memory and ownership
- Strings in: Ruby strings are passed as
:stringparameters and the FFI gem encodes them to null-terminated C strings. - Strings out: the wrapper reads the returned
:pointerwithread_string, then callsweaveffi_free_stringto release the Rust-owned buffer. - Bytes: an
FFI::MemoryPointeris allocated for inputs; outputs are read withread_string(len)and the Rust side is responsible for the buffer it returned. - Structs: wrappers hold an
FFI::AutoPointerwhosereleasecallback invokes the C_destroyfunction on GC. Use the explicitdestroymethod for deterministic cleanup. - Maps: keys and values are marshalled into parallel
FFI::MemoryPointerbuffers; the wrapper rebuilds a RubyHashfrom the returned arrays.
Async support
Async IDL functions are exposed as Ruby methods that return a
Concurrent::Promises::Future (when concurrent-ruby is present) or
a hand-rolled callback wrapper otherwise. The Ruby thread that calls
the function blocks on a Queue to receive the result from the C ABI
callback:
def self.fetch_contact(id)
q = Queue.new
context = FFI::Function.new(:void, [:pointer, :pointer]) do |err, result|
q << [err, result]
end
weaveffi_contacts_fetch_contact_async(id, context, nil)
err, result = q.pop
check_error!(ErrorStruct.new(err))
Contact.new(result)
end
When the IDL marks the function cancel: true, the wrapper accepts a
cancellation token and forwards it to the underlying
weaveffi_cancel_token.
Troubleshooting
LoadError: Could not open library 'libweaveffi.dylib'— the cdylib is not on the loader path. SetDYLD_LIBRARY_PATH/LD_LIBRARY_PATHor copy the library next to your script.FFI::NotFoundError: Function 'weaveffi_*' not found— the cdylib does not export the symbol. Rebuild the Rust crate after regenerating the IDL.- Segmentation faults on Ruby exit — keep references to FFI callbacks alive for the lifetime of the call. Letting them be garbage-collected mid-call corrupts the C side.
- Strings come back as binary garbage — UTF-8 strings should round
trip through
read_string; for binary data useread_bytes(length)with theout_lenreturned by the C ABI.
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 bindings across targets.
- Memory Ownership — allocation rules; freeing strings, bytes, structs, and errors across the FFI boundary.
- Error Handling — the uniform error model and how each target surfaces failures.
- Async Functions — IDL declaration, the C ABI callback contract, and per-target async surfaces.
- Annotated Rust Extraction — extract an IDL from annotated Rust source instead of writing YAML by hand.
- Generator Configuration — customise per-target names and the C ABI prefix via
weaveffi.tomlor inlinegenerators:blocks.
Memory Ownership
Overview
WeaveFFI exposes Rust functionality through a stable C ABI. Because Rust and the consumer languages (C, Swift, Kotlin, Python, …) have different memory models, every allocation that crosses the boundary follows strict ownership rules.
Golden rule: whoever allocates owns it, and ownership must be
explicitly transferred back for deallocation. Rust allocates; the
consumer frees through the designated weaveffi_free_* functions or
the matching _destroy symbol.
When to use
Read this guide when:
- You are writing a consumer in C/C++ where the compiler will not free anything for you.
- You are debugging a leak, double-free, or use-after-free in a generated binding.
- You are extending a generator and need to verify the ownership contract for a new type.
- You are reviewing PRs that add new IDL types that involve heap-allocated data.
For higher-level languages (Swift, Kotlin, Python, .NET, Dart, Ruby, Go) the generated wrappers handle most of this automatically; the rules below explain what those wrappers are doing under the hood.
Step-by-step
Strings
Rust returns NUL-terminated, UTF-8, heap-allocated C strings created
via CString::into_raw. The consumer must free them with
weaveffi_free_string.
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);
Generated wrappers do the same with defer:
let raw = weaveffi_calculator_echo(...)
defer { weaveffi_free_string(raw) }
return String(cString: raw!)
Byte buffers
Byte buffers are returned as const uint8_t* plus an out_len. Free
them with weaveffi_free_bytes(ptr, len) — the length must match what
the C ABI returned.
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;
}
process_data(buf, out_len);
weaveffi_free_bytes((uint8_t*)buf, out_len);
Struct lifecycle
Structs are opaque on the consumer side. The lifecycle is:
*_createallocates and returns a pointer; the consumer owns it.*_destroyfrees the struct. Call exactly once.*_get_<field>getters read fields. Primitive getters (i32,f64,bool) return values directly. String/bytes getters return new owned copies that must be freed.
weaveffi_error err = {0, NULL};
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;
}
int32_t age = weaveffi_contacts_Contact_get_age(contact);
const char* name = weaveffi_contacts_Contact_get_name(contact);
weaveffi_free_string(name);
weaveffi_contacts_Contact_destroy(contact);
The generated Swift wrapper invokes _destroy from deinit and frees
returned strings with defer:
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)
}
}
Error struct lifecycle
Every C ABI function takes a trailing weaveffi_error* out_err. On
failure Rust writes a non-zero code and a Rust-allocated message.
Clearing the error frees the message:
weaveffi_error err = {0, NULL};
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);
}
result = weaveffi_calculator_add(1, 2, &err);
Generated wrappers convert non-zero codes into language-native
exceptions (throw, raise, Result::Err).
Thread safety
Generated FFI functions are expected to be called from a single thread unless the module’s documentation says otherwise. Concurrent calls from multiple threads can cause data races and undefined behaviour. Synchronise externally — for example with a mutex or a serial dispatch queue:
let queue = DispatchQueue(label: "com.app.weaveffi")
queue.sync {
let result = try? Calculator.add(a: 1, b: 2)
}
Reference
| 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 |
Pitfalls
- Use-after-free — reading a string after freeing it, or accessing
a struct after
_destroy. Once the consumer frees something, the pointer is invalid. - Double-free — freeing the same pointer twice (e.g. calling
weaveffi_free_stringtwice or invoking_destroyafter the wrapper has already done so). - Wrong length to
weaveffi_free_bytes— always free with the exact length the C ABI returned inout_len. - Forgetting to clear error structs —
err.messageis Rust-allocated; failing to callweaveffi_error_clearafter a non-zero code leaks that string. - Calling FFI from multiple threads without synchronisation — the default contract is single-threaded; synchronise externally if you need parallelism.
- Manually freeing pointers passed in as borrowed parameters —
borrowed inputs (
&str,&[u8],const T*) are owned by the caller and must not be passed toweaveffi_free_*.
Error Handling
Overview
WeaveFFI uses a uniform error model across the FFI boundary. Every
generated function carries an out-error parameter (weaveffi_error*)
that reports success or failure through an integer code and an
optional message string. Each generator maps that to its target’s
idiomatic error mechanism (exceptions, throws, Result, etc.) so
consumers rarely touch the C-level struct directly.
When to use
Reach for this guide when:
- You are designing an IDL and want to surface stable, named error codes to consumers.
- You are writing the Rust implementation of a module and need to return errors over the C ABI.
- You are debugging an “unknown error” surface in a generated binding.
- You are reviewing or extending a generator and need to know what the error contract guarantees.
Step-by-step
Define an error domain in the IDL
version: "0.3.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: get_contact
params:
- { name: id, type: handle }
return: string
The validator enforces:
code = 0is reserved for success; non-zero is required.- All names within a domain are unique.
- All numeric codes within a domain are unique.
- The domain
namemust not collide with any function name in the module. - The domain
namemust not be empty.
Set errors from the Rust implementation
#![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 {
abi::error_set_ok(out_err);
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 a message |
result_to_out_err(result, out_err) | Maps Result<T, E> (Ok clears, Err sets -1) |
Prefer the codes you defined in the IDL (e.g. not_found = 1) so
consumers can react meaningfully.
Handle errors in C
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:
- Zero-initialise:
weaveffi_error err = {0, NULL};. - Call the function with
&erras the last argument. - Check
err.code; if non-zero, readerr.messageand callweaveffi_error_clear(&err). - Reuse the struct for subsequent calls.
Handle errors in Swift
do {
let contact = try Contacts.getContact(id: handle)
print(contact)
} catch let e as WeaveFFIError {
print("Failed: \(e)")
}
The generated wrapper calls try check(&err) after every C call,
which throws WeaveFFIError and clears the C-side struct.
Handle errors in Kotlin / Android
try {
val contact = Contacts.getContact(id)
println(contact)
} catch (e: RuntimeException) {
println("Failed: ${e.message}")
}
The JNI shim throws RuntimeException with the message and clears the
C-side struct before returning.
Handle errors in Node.js
import { Contacts } from "weaveffi";
try {
const contact = Contacts.getContact(id);
console.log(contact);
} catch (e) {
console.error("Failed:", (e as Error).message);
}
The N-API addon throws a JavaScript Error carrying the message.
Handle errors in WASM
The minimal WASM target uses numeric return codes. Inspect the return value after each call:
const result = instance.exports.weaveffi_contacts_get_contact(id);
if (result === 0) {
console.error("call failed — inspect log");
}
The WASM error surface is still evolving. Future versions will surface richer error information.
Reference
| Layer | Error mechanism | How a non-zero code surfaces |
|---|---|---|
| C ABI | weaveffi_error { code, message } | Consumer inspects struct after every call |
| Swift | WeaveFFIError (throws) | try raises a Swift Error |
| Kotlin | RuntimeException | try/catch (or rethrown by the JNI shim) |
| Node.js | JavaScript Error | N-API addon throws |
| Python | WeaveffiError exception | try/except |
| Ruby | WeaveFFI::Error (StandardError) | begin/rescue |
| Dart | WeaveffiException | try/on WeaveffiException catch |
| .NET | WeaveffiException | try/catch |
| Go | error return value | Standard if err != nil { ... } |
| WASM | Numeric return code | Caller checks the value |
| Field | Type | Description |
|---|---|---|
code | int32_t | 0 = success, non-zero = error |
message | const char* | NULL on success; Rust-allocated string on error |
See the Memory Ownership Guide for the freeing contract
on err.message.
Pitfalls
- Forgetting to call
weaveffi_error_clear— the message is Rust-allocated. Skipping the clear leaks the string. - Reading
err.messageafter clearing — the pointer is invalid as soon asweaveffi_error_clearreturns. - Using
code = 0as a domain value — the validator rejects this because0always means success. - Reusing custom codes across modules and assuming they are unique — error domains are scoped to a single module. Document cross-module conventions if you need them.
- Not initialising the struct — always start with
{0, NULL}(or the language equivalent). Stalecodevalues from earlier calls produce confusing failures. - Ignoring the return value when
code != 0— Rust does not promise the return value is meaningful on failure. For pointer returns it is typicallyNULL; do not free it.
Async Functions
Overview
WeaveFFI exposes asynchronous Rust operations through a single
callback-based C ABI and language-native async wrappers in every
target. Mark a function with async: true (and optionally
cancellable: true) in the IDL and the generators emit the right
shape per target — async throws in Swift, suspend fun in Kotlin,
Promise<T> in JS, async def in Python, Task<T> in .NET, and so
on.
When to use
Use async functions for:
- I/O-bound work (network, disk, database).
- Long-running operations that should not block the consumer’s event loop (UI threads, JS event loop, asyncio loop).
- Operations the consumer should be able to cancel — combine with
cancellable: true.
Avoid async for:
- Short CPU-bound work (math, parsing, validation). The callback overhead is more expensive than the call itself.
- Functions whose Rust implementation is purely synchronous and finishes in microseconds.
Step-by-step
1. Declare the function in the IDL
version: "0.3.0"
modules:
- name: net
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 |
2. Implement it in Rust
The generated C ABI symbol takes a callback pointer and an opaque
void* context. The Rust worker invokes the callback exactly once
when it is done. The pattern from samples/async-demo/src/lib.rs:
#![allow(unused)]
#![allow(unsafe_code)]
#![allow(non_camel_case_types)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]
fn main() {
use std::ffi::c_void;
use std::os::raw::c_char;
use weaveffi_abi::{self as abi, weaveffi_error};
pub type weaveffi_net_fetch_data_callback =
extern "C" fn(context: *mut c_void, err: *mut weaveffi_error, result: *const c_char);
#[no_mangle]
pub extern "C" fn weaveffi_net_fetch_data(
url: *const c_char,
callback: weaveffi_net_fetch_data_callback,
context: *mut c_void,
) {
let url_str = abi::c_ptr_to_string(url).unwrap_or_default();
let ctx = context as usize;
std::thread::spawn(move || {
let payload = std::ffi::CString::new(format!("payload from {url_str}"))
.unwrap()
.into_raw();
callback(ctx as *mut c_void, std::ptr::null_mut(), payload);
});
}
}
3. Call it from each target
Swift:
let payload = try await Net.fetchData("https://example.com/data")
Kotlin/Android:
val payload = Net.fetchData("https://example.com/data")
Node.js:
const payload = await fetchData("https://example.com/data");
Python:
payload = await fetch_data("https://example.com/data")
.NET:
var payload = await Net.FetchDataAsync("https://example.com/data");
Dart:
final payload = await fetchData('https://example.com/data');
4. Cancel a running operation
For cancellable: true functions the wrapper plumbs the target’s
native cancellation primitive into the C ABI cancel token. Cancellation
is observed by the Rust worker, but the callback is always
invoked exactly once — either with the result or with a Cancelled
error. The pin/unpin pair (see Reference) runs on the cancellation
path identically to the success path.
let task = Task { try await Net.uploadFile(path: "x", data: data) }
task.cancel()
task = asyncio.create_task(net.upload_file("x", data))
task.cancel()
Reference
C ABI shape
typedef void (*weaveffi_callback_string)(
const char* result,
const weaveffi_error* err,
void* user_data
);
void weaveffi_net_fetch_data(
const uint8_t* url, size_t url_len,
weaveffi_callback_string on_complete,
void* user_data
);
For cancellable: true:
uint64_t weaveffi_net_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);
Per-target async surface
| Target | Async surface | Cancellation hook |
|---|---|---|
| C | Raw callback | weaveffi_cancel(handle) |
| C++ | std::future<T> | weaveffi_cancel_token* argument |
| Swift | async throws | withTaskCancellationHandler |
| Kotlin | suspend fun | invokeOnCancellation |
| Node.js | Promise<T> | AbortSignal (when cancellable: true) |
| Python | async def | asyncio.CancelledError |
| .NET | Task<T> | CancellationToken |
| Dart | Future<T> (runs on isolate) | Future.timeout / cancellation token |
| WASM | Promise<T> (synchronous shim today) | n/a |
| Go | Not async-capable — generator skips today | n/a |
| Ruby | Not async-capable — generator skips today | n/a |
Pin / unpin matrix
Every binding pins the user-supplied void* context and the callback
closure for the lifetime of the operation, then releases them exactly
once on the callback path. The matrix below is the contract every
generator implements; each row is verified by a
{generator}_async_pins_callback_for_lifetime unit test plus the
1000-call stress test under examples/{target}/async_stress.{ext}.
| Target | Pin (allocate / retain) | Unpin (free / release) on callback | Notes |
|---|---|---|---|
| Swift | Unmanaged.passRetained(ContinuationRef(...)) | Unmanaged.fromOpaque(ctx).takeRetainedValue() | The retained +1 is dropped exactly once when the continuation resumes. |
| .NET | GCHandle.Alloc(callback, GCHandleType.Normal) | GCHandle.FromIntPtr(context).Free() | The catch path also frees the handle on synchronous failure. |
| Kotlin | JNI (*env)->NewGlobalRef(env, callback) | (*env)->DeleteGlobalRef(env, ctx->callback) | The JNI shim mallocs and frees the per-call context exactly once. |
| Node.js | napi_create_promise(env, &deferred, &promise) | napi_resolve_deferred or napi_reject_deferred | The N-API runtime owns the deferred; the per-call context is malloc-ed and freed exactly once. |
| Python | _cb = ctypes.CFUNCTYPE(...)(impl) (kept by helper) | _ev.set() in the callback’s finally releases the helper’s _ev.wait() | The helper blocks on the event so _cb (and its trampoline) outlive the callback. |
| C++ | new std::promise<T>() plus the lambda capture | delete p; once at the end of the lambda | The lambda owns the heap promise on every exit branch. |
| Dart | NativeCallable<...>.listener(...) | callable.close() in finally and on the catch path | Pointer-typed parameters are kept alive in whenComplete. |
| WASM | _registerTrampoline per signature plus _asyncContexts.set(ctxId, ...) per call | _asyncContexts.delete(ctxId) in the trampoline | Per-call resolver closures are removed after resolve/reject. |
| Go | Not async-capable; async: true is skipped today | n/a | Re-enabling Go async requires solving channel-vs-callback lifetime. |
| Ruby | Not async-capable; async: true is skipped today | n/a | Future async impl must rb_global_variable the callback and release it on completion. |
Audit invariants
For every async-capable target:
- The
void* contexthas exactly one owner at any moment. - The callback closure is pinned by an explicit “+1” allocation
(
GCHandle.Alloc,Unmanaged.passRetained,NewGlobalRef,NativeCallable.listener, …) before the C worker can see it, and released by the matching “-1” exactly once on the callback path. - Synchronous failure of the C call (the callback never fires) is
handled in a
catch/trythat frees the pin so it does not leak. - The stress test asserts
weaveffi_tasks_active_callbacks()returns to zero after 1000 concurrent calls.
Pitfalls
- Async void functions — the validator emits a warning. They are valid but almost always indicate a missing return type.
- Forgetting
cancellable: true— without it,weaveffi_cancelis a no-op for that function. - Using async for CPU-bound work — the callback overhead exceeds the work being done; keep it synchronous.
- Calling Go/Ruby async functions — the generators skip async functions entirely for these targets today. Either keep the function synchronous or run the async path from a different consumer.
- Letting the callback closure get garbage-collected — every generator pins it explicitly. Do not strip those pins when editing generated code by hand.
- Returning
nullinstead of invoking the callback — the contract is that the callback fires exactly once for every async call, including on cancellation.
Annotated Rust Extraction
Overview
Instead of hand-writing an IDL, you can annotate your Rust source with
WeaveFFI marker attributes and let weaveffi extract produce the IDL
for you. The result keeps the IDL co-located with the implementation
and eliminates drift between the two — change the Rust signatures and
re-run extract.
When to use
Reach for weaveffi extract when:
- The Rust implementation already exists and you want a starting IDL.
- The IDL changes whenever signatures change, and you want a single source of truth.
- You are scaffolding a new module and would rather decorate Rust than write YAML by hand.
Skip extraction when:
- You want to design the API before any Rust exists — author the IDL directly.
- You need iterator return types (
iter<T>), error domains, struct field defaults, orsince:without an accompanying#[deprecated]attribute. See Pitfalls.
Step-by-step
1. Annotate the Rust source
WeaveFFI recognises a small family of marker attributes by name only —
there is no proc-macro crate. Define them as no-op attribute macros, or
add #![allow(unused_attributes)] and ignore the warning.
#![allow(unused)]
#![allow(unused_attributes)]
fn main() {
mod inventory {
/// A product in the catalog.
#[weaveffi_struct]
#[weaveffi_builder]
struct Product {
/// Stable identifier.
id: i32,
name: String,
price: f64,
tags: Vec<String>,
}
/// Product availability.
#[weaveffi_enum]
#[repr(i32)]
enum Availability {
InStock = 0,
OutOfStock = 1,
Preorder = 2,
}
/// Fired when a product is ready for pickup.
#[weaveffi_callback]
fn OnReady(product_id: i32) {}
/// Subscribe to OnReady events.
#[weaveffi_listener(event_callback = "OnReady")]
fn ready_listener() {}
/// Look up a product by ID.
#[weaveffi_export]
fn get_product(id: i32) -> Option<Product> {
todo!()
}
/// Append to a search index.
#[weaveffi_export]
fn index(buf: &mut SearchIndex, query: &str) {
todo!()
}
/// Open a long-lived session handle.
#[weaveffi_export]
fn open_session() -> *mut Session {
todo!()
}
/// Replaced by `search_v2` in 0.3.0.
#[weaveffi_export]
#[deprecated(since = "0.2.0", note = "use search_v2 instead")]
fn search(query: String, limit: i32) -> Vec<Product> {
todo!()
}
/// Long-running fetch.
#[weaveffi_export]
#[weaveffi_async]
#[weaveffi_cancellable]
fn refresh_catalog() -> i32 {
todo!()
}
mod nested {
/// Lives inside `inventory::nested`.
#[weaveffi_export]
fn helper() -> i32 {
0
}
}
}
}
2. Run weaveffi extract
weaveffi extract src/api.rs # YAML to stdout
weaveffi extract src/api.rs -o api.yml # YAML to file
weaveffi extract src/api.rs -f json -o api.json # JSON to file
weaveffi extract src/api.rs | weaveffi generate -o generated
The extracted IDL is validated automatically. Validation warnings (such as cross-module references that needed resolution) are printed to stderr but do not prevent output.
3. Validate and generate
weaveffi validate api.yml
weaveffi generate api.yml -o generated/
Reference
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 |
Attribute reference
The extractor matches attributes by their final ident. Path-style
attributes are not currently recognised; use the underscore form
(e.g. #[weaveffi_export], not #[weaveffi::export]).
| Attribute | Where it goes | Effect |
|---|---|---|
#[weaveffi_export] | free fn | Emits a Function in the enclosing module. |
#[weaveffi_struct] | named-field struct | Emits a StructDef. |
#[weaveffi_builder] | struct (with weaveffi_struct) | Sets builder: true on the emitted struct. |
#[weaveffi_enum] + #[repr(i32)] | enum | Emits an EnumDef. Every variant must have an explicit = N discriminant. |
#[weaveffi_async] | exported fn | Sets async: true. The Rust async fn keyword has the same effect. |
#[weaveffi_cancellable] | exported fn | Sets cancellable: true (typically combined with #[weaveffi_async]). |
#[weaveffi_callback] | free fn | Emits a module-level CallbackDef using the function’s name and parameters. |
#[weaveffi_listener(event_callback = "Name")] | free fn | Emits a ListenerDef referencing the named callback. |
#[deprecated(since = "...", note = "...")] | exported fn | Populates since and deprecated. Bare #[deprecated] sets deprecated = "deprecated". |
Doc comments (///) on items, fields, and enum variants become the
doc field in the IR.
Type mapping
| 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 |
&str | BorrowedStr | &str |
&[u8] | BorrowedBytes | &[u8] |
*mut T / *const T | TypedHandle("T") | handle<T> |
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} |
&T (other) | inner type | T |
&mut T (other) | inner type, mutable | T |
| Any other identifier | Struct(name) | name |
Compositions work recursively — Option<Vec<i32>> becomes [i32]?
and Vec<Option<String>> becomes [string?].
&mut T parameters are reduced to T and the surrounding Param
record gets mutable: true. &T for any non-str/[u8] type is
also reduced to T with mutable: false.
Round-trip integrity
The roundtrip_kitchen_sink integration test in
crates/weaveffi-cli/tests/extract_roundtrip.rs proves that the
hand-annotated form of the kitchen-sink IDL round-trips through
weaveffi extract and matches the original IR for every supported
feature: modules, nested modules, structs (including builders), enums,
callbacks, listeners, every primitive type, borrowed types, typed
handles, optional/list/map composites, async, cancellable, and
deprecated/since.
Pitfalls
The extractor parses syntax, not semantics. The items below cannot be inferred from Rust source alone and either must be added to the generated IDL by hand or are documented as round-trip gaps.
- Iterator return types (
iter<T>). No equivalent Rust syntax; add theiter<T>return manually after extraction. - Error domains (
module.errors). The extractor never emitserrors:blocks; add them by hand. - Struct field default values. The IDL’s
default:field cannot be derived from Rust syntax (Rust struct fields have no default expressions). - Standalone
since:without#[deprecated].sinceis only recovered when paired with#[deprecated(since = "...")]. To setsinceon a non-deprecated function, edit the YAML manually. - Doc comments on parameters. Rust accepts
///onfnparameters but most formatters strip them; when present, the extractor preserves them, but plan forParam.docto be lossy. - Generics, trait
implblocks, and macros. The extractor never resolves generics, walksimplblocks, or expands macros. Items produced by proc-macros and declarative macros are invisible. - External
mod foo;declarations. Only inline modules (mod foo { ... }) are processed; declarations that point to other files are skipped. - Tuple and unit structs. Only structs with named fields work
with
#[weaveffi_struct]. - Enums must use
#[repr(i32)]with explicit discriminants. Rust-style enums with payloads cannot be extracted.
Generator Configuration
Overview
WeaveFFI ships with sensible defaults so weaveffi generate api.yml
just works. When you need to override package names, namespaces, or
the C ABI prefix, you have two options that compose with each other:
- A TOML file (
weaveffi.toml) passed via--config. Per-environment values that vary by machine or CI runner. - An inline
generators:block inside the IDL. Project-wide values every contributor inherits without remembering a flag.
When the same option appears in both, the inline IDL value wins.
When to use
- Use the TOML config when one developer or one pipeline needs to swap a value without changing the IDL.
- Use the inline
generators:block when the value is part of the project contract (Swift module name, Go module path, custom C ABI prefix). Checking it into the IDL guarantees consistency. - Use both when there is a project-wide default that an environment occasionally needs to override.
Step-by-step
1. Pass a TOML config file
weaveffi generate api.yml -o generated --config weaveffi.toml
[swift]
module_name = "MyApp"
[android]
package = "com.example.myapp"
[node]
package_name = "@myorg/myapp"
[wasm]
module_name = "myapp_wasm"
[c]
prefix = "myapp"
[global]
strip_module_prefix = true
Every section and key is optional; omit anything you want defaulted.
The [global] table accepts the alias [weaveffi].
2. Embed generators: in the IDL
version: "0.3.0"
modules:
- name: math
functions:
- name: add
params:
- { name: a, type: i32 }
- { name: b, type: i32 }
return: i32
generators:
swift:
module_name: MyAppFFI
android:
package: com.example.myapp
c:
prefix: myapp
cpp:
namespace: myapp
header_name: myapp.hpp
standard: "20"
dart:
package_name: my_dart_pkg
go:
module_path: github.com/example/myapp
ruby:
module_name: MyApp
gem_name: myapp
weaveffi:
strip_module_prefix: true
pre_generate: "cargo build --release"
Unknown target keys are silently ignored, so an older weaveffi CLI
can still read an IDL written for a newer one.
3. Verify the result
weaveffi generate api.yml -o generated --config weaveffi.toml
ls generated/
For day-to-day project recipes:
# iOS / macOS
[swift]
module_name = "MyAppFFI"
[c]
prefix = "myapp"
# Android
[android]
package = "com.example.myapp.ffi"
[c]
prefix = "myapp"
# Node
[node]
package_name = "@myorg/myapp-native"
When you set [c] prefix = ... and do not explicitly set
[cpp] c_prefix = ..., the CLI copies the C prefix into the C++ wrapper
config automatically so the C++ header keeps calling the same symbols
the C ABI exports.
4. Wire it into CI
weaveffi diff --check enforces that the committed bindings still
match the IDL. A typical guard job:
# .github/workflows/ci.yml
- name: Verify generated bindings are up to date
run: weaveffi diff api.yml --out generated --check
weaveffi validate --format json and weaveffi lint --format json
are designed to be parsed by quality dashboards:
weaveffi --quiet validate api.yml --format json | jq '.ok'
weaveffi --quiet lint api.yml --format json > lint-report.json || \
(cat lint-report.json && exit 1)
Reference
TOML config files and inline IDL generators: blocks share the same
section names and key names. Pick the location that fits your workflow;
the keys are identical.
Per-target sections
| Section | Key | Type | Default | Description |
|---|---|---|---|---|
[swift] | module_name | string | "WeaveFFI" | Swift module name in Package.swift and the Sources/ directory |
[swift] | strip_module_prefix | bool | false | Strip the IR module prefix from emitted Swift symbols |
[android] | package | string | "com.weaveffi" | Java/Kotlin package declaration in the JNI wrapper |
[android] | strip_module_prefix | bool | false | Strip the IR module prefix from emitted Java/Kotlin symbols |
[node] | package_name | string | "weaveffi" | npm package name in the Node.js loader |
[node] | strip_module_prefix | bool | false | Strip the IR module prefix from emitted JS/TS symbols |
[wasm] | module_name | string | "weaveffi_wasm" | Module name in the WASM JS loader |
[c] | prefix | string | "weaveffi" | Prefix prepended to every C ABI symbol ({prefix}_{module}_{function}) |
[cpp] | namespace | string | "weaveffi" | C++ namespace for the wrapper |
[cpp] | header_name | string | "weaveffi.hpp" | Header file name for the C++ output |
[cpp] | standard | string | "17" | C++ standard for the generated CMakeLists.txt |
[cpp] | c_prefix | string | inherits [c] | C ABI prefix that the C++ wrappers call into |
[python] | package_name | string | "weaveffi" | Python package name |
[python] | strip_module_prefix | bool | false | Strip the IR module prefix from emitted Python symbols |
[dotnet] | namespace | string | "WeaveFFI" | .NET namespace |
[dotnet] | strip_module_prefix | bool | false | Strip the IR module prefix from emitted C# symbols |
[dart] | package_name | string | "weaveffi" | Dart package name in pubspec.yaml |
[go] | module_path | string | "weaveffi" | Go module path in go.mod |
[ruby] | module_name | string | "WeaveFFI" | Ruby module that wraps the bindings |
[ruby] | gem_name | string | "weaveffi" | Ruby gem name |
[global] section
| Key | Type | Default | Description |
|---|---|---|---|
strip_module_prefix | bool | false | Shorthand: enable strip_module_prefix on every target that supports it |
pre_generate | string | none | Shell command run before any generator starts |
post_generate | string | none | Shell command run after every generator finishes |
The alias [weaveffi] is accepted for the [global] section.
Performance and CI flags
-
The orchestrator dispatches every selected generator in parallel using rayon. The pre- and post-generate hooks still run serially around the whole batch.
-
Each generator persists a hash under
{out_dir}/.weaveffi-cache/{target}.hash. Only generators whose hash changed are re-run; pass--forceto invalidate every entry. -
weaveffi diff --checkexit codes:Code Meaning 0The committed output matches the IDL exactly. 2One or more files would change in place. 3One or more files would be added or removed. -
weaveffi validate --format jsonemits structured success/failure:{ "ok": true, "modules": 2, "functions": 8, "structs": 3, "enums": 1 }{ "ok": false, "errors": [ { "code": "DuplicateFunctionName", "module": "math", "function": "add", "message": "duplicate function name in module 'math': add", "suggestion": "function names must be unique within a module; rename the duplicate" } ] } -
weaveffi lint --format jsonreturns the warning list with stablecode/location/messagefields:{ "ok": false, "warnings": [ { "code": "DeepNesting", "location": "math::compute::matrix", "message": "deep type nesting at math::compute::matrix (depth 4, max recommended 3)" } ] }
Pitfalls
- Inline value overrides TOML silently — there is no warning when both are set. If a TOML override “doesn’t take”, check for an inline block in the IDL.
[c] prefixrewrites every generator — picking a custom prefix also rewrites the runtime symbols ({prefix}_free_string, …). The Rust cdylib must be built with the same prefix. The C++ wrapper picks it up automatically; if you set both[c] prefixand[cpp] c_prefixmake sure they agree.strip_module_prefix = trueflattens names — collisions across modules become possible. Pick one or the other consistently.- Hooks run shell commands as-is —
pre_generateandpost_generateare passed straight tosh -c. Quote them carefully and never include untrusted input. - Cache covers IR, generator name, generator config, and CLI version — changing the IR, any generator config field, or upgrading the CLI invalidates the per-generator cache and triggers re-emission.
- Older CLIs ignore unknown keys — adding a new generator key with a project-wide implication does not error out on older toolchains. Pin the CLI version in CI when you need that guarantee.
Tutorials
Each tutorial follows the same shape: Goal, Prerequisites, Step-by-step, Verification, Cleanup, Next steps. Pick the target you’re shipping to and follow it end-to-end.
- Calculator — fastest path: generate every target, build the cdylib, run the C/Node/Swift consumers from the in-tree sample.
- Swift iOS — Rust → SwiftPM → Xcode iOS app.
- Android — Rust → AAR → Android Studio app on emulator/device.
- Python — Rust → ctypes package →
pip installandpython demo.py. - Node.js — Rust → N-API addon →
npm publishshape.
Calculator end-to-end
Goal
Take the in-tree samples/calculator IDL, generate bindings for every
target, build the cdylib, and run the calculator from a real consumer
(C, Node.js, Swift, then optionally Android and WASM). By the end you
will have produced bindings, executed them on at least one host, and
seen the same Rust add(a, b) answer come back through three different
runtimes.
Prerequisites
- Rust toolchain (stable channel) with
cargoonPATH. - The WeaveFFI CLI (
cargo install weaveffi-cliorcargo run -p weaveffi-cli --if you are working in the repo). - macOS or Linux for the C/Node/Swift steps; Windows works for C and Node but the Swift step requires macOS.
- For the optional Android and WASM steps:
- Android Studio with the NDK installed.
rustup target add wasm32-unknown-unknown.
Step-by-step
1. Generate every target
weaveffi generate samples/calculator/calculator.yml -o generated
The output appears 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, and Gradle skeletongenerated/node— N-API loader and.d.tsgenerated/wasm— minimal WASM loader
2. Build the Rust sample
cargo build -p calculator
The cdylib lands in target/debug/:
- macOS:
libcalculator.dylib - Linux:
libcalculator.so - Windows:
calculator.dll
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. Run the Swift example (macOS / 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
DYLD_LIBRARY_PATH=../../target/debug .build/debug/App
On Linux replace DYLD_LIBRARY_PATH with LD_LIBRARY_PATH.
6. Optional: Android and WASM
- Open
generated/androidin Android Studio and build the:weaveffiAAR. Combine with the steps in the Android tutorial. - For WASM, run
cargo build --target wasm32-unknown-unknown --releaseand load the.wasmfile withgenerated/wasm/weaveffi_wasm.js.
Verification
You should see the same calculator output from each consumer, e.g.
2 + 3 = 5. Concretely:
- The C example prints
2 + 3 = 5(or whatever expressionexamples/c/main.cexercises) without anyweaveffi: errormessages. npm startexits with code0and prints the calculator results followed by theDone.banner.- The Swift binary launches, prints the same arithmetic, and exits cleanly.
If the host cannot find the cdylib, you will see
dyld: Library not loaded (macOS) or error while loading shared libraries (Linux). Re-export DYLD_LIBRARY_PATH /
LD_LIBRARY_PATH and rerun.
Cleanup
rm -rf generated/
cargo clean -p calculator
rm -rf examples/c/c_example examples/swift/.build
The generated/ directory is safe to delete and recreate; nothing
else in the repository depends on its contents.
Next steps
- Walk through the per-target details in Generators.
- Read the Memory Ownership and Error Handling guides for the contracts every consumer must follow.
- Try a target-specific tutorial: Swift iOS, Android, Python, or Node.js.
Swift iOS App
Goal
Build a small Rust greeter library, generate Swift bindings with WeaveFFI, and call them from a SwiftUI iOS app running in the simulator.
Prerequisites
-
Rust toolchain (stable channel).
-
Xcode 15 or later with the iOS SDK installed.
-
WeaveFFI CLI (
cargo install weaveffi-cli). -
iOS Rust targets:
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
Step-by-step
1. Author the IDL
Save as greeter.yml:
version: "0.3.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
You should see, among other targets:
generated/
├── c/
│ └── weaveffi.h
├── swift/
│ ├── Package.swift
│ └── Sources/
│ ├── CWeaveFFI/
│ │ └── module.modulemap
│ └── WeaveFFI/
│ └── WeaveFFI.swift
└── scaffold.rs
3. Implement the Rust 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" }
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
}
// Emit the WeaveFFI C ABI runtime symbols (free_string, free_bytes,
// error_clear, cancel_token_*) — one line per cdylib.
abi::export_runtime!();
}
Use scaffold.rs as the template for the rest of the API
(weaveffi_greeter_greeting, the Greeting lifecycle, getters, …).
4. Build for iOS targets
cargo build -p mygreeter --target aarch64-apple-ios --release
cargo build -p mygreeter --target aarch64-apple-ios-sim --release
cargo build -p mygreeter --target x86_64-apple-ios --release
Combine the simulator architectures with lipo and bundle everything
in an XCFramework so Xcode can pick the right slice automatically:
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
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. Wire it into Xcode
- Create a new iOS App in Xcode (SwiftUI or UIKit).
- Drag
MyGreeter.xcframeworkinto the project navigator. Confirm it appears under Build Phases > Link Binary With Libraries. - File > Add Package Dependencies > Add Local… and pick
generated/swift/. The package contributes theCWeaveFFIandWeaveFFItargets. - Build Settings > Header Search Paths: add the path to
generated/c/(e.g.$(SRCROOT)/../generated/c). - Build Settings > Library Search Paths: add the path to the
matching Rust static library
(
$(SRCROOT)/../target/aarch64-apple-ios/releasefor device builds). - Build Phases > Dependencies: ensure
WeaveFFIis listed.
6. Call from Swift
import SwiftUI
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(_:)— returnsString.Greeter.greeting(_:_:)— returns aGreetinginstance with.messageand.langproperties;deinitcalls the Rust destructor automatically.Greeting— the wrapper class around the opaque Rust pointer.
Verification
-
Select an iOS Simulator target and press Cmd+R.
-
Tap Greet in the running app; the label changes to
Hello, Swift!. -
Re-run on a physical device after building for
aarch64-apple-iosto confirm the device path also works. -
Common error mappings:
Symptom Likely cause Undefined symbols for architecture arm64Static library not linked or the search path is wrong. Module 'CWeaveFFI' not foundHeader search path does not point at generated/c/.No such module 'WeaveFFI'Local Swift package not added under Add Package Dependencies > Add Local…. Crash when running on Intel simulator Build for x86_64-apple-iosand combine withlipo.
Cleanup
rm -rf generated/ MyGreeter.xcframework
cargo clean -p mygreeter
Remove the MyGreeter.xcframework reference from the Xcode project
and undo the Header Search Paths / Library Search Paths
edits.
Next steps
- See the Swift generator reference for the full type mapping.
- Read the Memory Ownership guide to understand
struct lifecycle and
deinitrules. - Try the Calculator tutorial for a simpler end-to-end walkthrough or Android for a JVM target.
Android App
Goal
Build a small Rust greeter library, generate Kotlin/JNI bindings with WeaveFFI, and call them from an Android Studio app running on an emulator or a physical device.
Prerequisites
-
Rust toolchain (stable channel).
-
Android Studio with the NDK installed (via SDK Manager).
-
WeaveFFI CLI (
cargo install weaveffi-cli). -
Android Rust targets:
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
Step-by-step
1. Author the IDL
Save as greeter.yml:
version: "0.3.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
You should see, 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. Implement 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
}
// Emit the WeaveFFI C ABI runtime symbols (free_string, free_bytes,
// error_clear, cancel_token_*) — one line per cdylib.
abi::export_runtime!();
}
Use scaffold.rs for the rest of the API (weaveffi_greeter_greeting,
the Greeting lifecycle, getters, …).
4. Configure the NDK toolchain
export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/$(ls $HOME/Library/Android/sdk/ndk | sort -V | tail -1)"
export PATH="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH"
Replace darwin-x86_64 with linux-x86_64 on Linux. Add the matching
linker = ... entries in .cargo/config.toml:
[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"
5. Cross-compile for every ABI
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
You should now have:
target/aarch64-linux-android/release/libmygreeter.so
target/armv7-linux-androideabi/release/libmygreeter.so
target/x86_64-linux-android/release/libmygreeter.so
6. Wire it into Android Studio
-
Create a new Android project (Empty Activity, Kotlin,
minSdk21+). -
Include the generated module in the root
settings.gradle:include ':weaveffi' project(':weaveffi').projectDir = new File('generated/android') -
Add it as a dependency in your app’s
build.gradle:dependencies { implementation project(':weaveffi') } -
Copy the cdylib into
jniLibsper ABI:mkdir -p app/src/main/jniLibs/{arm64-v8a,armeabi-v7a,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 -
Confirm the JNI
CMakeLists.txtingenerated/android/src/main/cpp/includestarget_include_directories(... PRIVATE ../../../../c)so it can findweaveffi.h.
7. Call from Kotlin
import com.weaveffi.WeaveFFI
import com.weaveffi.Greeting
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.textView).text = WeaveFFI.hello("Android")
Greeting.create("Hi", "en").use { g ->
println("${g.message} (${g.lang})")
}
}
}
The generated WeaveFFI companion object loads the cdylib lazily and
exposes:
WeaveFFI.hello(name: String): StringWeaveFFI.greeting(name: String, lang: String): Long— opaque handle that theGreetingwrapper consumes.
Greeting implements Closeable; either call .close() or use
use { ... } for deterministic cleanup.
Verification
-
Sync Gradle in Android Studio.
-
Pick an emulator or a connected device and press Run (Shift+F10).
-
The text view should display
Hello, Android!and Logcat should showHi (en)from theGreetingblock. -
Common error mappings:
Symptom Likely cause UnsatisfiedLinkError: dlopen failedThe cdylib is missing from jniLibs/or built for the wrong ABI.RuntimeExceptionfrom JNIA WeaveFFI error was raised; inspect the message. Linker errors during cargo buildANDROID_NDK_HOMEis not set or the NDK toolchain is missing fromPATH.No implementation found for native methodJNI symbol names do not match the Kotlin package; re-run weaveffi generate.
Cleanup
rm -rf generated/ app/src/main/jniLibs
cargo clean -p mygreeter
Drop the include ':weaveffi' line from settings.gradle and remove
the dependency from your app module if you do not want to keep the
generated bindings around.
Next steps
- See the Android generator reference for the full type mapping and JNI conventions.
- Read Error Handling — JNI shims convert C
errors to
RuntimeExceptionautomatically. - Try the Calculator tutorial for a simpler end-to-end walkthrough or Swift iOS for a sibling mobile target.
Python Package
Goal
Build a small Rust greeter library, generate Python ctypes bindings with WeaveFFI, install the package locally, and call it from a Python script.
Prerequisites
- Rust toolchain (stable channel).
- Python 3.7 or later (
python3 --version). - WeaveFFI CLI (
cargo install weaveffi-cli). pip(ships with Python).
Step-by-step
1. Author the IDL
Save as greeter.yml:
version: "0.3.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
Among other targets, you should see:
generated/
├── c/
│ └── weaveffi.h
├── python/
│ ├── pyproject.toml
│ ├── setup.py
│ ├── README.md
│ └── weaveffi/
│ ├── __init__.py
│ ├── weaveffi.py
│ └── weaveffi.pyi
└── scaffold.rs
The Python target uses ctypes — no native extension to compile on the Python side.
3. Implement 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
}
// Emit the WeaveFFI C ABI runtime symbols (free_string, free_bytes,
// error_clear, cancel_token_*) — one line per cdylib.
abi::export_runtime!();
}
Use scaffold.rs for the rest of the API.
4. Build the cdylib
cargo build -p mygreeter --release
Produces:
| 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 .
Use pip install -e . for an editable install during development.
6. Make the cdylib findable
The generated loader looks for libweaveffi.dylib (macOS),
libweaveffi.so (Linux), or weaveffi.dll (Windows). Symlink or copy
your cdylib to the expected name and set the loader path.
macOS:
cp target/release/libmygreeter.dylib target/release/libweaveffi.dylib
DYLD_LIBRARY_PATH=target/release python demo.py
Linux:
cp target/release/libmygreeter.so target/release/libweaveffi.so
LD_LIBRARY_PATH=target/release python demo.py
Windows: place weaveffi.dll next to your script or add its
directory to PATH. For production, copy the cdylib into the package
directory and update weaveffi.py’s loader path.
7. Use the bindings
Save as demo.py:
from weaveffi import hello, greeting, WeaveffiError
print(hello("Python"))
try:
g = greeting("Python", "en")
print(f"{g.message} ({g.lang})")
except WeaveffiError as e:
print(f"Error {e.code}: {e.message}")
Struct wrappers free the Rust allocation when garbage-collected; for
deterministic cleanup, del g after you are done with the object.
Verification
-
pip show weaveffilists the package. -
Running
demo.pyprintsHello, Python!andHi (en)(or whateverGreetingyou constructed). -
mypy demo.pyreports no errors thanks to the generatedweaveffi.pyistub. -
Common error mappings:
Symptom Likely cause OSError: dlopen ... not foundCdylib not on the loader path; set DYLD_LIBRARY_PATH/LD_LIBRARY_PATH.WeaveffiError: ...at runtimeRust returned a non-zero error code; inspect e.codeande.message.ModuleNotFoundError: No module named 'weaveffi'Package not installed; rerun pip install .fromgenerated/python/.mypy complains about weaveffiMake sure weaveffi.pyiships next toweaveffi.pyin the package.
Cleanup
pip uninstall weaveffi
rm -rf generated/
cargo clean -p mygreeter
Next steps
- See the Python generator reference for the full type mapping and memory contract.
- Read Error Handling for the cross-target error model.
- Try the Calculator tutorial for a simpler end-to-end walkthrough or Node.js for a sibling scripting target.
Node.js npm Package
Goal
Build a small Rust greeter library, generate Node.js bindings with
WeaveFFI, build the N-API addon, and call the bindings from a
JavaScript script. By the end you will have an npm-installable
package shape ready to publish.
Prerequisites
- Rust toolchain (stable channel).
- Node.js 16 or later and
npm. - WeaveFFI CLI (
cargo install weaveffi-cli). - A C compiler in the
PATH(Xcode CLT on macOS,build-essentialon Linux, MSVC build tools on Windows) for the N-API addon build.
Step-by-step
1. Author the IDL
Save as greeter.yml:
version: "0.3.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
Among other targets you should see:
generated/
├── c/
│ └── weaveffi.h
├── node/
│ ├── index.js
│ ├── types.d.ts
│ └── package.json
└── scaffold.rs
3. Implement 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
}
// Emit the WeaveFFI C ABI runtime symbols (free_string, free_bytes,
// error_clear, cancel_token_*) — one line per cdylib.
abi::export_runtime!();
}
Use scaffold.rs for the rest of the API. You also need an N-API
addon crate that bridges Node’s runtime to the C ABI — see
samples/node-addon in the WeaveFFI repository for a working example
to copy.
4. Build the cdylib and the N-API addon
cargo build -p mygreeter --release
cargo build -p node-addon --release
Copy the addon into the generated package as index.node:
macOS:
cp target/release/libindex.dylib generated/node/index.node
Linux:
cp target/release/libindex.so generated/node/index.node
Windows:
copy target\release\index.dll generated\node\index.node
5. Run the bindings locally
Save as generated/node/demo.js:
const weaveffi = require("./index");
const msg = weaveffi.hello("Node");
console.log(msg);
Run it (the cdylib must be on the loader path):
macOS:
cd generated/node
DYLD_LIBRARY_PATH=../../target/release node demo.js
Linux:
cd generated/node
LD_LIBRARY_PATH=../../target/release node demo.js
For TypeScript consumers, the generated types.d.ts is enough:
import * as weaveffi from "./index";
const msg: string = weaveffi.hello("TypeScript");
const g: weaveffi.Greeting = weaveffi.greeting("TS", "en");
console.log(`${g.message} (${g.lang})`);
6. Prepare for publishing
Edit generated/node/package.json:
{
"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"]
}
files must include index.node. For multi-platform packages,
publish per-platform optional dependencies (e.g.
@myorg/greeter-darwin-arm64) and use an install script to pick the
right binary.
7. Publish
cd generated/node
npm pack
npm publish
For scoped packages, append --access public. Consumers then run:
npm install @myorg/greeter
const { hello } = require("@myorg/greeter");
console.log(hello("npm"));
Verification
-
node demo.jsprintsHello, Node!and exits with code0. -
npm packproduces a.tgzcontainingindex.node,types.d.ts, andindex.js. -
TypeScript consumers see the
Greetinginterface andhellosignature without manual type declarations. -
Common error mappings:
Symptom Likely cause Error: Cannot find module './index.node'The compiled addon is missing; copy the platform-specific binary in. Error: dlopen ... not foundCdylib not on the loader path; set DYLD_LIBRARY_PATH/LD_LIBRARY_PATH.TypeError: weaveffi.hello is not a functionThe N-API addon did not export the expected symbols; rebuild after IDL edits. Crashes on require()Addon built for the wrong Node.js version or architecture; rebuild.
Cleanup
rm -rf generated/
cargo clean -p mygreeter
cargo clean -p node-addon
If you published a test version, mark it as deprecated with
npm deprecate @myorg/greeter@0.1.0 "test publish".
Next steps
- See the Node generator reference for the
full type mapping and
types.d.tslayout. - Read Memory Ownership for struct lifecycle semantics.
- Try the Calculator tutorial for a simpler end-to-end walkthrough or Python for a sibling scripting target.