Generating and linking from C against a Rust library on Windows

October 21, 2018 - 4 minute read -

I started working on xi-lib, which aims to provide a simple C interface to xi-editor. My initial research was focused on building a C library in Rust.

The Rust FFI

Rust offers a powerful FFI interface. We can easily use extern "C" fn to create functions that can be called from C. A simple example:

#[no_mangle]
pub extern "C" fn test() {
	println!("hello world");
}

Using #[no_mangle] ensures that the compiler does no additional name-mangling, allowing us to call this function from C, while extern "C" ensures that the function gets exported. There are a few other primitives that help writing FFI interfaces for C, which can be found in the documentation.

Generating header files

C/C++ code uses header files to define interfaces. In order to provide an FFI interface for C, we must provide a suitable headerfile cbindgen to the rescue! Cbindgen can create a header file for you, based on your extern "C" interfaces. There are two ways of using cbindgen. I personally prefer to always generate a header file during a build, so we will use a build.rs to call cbindgen during a build.

In our Cargo.toml add our build-dependency:

[build-dependencies]
cbindgen = "0.6.6"

Add a build.rs:

extern crate cbindgen;

use std::env;
use std::path::Path;
use cbindgen::Config;

fn main() {
    let crate_env = env::var("CARGO_MANIFEST_DIR").unwrap();
    let crate_path = Path::new(&crate_env);
    let config = Config::from_root_or_default(crate_path);
    cbindgen::Builder::new()
      .with_crate(crate_path.to_str().unwrap())
      .with_config(config)
      .generate()
      .expect("Unable to generate bindings")
      .write_to_file("headers/xilib.h");
}

We nwo generate a new header file during build. Note how we are configuring cbindgen to use a configuration file.

cbindgen configuration

cbindgen comes with a good set of defaults. However in our case, those were not alway sufficient. For once, cbindgen defaults to build C++ bindings, while we want C bindings. In addition it tries to parse dependent libraries for types, in case they are needed. So far I don’t want or need this, as I am trying to build a very clear abstraction that does not accidentally leak an internal type in the header files. So we will disable it.

Our cbindgen.toml now looks like:

language = "C"
include_version = true

[parse]
parse_deps = false

Let’s do a simple run:

$ cargo build

We now have a proper C library with a header file. But how can we use it?

Generating DLLs

At this point we need to generate a library that contians our Rust code in an ingestible format for our C program. Rust builds rlibs, a binary format of a library that is targeting other rust code. C/C++ linkers do not support rlibs, so we need to go and build a dynmic or a static library. In my case I want a dynamic library.

We have to modify our Cargo.toml again. We are adding the configuration crate-type indication that we want to build a dynamic library for C by using the argument cdylib. On MacOS this will produce a .dynlib, on Linux an .so and on Windows a .dll.

[lib]
name = "xilib"
crate-type = ["cdylib"]

Building now results in a xilib.dll in target/debug. Now off to the final step, linking!

The C program

Let’s create a siple C program, main.c:

#include <xilib.h>
int main() {
    foo();
    return 0;
}

In order to link it, we must:

  1. Provide the pathg to xilib.h which was generated by cbindgen
  2. Provide the definition of the library interface in form of a *.lib file.

For 2., cargo alreayd produced us an additional xilib.dll.lib file. So let’s link it all together:

$ cl.exe /Iheaders/ target/debug/xilib.dll.lib main.c

We now can start our newly created main.exe. We must ensure that xilib.dll is in the same directory, and voila:

$ main.exe
hello world

Success.