Foreign Function Interface (FFI)

CString and CStr

Rust strings (String and &str) and C strings (char* and const char*) have fundamentally different memory layouts and invariants. Rust provides std::ffi::CString (owned, heap-allocated) and core::ffi::CStr (borrowed slice, re-exported in std::ffi) to bridge C's null-terminated byte strings safely.

Rust `&str` ("hello"):
  +---+---+---+---+---+
  | h | e | l | l | o |   (fat pointer: ptr + len = 5; NO trailing '\0'!)
  +---+---+---+---+---+   (guaranteed valid UTF-8; MAY contain interior '\0')

C `const char*` / Rust `&CStr` (c"hello"):
  +---+---+---+---+---+----+
  | h | e | l | l | o | \0 |  (terminated by first '\0'; NO interior '\0' bytes!)
  +---+---+---+---+---+----+  (arbitrary non-zero bytes; NOT guaranteed UTF-8)

Comparing Rust Strings and C Strings

Type Ownership Null-Terminated (\0)? Interior \0 Allowed? Guaranteed Valid UTF-8?
&str / String Borrowed / Owned No Yes Yes
&CStr (core::ffi::CStr) Borrowed (c"...") Yes (at the end) No No (arbitrary bytes)
CString (std::ffi::CString) Owned (Heap) Yes (appended automatically) No (NulError on creation) No (arbitrary bytes)

Compile-Time C-String Literals (c"...")

Stabilized in Rust 1.77 and part of the Rust 2024 edition, C-string literals (c"...") construct a &'static core::ffi::CStr at compile time with zero runtime allocation or length scanning, and verify at compile time that the string contains no interior \0 byte:

use std::ffi::{CStr, CString, NulError};

fn main() -> Result<(), NulError> {
    // 1. Compile-time `&'static CStr` literal via `c"..."` (Zero allocation!)
    let static_cstr: &'static CStr = c"SELECT * FROM metrics";
    assert_eq!(static_cstr.to_bytes(), b"SELECT * FROM metrics");
    assert_eq!(static_cstr.to_bytes_with_nul(), b"SELECT * FROM metrics\0");

    // 2. Runtime owned `CString` allocated from a dynamic Rust `String` or `&str`
    let dynamic_table = String::from("events_2026");
    let owned_cstring = CString::new(dynamic_table)?;
    println!("CString as_c_str(): {:?}", owned_cstring.as_c_str());

    // 3. Attempting to construct a `CString` with an interior null byte returns `Err(NulError)`
    let bad = CString::new("hello\0world");
    println!(
        "Interior null rejected at index: {:?}",
        bad.unwrap_err().nul_position()
    );

    Ok(())
}
CString as_c_str(): "events_2026"
Interior null rejected at index: 5

Avoiding the Temporary CString::new(...).unwrap().as_ptr() Dangling Pointer Bug!

One of the most notorious FFI bugs in Rust is chaining .as_ptr() directly onto a temporary CString::new(...) expression inside a function call argument:

// ❌ DANGEROUS ANTI-PATTERN (caught by `dangling_pointers_from_temporaries` lint):
// unsafe { c_function(CString::new("hello").unwrap().as_ptr()); }

In CString::new("hello").unwrap().as_ptr(), the temporary CString can be dropped before or immediately after the pointer is created if assigned to a let ptr = ...; binding, leaving ptr pointing to freed heap memory!

Always bind the CString to a named local variable first so it stays alive on the stack for the entire duration of the C function call:

use core::ffi::c_char;
use std::ffi::CString;

unsafe extern "C" {
    fn strlen(s: *const c_char) -> usize;
}

fn measure_via_c(text: &str) -> usize {
    // 1. Bind `c_string` to a local variable so its heap buffer outlives `strlen`!
    let c_string = CString::new(text).expect("input contained interior null byte");

    // 2. Pass `c_string.as_ptr()` while `c_string` is still alive in scope
    // SAFETY: `c_string` is alive for the entire `strlen` call and is null-terminated.
    unsafe { strlen(c_string.as_ptr()) }
} // `c_string` is dropped safely here AFTER `strlen` has returned

fn main() {
    println!("Measured via C strlen: {}", measure_via_c("safe lifetime"));
}
Measured via C strlen: 13