# FFI Boundaries

Calling C from Rust and exposing Rust to C: declarations, layouts, strings, null handling, ownership,
callbacks, panics and threads. Most real-world unsafe Rust lives here, and most FFI bugs are contract
mismatches the compiler cannot see.

## Contents

- [Boundary checklist](#boundary-checklist)
- [Declaring foreign functions](#declaring-foreign-functions)
- [Types that cross the boundary](#types-that-cross-the-boundary)
- [Opaque handles](#opaque-handles)
- [Strings: CString and CStr](#strings-cstring-and-cstr)
- [Slices and buffers](#slices-and-buffers)
- [Ownership: who frees what](#ownership-who-frees-what)
- [Exporting Rust to C](#exporting-rust-to-c)
- [Callbacks and user data](#callbacks-and-user-data)
- [Panics and unwinding](#panics-and-unwinding)
- [Threads, errno and global state](#threads-errno-and-global-state)
- [Tools](#tools)
- [Testing FFI code](#testing-ffi-code)

## Boundary checklist

For every foreign function or exported function:

- [ ] Signature matches the C header exactly (types, pointer constness, return type, variadics, calling
      convention). Generated by bindgen, or checked against the header.
- [ ] Every pointer coming from C is checked for null (or the contract says it cannot be null and that is
      documented in `# Safety`).
- [ ] Every pointer's lifetime is known: borrowed for the call, owned by the handle, or transferred.
- [ ] Every allocation is freed by the allocator that created it.
- [ ] No Rust panic can unwind into C, and no C++ exception or `longjmp` can cross a frame not declared
      for it.
- [ ] Enums/bools from C are validated before becoming Rust enums/bools.
- [ ] Thread-safety of the C library is reflected in `Send`/`Sync` of the Rust wrapper.
- [ ] The raw bindings are private (`mod sys`), and the public API is a safe wrapper.

## Declaring foreign functions

Edition 2024 requires `unsafe extern` (accepted since 1.82 in every edition). The `unsafe` asserts that
the declarations are correct; individual items may be marked `safe` when calling them can never cause UB.

```rust
use std::ffi::{c_char, c_int, c_void};

#[link(name = "z")]
unsafe extern "C" {
    pub safe fn zlibVersion() -> *const c_char; // no preconditions: returns a static string
    pub fn crc32(crc: std::ffi::c_ulong, buf: *const u8, len: std::ffi::c_uint) -> std::ffi::c_ulong;
}

unsafe extern "C" {
    fn qsort(
        base: *mut c_void,
        n: usize,
        size: usize,
        cmp: Option<unsafe extern "C" fn(*const c_void, *const c_void) -> c_int>,
    );
}
```

- Items without a qualifier are `unsafe` to call. Mark `safe` only if every possible argument value is
  fine (no pointers, no preconditions on integers).
- `std::ffi::c_int`, `c_char`, `c_long`, `c_ulong`, `c_void` etc. are in `core::ffi` since 1.64; `c_long`
  differs between Linux (64-bit) and Windows (32-bit), so never hard-code `i64`.
- `size_t` maps to `usize`; `ssize_t`/`ptrdiff_t` to `isize`.
- Variadic C functions (`printf`) can be declared with `...` and called, but Rust cannot define them on
  stable.
- The `improper_ctypes` lint warns when a declaration uses a type with no C layout (e.g. `String`, `&str`,
  a non-`repr(C)` struct); `improper_ctypes_definitions` does the same for exported functions.

## Types that cross the boundary

| Rust type | C type | Notes |
|-----------|--------|-------|
| `#[repr(C)] struct` | `struct` | field order and padding follow C rules; `repr(Rust)` layout is unspecified |
| `#[repr(C)]` / `#[repr(u8)]` fieldless enum | `enum` | **only** for values Rust produces; receiving an unknown value from C is UB |
| integer newtype `#[repr(transparent)] struct Mode(c_int)` | `enum` / flags from C | validate with `TryFrom` or keep as integer |
| `bool` | `_Bool` / `bool` | same ABI as C `_Bool`; NOT the same as `int` flags: receive `c_int` and compare with 0 |
| `Option<&T>`, `Option<&mut T>`, `Option<NonNull<T>>`, `Option<Box<T>>` | `T*` (nullable) | guaranteed null-pointer optimisation |
| `Option<extern "C" fn(..)>` | nullable function pointer | a bare `extern "C" fn` must never be null |
| `*const c_char` | `const char*` | convert with `CStr` |
| `&[T]` | none | pass `ptr` + `len` separately |
| `char` | `uint32_t` | C `char` is `c_char`; Rust `char` must be a valid scalar value |
| `()` return | `void` | |

```rust
use std::ffi::c_int;

#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RawStatus(c_int); // whatever C sends is a valid c_int

#[derive(Debug)]
pub enum Status { Ok, Busy, Failed(c_int) }

impl From<RawStatus> for Status {
    fn from(s: RawStatus) -> Self {
        match s.0 {
            0 => Status::Ok,
            1 => Status::Busy,
            other => Status::Failed(other),
        }
    }
}
```

## Opaque handles

For C structs Rust never looks inside, use a zero-sized opaque type so Rust code cannot construct, move or
dereference one (the pattern bindgen generates and the Rustonomicon recommends; `extern type` is unstable):

```rust
use std::marker::{PhantomData, PhantomPinned};

#[repr(C)]
pub struct sqlite3 {
    _data: (),
    _marker: PhantomData<(*mut u8, PhantomPinned)>, // !Send, !Sync, !Unpin
}
```

The safe wrapper owns a `NonNull<sqlite3>`, frees it in `Drop`, and decides `Send`/`Sync` from the
library's threading documentation.

## Strings: CString and CStr

| Direction | Type | Pitfall |
|-----------|------|---------|
| Rust -> C, borrowed | `CString::new(s)?` then `c.as_ptr()` | the `CString` must outlive every use of the pointer (US010); interior NUL returns `NulError` |
| literal | `c"text"` (1.77) -> `&'static CStr` | no allocation, NUL included |
| C -> Rust, borrowed | `unsafe { CStr::from_ptr(p) }` | needs non-null, NUL-terminated, unchanged for the returned lifetime, which is unbounded: bind it to a real lifetime |
| C -> Rust, bytes | `CStr::from_bytes_with_nul(b)?` / `from_bytes_until_nul` (1.69) | safe, checked |
| to `&str` | `c.to_str()?` or `to_string_lossy()` | C strings are not guaranteed UTF-8 |
| Rust -> C, ownership transferred | `CString::into_raw()` | C must hand it back to a Rust free function calling `CString::from_raw` exactly once |

```rust
use std::ffi::{CStr, c_char};

/// Borrow a C string for as long as `_owner` is borrowed.
///
/// # Safety
///
/// `p` must be non-null, NUL-terminated, and not modified or freed while `_owner` is borrowed.
pub unsafe fn borrow_c_str<'a, O: ?Sized>(_owner: &'a O, p: *const c_char) -> &'a CStr {
    // SAFETY: forwarded from the contract; the lifetime is tied to `_owner` instead of being unbounded.
    unsafe { CStr::from_ptr(p) }
}
```

## Slices and buffers

```rust
use std::ffi::c_int;

unsafe extern "C" {
    /// Fills `out` with up to `cap` bytes and returns how many were written, or -1 on error.
    fn read_frame(out: *mut u8, cap: usize) -> c_int;
}

pub fn next_frame(cap: usize) -> Option<Vec<u8>> {
    let mut buf: Vec<u8> = Vec::with_capacity(cap);
    // SAFETY: buf has capacity `cap`; read_frame writes at most `cap` bytes into it.
    let n = unsafe { read_frame(buf.as_mut_ptr(), cap) };
    let n = usize::try_from(n).ok()?; // negative -> error
    assert!(n <= cap, "C library reported more bytes than capacity");
    // SAFETY: read_frame initialised the first n (<= cap) bytes.
    unsafe { buf.set_len(n) };
    Some(buf)
}
```

Receiving `(ptr, len)` from C: special-case `len == 0` (C often passes NULL), check `ptr` non-null
otherwise, then `slice::from_raw_parts` (see [undefined-behavior-catalog.md](undefined-behavior-catalog.md#slicefrom_raw_parts)).

## Ownership: who frees what

| Created by | Freed by | Mechanism |
|------------|----------|-----------|
| Rust `Box<T>` | Rust | `Box::into_raw` -> C -> exported `foo_free(p)` -> `Box::from_raw` |
| Rust `CString` | Rust | `into_raw` -> C -> `foo_string_free(p)` -> `CString::from_raw` |
| Rust `Vec<T>` | Rust | `ManuallyDrop` + `(ptr, len, cap)` -> C -> `Vec::from_raw_parts(ptr, len, cap)` |
| C `malloc` | C `free` | `libc::free(p.cast())`, or the library's own free function |
| C library object | the library | its destroy function in the wrapper's `Drop` |

Never mix: freeing `malloc` memory via `Box`/`Vec` drop, or a Rust allocation with `free()`, is UB (even
if the global allocator happens to be malloc today; a custom `#[global_allocator]` breaks it).

## Exporting Rust to C

```rust
use std::ffi::{CStr, c_char, c_int};
use std::panic::{AssertUnwindSafe, catch_unwind};

#[derive(Default)]
pub struct Counter {
    total: u64,
}

/// Creates a counter. Free it with `counter_free`.
#[unsafe(no_mangle)]
pub extern "C" fn counter_new() -> *mut Counter {
    Box::into_raw(Box::new(Counter::default()))
}

/// Adds the length of `word` to the counter. Returns 0 on success, -1 on bad input or panic.
///
/// # Safety
///
/// `c` must be a live pointer from `counter_new`, not used concurrently; `word` must be null or a
/// NUL-terminated string valid for the duration of the call.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_add(c: *mut Counter, word: *const c_char) -> c_int {
    if c.is_null() || word.is_null() {
        return -1;
    }
    // SAFETY: c is non-null and, per the contract, a live, exclusively used Counter.
    let counter = unsafe { &mut *c };
    // SAFETY: word is non-null and NUL-terminated per the contract.
    let word = unsafe { CStr::from_ptr(word) };
    catch_unwind(AssertUnwindSafe(|| {
        counter.total += word.to_bytes().len() as u64;
        0
    }))
    .unwrap_or(-1)
}

/// Frees a counter. Null is ignored.
///
/// # Safety
///
/// `c` must be null or a pointer from `counter_new` that has not been freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_free(c: *mut Counter) {
    if !c.is_null() {
        // SAFETY: c came from Box::into_raw in counter_new and is freed only once (contract).
        drop(unsafe { Box::from_raw(c) });
    }
}
```

- Exported functions taking pointers should be `unsafe extern "C" fn` (callable unsafely from Rust too);
  a safe `pub extern "C" fn` that dereferences its pointer argument is unsound for Rust callers (US023).
- `#[unsafe(no_mangle)]` is required in edition 2024 (US020): two crates exporting the same symbol is UB at
  link time.
- Build with `crate-type = ["cdylib"]` (or `staticlib`) and generate the header with cbindgen.

## Callbacks and user data

C callbacks take a function pointer plus a `void*` user-data pointer. Use a generic trampoline, and carry
panics back across the C frames instead of unwinding through them:

```rust
use std::any::Any;
use std::ffi::{c_int, c_void};
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};

type Visit = unsafe extern "C" fn(user: *mut c_void, value: c_int);

unsafe extern "C" {
    /// Calls `cb(user, v)` synchronously for every stored value; does not keep `user`.
    fn store_for_each(cb: Visit, user: *mut c_void);
}

struct Ctx<F> {
    f: F,
    panic: Option<Box<dyn Any + Send>>,
}

unsafe extern "C" fn trampoline<F: FnMut(i32)>(user: *mut c_void, value: c_int) {
    // SAFETY: `user` is the &mut Ctx<F> passed by for_each, alive and unaliased during the call.
    let ctx = unsafe { &mut *user.cast::<Ctx<F>>() };
    if ctx.panic.is_some() {
        return; // a previous call panicked: skip the rest
    }
    if let Err(payload) = catch_unwind(AssertUnwindSafe(|| (ctx.f)(value))) {
        ctx.panic = Some(payload);
    }
}

pub fn for_each<F: FnMut(i32)>(f: F) {
    let mut ctx = Ctx { f, panic: None };
    // SAFETY: trampoline::<F> matches `Visit` and receives a pointer to `ctx`, which outlives the
    // synchronous call; the library does not retain `user` (documented above).
    unsafe { store_for_each(trampoline::<F>, (&raw mut ctx).cast()) };
    if let Some(payload) = ctx.panic {
        resume_unwind(payload); // re-raise on the Rust side, after C has returned
    }
}
```

If the C library stores the callback (asynchronous callbacks), the user data must be heap-allocated
(`Box::into_raw`), outlive every possible call, and be freed by an unregister function; the closure must
then be `Send + 'static` if the library calls from other threads.

## Panics and unwinding

| ABI | Panic escaping a Rust function with this ABI | Foreign exception entering Rust through this ABI |
|-----|----------------------------------------------|-------------------------------------------------|
| `extern "C"` | aborts the process (since 1.81; was UB before) | UB |
| `extern "C-unwind"` (1.71) | unwinds into the caller (C++ can catch it as a foreign exception) | allowed: unwinds through Rust frames, running destructors |
| `extern "Rust"` (default) | unwinds normally | n/a |

- Wrap exported bodies in `catch_unwind` and translate to an error code; `AssertUnwindSafe` is needed for
  `&mut` captures, and it is your claim that a half-finished update is acceptable after a panic.
- Declare imported functions `extern "C-unwind"` when they may throw C++ exceptions (or call Rust callbacks
  that are allowed to unwind).
- `longjmp` over Rust frames (libpng/libjpeg error handling, Lua errors) is only acceptable across frames
  with no destructors to run; wrap such libraries in a small C shim that converts the jump into an error
  return.
- With `panic = "abort"` in the profile nothing unwinds, but a panic still kills the host process: library
  crates loaded into other programs should catch and convert anyway.

## Threads, errno and global state

- Read the C library's threading statement and encode it: "thread-safe" -> `Send + Sync`; "one thread at a
  time" -> `Send` only; "only the creating thread" -> neither (the default for raw pointers).
- `errno` is thread-local: read it immediately with `std::io::Error::last_os_error()` before any other call
  that may change it.
- `std::env::set_var` races with C code calling `getenv` on other threads (UB in glibc); in edition 2024 it
  is `unsafe` (US021). Set environment before spawning threads or pass configuration explicitly.
- Signal handlers may only call async-signal-safe functions; most of std (allocation, locking, printing) is
  not.
- `fork()` in a multi-threaded process: only async-signal-safe calls until `exec` (`CommandExt::pre_exec`
  is `unsafe` for exactly this reason).

## Tools

| Tool | Use |
|------|-----|
| `bindgen` | generate Rust declarations from C headers (in `build.rs` or ahead of time); keeps signatures in sync |
| `cbindgen` | generate C headers from exported Rust functions/types |
| `cxx` | safe C++ <-> Rust bridge with checked types for C++ (strings, vectors, unique_ptr) |
| `libc`, `windows-sys` / `windows` | maintained platform bindings; do not hand-write `stat` or `ioctl` structs |
| `cc` | compile C shims in `build.rs` |
| `-sys` crate convention | `foo-sys` holds raw bindings and linking; `foo` holds the safe wrapper |

## Testing FFI code

- Miri cannot run most foreign functions; structure code so the pointer logic on the Rust side can be
  tested with Rust stand-ins (a trait over the C API, or `#[cfg(miri)]` fakes).
- Run the real integration tests under AddressSanitizer (`-Zsanitizer=address`); for mixed C/Rust also
  compile the C side with `-fsanitize=address`, or use Valgrind on a debug build.
- Fuzz the safe wrapper's public API with cargo-fuzz (it uses ASan by default).
- Assert in tests that every `*_new` is paired with `*_free` (leak checks in ASan/LeakSanitizer).

Commands: [tooling-and-testing.md](tooling-and-testing.md).
