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:

  1. Borrowing a C string returned by C (*const c_char -> &CStr -> &str).
  2. Writing into a caller-allocated C buffer (*mut c_char, usize capacity).
  3. Transferring heap-allocated string ownership between Rust and C (CString::into_raw and CString::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 with U+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:

  1. Call CString::into_raw(cstring) to transfer ownership of the buffer to the C caller.
  2. Provide a companion Rust destructor function (free_rust_string(ptr: *mut c_char)) that reconstructs and drops the CString via unsafe { 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!