---
title: How to Use CString and CStr for FFI String Handling
summary: Use CString::new to convert Rust strings to C-compatible null-terminated strings and CStr::from_ptr to safely read C strings back into Rust.
lang: en
slug: how-to-use-cstring-and-cstr-for-ffi-string-handling
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-cstring-and-cstr-for-ffi-string-handling/
---

# How to Use CString and CStr for FFI String Handling



## When Rust strings meet C strings

You are writing a Rust wrapper for a C library. You have a string in Rust: `"Hello"`. You pass it to a C function. The C function crashes, or worse, it prints garbage and reads past the end of your buffer. The problem is not your logic. The problem is the null terminator.

Rust strings are just sequences of bytes. They track their length in memory. C strings are sequences of bytes that end with a zero byte. C does not know the length. C reads until it hits a zero. If you pass a Rust string to C, C keeps reading until it finds a zero somewhere in your process memory. You just handed C a key to your entire heap.

`CString` and `CStr` exist to bridge this gap. `CString` takes a Rust string, allocates heap memory, copies the bytes, and appends the zero terminator. `CStr` takes a raw pointer from C and scans forward until it finds the zero, giving you a safe view of the data.

## The contract of the null byte

Think of a `CString` as a package you are shipping to a warehouse that only understands a specific protocol. You pack your contents, then you attach a bright red "STOP" tag at the end. The warehouse robot reads items until it sees the tag. If you forget the tag, the robot keeps grabbing boxes until it hits a wall.

`CString` ensures the tag is there. It also refuses to create a package if your contents already contain a "STOP" tag. If your Rust string has a null byte in the middle, `CString::new` returns an error. This prevents silent data corruption. If you passed `"Hel\0lo"` to C, C would only see `"Hel"`. Rust would think you sent the whole string. The mismatch causes bugs that are nearly impossible to trace.

`CStr` is the robot reading the package. It takes a pointer and walks memory until it finds the zero. It does not own the memory. It borrows it. If the memory goes away while `CStr` is looking at it, you have undefined behavior.

## Minimal example: round-trip conversion

This example shows the full flow. Rust string to C pointer, back to Rust string.

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

fn main() {
    // Rust string has no null terminator.
    let rust_str = "Hello, C!";

    // CString::new checks for embedded nulls and appends a terminator.
    // It returns a Result because the input might contain a null byte.
    let c_string = CString::new(rust_str).expect("String contained a null byte");

    // as_ptr returns a raw pointer to the C-compatible buffer.
    // The buffer is valid as long as c_string is alive.
    let c_ptr: *const c_char = c_string.as_ptr();

    // SAFETY: c_ptr is valid because c_string is alive and we just created it.
    // The memory contains a null terminator guaranteed by CString.
    let c_str = unsafe { CStr::from_ptr(c_ptr) };

    // to_string_lossy converts bytes to a Rust String.
    // It replaces invalid UTF-8 sequences with the replacement character.
    let back_to_rust = c_str.to_string_lossy();
    println!("{}", back_to_rust);
}
```

**Convention aside:** The community prefers `CString::new` over manual allocation. It handles the null check and allocation in one step. If you see code manually allocating a `Vec<u8>` and pushing a zero, rewrite it to use `CString`. The compiler optimizes `CString` well, and the intent is clearer.

## What happens under the hood

When you call `CString::new`, Rust allocates a buffer on the heap. It copies the bytes from your string slice. It appends a single zero byte. The `CString` struct holds a pointer to this buffer and its length. The length includes the zero byte.

When you call `as_ptr`, `CString` returns the address of the buffer. It does not move the data. The `CString` still owns the memory. You can call `as_ptr` multiple times. The pointer remains valid until `c_string` is dropped.

When you call `CStr::from_ptr`, you enter `unsafe` territory. The function takes the pointer and scans memory byte by byte. It stops at the first zero. It returns a `&CStr` that borrows the memory. The lifetime of the `&CStr` is tied to the lifetime of the data behind the pointer. If the data is a `CString`, the `&CStr` must not outlive the `CString`.

When you call `to_string_lossy`, `CStr` reads the bytes it found. It checks if they are valid UTF-8. If they are, it returns a borrowed `&str` wrapped in a `Cow`. If they are not, it allocates a new `String`, replaces bad bytes with `�`, and returns that. `Cow` stands for Clone on Write. It avoids allocation when the data is already valid UTF-8.

**Convention aside:** Use `to_string_lossy` for C strings unless you have a guarantee of UTF-8. C strings are often just bytes. They might contain Latin-1, Shift-JIS, or binary data. `to_str` panics on invalid UTF-8. `to_string_lossy` keeps your program running and gives you a readable result.

## Realistic example: calling a C function

In real code, you call `extern "C"` functions. The `CString` must stay alive for the duration of the call.

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

/// Simulates a C function that takes a null-terminated string.
extern "C" {
    fn c_print_message(msg: *const c_char);
}

/// Calls a C function with a Rust string.
fn call_c_function() {
    // Create the C string. This allocates and appends the null terminator.
    let message = CString::new("Rust calling C!").expect("Null byte in string");

    // Pass the pointer to C.
    // The CString must stay alive for the duration of the call.
    // If message were dropped here, the pointer would dangle.
    unsafe {
        c_print_message(message.as_ptr());
    }
}

/// Receives a string from C and prints it.
fn receive_from_c(c_ptr: *const c_char) {
    // SAFETY: The caller guarantees c_ptr points to a valid null-terminated string.
    // The string must remain valid for the duration of this function.
    let c_str = unsafe { CStr::from_ptr(c_ptr) };

    // Convert to Rust string, handling potential encoding issues.
    let rust_str = c_str.to_string_lossy();
    println!("C said: {}", rust_str);
}
```

