Understanding Strings in Rust: A Comprehensive Guide

Choose the right Rust string type and understand the boundaries between bytes, Unicode scalar values, and user-perceived characters. Includes String, &str, OsString, and CString examples.

Rust strings differ in ownership, encoding, and the APIs they work with. This guide compares String, &str, operating-system strings, and C strings, then explains UTF-8 access. If owned and borrowed values are unfamiliar, read Rust ownership first.

1. &str: String Slice

The &str type is a string slice, which acts as a reference to a string. It is immutable and represents a view into a UTF-8 encoded string. For instance:

let s: &str = "hello";
Rust

In this example, s is a string slice pointing to the literal "hello". String literals are inherently &str, and they are always valid UTF-8 sequences.

2. String: Heap-Allocated String

The String type is a heap-allocated, growable, mutable, owned string type. Unlike &str, String can be modified:

let mut msg = String::from("hello");
msg.push_str(" world");
Rust

Here, msg is a String instance that starts with "hello" and can be expanded to "hello world". Internally, String maintains a pointer to its data, the length of the string, and its capacity. This allows dynamic resizing.

3. OsStr and OsString

OsStr and OsString are platform-specific types used for interoperability with the operating system’s string representations. OsStr is a string slice, while OsString is a heap-allocated string:

use std::ffi::OsStr;
use std::ffi::OsString;

let os_str: &OsStr = OsStr::new("platform_specific_string");
let os_string = OsString::from("platform_specific_string");
Rust

These types are especially useful for handling file paths and other OS-specific string operations.

4. CString and CStr

CString and CStr are used for working with C-style strings:

  • CString is a heap-allocated, null-terminated string.
  • CStr is a string slice that references a null-terminated array of bytes:
use std::ffi::CString;
use std::ffi::CStr;

let c_string = CString::new("hello").expect("CString::new failed");
let c_str: &CStr = c_string.as_c_str();
Rust

These are essential for FFI (Foreign Function Interface) when interfacing with C code.

Indexing and UTF-8 Encoding

Rust strings are UTF-8 encoded and can contain multi-byte characters. Due to this encoding, direct indexing by integer is not supported. For example:

let word = "नमस्ते";
Rust
let c = word.chars().nth(0).unwrap();
println!("{} is the first character in {}", c, word);
Rust

Unicode Scalar Values and Grapheme Clusters

A grapheme cluster is a user-perceived character and can contain multiple Unicode scalar values. chars() iterates scalar values, not grapheme clusters. In the following example, the precomposed é happens to be both one scalar value and one grapheme cluster. A decomposed e followed by a combining acute accent would produce two char values. The standard-library documentation explains this distinction; grapheme segmentation requires a separate Unicode-aware library.

let grapheme = "é".chars().nth(0).unwrap();
println!("Grapheme: {}", grapheme);
Rust

Conclusion

Use String when the function needs owned, growable UTF-8 text, and &str when it can borrow text. Choose the OS and C variants for their specific interoperability boundaries. Continue with references and borrowing to design function parameters, or revisit the char scalar type for the underlying Unicode distinction.

Share
Prabhat Kashyap

Prabhat Kashyap

Senior Technical Architect at HCLTech · working with Leonteq Security AG

I have 10+ years of experience building distributed systems and fintech platforms. I write about practical, non-obvious engineering details that official documentation often skips.

Newsletter

Get new engineering deep dives

Scala, JVM, AI, and backend systems. I send practical articles when there is something worth reading.

Join the newsletter