Question

Convert C Strings to Rust Strings and Back with Rust FFI

rustcffi

Question

A C library returns a string from this function:

const char *hello(void) {
    return "Hello World!";
}

How can Rust call hello() through FFI and safely obtain a Rust string representation of the returned const char *?

extern "C" {
    fn hello() -> *const c_char;
}

Also, how can a Rust string be converted into a C-compatible string when calling C code?

Short Answer

You will learn why C strings and Rust strings use different representations, how to read a C char * with CStr, and how to create an owned null-terminated C string with CString. You will also learn the ownership rules that prevent invalid frees and dangling pointers in FFI code.

Concept

C and Rust represent text differently:

  • A C string is a sequence of bytes ending in a zero byte (\0). A C API commonly exposes one as const char *.
  • A Rust &str is a UTF-8 byte sequence with a known length. It does not require, and normally does not contain, a trailing zero byte.
  • A Rust String owns UTF-8 text on the heap.

Rust provides two types specifically for this boundary:

  • std::ffi::CStr borrows and reads an existing null-terminated C string.
  • std::ffi::CString owns a null-terminated string suitable for passing to C.

For a pointer returned from C, use CStr::from_ptr inside an unsafe block. This is unsafe because Rust cannot verify that the pointer is valid, non-null, and terminated with \0.

Then choose a conversion based on the C library's encoding contract:

  • Use .to_str() when the bytes must be valid UTF-8. It returns Result<&str, Utf8Error>.
  • Use .to_string_lossy() when invalid UTF-8 should be replaced rather than treated as an error.

Ownership is equally important. The C function in the question returns a pointer to a string literal. That memory is static and borrowed: Rust must not free it. Other C APIs may return allocated memory and document a matching release function, such as mylib_free. In that case, call the library's documented release function exactly once.

Mental Model

Think of a C string as a row of letters followed by a red stop marker (\0). C code does not carry a length separately, so it reads forward until it finds that marker.

A Rust &str is more like a card that says both where the letters begin and exactly how many bytes belong to the text. It does not need a stop marker.

CStr is a careful reader that starts at a C pointer and reads until the red stop marker. CString is a Rust-owned package that adds the marker before handing text to C code.

Take Quiz

Syntax and Examples

Import the FFI types from the standard library:

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

Declare C functions with the C ABI:

unsafe extern "C" {
    fn hello() -> *const c_char;
}

Read a C string returned from C:

use std::ffi::CStr;

let pointer = unsafe { hello() };

if pointer.is_null() {
    eprintln!("C function returned a null pointer");
} else {
    let text = unsafe { CStr::from_ptr(pointer) }
        .to_str()
        .expect("hello returned non-UTF-8 text");

    println!("{text}");
}

CStr::from_ptr(pointer) creates a borrowed &CStr. It does not copy or free the C memory. Calling .to_str() validates UTF-8 and produces a borrowed Rust &str.

Create a C string to pass in the other direction:

use std::ffi::CString;

let name = CString::new("Ada")?;
let c_pointer = name.as_ptr();

// Call a C function with c_pointer while `name` is still alive.

CString::new returns an error if the input contains an interior zero byte, because C would interpret that byte as the end of the string.

Step by Step Execution

Consider this complete call sequence:

let pointer = unsafe { hello() };
assert!(!pointer.is_null());

let c_text = unsafe { CStr::from_ptr(pointer) };
let rust_text = c_text.to_str()?;
println!("{rust_text}");
  1. hello() runs in the C library and returns the address of the first byte of "Hello World!\0".
  2. pointer stores that raw address. A raw pointer has no automatic safety or lifetime guarantees.
  3. assert!(!pointer.is_null()) verifies that there is an address to read.
  4. CStr::from_ptr(pointer) scans memory until it finds the terminating \0, producing a borrowed &CStr.
  5. c_text.to_str()? checks whether the bytes before \0 are valid UTF-8. For Hello World!, they are, so Rust produces &str.
  6. println! prints the Rust string slice.

