CString and CStr for FFI
Tutorial
The Problem
C strings are null-terminated byte arrays: the string ends at the first \0. Rust strings (str) can contain \0 bytes and have an explicit length. Passing a Rust string directly to a C function expecting char * would either crash (no null terminator) or silently truncate at interior nulls. CString::new(s) validates that s contains no interior nulls and appends a terminating \0, producing a value safe to pass to any extern "C" function via .as_ptr().
🎯 Learning Outcomes
CString from a &str with CString::new(s) returning ResultCString::new to fail.as_ptr() for FFI calls.as_bytes_with_nul()CStr back to &str with .to_str() for reading C function outputCode Example
#![allow(clippy::all)]
// 493. CString and CStr for FFI
use std::ffi::CString;
#[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));
}
}Key Differences
CString::new explicitly validates and rejects interior nulls; OCaml's ctypes library silently truncates at the first NUL when coercing to C strings.CString owns the null-terminated buffer; CStr borrows one. OCaml's GC manages string lifetime automatically but the C caller must not hold the pointer after the GC moves the string.String/OsString/CString as three distinct types with compile-time checked conversions; OCaml uses string everywhere with runtime checks in FFI layers.CString::as_ptr() is only valid while the CString is alive; dropping it earlier is a use-after-free. OCaml's GC-managed strings can move, requiring pinning for long-lived C pointers.OCaml Approach
OCaml's C FFI uses string directly — the C bindings layer handles the null-termination:
external c_strlen : string -> int = "caml_string_length"
(* ocaml-ctypes uses Ctypes.string for null-terminated C strings *)
let strlen s = Ctypes.(coerce string (ptr char) s |> C.Functions.strlen)
The ctypes library provides Ctypes.CArray, Ctypes.string, and Ctypes.ocaml_string to manage the boundary between OCaml and C strings. OCaml strings can contain NUL bytes — passing them to C functions expecting null-terminated strings would truncate silently.
Full Source
#![allow(clippy::all)]
// 493. CString and CStr for FFI
use std::ffi::CString;
#[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));
}
}#[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));
}
}
Exercises
strlen wrapper**: Write fn safe_strlen(s: &str) -> Result<usize, NulError> that creates a CString and calls a hypothetical extern "C" fn strlen(*const i8) -> usize.*const i8 returned by a C function, wrap it in unsafe { CStr::from_ptr(ptr) } and convert to a String with .to_string_lossy().b"hel\x00lo" to CString::from_vec_unchecked (unsafe) and observe that .to_str() returns only "hel" — demonstrating the truncation hazard.