ExamplesBy LevelBy TopicLearning Paths
493 Fundamental

CString and CStr for FFI

Functional Programming

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

  • • Create a CString from a &str with CString::new(s) returning Result
  • • Understand that interior null bytes cause CString::new to fail
  • • Retrieve the raw pointer with .as_ptr() for FFI calls
  • • Inspect the bytes including the null terminator with .as_bytes_with_nul()
  • • Convert a CStr back to &str with .to_str() for reading C function output
  • Code 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

  • Null validation: Rust's CString::new explicitly validates and rejects interior nulls; OCaml's ctypes library silently truncates at the first NUL when coercing to C strings.
  • Ownership: 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.
  • Type separation: Rust has String/OsString/CString as three distinct types with compile-time checked conversions; OCaml uses string everywhere with runtime checks in FFI layers.
  • Safety: Rust's 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));
        }
    }
    ✓ Tests Rust test suite
    #[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

  • **Safe 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.
  • Read C output: Given a *const i8 returned by a C function, wrap it in unsafe { CStr::from_ptr(ptr) } and convert to a String with .to_string_lossy().
  • Null in the middle: Write a test that passes b"hel\x00lo" to CString::from_vec_unchecked (unsafe) and observe that .to_str() returns only "hel" — demonstrating the truncation hazard.
  • Open Source Repos