ffizz: Build a Beautiful C API in Rust
Tags: rust, ffi, api
Published: 2023-06-20 01:56:24

Foreign Function Interface, FFI, is an umbrella term for interfacing between programming languages. Most languages support a way to interface with C: C-style function calls, C-compatible memory layouts for data types, and so on. Interfacing two languages that are not C -- for example, Python to Rust -- typically involves gluing both languages together with some C code.

The choice of C as the _lingua franca_ for communication between modern programming languages is, I think, one of the great tragedies of the history of computing.

## Rust FFI Today

Rust supports two kinds of FFI: calling into Rust from another language; and calling into another language from Rust. Most of the thought and tooling that exists right now is organized around the second kind. For example, [bindgen](https://rust-lang.github.io/rust-bindgen/) is a popular tool that generates useful Rust wrappers from a C or C++ header file.

The tooling for the first kind -- calling Rust from another language -- is a bit less developed, and tends to rely on code generation that doesn't necessarily produce a natural C API. [`cbindgen`](https://github.com/mozilla/cbindgen/blob/master/docs.md), [`uniffi`](https://mozilla.github.io/uniffi-rs/), [`cxx`](https://cxx.rs/), and [Diplomat](https://rust-diplomat.github.io/book/) all take this course.

## Natural C APIs

It gets a bad reputation, but C can actually be a pleasure to write, when using a nicely designed API. For example, [`libcurl`](https://curl.se/libcurl/c/) provides a C API to support making HTTP requests from C. It's carefully and thoughtfully designed to minimize surprises and make correct usage easy. See, for example, [`curl_slist_append`](https://curl.se/libcurl/c/curl_slist_append.html), a succinct, efficient tool for creating lists of strings to pass to the API.

I don't know of any authoritative document, but in my experience good C APIs have a few properties:

* Allocate and free functions, for each type. In `libcurl`, these are [`curl_easy_init`](https://curl.se/libcurl/c/curl_easy_init.html) and [`curl_easy_cleanup`](https://curl.se/libcurl/c/curl_easy_cleanup.html). A C programmer will know that they must allocate a new object before using it, and that once that object is freed, it cannot be used again.
* An "owner" for every allocated object, responsible for freeing it and making sure it isn't freed while still in use. Ownership semantics are usually described in comments.
* Integer return values with negative numbers signaling an error and zero or positive values indicating success.
* Selective use of "output parameters" to support functions that have multiple results. For example, `int query_execute(query_t *query, rowset_t **rowset_out)` probably returns a negative error or positive number of rows matching the query, and writes a pointer to a newly allocated `rowset_t` in `*rowset_out`.
* Clear documentation of thread safety: what functions can be called concurrently, and what data structures can be accessed from multiple threads.

In general, the Rust FFI tools mentioned above do not generate a very natural C API. At best, they generate a C interface to the Rust API, with the expectation that the C developer will understand both the Rust API and how it is represented in C.

## Going it Alone

The alternative is to forget about the tools and create the perfect API by hand. This is not easy!

You'll need to write a C header file, complete with types, function declarations, and extensive documentation comments.

Then you'll need to implement those functions in Rust with `extern "C"`, and write Rust `struct` definitions to match the C types. Careful: nothing verifies that the header declarations and Rust function and type signatures match, and type layouts differ across architectures.

You'll also need to ensure that the C API does not create undefined behavior for the Rust code. All of those `extern "C"` functions are `unsafe`, after all. This usually involves writing clear but concise instructions in the header, and then convincing yourself that any C code satisfying those instructions maintains the invariants of the Rust code.

I set out on this course with [taskchampion-lib](https://github.com/GothenburgBitFactory/taskwarrior/tree/develop/taskchampion/lib) about three years ago. It quickly became clear that some tooling would help. 

## Ffizz

Thus was born [_ffizz_](https://docs.rs/ffizz/latest/ffizz/). This is a collection of tools for building natural C APIs in Rust.

The simplest is [`ffizz-header`](https://docs.rs/ffizz-header/latest/ffizz_header/), which supports building a header file from doc comments in the Rust source. While it's still up to the API designer to ensure that the C and Rust function declarations match, that's much easier when they are just a few lines apart in the same source file.

Strings are a very common data type, and Rust and C handle them differently, so passing strings back and forth can be a lot of work and is an easy place to introduce bugs.
The [`ffizz-string`](https://docs.rs/ffizz-string/latest/ffizz_string/) crate supplies a `FzString` type that manages conversion between types on demand, eliminating a lot of boilerplate and providing simple safety requirements.

Finally, [`ffizz-passby`](https://docs.rs/ffizz-passby/latest/ffizz_passby/) provides utility types to handle common methods of passing data across C API boundaries:

* Pass-by-value: values are copied as necessary when passed to or returned from functions. This is typically used for Rust types that implement Copy.
* Boxed objects: values that are allocated and freed and always referenced by a pointer. In Rust, these use `Box<T>`, while C uses a raw pointer.
* Unboxed objects: values that can be placed on the stack or in another struct. This storage location must be initialized before it is used, and is typically passed to Rust functions as a raw pointer. When the caller is finished with the value, it must be released, leaving the storage uninitialized again.

As an example, `FzString` is an unboxed object: it is up to the C caller to allocate enough space to store the value (which may contain a pointer and two 64-bit integers), initialize the space, and finally call a function like `fz_string_free` to free the associated memory when the value is no longer needed.

## Give It a Try!

If you've worked on this sort of interface before, please give ffizz a try, even if just experimentally. I've tried to solve general problems and not just what I needed for Taskchampion, but surely my imagination has come up short somehow. Let me know! Leave an issue on GitHub, or ping me on Mastodon at `@djmitche@mastodon.social`.