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.