**Convention aside:** Keep `unsafe` blocks small. Wrap only the FFI call or the pointer dereference. In `call_c_function`, the `unsafe` block contains only the call to `c_print_message`. The `CString` creation and `as_ptr` are safe operations. This isolates the risk and makes the code easier to audit.

## Ownership transfer: handing memory to C

Sometimes C needs to own the string. C might store the pointer and free it later. If you pass a `CString` pointer and let the `CString` drop, Rust frees the memory. C then accesses freed memory. This is a use-after-free bug.

Use `CString::into_raw` to transfer ownership. It consumes the `CString` and returns a raw pointer. Rust forgets about the memory. C is responsible for freeing it.

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

/// Transfers ownership of a string to C.
fn give_string_to_c() -> *mut c_char {
    // Create the C string.
    let c_string = CString::new("Owned by C").expect("Null byte");

    // into_raw consumes the CString and returns a raw pointer.
    // Rust no longer owns this memory. C must free it.
    c_string.into_raw()
}

/// Takes back ownership of a string from C.
fn take_string_from_c(ptr: *mut c_char) {
    // SAFETY: ptr must be a valid pointer returned by CString::into_raw.
    // It must not have been freed by C.
    let _c_string = unsafe { CString::from_raw(ptr) };

    // When _c_string drops, Rust frees the memory.
}
```

**Convention aside:** `into_raw` is not a memory leak. It is a handoff. The community calls this "leaking to C". Document clearly that C owns the memory. If C uses `free`, ensure the allocation strategy matches. `CString` uses the system allocator, so `free` works on most platforms. If C uses a custom allocator, you cannot use `CString::into_raw`. You must allocate with the custom allocator yourself.

## Pitfalls and compiler errors

Embedded null bytes break `CString`. If your input comes from user data or a file, it might contain nulls. `CString::new` returns `Err` in this case. Handle the error. Do not unwrap blindly.

```rust
let bad_str = "Hel\0lo";
let result = CString::new(bad_str);
// result is Err(CStringError { input: ..., null_byte: 3 })
```

Dangling pointers cause undefined behavior. If you create a `CString`, pass its pointer to C, and then drop the `CString` while C is still using it, C reads garbage. The compiler cannot catch this. You must manage lifetimes manually.

If you try to return a `&CStr` from a function where the `CString` is local, the compiler rejects you with `E0597` (borrowed value does not live long enough). The `CString` is dropped at the end of the function. The `&CStr` would point to freed memory.

```rust
fn bad_example() -> &'static CStr {
    let c_string = CString::new("Hello").unwrap();
    // E0597: c_string does not live long enough
    unsafe { CStr::from_ptr(c_string.as_ptr()) }
}
```

Fix this by returning the `CString` itself, or by using `into_raw` if C needs ownership.

Type mismatches happen when you pass `&str` where `*const c_char` is expected. The compiler rejects this with `E0308` (mismatched types). You must convert to `CString` first.

```rust
// E0308: mismatched types
// expected *const c_char, found &str
unsafe { c_print_message("Hello".as_ptr()) }; // Error: &str has no as_ptr
```

**Convention aside:** Use `c_char` for FFI types, not `i8`. `c_char` is a type alias that matches the C `char` type on the target platform. On most systems it is `i8`, but using `c_char` makes your code portable and signals intent.

## Decision matrix

Use `CString` when you need to pass a Rust string to C and must guarantee a null terminator. Use `CString::new` for simple conversions. Use `CString::from_vec_with_nul` when you already have a `Vec<u8>` that ends with a zero and want to avoid a second allocation.

Use `CStr::from_ptr` when you receive a raw pointer from C and need to read the string safely until the null byte. Use `to_string_lossy` to convert to a Rust `String` unless you guarantee UTF-8. Use `to_bytes` if you need the raw bytes including the terminator.

Use `&str` when you are working entirely within Rust and do not need FFI compatibility. Use `String` when you need an owned Rust string. Never pass `&str` or `String` directly to C.

Use `Vec<u8>` when you are dealing with binary data that might contain null bytes or is not text at all. C strings cannot represent embedded nulls. If your data has nulls, pass a pointer and a length, not a `CString`.

Use `CString::into_raw` when you need to hand ownership of the string to C so C can free it. Use `CString::from_raw` when C returns ownership back to Rust. Treat the raw pointer as a token of ownership. If you lose the token, you leak memory.

## Where to go next

- [What Can You Do Inside an unsafe Block?](/en/what-can-you-do-inside-an-unsafe-block/)
- [How to Access Mutable Statics in Rust](/en/how-to-access-mutable-statics-in-rust/)
- [How to Share Rust Code Between Mobile Platforms](/en/how-to-share-rust-code-between-mobile-platforms/)