Bridging Rust to Swift, pt. 2: cbindgen

We concluded our previous post with building a C compatible library which could be accessed from C/Swift - with the caveat that we needed to provide the appropriate header files.

Hold on (for one more day)?

Headers? Our rust code doesn’t have any C style header files to describe its public API. Nor does rust need them. But, in order to access the library from a C environment, we are going to have to provide one. This header file should contain the interface that is represented by all the exported functions in our rust code. While we could write one manually, it’s much easier to have one generated for us.

Enter cbindgen

cbindgen generates C/C++11 headers for Rust libraries that expose a public C API. We had tagged these by prefixing pub unsafe extern "C" to their Rust definitions. Complete documentation tocbindgen can be found here. A short primer on using it follows.

Installing cbindgen is as simple as running cargo install --force cbindgen. The force argument simply updates it to the latest version if it’s already installed. Once installed, we can either run it during our build process or as a standalone program.

Run as standalone program

Let’s download the template config .toml file from the cbindgen code respository. For our current project, most of the defaults should be fine. Let’s switch the language for the generated header file to C from C++.

Run cbindgen --output swift_bridged.h from the root of our rust project. If all goes well, we should have a nice header file generated with the following content.

#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>


uint32_t bodevAddIntegers(uint32_t a, uint32_t b);

Run with build script

We could alternatively have the C header file generated automatically when we build our Rust project. We can use a cbindgen.toml file, like the one above, to configure cbindgen with a build script, like the one below. Alternatively, it could be configured using a Builder interface.

extern crate cbindgen;

use std::env;

fn main() {
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

    cbindgen::generate(crate_dir)
        .expect("Unable to generate bindings")
        .write_to_file("swift_bridged.h");
}

Finally, we need to add cbindgen as a build dependency via cargo add --build cbindgen. This adds the latest version of cbindgen as a build dependency on our rust project. Run cargo build, and we should have an identical header file to one generated by the standalone program.

Choosing between the standalone program or build script is largely a matter of choice on how we want to distribute the header file. In the next post, we will look at building binary frameworks to make accessing the C library easier.