Generating and linking from C against a Rust library on Windows
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:
- Provide the pathg to xilib.h which was generated by cbindgen
- 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.