The returned &str borrows the C memory. Do not keep it after the C library says that memory is invalid, freed, or changed.

Real World Use Cases

  • System APIs: Read file paths, environment values, error messages, or host names returned as C strings.
  • Database and networking libraries: Pass connection URLs, SQL statements, request headers, or error text across an FFI boundary.
  • Game and graphics engines: Send asset names or shader source paths from Rust into a C or C++ engine API.
  • Native plugins: Exchange configuration values and user-visible messages between a Rust host and a C plugin.
  • Embedded libraries: Call vendor SDK functions that accept const char * names and return diagnostic strings.
Take Quiz

Real Codebase Usage

In production Rust FFI wrappers, developers usually keep raw pointers inside a small unsafe layer and expose safe Rust APIs above it.

Validate pointers at the boundary

use std::ffi::CStr;
use std::os::raw::c_char;

fn c_message_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
    if ptr.is_null() {
        return None;
    }

    unsafe { CStr::from_ptr(ptr).to_str().ok() }
}

This uses a guard clause to avoid dereferencing a null pointer.

Return an owned String when C memory may not live long enough

fn copied_message(ptr: *const c_char) -> Result<String, Box<dyn std::error::Error>> {
    if ptr.is_null() {
        return Err("null C string pointer".into());
    }

    Ok(unsafe { CStr::from_ptr(ptr) }.to_str()?.to_owned())
}

to_owned() copies the content into Rust-owned memory. This is useful when the C library reuses an internal buffer or when you will release the C allocation.

Keep CString alive during the C call

use std::ffi::CString;
use std::os::raw::c_char;

unsafe extern "C" {
    fn set_name(name: *const c_char);
}

fn set_name_from_rust(name: &str) -> Result<(), std::ffi::NulError> {
    let name = CString::new(name)?;
    unsafe { set_name(name.as_ptr()) };
    Ok(())
}

The CString local remains alive until set_name_from_rust returns. This is safe only if set_name copies the content during the call. If C stores the pointer for later use, the wrapper needs a longer-lived CString and a documented cleanup strategy.

Common Mistakes

Using String::from_raw_parts or String::from_raw

Do not treat a C pointer as Rust String allocation:

// Incorrect: C memory may not use Rust's allocator or String layout.
// let value = unsafe { String::from_raw_parts(ptr as *mut u8, len, capacity) };

Use CStr::from_ptr to read a C string. Rust may free String memory with Rust's allocator, which is invalid for most C allocations.

Forgetting the null check

// Incorrect if `hello()` may return null.
let text = unsafe { CStr::from_ptr(hello()) };

Check pointer.is_null() first unless the C API guarantees a non-null result.

Assuming every C string is UTF-8

let text = unsafe { CStr::from_ptr(ptr) }.to_str()?;

This is correct when the API promises UTF-8, but it returns an error for arbitrary bytes or locale-encoded text. Use to_string_lossy() for display-only fallback, or handle bytes through CStr::to_bytes() when text encoding is unknown.

Freeing a borrowed or static C string

The hello function returns a string literal. Do not call free, CString::from_raw, or any deallocation function on it. Only release memory when the C library documents that you own the returned pointer and specifies how to release it.

Letting a CString drop too early

// Incorrect if C stores `ptr` and uses it after this function returns.
fn pointer_for_later() -> *const c_char {
    CString::new("Ada").unwrap().as_ptr()
}

The temporary CString is dropped immediately, leaving a dangling pointer. Keep the CString alive for as long as C needs the pointer, or transfer ownership only through an explicitly documented ownership contract.

Omitting extern "C"

// Incorrect for a normal C library declaration.
unsafe extern {
    fn hello() -> *const c_char;
}

Use unsafe extern "C" so Rust uses the C calling convention.

Comparisons

