Rust FFI: Wrapping C API in Rust struct
14.6.2015The 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.
Constructor
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>
{
unsafe
{
// 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(*self.foo, param as c_int); }
}
Goes in the impl Foo {}
block as well.
Destructor
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(*self.foo); }
}
}
Conclusion
We can now use Foo
as if it were implemented in Rust :-)
{
let foo = Foo::new().unwrap();
foo.bar(3);
}
// 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.