Foreign Function Interface (FFI)
Handling C Strings
Handling C strings across FFI involves three distinct patterns depending on who allocates and owns the string buffer:
- Borrowing a C string returned by C (
*const c_char->&CStr->&str). - Writing into a caller-allocated C buffer (
*mut c_char, usizecapacity). - Transferring heap-allocated string ownership between Rust and C (
CString::into_rawandCString::from_raw).
Pattern 1: Borrowing C-Owned String:
`*const c_char` ---> `unsafe { CStr::from_ptr(ptr) }` ---> `.to_str()` / `.to_string_lossy()`
Pattern 2: Transferring Rust-Allocated String to C (and Reclaiming It):
`CString` --.into_raw()--> `*mut c_char` (Given to C) --`CString::from_raw(ptr)`--> Dropped in Rust!
1. Borrowing a *const c_char from C (CStr::from_ptr)
When a C function returns a const char* (or passes one into a Rust callback), wrap it with unsafe { CStr::from_ptr(ptr) } after checking for ptr.is_null():
- Call
.to_str()(Result<&str, Utf8Error>) if invalid UTF-8 should be rejected as an error. - Call
.to_string_lossy()(Cow<'_, str>) if invalid UTF-8 bytes should be replaced withU+FFFD REPLACEMENT CHARACTER(``).
use core::ffi::{CStr, c_char};
use std::borrow::Cow;
/// Safely borrows a nullable C string pointer for the lifetime `'a`.
///
/// # Safety
///
/// If `ptr` is non-null, it must point to a valid null-terminated C string
/// whose memory remains valid and unmutated for `'a`.
pub unsafe fn borrow_c_string<'a>(ptr: *const c_char) -> Option<Cow<'a, str>> {
if ptr.is_null() {
return None;
}
// SAFETY: Caller guarantees `ptr` is a valid null-terminated string for `'a`.
let c_str = unsafe { CStr::from_ptr(ptr) };
Some(c_str.to_string_lossy())
}
fn main() {
let raw_ptr: *const c_char = c"status: healthy".as_ptr();
// SAFETY: `raw_ptr` points to a static null-terminated literal.
let text = unsafe { borrow_c_string(raw_ptr) }.unwrap();
println!("Borrowed C string: {text}");
}
Borrowed C string: status: healthy
2. Returning an Owned String to C (into_raw and from_raw)
If a Rust function allocates a CString and returns its *mut c_char pointer to C, Rust must not drop the CString when the function returns. Instead:
- Call
CString::into_raw(cstring)to transfer ownership of the buffer to the C caller. - Provide a companion Rust destructor function (
free_rust_string(ptr: *mut c_char)) that reconstructs and drops theCStringviaunsafe { drop(CString::from_raw(ptr)) }.
Never pass a pointer returned by CString::into_raw to C's free()! Rust's global allocator (alloc::System, jemalloc, or mimalloc) may not be the same heap as the C runtime's free(), and CString tracks capacity metadata that CString::from_raw needs.
use core::ffi::c_char;
use std::ffi::{CStr, CString};
/// Allocates a greeting string on the Rust heap and returns ownership to C.
/// The caller MUST eventually pass the returned pointer to `rust_string_free`.
#[unsafe(no_mangle)]
pub extern "C" fn rust_greeting_new(name_ptr: *const c_char) -> *mut c_char {
let name = if name_ptr.is_null() {
"Guest"
} else {
// SAFETY: If non-null, C caller provides a valid null-terminated string.
unsafe { CStr::from_ptr(name_ptr) }.to_str().unwrap_or("Guest")
};
match CString::new(format!("Welcome, {name}!")) {
Ok(cstr) => cstr.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
/// Frees a string previously allocated by `rust_greeting_new`.
///
/// # Safety
///
/// `ptr` must either be null or a pointer returned by `rust_greeting_new`
/// that has not yet been freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_string_free(ptr: *mut c_char) {
if !ptr.is_null() {
// SAFETY: Reconstructs the `CString` from the pointer produced by `into_raw`
// and immediately drops it, freeing the Rust heap allocation.
unsafe {
drop(CString::from_raw(ptr));
}
}
}
fn main() {
let raw_greeting = rust_greeting_new(c"Ada".as_ptr());
assert!(!raw_greeting.is_null());
// Inspect the C string before freeing it
let message = unsafe { CStr::from_ptr(raw_greeting) }.to_str().unwrap();
println!("Allocated greeting: {message}");
// Reclaim and free via the matching Rust deallocator
unsafe {
rust_string_free(raw_greeting);
}
}
Allocated greeting: Welcome, Ada!