A tool for separating the implementation of FFI interfaces from language-specific binding generation, allowing each to reside in different crates.
Making FFI (Foreign Function Interface) for a Rust library is not an easy task. This involves a large amount of boilerplate code that wraps the Rust API in extern "C" functions and #[repr(C)] structures.
It's already hard for a single language, but when you need to add more languages, the situation becomes more complex.
The root cause of this complexity in a multi-language scenario is that you must either:
- Make separate crates for each language's FFI. For example, you have a Rust library "foo" and cdylib/staticlib crates "foo-c", "foo-csharp", "foo-java", etc. Each crate contains its own independent wrappers for "foo". Code duplication is huge.
- Generate all language-specific libraries from the same source. In this case, you just replace code duplication with code complexity: the single FFI library must conform to all language targets.
A small example: cbindgen supports wrapper types (Option, MaybeUninit) in extern "C" functions, but csbindgen (a binding generator for C#) doesn't understand them. The following FFI function works for C but can't be used for C#:
#[no_mangle]
pub extern "C" fn foo(dst: &mut MaybeUninit<Foo>, src: &Foo) { ... }And there are more such quirks, which make it hard to support a common source for multiple languages.
The proposed solution is to create a common Rust library (e.g., "foo-ffi") that wraps the original "foo" library in FFI-compatible functions, but does not add extern "C" and #[no_mangle] modifiers. Instead, it marks these functions with the #[prebindgen] macro.
#[prebindgen]
pub fn foo(dst: &mut MaybeUninit<Foo>, src: &Foo) { ... }The dependent language-specific crates ("foo-c", "foo-cs", etc.) in this case contain only autogenerated code based on these marked functions, with the necessary extern "C" and #[no_mangle] added, stripped-out wrapper types, etc.
Several tools solve adjacent parts of the Rust FFI problem:
- Language-specific generators such as
cbindgenandcsbindgenare mature and integrate well with their target ecosystems. Their drawback in a multi-language project is that each generator expects a slightly different Rust FFI surface, so a single hand-writtenextern "C"crate either becomes a compromise or has to be duplicated per language. UniFFIis production-ready and widely used, especially for Kotlin, Swift, and Python bindings. It supports both UDL files and proc-macros, generates Rust scaffolding plus foreign bindings, and has a strong object model with records, interfaces, errors, callbacks/foreign traits, and async functions. Its trade-off is that it owns the binding model: APIs must fit UniFFI's supported type/object model, proc-macro binding generation discovers metadata from a compiled library, complex values go through UniFFI's lifting/lowering layer, and it generates UniFFI bindings rather than source tailored for existing tools or C/C++ header generators.Diplomatprovides a mature bridge-crate model and generates idiomatic bindings for several languages from one Rust-defined API. Its trade-off is the shared generated C ABI hub: all targets are shaped by the same bridge surface, which is not always ideal when different languages need different ownership, performance, or API projections.BoltFFIis an end-to-end SDK/package generator with strong built-in support for Swift, Kotlin, Java, C#, TypeScript/WASM, async functions, streams, and platform packaging. Its trade-off is that it is a complete toolchain: it owns build, generation, and packaging, while some projects need a smaller layer that composes with existing generators and custom build logic.
What makes prebindgen different:
- No separate tooling step: the destination crate generates bindings from its
build.rsby pulling the marked pieces of the source crate, then includes the generated Rust withinclude!(). - Works with existing binding generators:
prebindgenis not intended to replace tools such ascbindgenorcsbindgen; it prepares Rust source adapted and tweaked for the chosen generator when needed. - Per-language ABI freedom: destination crates choose their own adapter and wire representation instead of sharing one generated ABI for every language.
- Opt-in consumers: each language-specific crate declares exactly which annotated functions and types it exports, so C, JNI, and other targets can expose different subsets of the same source crate.
- Cross-compilation-aware generation:
prebindgenhas been proven in cross-compilation scenarios where the generated source depends on the destination platform. - Performance-oriented projections: adapters can specialize ownership and value passing for a target language, such as inline C value types or JNI handle/value-class projections, without constraining other backends.
Each element to be exported is marked in the source crate with the #[prebindgen] macro. When the source crate is compiled, these elements are written to an output directory. The destination crate's build.rs reads these elements and creates FFI-compatible functions and proxy structures for them. The generated source file is included with the include!() macro in the dependent crate and parsed by the language binding generator (e.g., cbindgen).
It's important to keep in mind that [build-dependencies] and [dependencies] are different. The #[prebindgen] macro collects sources when compiling the [build-dependencies] instance of the source crate. Later, these sources are used to generate proxy calls to the [dependencies] instance, which may be built with a different feature set and for a different architecture. A set of assertions is added to the generated code to catch possible divergences, but it's the developer's job to manually resolve these errors.
Mark the types and functions that form your FFI surface with the prebindgen macro and export the prebindgen output directory path. The source crate stays plain idiomatic Rust — opaque handles are ordinary types returned by value, fallible calls return Result<T, E>; the language adapter does the C-ABI lowering, so there is no #[repr(C)] here.
// example-flat/src/lib.rs
use prebindgen_proc_macro::{features, prebindgen, prebindgen_out_dir};
// Path to the prebindgen output directory; the destination crate's `build.rs`
// reads the collected code from this path.
pub const PREBINDGEN_OUT_DIR: &str = prebindgen_out_dir!();
// Features with which the crate is compiled. This constant is used
// in the generated code to validate that it's compatible with the actual crate.
pub const FEATURES: &str = features!();
// An opaque handle — a plain Rust type, returned by value.
pub struct Calculator {
value: f64,
}
#[prebindgen]
pub fn calculator_new() -> Calculator {
Calculator { value: 0.0 }
}
#[prebindgen]
pub fn calculator_get_value(c: &Calculator) -> f64 {
c.value
}Call init_prebindgen_out_dir() in the source crate's build.rs:
// example-flat/build.rs
fn main() {
prebindgen::init_prebindgen_out_dir();
}Add the source FFI library to both dependencies and build-dependencies, and drive the lang::Cbindgen adapter from build.rs:
# example-cbindgen/Cargo.toml
[dependencies]
example-flat = { path = "../example-flat" }
prebindgen = "0.5"
konst = "0.3" # the generated file emits a konst feature guard
[build-dependencies]
example-flat = { path = "../example-flat" }
prebindgen = "0.5"
cbindgen = "0.29"
syn = { version = "2", features = ["full"] }Declare which #[prebindgen]-marked items to export and how to name them, then let the adapter resolve types and emit the Rust file of extern "C" wrappers. Items are opt-in — only what you declare is generated.
// example-cbindgen/build.rs
use syn::parse_quote as pq;
fn main() {
// Read the items captured from the common FFI crate.
let source = prebindgen::Source::new(example_flat::PREBINDGEN_OUT_DIR);
// Configure the C adapter: declare the items to export and how to name them.
let cbindgen = prebindgen::lang::Cbindgen::new()
.source_module(pq!(example_flat))
.free_memory_function("example_free")
.mangle_type_name(|base| format!("{base}_t"))
.mangle_destructor(|base| format!("{base}_drop"))
.mangle_function(|n| n.to_string())
.opaque_ptr(pq!(Calculator))
.function(pq!(calculator_new))
.function(pq!(calculator_get_value)).panic();
// Resolve types and write the Rust file of `extern "C"` wrappers.
let mut registry =
prebindgen::core::Registry::from_items(source.items_all()).unwrap();
let bindings_file = registry.write_rust(&cbindgen, "example_flat.rs").unwrap();
// Pass the generated file to cbindgen for C header generation.
generate_c_headers(&bindings_file);
}Include the generated Rust file in your project to build the static or dynamic FFI-compatible library:
// lib.rs
include!(concat!(env!("OUT_DIR"), "/example_flat.rs"));See example projects in the examples directory:
- example-flat: Common FFI library (plain Rust,
#[prebindgen]-annotated) demonstrating prebindgen usage - example-cbindgen: Language-specific binding using
lang::Cbindgen+ cbindgen for C headers - perftest-flat / perftest-c / perftest-kotlin: A shared flat library and its performance-oriented C and Kotlin/JNI bindings
- covertest-kotlin: A Kotlin/JNI binding that exercises every
lang::JniGenfeature and verifies behavior withcheck(...)asserts (see its README)
- prebindgen API Reference: docs.rs/prebindgen
- prebindgen-proc-macro API Reference: docs.rs/prebindgen-proc-macro