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