Open-source project
boltffi/boltffi avatar
boltffi/boltffi

BoltFFI: zero-copy bindings from Rust to seven languages

A high-performance multi-language bindings generator for Rust, up to 1,000x faster than UniFFI. Ship Rust libraries that feels native to Python, Swift, Kotlin, and more

903 stars54 forksRustMIT

At a glance

What is it?
BoltFFI is a Rust bindings generator that avoids the serialization most FFI tools impose, passing primitives as raw values and structs as shared pointers. One pack command produces an xcframework, JNI libraries, a NuGet package, a wasm bundle and a Python wheel, each written in that language's idioms.
Who is it for?
BoltFFI suits a Rust library that needs to be called from a mobile or desktop app and is being called often enough that serialization overhead shows up, since the zero-copy path is the entire point and the generated code uses each target language's own async idioms.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem is serialization, not the language boundary

BoltFFI describes itself as a high-performance multi-language bindings generator for Rust, and the diagnosis in the project documentation is that serialization-based FFI is slow.

The specific complaints are about what two well known tools do with every call. UniFFI serializes every value to a byte buffer. wasm-bindgen turns every struct into a JavaScript object. The documentation's point is that this overhead shows up even when you are making tens or hundreds of FFI calls per second, which is exactly the range a mobile app or a game engine lives in.

The approach BoltFFI takes is zero-copy where possible. Primitives pass as raw values. Structs with primitive fields pass as pointers to memory both sides can read directly. WASM uses a wire buffer format that avoids per-field allocation. Only strings and collections go through encoding.

That last clause is the honest boundary of the claim, and it is in the project's own words: only strings and collections go through encoding. So a struct of primitives is free, and a struct containing a string is not. Any design where you pass strings across the boundary pays for it, and the amount you pay depends on how much you allocate per call.

The alternative tools are named in the documentation, which is useful. UniFFI is Mozilla's binding generator using a serialization-based approach. Diplomat is focused on C and C++ interop. And cxx is safe C++ and Rust interop.

The distinction from cxx is worth drawing out, because it is not obvious. cxx is not a bindings generator at all; it is a bridge for a specific pair of languages, with a shared type system and generated glue, and it does not produce Swift, Kotlin or Python. BoltFFI is a generator aimed at many targets, and it is not trying to be a safe C++ bridge.

Two attributes and a build command

The authoring model is two attributes. Types are marked with #[data] and functions with #[export].

rust
use boltffi::*;

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

#[export]
pub fn distance(a: Point, b: Point) -> f64 {
    let dx = b.x - a.x;
    let dy = b.y - a.y;
    (dx * dx + dy * dy).sqrt()
}

The #[derive(Clone, Copy)] on the struct is not decoration. Because the point is passed as a pointer to memory both sides can read, the type has to be a plain old data layout that both languages agree on, which means no ownership, no lifetimes in the data, and a layout the generator can describe to a foreign type system.

The tool then produces one artefact set per target with a single command.

bash
boltffi pack all
# Produces: ./dist/apple/YourCrate.xcframework + Package.swift
# Produces: ./dist/android/jniLibs/<abi>/libyour_crate.so + Kotlin bindings
# Produces: ./dist/java/native/<host-target>/libyour_crate_jni.* + Java bindings
# Produces: ./dist/wasm/pkg/*.wasm + TypeScript bindings + npm package
# Produces: ./dist/csharp/packages/*.nupkg with RID native assets
# Produces: ./dist/python/wheelhouse/*.whl with Python package sources

The output shapes are the ones each ecosystem expects rather than a generic package. Apple gets an xcframework and a Package.swift so the result is a Swift Package Manager dependency. Android gets jniLibs laid out per ABI with the Kotlin bindings beside it. Java gets native libraries keyed by host and target. WebAssembly gets the .wasm plus TypeScript bindings and an npm package. C# gets .nupkg files carrying RID specific native assets. Python gets wheels in a wheelhouse with the package sources.

That last one is a deliberate choice. Producing a wheelhouse means pip can install the right wheel per platform, which is the step that usually makes cross language packaging painful.

The generated code uses each language's own async model

The documentation is specific that the generated bindings use each language's idioms rather than a lowest common denominator.

Swift gets async and await. Kotlin gets coroutines. Java gets CompletableFuture and functional interfaces. C# gets Tasks and async enumerables. TypeScript gets Promises. Errors become native exceptions in the managed targets, and C uses typed results and explicit cleanup functions.

This is a real design decision rather than a wrapper convenience. A binding generator that returns a callback and makes the caller unwrap it produces a Rust-shaped API in every target, and every consumer then writes the same adapter code. Getting CompletableFuture in the Java output means the Java call site reads like Java code.

The calling examples are short enough to show the point.

