Use when the user binds a Rust library to C (cbindgen) or imports a C library into Rust (bindgen), declares `extern "C"` functions, uses `#[repr(C)]` types, manages opaque pointers, or works with file descriptors via `BorrowedFd` / `OwnedFd`. Prevents `unsafe extern` requirement misses in 2024, `Box<dyn Fn>` callback ABI issues, raw `c_char` ownership confusion, and fd-leak via raw `RawFd`. Covers: C FFI patterns (`extern "C"`, `#[repr(C)]`, `#[unsafe(no_mangle)]` in 2024, `unsafe extern` block in 2024), bindgen workflow (build.rs config, header inclusion, allowlist / blocklist), cbindgen workflow (cbindgen.toml, generate C header from Rust API), opaque types (pointer-to-incomplete pattern), string passing (CString / CStr / *const c_char), error handling across FFI, function-pointer callbacks, `BorrowedFd` / `OwnedFd` / `AsFd` / `AsRawFd` for unix fd safety, Drop guards for foreign resources. Keywords: FFI, "extern C", "#[repr(C)]", "#[no_mangle]", "#[unsafe(no_mangle)]", "unsafe extern", bindgen, cbindgen, "
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Use when the user binds a Rust library to C (cbindgen) or imports a C library into Rust (bindgen), declares `extern "C"` functions, uses `#[repr(C)]` types, manages opaque pointers, or works with file descriptors via `BorrowedFd` / `OwnedFd`. Prevents `unsafe extern` requirement misses in 2024, `Box<dyn Fn>` callback ABI issues, raw `c_char` ownership confusion, and fd-leak via raw `RawFd`. Covers: C FFI patterns (`extern "C"`, `#[repr(C)]`, `#[unsafe(no_mangle)]` in 2024, `unsafe extern` block in 2024), bindgen workflow (build.rs config, header inclusion, allowlist / blocklist), cbindgen workflow (cbindgen.toml, generate C header from Rust API), opaque types (pointer-to-incomplete pattern), string passing (CString / CStr / *const c_char), error handling across FFI, function-pointer callbacks, `BorrowedFd` / `OwnedFd` / `AsFd` / `AsRawFd` for unix fd safety, Drop guards for foreign resources. Keywords: FFI, "extern C", "#[repr(C)]", "#[no_mangle]", "#[unsafe(no_mangle)]", "unsafe extern", bindgen, cbindgen, "opaque pointer", "opaque type", "CString", "CStr", "c_char", "*const c_char", "callback FFI", "function pointer", "BorrowedFd", "OwnedFd", "AsFd", "AsRawFd", RawFd, "fd ownership", "C library", "linking C", "Rust C interop".
license
MIT
compatibility
Designed for Claude Code. Requires Rust 1.85+, edition 2024.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
rust-impl-ffi-bindgen
C interop has two directions, and each has a generator. To call a C library FROM Rust, write unsafe extern "C" { ... } declarations (by hand for small APIs) or run bindgen against the header (for everything else). To expose a Rust library TO C, mark the exported fn items with extern "C" plus #[unsafe(no_mangle)] and run cbindgen to emit the matching .h file. The ABI surface in between is governed by #[repr(C)] types, opaque pointers, null-terminated strings (CString / CStr), function pointers, and Drop guards for foreign-allocated resources. On Unix, file descriptors crossing the boundary use OwnedFd (owning, closes on Drop) and BorrowedFd<'_> (non-owning, lifetime-bounded), never raw RawFd for ownership-bearing APIs.
User writes extern "C" { ... } and the compiler rejects it in edition 2024.
User adds #[no_mangle] and gets the unsafe_attr_outside_unsafe warning or error.
User wants to call a C library from Rust and asks "do I need bindgen".
User wants to expose a Rust API to C / other languages and asks "do I need cbindgen".
User defines a struct shared with C and the layout is wrong.
User passes a Rust String or &str to a C function and the call segfaults.
User receives a *const c_char from C and reads it as a Rust &str directly.
User stores a Rust closure in a struct passed to C and the callback ABI is wrong.
User passes file descriptors across FFI and asks "is RawFd correct" or hits double-close.
User reaches for transmute to convert FFI types instead of using CStr / CString / OwnedFd.
The 2024 edition rules: unsafe extern + unsafe attributes
Two changes ship with edition 2024 (Rust 1.85+). Every FFI declaration MUST use the new syntax:
// Rust 2024: unsafe is mandatory on the extern block.unsafeextern"C" {
// Default: every function in an unsafe extern block is `unsafe fn`,// because the compiler cannot verify the foreign code.pubfnstrlen(p: *const std::ffi::c_char) ->usize;
// Opt-in `safe fn` (since 1.82) marks an item whose contract the// caller can satisfy purely with safe Rust, e.g. pure math.pub safe fnsqrt(x: f64) ->f64;
}
// Rust 2024: no_mangle / link_section / export_name MUST be wrapped// in #[unsafe(...)] because they break global invariants (symbol uniqueness).#[unsafe(no_mangle)]pubextern"C"fnmy_rust_function(x: i32) ->i32 {
x * 2
}
ALWAYS write unsafe extern "C" { ... }, NEVER the bare extern "C" { ... }. The pre-2024 form is a hard error in edition 2024 and a future-compat warning in earlier editions. ALWAYS write #[unsafe(no_mangle)], NEVER plain #[no_mangle] in edition 2024.
Inside an unsafe fn body in edition 2024, the unsafe_op_in_unsafe_fn lint is warn-by-default. Wrap each individual unsafe operation in its own unsafe { ... } block to keep the soundness comment local. See [[rust-syntax-unsafe]] for the full unsafe-block hygiene rules.
#[repr(C)] : the only stable layout for FFI
#[repr(Rust)] (the default) is explicitly unspecified: the compiler may reorder fields, pick any padding, and change layout between versions. NEVER expose a #[repr(Rust)] type across FFI.
// CORRECT: C-compatible, fields in declaration order, C padding rules.#[repr(C)]pubstructPoint {
pub x: f64,
pub y: f64,
}
// CORRECT: single-field newtype with identical layout to inner.#[repr(transparent)]pubstructHandle(*mut std::ffi::c_void);
// CORRECT: enum with explicit discriminant size matches a C int.#[repr(C)]pubenumStatus {
Ok = 0,
InvalidArg = 1,
NotFound = 2,
}
// AVOID: #[repr(packed)] across FFI unless the C side also packs;// packed fields require &raw const / &raw mut (1.82+) to read, never &.
Choose #[repr(C)] for shared structs and shared enums with explicit discriminants. Choose #[repr(transparent)] for wrapper types that must keep the inner ABI (e.g. NonNull<T> newtypes). Choose #[repr(u8)] / #[repr(i32)] for C-style enums whose tag size MUST be fixed.
bindgen workflow: C headers to Rust
bindgen is invoked from build.rs and writes a generated bindings.rs into OUT_DIR. The crate consumes it via include!(concat!(env!("OUT_DIR"), "/bindings.rs")).
# Cargo.toml[package]name = "mylib-sys"edition = "2024"links = "mylib"# one *-sys crate per native lib (uniqueness rule)[build-dependencies]bindgen = "0.71"
ALWAYS use allowlist_* to keep the binding small: every type bindgen emits is part of your ABI surface. ALWAYS pair the -sys crate with a safe wrapper crate that exposes idiomatic Rust types; never expose raw bindings as the crate's public API. See references/methods.md for the full builder option list.
cbindgen workflow: Rust API to C header
cbindgen walks the public Rust API and emits a C / C++ header. Run it either from build.rs (for inclusion in your own build) or as a one-shot CLI before publishing.
# cbindgen.toml at crate rootlanguage = "C"header = "/* Generated by cbindgen. Do not edit. */"include_guard = "MYLIB_H"autogen_warning = "/* Warning, this file is autogenerated by cbindgen. */"no_includes = falsesys_includes = ["stdint.h", "stddef.h"]
cpp_compat = truestyle = "type"# use `typedef struct Foo Foo;` form[export]prefix = "mylib_"[export.rename]"Status" = "mylib_status_t"[enum]prefix_with_name = true
ALWAYS keep cbindgen.toml under source control. NEVER hand-edit the generated header: regenerate. Mark public functions with both #[unsafe(no_mangle)] and pub extern "C" so cbindgen emits them.
Opaque types: hide Rust internals behind a pointer
A C consumer holds an opaque pointer; the Rust struct's fields are not visible to C. Two patterns exist; both are stable and ABI-safe.
// Pattern A: zero-sized marker on the C side, real type only in Rust.// In Rust:pubstructEngine {
cfg: Vec<u8>,
state: SomeState,
}
#[unsafe(no_mangle)]pubextern"C"fnengine_new() -> *mut Engine {
Box::into_raw(Box::new(Engine { cfg: Vec::new(), state: SomeState::init() }))
}
#[unsafe(no_mangle)]pubunsafeextern"C"fnengine_free(p: *mut Engine) {
if p.is_null() { return; }
drop(unsafe { Box::from_raw(p) });
}
cbindgen emits a forward declaration so the C side sees struct Engine; as an incomplete type and can only handle Engine *.
// Pattern B: hand-written `pub struct Foo { _private: [u8; 0] }`// when writing the C header by hand instead of using cbindgen.#[repr(C)]pubstructFoo {
_private: [u8; 0],
_marker: core::marker::PhantomData<(*mutu8, core::marker::PhantomPinned)>,
}
ALWAYS pair Box::into_raw with a free-function that calls Box::from_raw exactly once. NEVER let C call free() on a pointer that Rust allocated, and NEVER let Rust Box::from_raw a pointer that C malloc-ed.
Strings across FFI: CString, CStr, c_char
Rust strings are not null-terminated and not necessarily valid for C consumption. std::ffi::{CString, CStr} are the bridge types.
use std::ffi::{CStr, CString, c_char};
// Rust to C: owned, null-terminated, must not contain interior nulls.pubfnpass_to_c(s: &str) ->Result<(), std::ffi::NulError> {
letowned = CString::new(s)?; // adds terminator, errors on \0unsafe { c_takes_string(owned.as_ptr()) }; // *const c_char, valid for the callOk(()) // owned dropped here, ptr now dangling
}
unsafeextern"C" {
fnc_takes_string(s: *const c_char);
fnc_returns_static() -> *const c_char;
}
// C to Rust: borrow a pointer the C side owns; lifetime tied to caller.pubfnread_from_c() ->Result<String, std::str::Utf8Error> {
// SAFETY: caller guarantees the pointer is valid, null-terminated, and// its bytes outlive this CStr borrow.letcstr = unsafe { CStr::from_ptr(c_returns_static()) };
cstr.to_str().map(|s| s.to_owned()) // copy to owned Rust String
}
ALWAYS construct CString::new(...) before sending a Rust string to C, and keep the CString alive for the entire duration of the C call. NEVER cast &str directly to *const c_char: a Rust &str lacks the trailing \0. NEVER take ownership of a *const c_char returned by a C function unless its API explicitly transfers ownership; if it does, wrap with CString::from_raw and let Rust free it.
File descriptors: BorrowedFd, OwnedFd, AsFd
On Unix, std::os::fd distinguishes ownership at the type level. ALWAYS prefer these over raw RawFd for any API that transfers or borrows an fd.
Type
Owns?
Closes on Drop?
Lifetime
Use for
RawFd (= i32)
no
no
none
The integer at the very edge of FFI, never as an API type.
BorrowedFd<'fd>
no
no
'fd
A short-lived non-owning view of an fd that someone else owns.
OwnedFd
yes
yes
static
Returning an fd from a constructor; storing an fd in a struct.
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
unsafeextern"C" {
fndup(fd: RawFd) -> RawFd;
fnclose(fd: RawFd) ->i32;
}
// CORRECT: take a borrow, the caller keeps ownership.pubfnduplicate(fd: BorrowedFd<'_>) -> std::io::Result<OwnedFd> {
// SAFETY: dup returns -1 on failure or a new owned fd on success.letraw = unsafe { dup(fd.as_raw_fd()) };
if raw < 0 {
Err(std::io::Error::last_os_error())
} else {
// SAFETY: raw is a valid new fd that nobody else owns.Ok(unsafe { OwnedFd::from_raw_fd(raw) })
}
}
// Convert an fd-bearing object (File, TcpStream, ...) to a borrowed view.fnpass_borrowed<T: AsFd>(t: &T) {
let_b: BorrowedFd<'_> = t.as_fd();
// ...
}
ALWAYS return OwnedFd from any function that produces a new fd, and ALWAYS take BorrowedFd<'_> (or &impl AsFd) for functions that only need to read or operate on an fd. NEVER store a RawFd in a struct alongside other state: there is no Drop to close it, and ownership becomes ambiguous. NEVER call close() on an fd you hold a BorrowedFd to: closure is the owner's job.
Callbacks: function pointers, not closures with captures
Rust closures are types with hidden state; they have no stable C ABI. The portable form is extern "C" fn(...) -> ... plus a separate *mut c_void user-data pointer for state.
use std::ffi::c_void;
typeCallback = extern"C"fn(event: i32, user: *mut c_void);
unsafeextern"C" {
fnregister_callback(cb: Callback, user: *mut c_void);
}
#[repr(C)]structMyState { hits: u32 }
extern"C"fnon_event(event: i32, user: *mut c_void) {
// SAFETY: register_callback was called with a *mut MyState.letstate = unsafe { &mut *(user as *mut MyState) };
state.hits = state.hits.saturating_add(1);
let_ = event;
}
pubfninstall(state: &mut MyState) {
unsafe { register_callback(on_event, state as *mut MyState as *mut c_void) };
}
ALWAYS use a plain extern "C" fn for the callback type and pass closure state via a void* user-data parameter. NEVER cast a Box<dyn Fn> or a closure-with-captures to a function pointer: the ABI does not match, even if the cast compiles.
A panic that unwinds across an extern "C" boundary is undefined behavior. Since Rust 1.81 the language aborts on an uncaught panic in extern "C" rather than continuing into C, which is safer but still terminates the process. Wrap the callback body with std::panic::catch_unwind if recovery matters; see references/examples.md.
Errors across FFI: status codes, out-parameters, or Result inside Rust
C has no Result<T, E>. Three idioms cover essentially every case:
Return an integer status code (0 = ok, negative or named enum for errors). Most ergonomic for callers in any language.
Out-parameter for the value, status for the error.int32_t fn(InputT in, OutputT* out).
Translate at the boundary. Inside Rust, work with Result<T, E>; at the extern "C" entry point, catch errors and convert to a status code, optionally setting a thread-local "last error" string.
NEVER let a Rust panic escape an extern "C" fn. ALWAYS catch_unwind if the function calls into Rust code that may panic, and translate the panic to a status code. See [[rust-impl-error-handling]] for the Rust-side Result design.
Drop guards for foreign resources
Foreign resources (file descriptors, library handles, malloc-ed buffers) escape Rust's RAII unless wrapped in a type with a Drop impl that calls the C deallocator.
pubstructCBuffer { ptr: *mutu8, len: usize }
implCBuffer {
pubfnnew(len: usize) ->Option<Self> {
// SAFETY: malloc returns null on failure or a valid len-byte allocation.letptr = unsafe { libc::malloc(len) as *mutu8 };
(!ptr.is_null()).then_some(Self { ptr, len })
}
pubfnas_slice(&self) -> &[u8] {
// SAFETY: ptr is valid for `len` bytes and not mutably aliased.unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
}
}
implDropforCBuffer {
fndrop(&mutself) {
// SAFETY: ptr came from libc::malloc, not from Rust's allocator.unsafe { libc::free(self.ptr as *mut _) }
}
}
ALWAYS pair every foreign allocator with the matching deallocator in a Drop impl. NEVER panic from Drop; if cleanup fails, log it. See [[rust-syntax-unsafe]] §"Drop and panic" for the full rationale.
Quick decision tree
C library to call from Rust ?
Header is small (under ~30 symbols) and stable -> hand-write unsafe extern "C" { ... }.
Header is large or generated -> bindgen from build.rs. Always pair with a safe wrapper crate.
Rust library to expose to C ?
Mark exports with #[unsafe(no_mangle)] pub extern "C" fn ....
Generate mylib.h with cbindgen. Keep cbindgen.toml in the repo.
Shared struct ?#[repr(C)]. Single-field wrapper that must match inner ABI -> #[repr(transparent)].
Hide Rust struct from C ? Box it into *mut T, expose only opaque pointer + new / free pair.
Strings ?CString::new(...) Rust to C; CStr::from_ptr(...) C to Rust. Always document who owns the bytes.
File descriptors ?OwnedFd if owning, BorrowedFd<'_> if borrowing, AsFd for generic input. Never RawFd in API types.
Callbacks ?extern "C" fn(..., *mut c_void), user-data void* for closure state. Never cast a Rust closure to a fn pointer.
Errors ? Status-code return or out-parameter pattern, catch_unwind around any Rust body that may panic.
Reference files
references/methods.md : full bindgen::Builder and cbindgen::Config option index, std::os::fd API table, std::ffi API table, ABI-relevant attributes (#[repr(...)], #[unsafe(...)], #[link], extern "ABI").
references/anti-patterns.md : the seven mandatory anti-patterns plus six more drawn from real GitHub issues (linkage cycles, missing links = ..., transmuting &str to *const c_char, etc.).