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 asconst char *. - A Rust
&stris a UTF-8 byte sequence with a known length. It does not require, and normally does not contain, a trailing zero byte. - A Rust
Stringowns UTF-8 text on the heap.
Rust provides two types specifically for this boundary:
std::ffi::CStrborrows and reads an existing null-terminated C string.std::ffi::CStringowns 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 returnsResult<&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.
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}");
hello()runs in the C library and returns the address of the first byte of"Hello World!\0".pointerstores that raw address. A raw pointer has no automatic safety or lifetime guarantees.assert!(!pointer.is_null())verifies that there is an address to read.CStr::from_ptr(pointer)scans memory until it finds the terminating\0, producing a borrowed&CStr.c_text.to_str()?checks whether the bytes before\0are valid UTF-8. ForHello World!, they are, so Rust produces&str.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.
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 operation | Main purpose | Ownership | Null terminator | UTF-8 requirement |
|---|---|---|---|---|
&str | Borrowed Rust text | Borrowed | No | Yes |
String | Owned Rust text | Owned | No | Yes |
&CStr | Borrowed C-compatible string | Borrowed | Yes | No; it is bytes |
CString | Owned C-compatible string | Owned | Yes | Input is Rust UTF-8 text |
CStr::to_str() | View C bytes as &str | Borrowed result | Reads up to \0 | Yes, validated |
CStr::to_string_lossy() | Display C bytes even if malformed | Cow<str> | Reads up to \0 | No; replacements may occur |
CStr versus CString:
- Use
CStrfor a pointer that C already owns, such as a returnedconst char *. - Use
CStringfor Rust text that must be passed asconst char *to C.
Borrowed versus copied conversion:
cstr.to_str()returns&strborrowing C memory. It is efficient but only valid while that C memory remains valid.cstr.to_str()?.to_owned()returns an independent RustString. It copies data but is safer for long-term storage.
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_ptrrequires a non-null, valid pointer to a\0-terminated byte sequence.CString::newrejects interior\0bytes.- Keep a
CStringalive for every moment C may useas_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.
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.
Keep learning
Related questions
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.