Rust FFI: Wrapping C API in Rust struct


The Chapter on FFI in Rust Book describes the basics of interfacing with native C libararies in Rust. In this tutorial I’ll have a look at how to create higher-level abstractions above such APIs.

C library APIs are very often designed as a collection of functions that operate on an opaque pointer to a struct.

A very simple example of such an API might be:

struct foo;

int foo_create(struct foo** result);
void foo_bar(struct foo* f, int param);
void foo_destroy(struct foo* f);

where foo_create() is a constructor, foo_bar() is a method operating on foo and foo_destroy() is a destructor.

This is what a low-level Rust interface for this API would probably look like:

pub type foo = *mut c_void;

#[link(name = "foo")]
extern "C"
  pub fn foo_create(result: *mut foo) -> c_int;
  pub fn foo_bar(f: foo, param: c_int);
  pub fn foo_destroy(f: foo);

An interface like this may even be auto-generated using the rust-bindgen tool. The files it generates are sometimes a little rough around the edges but it mostly gets the job done pretty well.

However, using this API in ordinary Rust code isn’t exactly going to be a pinnacle of comfort – you’d have to pass around that raw pointer and the whole thing wouldn’t really fit well with Rust’s ownership model.

High-level API

Having foo as a Rust struct and an impl to go with would be much nicer. There’s a couple of gotchas along the way, though.

First question is what type to hold inside the struct. You might be tempted to do something like this:

use ffi;  // Here the low-level API is defined as described above

struct Foo
  foo: ffi::foo;

…simply containing the raw pointer inside the struct. This works, however, when attempting to use the thing in threaded code, you’ll run into an error like this:

error: the trait `core::marker::Send` is not implemented for the type `*mut c_void`
note: `*mut c_void` cannot be sent between threads safely

As of writing this post and as of Rust 1.0, there’s no way to implement Send for anyhting, since it is a built-in trait. The compiler decides what is or is not Send automagically.

The solution is to use Unique<T> instead.

Unique<T> is basically a wrapper around a raw pointer that is Send and Sync as long as the type it points to (T) is Send and Sync.

And as it happens, c_void is Send and Sync. Therefore, this is our struct using Unique<T>:

use ffi;  // Here the low-level API is defined as described above

struct Foo
  foo: Unique<c_void>

NOTE: We assume here that the underlying C type can actually be moved among threads. There are cases where this might not really be true. You always need to check this with the C library’s documentation.


Writing the constructor is a little tricky, since the low-level API function requires a pointer to pointer to store the result at, so you need to have an uninitialized pointer first. Here’s how to do it:

pub fn new() -> Result<Foo, i32>
    // An uninitialized raw pointer:
    let mut foo: ffi::foo = mem::uninitialized();
    // Low-level constructor, pass a pointer to our pointer:
    let ret = ffi::foo_create(&mut foo);
    // Suppose the constructor returns 0 on succes and non-zero result on error:
    match ret
      // A new instance of Unique is created here
      //  handing over the ownership of the raw pointer to it:
      0 => Ok(Foo { foo: Unique::new(foo) }),
      e => Err(e as i32),

This goes in the impl Foo {} block, obviously.

Member functions

These are usually fairly straight-forward:

pub fn bar(&mut self, param: i32)
  // Note that the Unique pointer is dereferenced to yield the raw pointer:
  unsafe { ffi::foo_bar(*, param as c_int); }

Goes in the impl Foo {} block as well.


Foo needs to implement the Drop trait to properly call foo_destroy() when its lifetime ends:

impl Drop for Foo
  fn drop(&mut self)
    unsafe { ffi::foo_destroy(*; }


We can now use Foo as if it were implemented in Rust :-)

  let foo = Foo::new().unwrap();;
// Foo has gone out of scope now and the foo_destroy() function has been called internally

It also won’t cause errors when moved into another thread.