Expand description
Traits and helpers to convert between idiomatic Rust values and C-compatible representations when crossing an FFI boundary.
The crate is built around two conversion traits, CReprOf and AsRust,
and two supporting traits, CDrop and RawPointerConverter. Derive
macros for all four are provided by the companion
ffi-convert-derive crate and
re-exported here.
Common containers (arrays, string arrays, ranges) live in the separate
ffi-convert-extra-ctypes
crate and can be pulled in on demand.
§Philosophy
ffi-convert’s memory-management model makes as few assumptions as
possible about how the C side allocates, holds, or frees memory.
Two traits cover the two directions across the FFI boundary:
- Incoming from C —
AsRusttakes a&CFooand returns an ownedFoobuilt by deep-copying every field. It is a defensive copy: onceas_rustreturns, the resulting Rust value does not reference any C-owned memory, and nothing else in the crate reads from the original pointer afterwards. The C caller is free to keep, reuse, or release the pointer however its own rules require. - Outgoing to C —
CReprOfconsumes aFooand produces aCFoothat owns any heap memory its pointer fields reference. TheCFoois then handed to C as a raw pointer; to release everything, C sends the pointer back to Rust through afree-style FFI function that lets the value drop (releasing its pointer fields viaCDrop).
CPizza::c_repr_of(pizza)
┌───────────────────────────────┐
│ ▼
┌──────────┐ ┌──────────┐
│ Pizza │ │ CPizza │
│ (Rust) │ │ (C) │
└──────────┘ └──────────┘
▲ │
└───────────────────────────────┘
c_pizza.as_rust()§Quick example
Define the Rust type you want to expose, then define a #[repr(C)] mirror
and derive the conversion traits. The mirror’s fields use C-compatible
types (see the mapping table).
use ffi_convert::{AsRust, CDrop, CReprOf, RawBorrow, RawPointerConverter};
use std::ffi::{c_char, c_float};
pub struct Sauce {
pub spiciness: f32,
}
#[repr(C)]
#[derive(CReprOf, AsRust, CDrop, RawPointerConverter)]
#[target_type(Sauce)]
pub struct CSauce {
pub spiciness: c_float,
}
pub struct Pizza {
pub name: String,
pub base: Option<Sauce>,
pub weight: f32,
}
#[repr(C)]
#[derive(CReprOf, AsRust, CDrop, RawPointerConverter)]
#[target_type(Pizza)]
pub struct CPizza {
pub name: *const c_char,
#[nullable]
pub base: *const CSauce,
pub weight: c_float,
}Two things to notice:
CSaucederivesRawPointerConverterbecauseCPizza::basestores a*const CSauce;CPizzaderives it too so it can itself be handed to C as a*const CPizza. In both cases the derivedCReprOfturns a value into a raw pointer viainto_raw_pointer.CPizza::basecarries#[nullable]because the Rust field isOption<Sauce>. The attribute tells the derives to mapNoneto a null pointer on the way out and a null pointer toNoneon the way back.
With the derives in place, let’s write an FFI wrapper with three small functions — one to read a C-owned value, one to hand a Rust value to C, and one to free it:
// Read a CPizza handed to us by C: deep-copy its contents into an owned
// Rust `Pizza`, then run whatever logic we need. The original pointer is
// untouched; C keeps ownership of it.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn inspect_pizza(c_pizza: *const CPizza) {
let c_pizza = unsafe { CPizza::raw_borrow(c_pizza) }
.expect("c_pizza must not be null");
let pizza: Pizza = c_pizza.as_rust().expect("invalid CPizza contents");
println!("{} ({}g)", pizza.name, pizza.weight);
}
// Build a Rust `Pizza`, convert it to `CPizza`, and hand C a raw pointer
// via [`RawPointerConverter::into_raw_pointer`]. The caller must
// eventually invoke `free_pizza` to release the allocation.
#[unsafe(no_mangle)]
pub extern "C" fn make_pizza() -> *const CPizza {
let pizza = Pizza {
name: "Margarita".to_owned(),
base: Some(Sauce { spiciness: 1.5 }),
weight: 450.0,
};
CPizza::c_repr_of(pizza)
.expect("pizza name contains an interior NUL byte")
.into_raw_pointer()
}
// Reclaim a pointer produced by `make_pizza`.
// [`RawPointerConverter::drop_raw_pointer`] takes ownership back and
// drops the value, releasing every inner pointer field via [`CDrop`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_pizza(c_pizza: *const CPizza) {
let _ = unsafe { CPizza::drop_raw_pointer(c_pizza) };
}§Type mapping
T: CReprOf<U> + AsRust<U> — in the table below, T is the C-compatible
Rust type and U is the idiomatic Rust type.
| C type | Rust type (U) | C-compatible Rust type (T) | Provided by |
|---|---|---|---|
any scalar (int, …) | same scalar | same scalar | ffi-convert |
const char* | String | *const std::ffi::c_char | ffi-convert |
const T* | U | *const T | ffi-convert |
T* | U | *mut T | ffi-convert |
const T* (nullable) | Option<U> | *const T with #[nullable] | ffi-convert |
T[N] | [U; N] | [T; N] | ffi-convert |
CArrayT | Vec<U> | CArray<T> | ffi-convert-extra-ctypes |
CStringArray | Vec<String> | CStringArray | ffi-convert-extra-ctypes |
CRangeT | Range<U> | CRange<T> | ffi-convert-extra-ctypes |
The derives accept both *const T and *mut T for any pointer row.
§Traits at a glance
| Trait | Direction | Purpose |
|---|---|---|
CReprOf<U> | Rust → C | Consume an idiomatic Rust value and produce its C-compatible twin. |
AsRust<U> | C → Rust | Produce an owned Rust value from a borrowed C-compatible value. |
CDrop | cleanup | Free heap data owned by a C-compatible struct. |
RawPointerConverter | pointer boxing | Box a value into *const T / *mut T and take it back. |
RawBorrow | pointer borrowing | Borrow &T from a raw pointer without taking ownership. Returns an error if the pointer is null. |
RawBorrowMut | pointer borrowing | Borrow &mut T from a raw pointer without taking ownership. Returns an error if the pointer is null. |
CReprOf, AsRust, CDrop, and RawPointerConverter all have
derive macros.
§Deriving the traits
The derives are the intended way to use the crate. Typical derive
combinations on a #[repr(C)] type are:
#[derive(CReprOf, CDrop)]for a type created in Rust and read from C#[derive(AsRust)]for a type created in C and read in Rust#[derive(AsRust, CReprOf, CDrop)]for a type created and read in C and Rust
Deriving CDrop and CReprOf together is recommended: CDrop assumes raw
pointers were initialized the way the CReprOf derive initializes them.
The derives expect:
#[target_type(Path)]on every struct or enum that derivesCReprOforAsRust, pointing at the idiomatic Rust type being mirrored.#[nullable]on every pointer field whose Rust counterpart is anOption. The attribute is shared by all three derives:CReprOfreads it to emit a null forNone,AsRustto returnNoneon a null pointer, andCDropto skip the free on null. A mismatch between the Rust-sideOption<T>and the C-side#[nullable]is a compile error.RawPointerConverterto be implemented on any nested C-compatible struct reached through a pointer field, usually by#[derive(RawPointerConverter)].
The available attributes are:
| Attribute | Applies to | Used by | Purpose |
|---|---|---|---|
#[target_type(Path)] | struct / enum | CReprOf, AsRust | The idiomatic Rust type this C-compatible type mirrors. |
#[no_drop_impl] | struct / enum | CDrop | Only implement CDrop; skip the blanket Drop impl so you can write one manually. |
#[as_rust_extra_field(name = expr)] | struct | AsRust | Initialize an extra field on the Rust side that has no C counterpart. Repeatable; self (the C-side value) is in scope inside expr. |
#[nullable] | pointer field | CReprOf, AsRust, CDrop | Treat a *const T / *mut T as Option<…>. Required for every optional pointer field. |
#[target_name(ident)] | field | CReprOf, AsRust | Name of the corresponding field on the Rust side when it differs from the C-side name. |
#[c_repr_of_convert(expr)] | field | CReprOf, AsRust | Override the CReprOf conversion with a custom expression. The owned input: TargetType is in scope. The field is also skipped by AsRust. |
§Constraints
- C strings: a field is recognized as a C string only when the
pointee’s type name is literally
c_char—*const std::ffi::c_char,*mut std::ffi::c_char, and*const c_charall qualify. Atypealias forc_charis not recognized. - Multi-level pointer fields (such as
*const *const CFoo) are accepted by theAsRustderive only when the field is also#[nullable]. - Enums with data:not supported. the derives accept enums only when every variant is a unit variant.
§Interop checklist
A typical FFI-exposed function follows this pattern:
- Receive a
*const CInputfrom C and convert it withAsRust, or borrow it withRawBorrowif the C side keeps ownership. - Run the Rust logic.
- Build a
COutputwithCReprOfand return it to C viaRawPointerConverter::into_raw_pointer. - Expose a
free-style function that takes the pointer back withRawPointerConverter::from_raw_pointerand lets the value drop.
Structs§
- Unexpected
Null Pointer Error - Returned when a raw pointer was expected to be non-null but was null.
Enums§
- AsRust
Error - Error returned by
AsRust::as_rust. - CDrop
Error - Error returned by
CDrop::do_drop. - CRepr
OfError - Error returned by
CReprOf::c_repr_of.
Traits§
- AsRust
- Non-consuming conversion from a
#[repr(C)]value back to an owned, idiomatic Rust value. - CDrop
- Releases heap memory referenced by a C-compatible value behind raw pointer
fields (typically data that was moved into a
Boxand leaked viaBox::into_raw). - CReprOf
- Consuming conversion from an idiomatic Rust value to its
#[repr(C)]mirror. - RawBorrow
- Turn a
*const Tinto a borrowed&Twithout taking ownership. - RawBorrow
Mut - Mutable counterpart of
RawBorrow. - RawPointer
Converter - Moves a Rust value onto the heap and exposes it as a raw pointer suitable for crossing an FFI boundary, then takes it back on the return trip.
Derive Macros§
- AsRust
- Derive
AsRust<T>for a struct or unit enum. - CDrop
- Derive
CDropand (by default) [Drop] for a struct or unit enum. - CReprOf
- Derive
CReprOf<T>for a struct or unit enum. - RawPointer
Converter - Derive
RawPointerConverter<Self>for a struct.