🎓 A1Learner
🔍

Rust FFI C Strings in Depth

A C string is text that ends with a NUL byte. A C string may hold bytes that are not valid UTF-8, and a Rust string may hold a NUL in the middle.

Send text, keep it alive

strlen is a real C function. It counts bytes up to the NUL.

let text = CString::new("Rust says hi").unwrap();
let counted = unsafe { strlen(text.as_ptr()) };

A NUL inside stops the trip

CString::new checks the whole string first. A NUL in the middle is an error here, not a surprise later.

let bad = CString::new("a\0b");
println!("ok = {}", bad.is_ok());

Strict reading can fail

These bytes hold 0xff. No UTF-8 text has that byte on its own.

let bytes = [104u8, 105, 0xff, 0];
let cstr = CStr::from_bytes_with_nul(&bytes).unwrap();
println!("strict ok = {}", cstr.to_str().is_ok());

Lossy reading swaps the damage

to_string_lossy keeps going. Each bad byte becomes the replacement character.

println!("lossy = {}", cstr.to_string_lossy());

Read what C hands back

from_ptr wraps the C bytes without copying. to_str checks them as UTF-8.

let said = unsafe { CStr::from_ptr(greeting()) };
println!("C says: {}", said.to_str().unwrap());

Example

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

unsafe extern "C" {
    fn strlen(s: *const c_char) -> usize;
    fn greeting() -> *const c_char;
}

fn main() {
    let text = CString::new("Rust says hi").unwrap();
    let counted = unsafe { strlen(text.as_ptr()) };
    println!("C counted {counted} bytes");

    let bad = CString::new("a\0b");
    match bad {
        Ok(_) => println!("no error"),
        Err(e) => println!("failed: {e}"),
    }

    let bytes = [104u8, 105, 0xff, 0];
    let cstr = CStr::from_bytes_with_nul(&bytes).unwrap();
    println!("strict ok = {}", cstr.to_str().is_ok());
    println!("lossy = {}", cstr.to_string_lossy());

    let said = unsafe { CStr::from_ptr(greeting()) };
    println!("C says: {}", said.to_str().unwrap());
}

Key points

  • Use CString when you send text to C.
  • It owns the bytes.
  • It adds the final NUL for you
  • Use CStr when you read text from C.
  • It borrows the bytes.
  • It makes no copy
  • CString::new fails when the text holds a NUL byte.
  • C would stop reading at that byte, so Rust refuses the string first
  • to_str fails when the bytes are not valid UTF-8.
  • to_string_lossy never fails.
  • It swaps bad bytes for the replacement character
  • Keep the CString alive while C reads its pointer.
  • The pointer dies with the CString
  • Text leaves Rust in a CString and returns borrowed as a CStr.
  • File paths are a different tool.
  • Paths arrive as OsString, because a path is not always valid UTF-8 and never needs a NUL.
  • Use CString for C library text.
  • C text ends with a NUL byte, unlike Rust text.

In this example, both directions get a turn. A CString carries "Rust says hi" to the real C strlen. It counts 12 bytes. A string with a NUL in the middle never leaves. CString::new returns an error that names the exact position of the NUL. Then Rust reads bytes that C might hand back. The strict to_str refuses the 0xff byte. The lossy reader swaps it for a replacement character. At the end, CStr::from_ptr reads a string from a small C library function. It prints the whole line: hello from C.

Note

Use OsStr for paths

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

C hands you a *const c_char. The bytes might not be valid UTF-8. You must print something either way. What do you use?