swift
let d = distance(a: Point(x: 0, y: 0), b: Point(x: 3, y: 4)) // 5.0
kotlin
val d = distance(a = Point(x = 0.0, y = 0.0), b = Point(x = 3.0, y = 4.0)) // 5.0
typescript
import { distance } from 'your-crate';
const d = distance({ x: 0, y: 0 }, { x: 3, y: 4 }); // 5.0

The TypeScript one is the interesting case, because that is where wasm-bindgen's approach is being replaced. Instead of turning every struct into a JavaScript object, the object literal in the call is constructed once and the call itself is the cheap part.

The C target works differently, because C has no exceptions and no ownership. A package named mylib exposes a header and a set of functions.

c
#include "mylib.h"

int main(void) {
    MylibPoint start = {0.0, 0.0};
    MylibPoint end = {3.0, 4.0};
    return mylib_distance(start, end) == 5.0 ? 0 : 1;
}

The C package includes CMake targets and pkg-config files for shared and static linking, and the documentation points at a page covering CMake, Make and Meson alongside the ownership rules, since C has no way to express who frees what.

The support table tells you where the edges are

The language table in the documentation is the most useful thing on the page for an evaluator, because it separates what works from what is planned.

Swift, Kotlin, Java, C#, WASM with TypeScript, and Python are all listed as full support. Kotlin Multiplatform is experimental, for the JVM and Android. C is experimental with synchronous APIs. C++, Ruby, Scala and Go are planned. Dart is in progress. Lua and R are listed as potential.

The Kotlin Multiplatform entry has the most specific constraints, and they are the kind that save you a day. It generates a shared commonMain API with jvmMain and androidMain implementations, it currently supports synchronous infallible free functions using booleans, signed integers and floating point values, including Rust parameter defaults. Kotlin/Native targets such as iOS and macOS are not supported yet.

So KMP here means the JVM and Android legs, not the full cross platform story its name suggests, and the supported surface is arithmetic on numbers. Enabling it is a config toggle.

toml
[targets.kotlin_multiplatform]
enabled = true

And generating or packaging it uses the experimental flag on both commands.

bash
boltffi generate kmp --experimental
boltffi pack kmp --experimental

Unsupported APIs fail generation, rather than being silently skipped, which is the safer behaviour for a binding generator where a missing function would surface as a runtime error in someone else's application.

The C target has a similar shape. You enable [targets.c] in boltffi.toml and then run boltffi pack c --experimental --deny-skipped. The deny-skipped flag is the same idea in the other direction, refusing to produce a package that quietly omitted things.

The C# output is worth one more note, because the artefact is unusual. The .nupkg files carry RID specific native assets, so the NuGet package itself contains the binaries for each runtime identifier rather than referencing them separately.

Installing the CLI, the crate and the crate type

The CLI installs from crates.io.

bash
cargo install boltffi_cli

Prebuilt CLI archives are attached to each GitHub release, and each archive has a matching .sha256 checksum file. The documentation names the Windows ARM64 case specifically, where you download boltffi-windows-arm64.zip and boltffi-windows-arm64.zip.sha256. Publishing checksums alongside binaries is the minimum you should expect from a tool that produces native libraries.

The library side is added to your crate.

bash
cargo add boltffi

And the crate type has to change so that a shared library and a static library are produced.

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

The cdylib entry is what the native targets link against, and the staticlib entry covers the C and C# side that wants to link statically.

The repository layout shows the size of the project. There are twelve workspace members: boltffi itself, boltffi_core, boltffi_macros, boltffi_bindgen, boltffi_cli, boltffi_ast, boltffi_binding, boltffi_backend, boltffi_scan, boltffi_verify, boltffi_tests and boltffi_dart_runtime. The separation into ast, bindgen, backend and binding crates is the shape of a compiler pipeline, and boltffi_scan and boltffi_verify suggest the tool scans a crate and then checks what it produced.

The workspace uses resolver version 3 and edition 2024, and the release profile keeps debug information at level 1. There is a release-lto profile that inherits from release with opt-level 3, debug none and symbols stripped, which is what you would use for a shipped artefact rather than for symbolication.

The justfile has a setup-targets recipe that installs the Rust targets for cross compilation, covering aarch64 and x86_64 for Apple platforms, aarch64-ios plus the simulator variants, and four Android targets. That is the practical cost of producing the artefact set the pack command promises.

Reading the benchmark tables with the right amount of scepticism

The documentation publishes two benchmark tables, one against UniFFI for Swift and Kotlin, one against wasm-bindgen for WASM.

Against UniFFI, the noop and echo_i32 benchmarks are reported at under 1 ns for BoltFFI against 1,416 ns, which the table marks as greater than 1000x. The counter_increment case with one thousand calls is 1,083 ns against 1,388,895 ns, or 1,282x. The struct case, generate_locations with one thousand structs, is 4,167 ns against 1,276,333 ns, and with ten thousand structs it is 62,542 ns against 12,817,000 ns.

