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:| Rust | C 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 |
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
- Calling C libraries โ pass Rust strings to any C API that expects `const char*`.
- Receiving C strings โ safely convert C function outputs back to Rust `&str` or `String`.
- Preventing FFI bugs โ `CString::new()` validates your string before it reaches C, catching interior nulls at the Rust boundary.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Null-terminated string | Manual `Bytes` with appended `'\000'` | `CString::new("...")` โ validates + adds `\0` |
| Borrowed C string | Manual `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")