Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Primitive types in FFI

So far, the exercises have passed data between C and Rust mostly as pointers and plain integers. The following sections cover how other types cross the FFI boundary, starting with primitive types, then structs, enums and unions, slices, and opaque types.

A C header describes its parameters in C’s own types: int, long, char, size_t. Rust has i32, i64, u8, usize. Writing a binding means pairing them up, and most of those pairs work on every platform you’re likely to target.

Take this function:

int scale(int value, int factor);

On Windows, macOS, and Linux, on x86-64 and aarch64, an int is the same as an i32 in Rust. If you only target those platforms, the Rust code looks like this:

unsafe extern "C" {
    fn scale(value: i32, factor: i32) -> i32;
}

The mapping

Here is how the common primitive types map from C to Rust when targeting Windows, macOS, or Linux on x86-64 or aarch64:

C typeRust typeAlias
chari8 (or u8 on aarch64 Linux)c_char
signed chari8c_schar
unsigned charu8c_uchar
shorti16c_short
unsigned shortu16c_ushort
inti32c_int
unsigned intu32c_uint
longi64 (or i32 on Windows)c_long
unsigned longu64 (or u32 on Windows)c_ulong
long longi64c_longlong
unsigned long longu64c_ulonglong
floatf32c_float
doublef64c_double
size_tusize–
ptrdiff_tisize–
int32_t, uint64_t, …i32, u64, …–
void **mut c_void–
T **mut T–
const T **const T–

The names in the last column are Rust type aliases for each C type. Each alias stands for whichever Rust type matches that C type on the platform you compile for.

They are defined in core::ffi, and std::ffi re-exports them, so std::ffi::c_int and core::ffi::c_int are the same type. You’ll also see std::os::raw::c_int in bindgen output and libc::c_int from the libc crate. All of them resolve to the same alias.

The aliases are for code that targets platforms where a fixed-width Rust type doesn’t match. On MSP430 or AVR, for example, C’s int is 16 bits, so c_int is i16. If your code has to build there as well as on x86-64, where c_int is an i32, the alias is the right option.

c_char and u8

A C char is a byte, and Rust’s byte type is u8, so writing *const u8 in a binding where the header says const char * looks harmless, and it’s the most common mistake in hand-written bindings: c_char is not always u8. It is i8 on x86-64, and on macOS and Windows for aarch64. It is u8 on aarch64 Linux, on 32-bit ARM, and on RISC-V.

The u8 binding might just work on your own machine, which is what makes this easy to miss. Then someone builds for a target where c_char is an i8 and the code stops compiling. Anything that hands you a c_char no longer lines up with the u8 you wrote: CStr::as_ptr gives a *const c_char, and passing it where a *const u8 is expected is now a type mismatch. So write c_char in the signature.

The second issue is that c_char is not the type Rust’s byte methods work on. b'a' literals, u8::to_ascii_lowercase, str::as_bytes, and CStr::to_bytes all speak u8. So cast a c_char to u8 as soon as you have the value, do the work there, and cast back on the way out:

use std::ffi::c_char;

/// Lowercases `byte` if it is an ASCII letter, and returns it unchanged
/// otherwise.
#[unsafe(no_mangle)]
pub extern "C" fn lower_byte(byte: c_char) -> c_char {
    (byte as u8).to_ascii_lowercase() as c_char
}

Two details about those casts are worth naming:

  • They change the type, not the value. as between c_char and u8 keeps the bit pattern exactly, and casting back gives you the same byte again.
  • They are allowed because the layouts match. c_char and u8 have the same size and the same alignment on every target, and every bit pattern is valid for both. That makes the cast purely a compile time check.

Head to the exercise

The exercise gives you a C library over bytes with its bindings already written, and two small wrappers to finish.

Exercise

The exercise for this section is located in 02_basics/02_primitives