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?