Skip to main content

Crate ffi_convert

Crate ffi_convert 

Source
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 CAsRust takes a &CFoo and returns an owned Foo built by deep-copying every field. It is a defensive copy: once as_rust returns, 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 CCReprOf consumes a Foo and produces a CFoo that owns any heap memory its pointer fields reference. The CFoo is then handed to C as a raw pointer; to release everything, C sends the pointer back to Rust through a free-style FFI function that lets the value drop (releasing its pointer fields via CDrop).
            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:

  • CSauce derives RawPointerConverter because CPizza::base stores a *const CSauce; CPizza derives it too so it can itself be handed to C as a *const CPizza. In both cases the derived CReprOf turns a value into a raw pointer via into_raw_pointer.
  • CPizza::base carries #[nullable] because the Rust field is Option<Sauce>. The attribute tells the derives to map None to a null pointer on the way out and a null pointer to None on 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 typeRust type (U)C-compatible Rust type (T)Provided by
any scalar (int, …)same scalarsame scalarffi-convert
const char*String*const std::ffi::c_charffi-convert
const T*U*const Tffi-convert
T*U*mut Tffi-convert
const T* (nullable)Option<U>*const T with #[nullable]ffi-convert
T[N][U; N][T; N]ffi-convert
CArrayTVec<U>CArray<T>ffi-convert-extra-ctypes
CStringArrayVec<String>CStringArrayffi-convert-extra-ctypes
CRangeTRange<U>CRange<T>ffi-convert-extra-ctypes

The derives accept both *const T and *mut T for any pointer row.

§Traits at a glance

TraitDirectionPurpose
CReprOf<U>Rust → CConsume an idiomatic Rust value and produce its C-compatible twin.
AsRust<U>C → RustProduce an owned Rust value from a borrowed C-compatible value.
CDropcleanupFree heap data owned by a C-compatible struct.
RawPointerConverterpointer boxingBox a value into *const T / *mut T and take it back.
RawBorrowpointer borrowingBorrow &T from a raw pointer without taking ownership. Returns an error if the pointer is null.
RawBorrowMutpointer borrowingBorrow &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 derives CReprOf or AsRust, pointing at the idiomatic Rust type being mirrored.
  • #[nullable] on every pointer field whose Rust counterpart is an Option. The attribute is shared by all three derives: CReprOf reads it to emit a null for None, AsRust to return None on a null pointer, and CDrop to skip the free on null. A mismatch between the Rust-side Option<T> and the C-side #[nullable] is a compile error.
  • RawPointerConverter to be implemented on any nested C-compatible struct reached through a pointer field, usually by #[derive(RawPointerConverter)].

The available attributes are:

AttributeApplies toUsed byPurpose
#[target_type(Path)]struct / enumCReprOf, AsRustThe idiomatic Rust type this C-compatible type mirrors.
#[no_drop_impl]struct / enumCDropOnly implement CDrop; skip the blanket Drop impl so you can write one manually.
#[as_rust_extra_field(name = expr)]structAsRustInitialize 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 fieldCReprOf, AsRust, CDropTreat a *const T / *mut T as Option<…>. Required for every optional pointer field.
#[target_name(ident)]fieldCReprOf, AsRustName of the corresponding field on the Rust side when it differs from the C-side name.
#[c_repr_of_convert(expr)]fieldCReprOf, AsRustOverride 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_char all qualify. A type alias for c_char is not recognized.
  • Multi-level pointer fields (such as *const *const CFoo) are accepted by the AsRust derive 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:

  1. Receive a *const CInput from C and convert it with AsRust, or borrow it with RawBorrow if the C side keeps ownership.
  2. Run the Rust logic.
  3. Build a COutput with CReprOf and return it to C via RawPointerConverter::into_raw_pointer.
  4. Expose a free-style function that takes the pointer back with RawPointerConverter::from_raw_pointer and lets the value drop.

Structs§

UnexpectedNullPointerError
Returned when a raw pointer was expected to be non-null but was null.

Enums§

AsRustError
Error returned by AsRust::as_rust.
CDropError
Error returned by CDrop::do_drop.
CReprOfError
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 Box and leaked via Box::into_raw).
CReprOf
Consuming conversion from an idiomatic Rust value to its #[repr(C)] mirror.
RawBorrow
Turn a *const T into a borrowed &T without taking ownership.
RawBorrowMut
Mutable counterpart of RawBorrow.
RawPointerConverter
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 CDrop and (by default) [Drop] for a struct or unit enum.
CReprOf
Derive CReprOf<T> for a struct or unit enum.
RawPointerConverter
Derive RawPointerConverter<Self> for a struct.