๐Ÿฆ€ Functional Rust

493: CString and CStr for FFI

Difficulty: 1 Level: Intermediate Bridge Rust strings to C โ€” the null-terminated string types you need for foreign function interfaces.

The Problem This Solves

C strings are null-terminated: the string `"hello"` is stored as 6 bytes โ€” `h`, `e`, `l`, `l`, `o`, `\0`. The `\0` marks the end. C functions expect a `*const char` pointing to this null-terminated sequence. Rust's `String` and `&str` have no null terminator โ€” they track length separately. You can't pass a `&str` directly to a C function. More dangerously: if a string contains an interior null byte (`"hel\0lo"`), a C function would think it's only 3 characters long, silently truncating. Rust's `CString::new()` validates this โ€” it returns an error if the string contains any null bytes. You catch the bug at the boundary, not as a silent data corruption. `CString` (owned) and `CStr` (borrowed) are the safe wrappers for this. `CString` adds the null terminator and validates no interior nulls. `CStr` is the borrowed view used to receive C strings coming in. The pattern: use `CString` when sending to C, use `CStr` when receiving from C.

The Intuition

Think of the relationship:
RustC FFI
`String`owns `char` data, Rust-managed
`&str`borrowed `char`, no null terminator
`CString`owns `char` data, null-terminated, validated
`CStr`borrowed `const char`, null-terminated
`CString::new("hello")` returns `Result<CString, NulError>` โ€” the error case catches interior nulls. Once you have a `CString`, call `.as_ptr()` to get the `*const c_char` to pass to C. `CStr::from_ptr(ptr)` is `unsafe` because Rust has no way to verify that the C pointer is valid or that the string is properly terminated. You're asserting: "I know this pointer is safe." Inside the `unsafe` block, you extract the string and convert it to a Rust `&str` via `.to_str()` (validates UTF-8) or `.to_string_lossy()` (replaces invalid UTF-8).

How It Works in Rust

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

// CString โ€” owned null-terminated string
let cs = CString::new("hello").expect("no interior nulls");
//                              ^^ returns Err if string contains '\0'

// Get raw pointer for FFI
let ptr: *const c_char = cs.as_ptr();
// Pass `ptr` to a C function โ€” it's valid as long as `cs` is alive

// Interior null โ†’ explicit error (not silent truncation)
match CString::new("hel\0lo") {
 Ok(_)  => unreachable!(),
 Err(e) => println!("null at byte {}", e.nul_position()), // 3
}

// CStr โ€” borrowed view of a C string
// From known-good bytes (include the \0!)
let bytes: &[u8] = b"hello\0";
let cstr = CStr::from_bytes_with_nul(bytes).expect("valid");

// Convert CStr to Rust types
cstr.to_str()            // Result<&str, Utf8Error> โ€” validates UTF-8
cstr.to_string_lossy()   // Cow<str> โ€” replaces invalid UTF-8

// Round-trip: Rust String โ†’ CString โ†’ CStr โ†’ &str
let original = "round trip";
let cstring = CString::new(original).unwrap();
let cstr_ref: &CStr = cstring.as_c_str();
let back: &str = cstr_ref.to_str().unwrap();
assert_eq!(original, back);

// Receiving a C string pointer (unsafe โ€” you're asserting pointer validity)
unsafe {
 let received = CStr::from_ptr(ptr);
 println!("{}", received.to_string_lossy());
}

// Check that CString includes the null terminator
let bytes_with_null = cstring.as_bytes_with_nul();
assert_eq!(bytes_with_null.last(), Some(&0u8));

What This Unlocks

Key Differences

ConceptOCamlRust
Null-terminated stringManual `Bytes` with appended `'\000'``CString::new("...")` โ€” validates + adds `\0`
Borrowed C stringManual `Bytes` slice`CStr` โ€” borrowed, length from `\0` terminator
Interior null check`String.exists ((=) '\000')` โ€” manual`CString::new()` โ†’ `Err(NulError)`
C pointer`Bytes` pointer via `Bigarray`/`Ctypes``.as_ptr()` โ†’ `*const c_char`
Receive from C`Ctypes.CArray.to_list``unsafe { CStr::from_ptr(ptr) }`
To Rust string`Bytes.to_string` (no validation)`.to_str()` (validates UTF-8) / `.to_string_lossy()`
// 493. CString and CStr for FFI
use std::ffi::{CString, CStr, c_char};

fn main() {
    // CString โ€” owned, null-terminated
    let cs = CString::new("hello").expect("no interior nulls");
    println!("CString: {:?}", cs);
    println!("as_bytes_with_nul: {:?}", cs.as_bytes_with_nul());
    println!("as_ptr: {:?}", cs.as_ptr()); // *const c_char for FFI

    // Interior null โ†’ error
    match CString::new("hel\0lo") {
        Ok(_)  => println!("ok"),
        Err(e) => println!("interior null at pos {}", e.nul_position()),
    }

    // CStr โ€” borrowed, from raw bytes
    let bytes: &[u8] = b"hello\0"; // must include \0
    let cstr = CStr::from_bytes_with_nul(bytes).expect("valid c-string");
    println!("CStr: {:?}", cstr);
    println!("to_str: {:?}", cstr.to_str());
    println!("to_string_lossy: {}", cstr.to_string_lossy());

    // Round-trip: CString โ†’ CStr โ†’ &str
    let original = "round trip";
    let cstring = CString::new(original).unwrap();
    let cstr_ref: &CStr = cstring.as_c_str();
    let back: &str = cstr_ref.to_str().unwrap();
    println!("round-trip: '{}' == '{}' โ†’ {}", original, back, original == back);

    // Simulate receiving a C string pointer (unsafe)
    let ptr: *const c_char = cstring.as_ptr();
    let received = unsafe { CStr::from_ptr(ptr) };
    println!("from_ptr: {}", received.to_string_lossy());
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn test_new()         { assert!(CString::new("hello").is_ok()); }
    #[test] fn test_interior_null(){ assert!(CString::new("hel\0lo").is_err()); }
    #[test] fn test_roundtrip()   { let c=CString::new("hi").unwrap(); assert_eq!(c.to_str().unwrap(),"hi"); }
    #[test] fn test_null_bytes()  { let c=CString::new("hi").unwrap(); let b=c.as_bytes_with_nul(); assert_eq!(b.last(),Some(&0u8)); }
}
(* 493. CString for FFI โ€“ OCaml *)
(* OCaml uses Ctypes for FFI; strings need null termination *)
let () =
  (* In OCaml FFI, strings to C functions need null termination *)
  (* OCaml strings are NOT null-terminated by default *)
  let s = "hello" in
  (* Create null-terminated version: *)
  let cs = Bytes.create (String.length s + 1) in
  Bytes.blit_string s 0 cs 0 (String.length s);
  Bytes.set cs (String.length s) '\000';
  Printf.printf "c-string len=%d (including null)\n" (Bytes.length cs);

  (* Check for interior nulls *)
  let has_null s =
    String.exists ((=) '\000') s
  in
  Printf.printf "has_null 'hello': %b\n" (has_null s);
  Printf.printf "has_null 'hel\000lo': %b\n" (has_null "hel\000lo")