safer_ffi: exporting Rust to C without scattering unsafe blocks
Write safer FFI code in Rust without polluting it with unsafe code
At a glance
- What is it?
- safer_ffi is a Rust framework for writing foreign function interfaces without filling your code with unsafe blocks. You mark a struct with a derive attribute and a function with an export attribute, and the crate generates the C header as a build step so the interface on both sides stays in one place.
- Who is it for?
- Adopt safer_ffi if you are exposing a Rust library to C and want the unsafe surface confined to the framework rather than spread across your own code, and if the extra build step for header generation fits your release process. Do not adopt it if you need bidirectional calls with arbitrary C callbacks, since the framework is built around exporting Rust into a C-facing library rather than around a general interop bridge, and do not expect the crate to be a finished 1.0.
- 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 172 days 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 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The unsafe blocks move into the framework
safer_ffi is a framework that helps you write foreign function interfaces without polluting your Rust code with unsafe blocks, while making the functions easier to read and maintain. The name is the argument: the unsafe code still has to exist somewhere, because an FFI boundary is inherently unsafe, but it gets collected in one place instead of being scattered through the code you are actually writing.
The project is written in Rust and published on crates.io as safer-ffi, with a user guide built with mdbook, API documentation on docs.rs, and a CI badge on the master branch. The licence is MIT, and the author of the crate manifest is Daniel Henry-Mantilla.
The keyword list on the crate is worth a look, because it describes the design constraints: ffi, no_std, framework, safety and bindings. The no_std keyword is the interesting one, since it means the crate is built to compile without the standard library, with a std feature layered on top rather than assumed. That matters for anyone shipping a Rust library to an embedded target that also has to be callable from C.
There is a mismatch between the README and the crate manifest that you will notice immediately. The README states a minimum supported Rust version of 1.66.1, while the Cargo.toml in the repository declares rust-version 1.85.0 with edition 2024 and a version of 0.2.0-alpha.0. The README documents the published crate, and the manifest describes the working tree, and the two have drifted apart.
Two attributes replace the boilerplate
The whole API surface for the common case is two attributes. The first is #[derive_ReprC], applied to a struct that has to be visible from C, and the second is #[ffi_export], applied to a function you want to export.
```rust ,no_run use ::safer_ffi::prelude::*;
/// A `struct` usable from both Rust and C #[derive_ReprC] #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Point { x: f64, y: f64, }
/* Export a Rust function to the C world. */ /// Returns the middle point of `[a, b]`. #[ffi_export] fn mid_point(a: &Point, b: &Point) -> Point { Point { x: (a.x + b.x) / 2., y: (a.y + b.y) / 2., } }
/// Pretty-prints a point using Rust's formatting logic. #[ffi_export] fn print_point(point: &Point) { println!("{:?}", point); } ```
The prelude import brings the attributes into scope, and the derive generates the conversion machinery that makes the type usable across the boundary. Note that the struct still carries #[repr(C)], because the layout has to match what C expects, and that is not something a macro can decide for you.
The functions are plain Rust functions. There is no wrapper type, no builder and no registration step, which is what makes the exported functions readable. The unsafe work happens in the expansion of the attribute, so a reader of your code sees a function signature and a body and nothing else.
The functions are also private by default in this example, and the export attribute is what makes them reachable, which is a nicer property than a public symbol nobody asked to expose.
The crate layout decides how the library is built
Using safer_ffi in your own crate takes three steps, and the first one is the Cargo.toml, where the crate types and a feature gate do the work.
[package]
name = "crate_name"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = [
"staticlib", # Ensure it gets compiled as a (static) C library
# "cdylib", # If you want a shared/dynamic C library (advanced)
"lib", # For `generate-headers` and other downstream rust dependents
]
[[bin]]
name = "generate-headers"
required-features = ["headers"] # Do not build unless generating headers.The crate-type list is the heart of it. The staticlib entry ensures the crate is compiled as a static C library, cdylib is commented out as the advanced shared library option, and the plain lib entry is needed so that the generate-headers binary and the integration tests, doctests and examples can depend on the crate.
The [[bin]] section declares a binary called generate-headers with required-features set to headers, so it is not built unless you ask for header generation. That is the trick that keeps the header generation step out of your normal build.
The features section then wires it up. The headers feature is defined as headers = ["safer-ffi/headers"], so opting in enables the upstream feature as well, and the guide notes the alternative of adding the dependency with cargo add safer-ffi and setting the version to the last released one, which you can find with cargo search safer-ffi.
The last step is a tiny binary in src/bin/generate-headers.rs that calls the generate_headers function from your library and returns a standard Result, which is where the header file path is decided.
Header generation is a build step, and it renames types
The compilation and header generation sequence is two commands:
# Compile the C library (in `target/{debug,release}/libcrate_name.ext`)
cargo build # --release
# Generate the C header
cargo run --features headers --bin generate-headersThe second command runs the binary that the Cargo.toml declared, and it produces the C header for the functions you exported. The header generation lives behind the cfg(feature = "headers") attribute in your library, so the function only exists in the build that asks for it.
The generated header is where the framework makes a naming decision you should know about, because it will surprise you the first time you compile C against it. The Point struct becomes Point_t in C, with a typedef, and the doc comments become C comments, while the fields get a placeholder where the documentation was missing.
The header carries a do not manually edit this file banner, and the include guard is derived from the crate name with the conventional double underscore wrapper, plus an extern "C" block guarded by __cplusplus so the same header works in C++.
The benefit is the one the framework is selling. The C header is generated from the Rust source, so the two cannot drift. There is no separate header file in the repository to keep in sync with the implementation, and the doc comments on the Rust side end up in the C header as documentation.
Linking the C program, and the flags you have to discover
The example C program is small, and it shows the shape of the calling convention.
#include <stdlib.h>
#include "rust_points.h"
int
main (int argc, char const * const argv[])
{
Point_t a = { .x = 84, .y = 45 };
Point_t b = { .x = 0, .y = 39 };
Point_t m = mid_point(&a, &b);
print_point(&m);
return EXIT_SUCCESS;
}The compile command is one line, and it is the line where most people get stuck:
cc -o main{,.c} -L target/debug -l crate_name -l{pthread,dl,m}
# Now feel free to run the compiled binary
./mainThe -L and -l flags point the linker at the built static library, and the trailing -l flags are the extra native libraries the Rust standard library needs. The documentation is explicit that those vary depending on the version of the Rust standard library and on the system doing the compiling, and that the reliable way to know them is to ask rustc itself.
The query is a single command that prints the native static libraries to stderr:
rustc --crate-type=staticlib --print=native-static-libs -</dev/nulland the guide adds the practical touch, that using something like sed with a pattern extracting the native-static-libs line is a convenient way to get just the flags. It also notes the ideal case, that you would not query this in a vacuum with /dev/null as the input Rust code, but rather apply it to the code you are actually compiling.
That is a real usability wart worth naming. The framework removes the unsafe from your Rust code and hands you a different problem, which is discovering the link flags for a static Rust library on your particular toolchain.
The feature matrix is the actual surface
The crate manifest is where the real scope of the framework shows, and it is a long feature list.
The base is default = ["std"], with std itself pulling in alloc, the scopeguard crate's use_std feature and the optional stabby crate's std feature. There is a bare alloc feature for no_std targets, and a nightly feature for the parts that need a nightly compiler.
The interoperability features are where the choices live. async-fn enables the async-fn feature of the procedural macro crate. dyn-traits adds support for trait objects and, as a FIXME comment admits, is not fine-grained because it also forces std. futures and tokio build on that with the futures dependency, tokio and async-compat. serde and stabby are optional dependencies, each enabled by its own feature. headers enables the header generation, python-headers does the same for Python, and c-headers-with-fn-style tweaks the generated.h ever so slightly.
There is also a docs feature that documents under all the other features except debug or experimental, which is a nice touch, since it means the published documentation is generated with the widest feature set the crate supports.
Two features are marked private. The js feature is explicitly not part of the crates.io package and pulls in the napi machinery, the once_cell dependency and the JavaScript flavour of the macro crate. And internal-tests is the feature that turns on everything the test suites need, which is how the test matrix is kept out of the normal build.
The repository root shows the same spread in file form, with ffi_tests/, js_tests/, nodejs_tests/, a napi-dispatcher/ directory, run-sh/ for the shell test runners and safer-ffi-build/ for the build support.
Where the project stands
The release history is the part to be careful about, because the published versions and the repository are at different points.
The published tags are v0.1.10 from 2024-07-30, v0.1.11 from 2024-07-31 and v0.1.12 from 2024-08-01, three releases in three days, which was the last burst of publishing. The repository manifest carries version 0.2.0-alpha.0, so the working tree is a pre-release of the next minor version, and the last push was on 2026-05-04. The repository is not archived.
So the honest summary is that the published crate is a 0.1 line from 2024 and the tree is on a 0.2 alpha, with the work in between. If you pin the crate from crates.io you get the 0.1 behaviour and the README's minimum Rust version of 1.66.1, and if you build from the repository you are on edition 2024 with a Rust 1.85.0 floor and a feature set that has grown since.
The rest of the repository is documentation and infrastructure. There is a guide/ directory, which is the mdbook user guide the README points at first, a rust-toolchain.toml, a .rustfmt.toml, a Cargo.lock, an examples/ directory with the point example the quickstart uses, and a scripts/ directory. The examples/point directory is the one to look at first if you want to see the smallest complete project, because the quickstart builds it with make and it exercises the whole pipeline in one step.
Editorial conclusion
Adopt safer_ffi if you are exposing a Rust library to C and want the unsafe surface confined to the framework rather than spread across your own code, and if the extra build step for header generation fits your release process. Do not adopt it if you need bidirectional calls with arbitrary C callbacks, since the framework is built around exporting Rust into a C-facing library rather than around a general interop bridge, and do not expect the crate to be a finished 1.0. Before you start, check the crate version on crates.io, because the published releases stop in 2024 while the repository manifest shows a 0.2.0 alpha, and read the feature list, since the headers feature and the std feature are both required for the workflow described in the guide.
Frequently asked questions
What does safer_ffi actually do?
It is a framework for writing FFI code in Rust without filling your code with unsafe blocks. You mark types with #[derive_ReprC] and functions with #[ffi_export], and the crate handles the unsafe machinery and generates the matching C header so the two sides cannot drift apart.
How do I generate the C header for a Rust crate using safer_ffi?
Add the headers feature and declare a generate-headers binary with required-features set to headers in Cargo.toml, then run cargo run --features headers --bin generate-headers. The header is written from the same source as the Rust code and carries a do not manually edit this file banner.
What crate types does a safer_ffi project need?
The guide sets crate-type to staticlib so the crate is compiled as a static C library, with cdylib commented out as the advanced shared library option, and lib included so the generate-headers binary and the tests, doctests and examples can depend on the crate.
Which extra linker flags do I need when linking a C program against Rust?
They vary with the Rust standard library version and the system, so the documentation recommends asking rustc with rustc --crate-type=staticlib --print=native-static-libs -</dev/null, which prints the native static libraries to stderr, and extracting that line with sed gives you the -l flags to pass to cc.
Is safer_ffi still being released?
The published tags stop at v0.1.12 from 2024-08-01, while the repository manifest shows version 0.2.0-alpha.0 and the last push was on 2026-05-04. The README states a minimum supported Rust version of 1.66.1, and the manifest in the tree requires Rust 1.85.0 and edition 2024.
Official sources
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.
[](https://hysenlabs.com/projects/getditto-safer-ffi)Was this useful?