CString / CStr
CString / CStr
Level 13 — Rust Nul-terminated string types for safely passing text data across the C FFI boundary:
CString(owned) andCStr(borrowed view).
1. Prerequisites
- FFI (Foreign Function Interface) — FFI interoperability.
- String vs &str — Rust string types.
2. Term Category
Rust Standard Types (null-terminated C string abstractions): Nul-terminated string types for safely passing text data across the C FFI boundary: CString (owned) and CStr (borrowed view).
3. Explanation
(1) Design Motivation — "Why did we design this?"
Rust strings (String and &str) are UTF-8 encoded with an explicit byte length and are not nul-terminated. Conversely, C strings (char *) have no explicit length field and rely on a trailing nul byte (\0) to signal string termination.
Passing a Rust &str directly to C causes C functions to read past valid memory until hitting a random 0 byte (causing buffer overreads or segfaults). CString (owned heap container) and CStr (borrowed reference view) safely format and validate nul-terminated strings for Foreign Function Interface (FFI) boundaries.
(2) Reality Metaphor
A passport control border checkpoint: Rust strings are digital biometric e-passports with explicit page counts; C strings are paper scrolls stamped with an official NUL wax seal at the end. Crossing the C border requires adding the wax seal (CString) before handing the scroll to the border guard (CStr).
(3) Rust Code Examples
Short Snippet
use std::ffi::CString;
let c_str = CString::new("Hello C API").unwrap();
let raw_ptr: *const std::os::raw::c_char = c_str.as_ptr();
Fuller Example
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
pub fn pass_to_c_library(input: &str) -> Result<String, std::ffi::NulError> {
let c_string = CString::new(input)?;
let ptr: *const c_char = c_string.as_ptr();
// Simulate reading back from C via CStr
let borrowed: &CStr = unsafe { CStr::from_ptr(ptr) };
Ok(borrowed.to_str().unwrap().to_string())
}
fn main() {
let res = pass_to_c_library("Safe string").unwrap();
assert_eq!(res, "Safe string");
}
4. Common Mistakes & Pitfalls
Mistake 1: The 'Dangling Pointer' One-Liner (CString::new().unwrap().as_ptr())
The mistake: Invoking .as_ptr() directly on an un-bound temporary CString inside an FFI call argument list.
Why it is wrong: The temporary CString is dropped immediately at the end of the statement, deallocating the memory. The raw pointer passed to C becomes a dangling pointer causing Undefined Behavior.
Incorrect:
unsafe { c_func(CString::new("data").unwrap().as_ptr()); } // Dangling pointer UB!
Fix:
let c_str = CString::new("data").unwrap(); unsafe { c_func(c_str.as_ptr()); }
Mistake 2: Passing Strings with Interior Nul Bytes
The mistake: Attempting to construct a CString from text containing embedded \0 bytes.
Why it is wrong: C string functions stop reading at the first \0 byte, causing premature truncation. CString::new catches this and returns a NulError.
Incorrect:
let c_str = CString::new("hello\0world").unwrap(); // Panics with NulError!
Fix:
let c_str = CString::new("hello world").map_err(|e| e); // Handle NulError gracefully!
Mistake 3: Assuming C Strings Are Always Valid UTF-8
The mistake: Calling CStr::to_str().unwrap() on unknown raw C strings.
Why it is wrong: C strings are arbitrary non-zero byte sequences without encoding guarantees. If C returns invalid UTF-8 bytes, .to_str().unwrap() panics.
Incorrect:
let s = unsafe { CStr::from_ptr(raw_c_ptr) }.to_str().unwrap(); // Might panic on invalid UTF-8!
Fix:
let s = unsafe { CStr::from_ptr(raw_c_ptr) }.to_string_lossy(); // Safe lossy conversion!
5. Practice Exercises
Exercise 1: Safe C POSIX Environment Variable Reader FFI Wrapper
Scenario: Build a safe Rust FFI wrapper function get_c_env(var_name: &str) -> Option<String> calling the C standard library getenv(const char *name) function safely.
Requirements:
- Convert
var_nametoCString. - Invoke
libc::getenvsafely. - Convert returned
*const c_charto&CStrandString. - Write unit tests verifying environment reading.
Answer
Implementation
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn getenv(name: *const c_char) -> *const c_char;
}
pub fn get_c_env(var_name: &str) -> Option<String> {
let c_name = CString::new(var_name).ok()?;
unsafe {
let ptr = getenv(c_name.as_ptr());
if ptr.is_null() {
None
} else {
let c_str = CStr::from_ptr(ptr);
Some(c_str.to_string_lossy().into_owned())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_getenv_ffi() {
std::env::set_var("TEST_C_FFI_VAR", "hello_c");
let val = get_c_env("TEST_C_FFI_VAR");
assert_eq!(val, Some("hello_c".to_string()));
std::env::remove_var("TEST_C_FFI_VAR");
}
}
Technical Explanation
CString::new(var_name)appends the trailing\0byte required by Cgetenv.- Binds
c_nameto a local variable to prevent early deallocation before the unsafegetenvcall. - Converts raw pointer response to
&CStrand safely converts UTF-8 bytes intoString.
Exercise 2: Zero-Allocation Constant C String Passing via c"" Literals
Scenario: Demonstrate zero-allocation static C string literals (c"hello") introduced in Rust 1.77 for high-performance FFI logging.
Requirements:
- Define a static
&CStrusingc"..."literal syntax. - Pass
&CStrpointer to mock C logger function. - Test zero-allocation static string reference.
Answer
Implementation
use std::ffi::CStr;
pub fn log_c_message(msg: &'static CStr) -> usize {
msg.to_bytes().len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_c_literal() {
let static_c_str: &'static CStr = c"System initialization complete";
assert_eq!(log_c_message(static_c_str), 30);
}
}
Technical Explanation
- The
c"..."literal embeds a nul-terminated&CStrdirectly in the binary read-only.rodatasegment. - Eliminates dynamic heap allocation overhead for constant FFI strings.
Exercise 3: Custom FFI String Buffer Converter
Scenario: Build a utility rust_to_c_buffer(input: &str, buf: &mut [u8]) -> Result<(), &'static str> copying Rust text into a raw C byte buffer with a trailing nul byte.
Requirements:
- Copy bytes into buffer.
- Enforce trailing
\0byte. - Return error if buffer is too small.
Answer
Implementation
pub fn rust_to_c_buffer(input: &str, buf: &mut [u8]) -> Result<(), &'static str> {
if buf.len() <= input.len() {
return Err("Buffer too small for string and nul terminator");
}
buf[..input.len()].copy_from_slice(input.as_bytes());
buf[input.len()] = 0; // Add nul terminator!
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_c_buffer_copy() {
let mut buf = [0u8; 10];
assert!(rust_to_c_buffer("hello", &mut buf).is_ok());
assert_eq!(&buf[..6], b"hello\0");
assert!(rust_to_c_buffer("too_long_string", &mut buf).is_err());
}
}
Technical Explanation
- Manually constructs a nul-terminated byte array for zero-allocation C buffer outputs.
6. Related Terms
- FFI (Foreign Function Interface) — C FFI boundary.
extern "C"— C calling convention.
7. Key Takeaways
CStringis the owned, heap-allocated string type for sending data to C (*const c_char).&CStris the borrowed view for reading nul-terminated strings from C.- Never chain
.as_ptr()on un-bound temporaryCString::new(...)expressions. - Use
c"..."literals in Rust 1.77+ for zero-allocation static C strings.