DEV Community

Cover image for ffizz: Build a Beautiful C API in Rust
djmitche
djmitche

Posted on

ffizz: Build a Beautiful C API in Rust

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 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, uniffi, cxx, and Diplomat 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 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, 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 and curl_easy_cleanup. 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 about three years ago. It quickly became clear that some tooling would help.

Ffizz

Thus was born ffizz. This is a collection of tools for building natural C APIs in Rust.

The simplest is 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 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 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.

Top comments (2)

Collapse
 
sbalasa profile image
Santhosh Balasa

Rust library code:

// src/lib.rs

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn subtract(a: i32, b: i32) -> i32 {
    a - b
}
Enter fullscreen mode Exit fullscreen mode

For C code:

// main.c
#include <stdio.h>
#include <stdint.h>

int32_t add(int32_t a, int32_t b);
int32_t subtract(int32_t a, int32_t b);

int main() {
    int32_t result = add(5, 3);
    printf("Addition: %d\n", result);

    result = subtract(10, 4);
    printf("Subtraction: %d\n", result);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
djmitche profile image
djmitche

This is the typical example of FFI, but of course nobody's writing "libaddition-rs" and real APIs are much more complex. And, we don't expect library users to copy/paste C declarations into their source files! So, ffizz solves the next level of problems: how do I safely maintain a natural C API for my (real) Rust library?