Against wasm-bindgen, the numbers are 29,886 ns for a thousand particles against 13,532,530 ns, and 2,199 ns for a hundred locations against 283,753 ns, with the other rows ranging from 129x to 453x.

The shape of those numbers is consistent with the stated mechanism. A no-op call being under a nanosecond means nothing was copied, and the gap widens when values accumulate. The hundred to thousand struct case growing superlinearly for BoltFFI, from 2,199 ns to 21,931 ns, suggests allocation or copying on the target side, which is where the per-field cost the wire format is meant to avoid would show up.

Two caveats are worth stating. These are the project's own numbers, and the readme does not describe the hardware, the platform or the compiler flags behind them. And the comparison is not purely about BoltFFI: UniFFI serializing to a byte buffer and wasm-bindgen building a JavaScript object are two different costs, and a tool that did neither would be faster than both.

The documentation does point at the source, with the full benchmark code in the benchmarks directory, and the Cargo workspace excludes benchmarks/adapters/uniffi from the build, which means the UniFFI comparison adapter is maintained separately and is worth reading before treating the ratio as a specification rather than a measurement.

Where BoltFFI is the wrong choice

The case against it comes straight from the same place as the case for it.

If your Rust library is called once or twice per user action, the serialization cost is irrelevant and you have added a code generator, a CLI, a build step and a set of generated artefacts to a project that did not need them. A plain C ABI with hand written bindings, or cbindgen for the header, is less machinery and completely adequate at that call rate. The benchmark tables describe a workload of a thousand calls, which is a real workload, but it is not every workload.

The second limit is the type system. Zero-copy requires a plain old data layout, so a struct with primitive fields is cheap and a struct with a string is not. An API whose types are mostly owned data, with lifetimes, with trait objects, or with generics over other types, will not fit the model cleanly. The documentation does not spell out which Rust types are supported, and that is the question to ask before committing.

The third is the surface area of the tool itself. Twelve workspace crates, a CLI with subcommands and experimental flags, per-target configuration in boltffi.toml, and a BOLTFFI_TOML_SPEC.md documenting the configuration format. This is a substantial piece of machinery, and it comes with its own version to track, since the workspace version is 0.31.1 and the releases move regularly.

The fourth is simply the language coverage. Six targets are fully supported, two are experimental and four are planned. If the language you need is C++ or Go, the tool will not help you today, and cxx or a dedicated C++ bridge is the thing to evaluate.

There is also a governance angle worth a sentence. The repository carries an AI_POLICY.md file, which tells you the project expects substantial machine assistance in its development. That is not a defect, and for a code generator with a large test matrix it may be an advantage, but it is a fact to factor into your review of a tool that will generate code you then maintain.

Editorial conclusion

BoltFFI suits a Rust library that needs to be called from a mobile or desktop app and is being called often enough that serialization overhead shows up, since the zero-copy path is the entire point and the generated code uses each target language's own async idioms. It does not suit a Rust crate that will be called once or twice per session, where the benchmark numbers do not matter and the added toolchain would be pure overhead, and the C target and Kotlin Multiplatform support are both marked experimental. Before adopting it, read the benchmark code in the benchmarks directory rather than the summary tables, and check the support table for your target, since C++ and Ruby are listed as planned rather than supported.

Frequently asked questions

How do I install BoltFFI?

Install the CLI with cargo install boltffi_cli, or download a prebuilt archive from the GitHub releases where each archive has a matching .sha256 checksum file. Then add the library to your crate with cargo add boltffi and set crate-type to ["cdylib", "staticlib"] in Cargo.toml.

Which languages does BoltFFI generate bindings for?

Swift, Kotlin, Java, C#, WASM with TypeScript and Python are listed as full support. Kotlin Multiplatform is experimental for the JVM and Android, C is experimental with synchronous APIs, Dart is in progress, and C++, Ruby, Scala and Go are listed as planned.

How does BoltFFI avoid the cost of serialization?

It uses zero-copy where possible. Primitives pass as raw values, structs with primitive fields pass as pointers to memory both sides can read directly, and WASM uses a wire buffer format that avoids per-field allocation. Only strings and collections go through encoding.

What does boltffi pack produce?

One command produces the per-ecosystem artefacts: an xcframework plus Package.swift for Apple, jniLibs per ABI with Kotlin bindings for Android, native libraries with Java bindings, a wasm bundle with TypeScript bindings and an npm package, .nupkg files with RID native assets for C#, and a wheelhouse of Python wheels.

How does BoltFFI handle async in the generated code?

Each target gets its own idiom: Swift gets async and await, Kotlin gets coroutines, Java gets CompletableFuture and functional interfaces, C# gets Tasks and async enumerables, and TypeScript gets Promises. Errors become native exceptions in the managed targets, while C uses typed results and explicit cleanup functions.

Official sources

  1. boltffi/boltffi on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/boltffi-boltffi.svg)](https://hysenlabs.com/projects/boltffi-boltffi)

Was this useful?