BoltFFI 0.32 changes how the macros expand, and existing crates need a few edits. Read the migration guide

Use this page with an LLM

Markdown

Getting Started

This guide walks you through creating a Rust library and packaging it for use in other languages.

Prerequisites

  • Rust 1.70+ (rustup.rs)
  • For Apple: Xcode 15+ with command line tools
  • For Android: Android Studio with NDK
  • For Java: JDK 8+ with javac and a C compiler for the JNI bridge
  • For C#: .NET SDK 10.0+ for the current C# demo and generated binding tests
  • For WASM: Node.js 18+ and wasm-pack (cargo install wasm-pack)
  • For Python, Python 3.10+ with pip
  • For C, a C11 compiler such as GCC, Clang, or MSVC

Install BoltFFI

cargo install boltffi_cli

Create a Rust library

cargo new --lib mylib
cd mylib

Edit Cargo.toml:

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

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

Install BoltFFI:

cargo add boltffi

Write your code

Two attributes do most of the work:

  • #[data] marks a struct or enum as a record (value type, copied across the boundary)
  • #[export] exposes a function, or a class through its struct and impl blocks, to the target language

Replace src/lib.rs:

use boltffi::*;

boltffi::scaffolding!();

#[data]
#[derive(Clone, Copy)]
pub struct Point {
    pub x: f64,
    pub y: f64,
}

#[export]
pub fn distance(a: Point, b: Point) -> f64 {
    ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt()
}

boltffi::scaffolding!() goes once in src/lib.rs of every crate that uses BoltFFI. Without it, every annotated item fails to build with cannot find type `__BoltffiTag` in the crate root.

A signature can name a declared type by its own name, a path, a use rename or a type alias. After type Coordinate = Point;, a signature taking Coordinate works once boltffi generate, boltffi build or boltffi pack has run for the crate. A plain cargo build before that fails with cannot find macro `Coordinate` . Each declared name may appear once per crate, even in different modules.

Each distinct type an item names adds two levels of macro expansion, so an impl naming 64 or more declared types reaches rustc’s default recursion limit. Raise it at the crate root with #![recursion_limit = "256"], as the error suggests.

Types from other crates

A signature can use a #[data], #[error], class or callback declared in a dependency, by any path that names it. Each such dependency calls boltffi::scaffolding!() too.

  • A path dependency in your workspace is bound in full: all of its exports appear in the bindings.
  • A crates.io or git dependency contributes only the types your signatures reach.
  • Two crates declaring the same name are refused, since the bindings put every name in one namespace.

A dependency’s exported functions are compiled into that dependency, and the linker keeps them only when your crate uses the dependency. If a path dependency is bound but never used, boltffi generate fails and lists the missing symbols. Name the crate once in src/lib.rs:

use shapes as _;

custom_type! in a dependency is not supported yet.

Build, package, or generate

boltffi init              # creates boltffi.toml
boltffi pack all --release    # packages all enabled packaged targets

This creates:

  • dist/apple/ - XCFramework + Swift bindings
  • dist/android/ - JNI libraries + Kotlin bindings
  • dist/java/ - JNI library + Java bindings
  • dist/csharp/ - NuGet package + native runtime assets
  • dist/wasm/ - WASM module + TypeScript bindings
  • dist/python/ - Python source package + wheels

To build for a single platform:

boltffi pack apple 
boltffi pack android
boltffi pack java
boltffi pack csharp
boltffi pack wasm
boltffi pack python

Use in Swift

Add the package to Xcode (File → Add Package Dependencies → local path to dist/apple), then:

import MyLib

let p1 = Point(x: 0, y: 0)
let p2 = Point(x: 3, y: 4)
print(distance(a: p1, b: p2))  // 5.0

Use in Kotlin

Copy dist/android into your Android project, then:

import com.example.mylib.*

val p1 = Point(0.0, 0.0)
val p2 = Point(3.0, 4.0)
println(distance(p1, p2))  // 5.0

Use in Java

Add the generated sources and JNI library to your project, then:

import com.example.mylib.*;

Point p1 = new Point(0.0, 0.0);
Point p2 = new Point(3.0, 4.0);
System.out.println(distance(p1, p2));  // 5.0

On Java 16+ with min_version = 16 in boltffi.toml, records are used instead of classes. On Java 8+, the same API works with generated final classes.

Use in C#

Build the NuGet package and add it to your .NET project from a local package source:

boltffi pack csharp

This produces a .nupkg under dist/csharp/packages/ with the bindings and the native runtime assets bundled under runtimes/<rid>/native/. Reference it from your project just like any other NuGet dependency, then:

using MyLib;

var p1 = new Point(0.0, 0.0);
var p2 = new Point(3.0, 4.0);
Console.WriteLine(MyLib.Distance(p1, p2));  // 5.0

Use in TypeScript

The WASM package works in bundlers, Node.js, and browsers.

With a Bundler (Vite, webpack)

Import directly - your bundler handles WASM loading:

import { Point, distance } from 'mylib';

const p1: Point = { x: 0, y: 0 };
const p2: Point = { x: 3, y: 4 };
console.log(distance(p1, p2));  // 5.0

In Node.js

Await initialization before calling functions:

import { initialized, Point, distance } from 'mylib';

await initialized;

const p1: Point = { x: 0, y: 0 };
const p2: Point = { x: 3, y: 4 };
console.log(distance(p1, p2));  // 5.0

In a Browser (no bundler)

Use the web entrypoint and call init():

<script type="module">
  import init, { distance } from './pkg/web.js';
  
  await init();
  
  const p1 = { x: 0, y: 0 };
  const p2 = { x: 3, y: 4 };
  console.log(distance(p1, p2));  // 5.0
</script>

Use in Python

Install the wheel from dist/python/wheelhouse, then import the generated package.

python -m pip install dist/python/wheelhouse/mylib-0.1.0-*.whl
import mylib

p1 = mylib.Point(0, 0)
p2 = mylib.Point(3, 4)
print(mylib.distance(p1, p2))  # 5.0

Use in C

Enable the experimental target in boltffi.toml:

[targets.c]
enabled = true
output = "dist/c"

Then build the header and libraries:

boltffi pack c --experimental --deny-skipped

Save this as main.c:

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

int main(void) {
    MylibPoint start = {0.0, 0.0};
    MylibPoint end = {3.0, 4.0};
    printf("%g\n", mylib_distance(start, end));
    return 0;
}

On Linux or macOS:

cc -std=c11 main.c -I dist/c/include -L dist/c/lib -lmylib \
  -Wl,-rpath,"$PWD/dist/c/lib" -o example
./example

The program prints 5. See linking a C program for Windows commands and memory management for cleanup rules.

What you learned

  • #[data] creates records (value types)
  • #[export] exposes functions
  • boltffi pack handles build, codegen, and packaging in one step

Next steps

  • Types - how Rust types map to target languages
  • Records - structs, enums, default values
  • Classes - stateful objects with methods
  • Functions - parameters, return types, errors
  • Async - async functions and methods