prebindgen
A tool for separating the implementation of FFI interfaces from language-specific binding generation, allowing each to reside in different crates.
Problem
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#:
pub extern "C"
And there are more such quirks, which make it hard to support a common source for multiple languages.
Solution
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.
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.
Architecture
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.
Usage
1. In the Common FFI Library Crate (e.g., 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;
// 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!;
// Group structures and functions for selective handling
Call init_prebindgen_out_dir()
in the source crate's build.rs
:
// example-ffi/build.rs
2. In the Language-Specific FFI Binding Crate (e.g., example-cbindgen
)
Add the source FFI library to both dependencies and build-dependencies:
# example-cbindgen/Cargo.toml
[]
= { = "../example_ffi" }
[]
= { = "../example_ffi" }
= "0.4"
= "0.29"
= "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;
Include the generated Rust file in your project to build the static or dynamic FFI-compatible library:
// lib.rs
include!;
Examples
See example projects in the examples directory:
- example-ffi: Common FFI library demonstrating prebindgen usage
- example-cbindgen: Language-specific binding using cbindgen for C headers
Documentation
- prebindgen API Reference: docs.rs/prebindgen
- prebindgen-proc-macro API Reference: docs.rs/prebindgen-proc-macro