Getting Started

This chapter wires ezffi into a fresh crate end to end — your first exported function and the C program that calls it. By the end you'll have a working Counter library callable from C.

Dependencies

ezffi ships the proc macro and some Rust types ready to be export-compatible; cbindgen reads the macro-generated extern "C" items and produces the C header. If you wanna use any other header generator feel free, but cbindgen is the one I'm using in my tests until I write my own solution if I see it's needed.

[package]
name = "counter"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["lib", "staticlib"] # or `cdylib`, it depends on your use case

[dependencies]
ezffi = "0.1"

[build-dependencies]
cbindgen = "0.29"

The library

src/lib.rs:

#![allow(unused)]
fn main() {
#[ezffi::export]
pub struct Counter { value: u64 }

#[ezffi::export]
impl Counter {
    pub fn new() -> Self { Self { value: 0 } }
    pub fn increment(&mut self) { self.value += 1; }
    pub fn value(&self) -> u64 { self.value }
}
}

Wiring cbindgen

The header generation is a really important step. ezffi generates its headers in target/<profile>/include/ezffi so it's really important to generate yours in the same include dir, so we end up with the following structure:

include
├── ezffi
│   ├── ezffi.h
│   ├── slice.h
│   ├── ...
│   └── string.h
└── counter
    └── counter.h

The recommended build.rs using cbindgen is the following:

build.rs:

use std::env;
use std::path::Path;

fn main() {
    // cbindgen cannot expand macros in stable builds
    // unless you set this env var
    unsafe { std::env::set_var("RUSTC_BOOTSTRAP", "counter") };
    
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let out_dir   = env::var("OUT_DIR").unwrap();
    let pkg_name  = env::var("CARGO_PKG_NAME").unwrap();

    println!("cargo:rerun-if-changed=src/");
    println!("cargo:rerun-if-changed=cbindgen.toml");

    // `OUT_DIR` is `target/<profile>/build/<crate>-<hash>/out`
    let target_dir = Path::new(&out_dir)
        .ancestors()
        .nth(3)
        .expect("Failed to find target dir");
    let include_dir = target_dir.join("include").join(&pkg_name);
    std::fs::create_dir_all(&include_dir).unwrap();

    let config = cbindgen::Config::from_file(Path::new(&crate_dir)
        .join("cbindgen.toml"))
        .expect("Failed to read cbindgen.toml");

    cbindgen::Builder::new()
        .with_crate(&crate_dir)
        .with_config(config)
        .generate()
        .expect("Unable to generate bindings")
        .write_to_file(include_dir.join(format!("{pkg_name}.h")));
}

cbindgen.toml:

language = "C"
# Add here the relative .h path of any dependency you are
# using from `ezffi` or from other lib crates that expose
# an `ezffi` interface (e.g.: ezffi/string.h).
# In this case we don't use any of the `ezffi` exposed types
# so it is empty.
include = []

[parse.expand]
crates = ["counter"]

The generated header

After cargo build, target/debug/include/counter/counter.h looks roughly like:

#include <stdint.h>

typedef struct CounterCounter { uint64_t value; } CounterCounter;

CounterCounter counter_counter_new(void);
void           counter_counter_increment(CounterCounter *this_);
uint64_t       counter_counter_value(const CounterCounter *this_);

I talk more about why this is the header output, how it works internally and the rules I apply when creating C-compatible wrappers (and teach you what to expect from your code) in chapter #[ezffi::export].

The C side

main.c:

#include "counter/counter.h"
#include <stdio.h>

int main(void) {
    CounterCounter c = counter_counter_new();
    
    counter_counter_increment(&c);
    counter_counter_increment(&c);
    counter_counter_increment(&c);

    printf("%llu\n", (unsigned long long)counter_counter_value(&c));
    return 0;
}

Compile and run:

cargo build --release
gcc  main.c \
    -I target/release/include \
    -L target/release \
    -l counter \
    -o counter_demo
./counter_demo
# 3

Depending on your binary you may also need other libraries to be linked like -lpthread -ldl -lm, otherwise you'll get a compile-time error warning you of missing symbols (e.g. pow if you don't link m but some dependency uses it).

And that's it — you got your first C-compatible library and a bin consumer without having to worry about any implementation details.