FFI

8 Posts

build.rs, link, and pkg-config

5 minute

In FFI, declaring the C function is only the first step:

unsafe extern "C" {
    fn sensor_read(out: *mut u8, len: usize) -> i32;
}

This only tells Rust that a C ABI function named sensor_read exists.

The linker still needs to know:

which library contains this function?
where is the library file?
what other dependent libraries are needed?
during cross compilation, are these target libraries or host libraries?

C projects usually handle this with Makefiles, CMake, pkg-config, sysroots, and cross toolchains. Rust projects use #[link], build.rs, and build dependencies for the same job.

Read More

Static Libraries, Dynamic Libraries, and ABI

6 minute

The previous FFI articles focused on function-level boundaries: extern "C", strings, memory ownership, callbacks, and void *ctx.

When delivering Rust to a C project, there is another layer:

What file does the C project link?
What symbols are declared in the header?
Can those symbols keep a stable ABI?
After Rust internals change, does C need to change?

C developers know .a, .so, .h, and ABI. Rust delivered to C should return to those concepts instead of exposing Cargo internals.

Read More

FFI Callbacks and void *ctx

5 minute

C callback APIs usually look like this:

typedef void (*event_cb_t)(void *ctx, int event);

int device_register_callback(struct device *dev, event_cb_t cb, void *ctx);
void device_unregister_callback(struct device *dev);

The function pointer is the code to run. ctx carries the caller’s environment.

Inside Rust, closures express “code plus environment.” But across a C ABI, a capturing closure cannot directly become a C function pointer. C understands a fixed-ABI function address and a void *ctx.

So FFI callbacks are mainly a lifetime and ownership problem:

Where is the callback object stored?
If C stores ctx, is the Rust owner still alive?
Which thread invokes the callback?
Can C call after unregister?
Can destruction race with a running callback?

C Needs an extern Function, Not a Capturing Closure

A Rust function that captures no environment can be used as a C callback:

Read More

C Strings, Memory Ownership, and Error Returns

7 minute

In FFI, extern "C" is only the entry point.

The most fragile parts are usually:

whether a C string is a valid NUL-terminated string
who allocates returned pointers, who frees them, and which function frees them
whether errors cross the boundary as integer codes, output parameters, or text

C code handles these through API documentation and discipline. Rust can put some rules into types, but the language boundary still needs an explicit contract.

Read More

FFI: Letting Rust Call C

6 minute

Calling C from Rust can look like just writing an extern "C" declaration.

The real issue is different: a C function does not carry Rust’s borrowing, lifetime, Result, or destructor rules. Rust can call it, but the compiler cannot prove that the call satisfies the C API contract.

A better model is:

safe Rust API
-> validate and prepare arguments
-> call C inside a small unsafe block
-> convert error codes, output parameters, and raw pointers back to Rust types

The C header is the entry point

Suppose an existing C library exposes this header:

Read More

FFI: Letting C Call Rust

5 minute

Letting C call Rust does not mean exposing ordinary Rust functions directly to C.

C understands C ABI: symbol names, calling conventions, integers, pointers, struct layout, memory ownership, release functions, and error returns. Rust internals such as String, Vec, Result, traits, and panic should not cross the C boundary directly.

A stable model is:

C call
-> extern "C" wrapper
-> validate pointer and length
-> convert to safe Rust types
-> call internal Rust logic
-> convert result to C-compatible return values

This article covers C calling Rust. Rust calling C comes next.

Read More

unsafe: Keep C Risks at the Boundary

4 minute

C developers may first read unsafe as “the place where Rust lets me write C.”

That is too broad.

unsafe does not disable all Rust checks, and it does not turn a block back into C. It permits a small set of operations the compiler cannot prove safe, such as dereferencing raw pointers, calling FFI functions, and accessing mutable global state.

The key question is not whether unsafe can be written. It is how small the unsafe boundary can be.

Read More

Strings Are Not char Pointers

5 minute

C char * is powerful and vague.

It may mean mutable bytes, a NUL-terminated string, ASCII text, or simply the argument type required by a C API. The caller also has to know who frees the memory, whether it contains \0, what encoding it uses, and where the length comes from.

Rust does not put all of that into one type. It splits the common cases:

one byte: u8
bytes: &[u8] / Vec<u8>
UTF-8 text view: &str
owned UTF-8 text: String
C string view: &CStr
owned C string: CString

This article builds the first boundary: bytes, text, and C strings are different things.

Read More