Type or operationMain purposeOwnershipNull terminatorUTF-8 requirement
&strBorrowed Rust textBorrowedNoYes
StringOwned Rust textOwnedNoYes
&CStrBorrowed C-compatible stringBorrowedYesNo; it is bytes
CStringOwned C-compatible stringOwnedYesInput is Rust UTF-8 text
CStr::to_str()View C bytes as &strBorrowed resultReads up to \0Yes, validated
CStr::to_string_lossy()Display C bytes even if malformedCow<str>Reads up to \0No; replacements may occur

CStr versus CString:

  • Use CStr for a pointer that C already owns, such as a returned const char *.
  • Use CString for Rust text that must be passed as const char * to C.

Borrowed versus copied conversion:

  • cstr.to_str() returns &str borrowing C memory. It is efficient but only valid while that C memory remains valid.
  • cstr.to_str()?.to_owned() returns an independent Rust String. It copies data but is safer for long-term storage.
Take Quiz

Cheat Sheet

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
unsafe extern "C" {
    fn hello() -> *const c_char;
}

Read const char * returned from C:

let ptr = unsafe { hello() };
if ptr.is_null() {
    // Handle C null pointer.
} else {
    let text: &str = unsafe { CStr::from_ptr(ptr) }.to_str()?;
}

Copy into an owned Rust string:

let owned: String = unsafe { CStr::from_ptr(ptr) }.to_str()?.to_owned();

Handle non-UTF-8 input for display:

let text = unsafe { CStr::from_ptr(ptr) }.to_string_lossy();

Pass Rust text to C:

let value = CString::new("hello")?;
unsafe { c_function(value.as_ptr()) };

Rules:

  • CStr::from_ptr requires a non-null, valid pointer to a \0-terminated byte sequence.
  • CString::new rejects interior \0 bytes.
  • Keep a CString alive for every moment C may use as_ptr().
  • Never free C memory unless the API says you own it and tells you which release function to call.
  • Use extern "C" for declarations of standard C functions.

FAQ

How do I convert *const c_char to String in Rust?

Check for null, create a CStr with unsafe { CStr::from_ptr(ptr) }, then call .to_str()?.to_owned(). This requires the C bytes to be valid UTF-8.

Why is CStr::from_ptr unsafe?

Rust cannot check whether the raw pointer is non-null, points to readable memory, and eventually reaches a null terminator. Those are requirements promised by the C API or enforced by your wrapper.

Should I use *const c_char or *mut c_char for const char *?

Use *const c_char. It communicates that your Rust code must not modify the bytes through that pointer. Use *mut c_char only for C APIs that accept writable char * memory.

Does CStr::from_ptr copy the C string?

No. It creates a borrowed view. Call .to_owned() after .to_str() if you need an independent Rust String.

Can I use CString for a C string returned by a library?

Usually no. CString represents Rust-owned allocated C-string data. A pointer returned by C is generally borrowed or owned according to the C library's rules. Read it with CStr unless ownership was explicitly transferred to Rust in a compatible way.

What if the C library returns non-UTF-8 text?

Use CStr::to_bytes() to work with raw bytes, or to_string_lossy() when replacement characters are acceptable for display. Do not silently assume a locale-specific encoding is UTF-8.

Do I need to free the pointer returned by hello()?

No. The shown C function returns a static string literal. It must not be freed. For other APIs, follow that library's ownership and release documentation.

Why does CString::new fail for some strings?

It fails when the input contains \0. C uses that byte to mark the end, so an interior zero would truncate the value seen by C.

Take Quiz

Mini Project

Description

Build a small Rust wrapper around a C greeting library. The wrapper reads a static greeting returned by C and passes a user-provided Rust name to C as a null-terminated string. It demonstrates both directions of C-string conversion without transferring ownership of memory.

Goal

Print a greeting returned by C and send a Rust name safely to a C function.

Requirements

Use CStr to read the const char * returned by C. Check the returned pointer for null before reading it. Use CString to pass a Rust name to C. Handle invalid UTF-8 and interior null bytes with Result. Do not free the static pointer returned by C.

Take Quiz

Keep learning

Related questions

View more