Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Opaque types

Some types can’t cross the FFI boundary by value. A Vec is one of those, and so is a C FILE in the other direction. Neither can be mirrored field by field, because neither layout is guaranteed: Rust reserves the right to change Vec’s, and the C standard leaves FILE’s up to each C library.

Instead, pass a pointer across the FFI and keep the type behind it private. C calls this an opaque type, and it’s the shape most C libraries already use: FILE *, sqlite3 *, pthread_mutex_t *. One side owns the object, its layout, and its memory, and the other side only ever holds the address. So an opaque type is really a question of ownership: who creates the object, and who frees it.

A Rust type behind a C handle

The C side gets a type name with no definition, and functions that take a pointer to it:

typedef struct Counter Counter;

Counter *counter_new(uint32_t start);
void counter_add(Counter *counter, uint32_t amount);
uint32_t counter_value(const Counter *counter);
void counter_free(Counter *counter);

typedef struct Counter Counter; does two things. struct Counter declares a struct without defining it, and the typedef lets C code write Counter instead of struct Counter. The header never defines the struct, so C can pass Counter * around, but it can’t allocate one, copy one, or read a field.

On the Rust side, Counter is an ordinary Rust struct. The constructor puts it on the heap and hands ownership to C, and the destructor takes it back:

pub struct Counter {
    value: u32,
}

#[unsafe(no_mangle)]
pub extern "C" fn counter_new(start: u32) -> *mut Counter {
    Box::into_raw(Box::new(Counter { value: start }))
}

/// # Safety
///
/// `counter` must be null, or come from `counter_new` and not be used again.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_free(counter: *mut Counter) {
    if !counter.is_null() {
        // SAFETY: the pointer came from `Box::into_raw` in `counter_new`.
        drop(unsafe { Box::from_raw(counter) });
    }
}

Box::into_raw turns the box into a raw pointer without dropping the counter, and Box::from_raw turns the pointer back into a box, which drops the counter when the box goes out of scope.

The operations in between borrow the counter for the duration of one call. &mut *counter turns the raw pointer into a &mut Counter, and &*counter into a &Counter:

pub struct Counter { value: u32 }
/// # Safety
///
/// `counter` must come from `counter_new` and must not have been freed. No
/// other call may use the same counter for the duration of this one.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_add(counter: *mut Counter, amount: u32) {
    // SAFETY: the caller guarantees a live counter that nothing else uses.
    let counter = unsafe { &mut *counter };
    counter.value += amount;
}

/// # Safety
///
/// `counter` must come from `counter_new` and must not have been freed. No
/// other call may modify the same counter for the duration of this one.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_value(counter: *const Counter) -> u32 {
    // SAFETY: the caller guarantees a live counter that nothing modifies.
    let counter = unsafe { &*counter };
    counter.value
}

These references are only sound because of the promises in the # Safety sections: the pointer is live, and nothing else uses the counter while the reference exists. A &mut Counter must be the only way to reach the counter, so two overlapping calls on the same counter would be undefined behavior.

Two rules keep this sound:

  • Pair every constructor with a destructor. Every counter_new needs exactly one counter_free. After that, the pointer C holds is dangling.
  • Don’t keep references past the call. C may free the counter at any time, so a reference stored somewhere would outlive it.

A C type behind a Rust handle

In the other direction, a C library hands you a pointer to something you know nothing about:

typedef struct Widget Widget;

Widget *widget_open(int id);
int widget_id(const Widget *widget);
void widget_close(Widget *widget);

Don’t guess its fields. Rust has a pattern for a type that stands for “something whose layout is none of my business”:

use std::ffi::c_int;
use std::marker::{PhantomData, PhantomPinned};

#[repr(C)]
pub struct Widget {
    _data: (),
    _marker: PhantomData<(*mut u8, PhantomPinned)>,
}

unsafe extern "C" {
    fn widget_open(id: c_int) -> *mut Widget;
    fn widget_id(widget: *const Widget) -> c_int;
    fn widget_close(widget: *mut Widget);
}

Both fields are private, so the only way to get a Widget is a pointer from C. _data keeps the improper_ctypes lint quiet, which would otherwise flag a struct made only of PhantomData. The *mut u8 in the marker makes the type neither Send nor Sync, so Rust won’t assume it can be used from another thread. PhantomPinned makes it !Unpin, because the C library may rely on the object staying at the same address.

To make sure every widget is closed exactly once, wrap the pointer in a type that owns it. NonNull turns C’s null into None, and Drop closes the widget when the wrapper goes out of scope:

use std::ffi::c_int;
use std::marker::{PhantomData, PhantomPinned};
use std::ptr::NonNull;
#[repr(C)]
pub struct Widget { _data: (), _marker: PhantomData<(*mut u8, PhantomPinned)> }
unsafe extern "C" {
    fn widget_open(id: c_int) -> *mut Widget;
    fn widget_id(widget: *const Widget) -> c_int;
    fn widget_close(widget: *mut Widget);
}
// Stand in for the C library, so that this example links.
#[unsafe(export_name = "widget_open")]
extern "C" fn widget_open_impl(id: c_int) -> *mut Widget {
    Box::into_raw(Box::new(id)).cast()
}
#[unsafe(export_name = "widget_id")]
extern "C" fn widget_id_impl(widget: *const Widget) -> c_int {
    unsafe { *widget.cast::<c_int>() }
}
#[unsafe(export_name = "widget_close")]
extern "C" fn widget_close_impl(widget: *mut Widget) {
    drop(unsafe { Box::from_raw(widget.cast::<c_int>()) });
}
pub struct OwnedWidget(NonNull<Widget>);

impl OwnedWidget {
    pub fn open(id: c_int) -> Option<Self> {
        // SAFETY: `widget_open` returns either a valid widget or null.
        let widget = unsafe { widget_open(id) };
        NonNull::new(widget).map(OwnedWidget)
    }

    pub fn id(&self) -> c_int {
        // SAFETY: the widget is still open, because `OwnedWidget` only closes
        // it in `drop`.
        unsafe { widget_id(self.0.as_ptr()) }
    }
}

impl Drop for OwnedWidget {
    fn drop(&mut self) {
        // SAFETY: the widget came from `widget_open`, and `OwnedWidget` is its
        // only owner, so it is closed exactly once.
        unsafe { widget_close(self.0.as_ptr()) };
    }
}

Head to the exercise

The exercise has both directions, each with a twist the examples above don’t have. First, expose a Rust Stats type to C as a handle. Unlike the counter, it owns a Vec of its own, which has to be freed along with it. Then wrap a C Buffer in a Rust type that frees it exactly once.

Exercise

The exercise for this section is located in 02_basics/06_opaque_types