| Crates.io | prebindgen |
| lib.rs | prebindgen |
| version | 0.4.1 |
| created_at | 2025-07-03 22:36:29.757128+00 |
| updated_at | 2025-09-01 14:43:17.561981+00 |
| description | Separate FFI implementation and language-specific binding into different crates |
| homepage | |
| repository | https://github.com/milyin/prebindgen |
| max_upload_size | |
| id | 1737221 |
| size | 221,813 |
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:
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.
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.
example-ffi)Mark structures and functions that are part of the FFI interface with the prebindgen macro and export the prebindgen output directory path:
// example-ffi/src/lib.rs
use prebindgen_proc_macro::prebindgen;
// 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_proc_macro::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 = prebindgen_proc_macro::features!();
// Group structures and functions for selective handling
#[prebindgen]
#[repr(C)]
pub struct MyStruct {
pub field: i32,
}
#[prebindgen]
pub fn my_function(arg: i32) -> i32 {
arg * 2
}
Call init_prebindgen_out_dir() in the source crate's build.rs:
// example-ffi/build.rs
fn main() {
prebindgen::init_prebindgen_out_dir();
}
example-cbindgen)Add the source FFI library to both dependencies and build-dependencies:
# example-cbindgen/Cargo.toml
[dependencies]
example_ffi = { path = "../example_ffi" }
[build-dependencies]
example_ffi = { path = "../example_ffi" }
prebindgen = "0.4"
cbindgen = "0.29"
itertools = "0.14"
Convert #[prebindgen]-marked items to an FFI-compatible API (repr(C) structures, extern "C" functions, constants). Items that are not valid for FFI will be rejected by FfiConverter.
Generate target language bindings based on this source.
Custom filters can be applied if necessary.
// example-cbindgen/build.rs
use itertools::Itertools;
fn main() {
// Create a source from the common FFI crate's prebindgen data
let source = prebindgen::Source::new(example_ffi::PREBINDGEN_OUT_DIR);
// Create a converter with transparent wrapper stripping
let converter = prebindgen::batching::FfiConverter::builder(source.crate_name())
.edition(prebindgen::RustEdition::Edition2024)
.strip_transparent_wrapper("std::mem::MaybeUninit")
.strip_transparent_wrapper("std::option::Option")
.prefixed_exported_type("foo::Foo")
.build();
// Process items with filtering and conversion
let bindings_file = source
.items_all()
.batching(converter.into_closure())
.collect::<prebindgen::collect::Destination>()
.write("ffi_bindings.rs");
// 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"), "/ffi_bindings.rs"));
See example projects in the examples directory: