writing c library in rust

January 16, 2020

I want to write a library in Rust that can be called from C and just as easily called from Rust code. The tooling makes it pretty easy, but I had to look in a few places to figure how it is supposed to work and get tests running in both languages.

C library

To focus on the process of building and testing, the library will have a single function that adds two numbers. I wrote it in pure C first:

lib.c

int add(int a, int b) {
  return a + b;
}

lib.h

int add(int a, int b);

main.c

#include <stdio.h>
#include "lib.h"

int main() {
    int sum = add(1,2);
    printf("1 + 2 = %d\n", sum);
}

one-liner to compile and run the app

gcc *.c -o app && ./app

output: 1 + 2 = 3

then I wrote a simple automated test, based on tdd blog post

Rust library

cargo new add --lib

replace lib.rs with

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


#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        use crate::add;
        assert_eq!(add(2,2), 4);
    }
}

build and run with cargo test
which should have output like

$ cargo test
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running target/debug/deps/add-45abb08ccefdc53c

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests add

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Compile as static library

Add to Cargo.toml

[lib]
name = "add"
crate-type = ["staticlib"]  

Now cargo build generates a compiled library file: target/debug/libadd.a. I could have stopped there, but I expect to iterate on this a bit and I had read about a crate that generates the C header file…

Generate a header (command-line)

First, install the lovely cbindgen crate (using –force just to make sure everything is up to date with the latest):

cargo install --force cbindgen

the command-line tool is pretty neat:

touch cbindgen.toml    # can be empty for defaults
cbindgen --config cbindgen.toml --crate add --output add.h

The above command will generate “add.h” file at the root of the crate.

Generate a header (cargo build)

I prefer to have the header generation integrated with cargo build (or at least I think I will). Here are the steps:

Add to Cargo.toml:

[build-dependencies]
cbindgen = "0.12"

By default, the header file is buried in target/debug/build/add-... with a bunch of intermediary build files. I find that it is nice to put it at the root of my crate where it is easy to find. Below is a custom build file that puts it in the crate root (aka CARGO_MANIFEST_DIR, the directory that Cargo.toml is in).

build.rs:

extern crate cbindgen;

use std::env;
use std::path::PathBuf;


fn main() {
  let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")
        .expect("CARGO_MANIFEST_DIR env var is not defined"));

  let config = cbindgen::Config::from_file("cbindgen.toml")
        .expect("Unable to find cbindgen.toml configuration file");

  cbindgen::generate_with_config(&crate_dir, config)
        .expect("Unable to generate bindings")
        .write_to_file(crate_dir.join("add.h"));
}

As mentioned above, cbindgen.toml may be empty, but here’s some settings I like:

include_guard = "add_h"
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
language = "C"
includes = []
sys_includes = ["stdint.h"]
no_includes = true   

Confusingly no_includes means no extra includes. I prefer to have only the ones that I know are needed, rather than some random list of “common” headers.

Here’s my generated header:

#ifndef add_h
#define add_h

/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */

#include <stdint.h>

int32_t add(int32_t a, int32_t b);

#endif /* add_h */

Putting it all together

Example main.c in the root of the crate:

#include <stdio.h>
#include "add.h"

int main() {
    int sum = add(1,2);
    printf("1 + 2 = %d\n", sum);
}

compile and run:

gcc main.c add/target/debug/libadd.a -o app && ./app

outputs: 1 + 2 = 3

This make me unreasonably happy. Rust syntax can be a bit finicky and certainly takes a bit of getting used, but this kind of tooling could more than make up for that in accelerating the dev cycle.

For the full applications with a mini test suite in C, see github/ultrasaurus/rust-clib — the v0.1 branch is from when this blog post was written.