Getting Started
This guide walks you through installing WeaveFFI, defining an API as a language-neutral IDL, generating multi-language bindings from it, implementing the native library behind the generated C ABI, and calling it from C.
WeaveFFI works with any native library that exposes a C ABI, so the producer
can be written in Rust, C, C++, Zig, or anything else that can speak C. This
guide implements it in Rust because that’s the quickest to set up. If you’re
writing a Rust producer, you can also let the #[weaveffi::module] macro
generate the C ABI and derive the IDL for you, instead of hand-writing YAML
(see step 2).
Prerequisites
You need the Rust toolchain (stable channel) to install the CLI, and for this guide’s Rust producer. Verify with:
rustc --version
cargo --version
The CLI is the only hard requirement. The library you generate bindings for can be written in any language that exposes a C ABI.
1) Install WeaveFFI
Install the CLI from crates.io:
cargo install weaveffi-cli
This puts the weaveffi binary on your PATH.
2) Define your API as an IDL
Describe the API once in a language-neutral IDL. Create math.yml with a
record and a function:
version: "0.5.0"
package:
name: my-math
version: "0.1.0"
modules:
- name: math
structs:
- name: Point
fields:
- { name: x, type: f64 }
- { name: y, type: f64 }
functions:
- name: add
params:
- { name: a, type: i32 }
- { name: b, type: i32 }
return: i32
The optional package: block sets the name and version stamped into every
generated package manifest (package.json, pyproject.toml, Package.swift,
and so on). The IDL also supports primitives (i32, f64, bool, string,
bytes, handle), optionals (string?), lists ([i32]), interfaces
(objects with constructors, methods, and statics), and typed error domains
(opt in per function with throws: true). See the
IDL Schema reference for the full
specification.
Prefer not to hand-write YAML? Run
weaveffi new my-projectto scaffold a starter project (an example IDL plus aCargo.tomlandsrc/lib.rsstub) you can edit instead.
Writing a Rust producer? You can make annotated Rust the single source of truth instead of a separate IDL: annotate a module with
#[weaveffi::module]and point the generator straight at the source. The macro emits the C ABI and derives the IDL from your code, so you write nounsafeglue. See The Rust Producer Macro. The rest of this guide uses the IDL.
3) Generate bindings
Run the generator to produce bindings for all targets:
weaveffi generate math.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
4) 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. Functions and constructors take an out_err
parameter for error reporting (destructors and getters don’t):
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/MyMath/MyMath.swift)
Structs become classes that own an OpaquePointer and free it on deinit.
Module functions are grouped under a Swift enum namespace. Because add
doesn’t declare throws: true, its Swift wrapper is a plain non-throwing
function:
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) -> 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
5) Implement the library behind the C ABI
The generated C header (generated/c/weaveffi.h) is the contract your native
library must satisfy, and it’s the same contract every language binding calls
into. You can implement it in any language that can expose a C ABI; here we use
Rust, starting from the generated scaffold.rs, which already contains a
#[no_mangle] extern "C" stub (with a todo!() body) for every symbol in the
header.
Create a library crate, add the WeaveFFI ABI helpers, and build a cdylib:
cargo new --lib my-math
cd my-math
cargo add weaveffi-abi
In Cargo.toml:
[lib]
crate-type = ["cdylib"]
Copy scaffold.rs into src/lib.rs and fill in the bodies. Implementing add
looks like this (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.
Tip for Rust producers: the
#[weaveffi::module]macro generates these#[no_mangle] extern "C"thunks for you from safe Rust, so you never fill in stubs by hand. See The Rust Producer Macro.
Build with:
cargo build
This produces a shared library (libmy_math.dylib on macOS,
libmy_math.so on Linux, my_math.dll on Windows). The exported symbols match
generated/c/weaveffi.h by construction.
6) 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. - Read the IDL Schema reference for all supported types and features.
- Writing a Rust producer? See The Rust Producer Macro to skip the scaffold and generate the C ABI directly from annotated Rust.
- See the Calculator tutorial for a full end-to-end walkthrough including Swift and Node.js.
- 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.