<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Manish Sah</title>
    <description>The latest articles on DEV Community by Manish Sah (@csemanish12).</description>
    <link>https://dev.to/csemanish12</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F518218%2F515afa25-1c3d-4b7d-8387-0c66d2377fa7.jpeg</url>
      <title>DEV Community: Manish Sah</title>
      <link>https://dev.to/csemanish12</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/csemanish12"/>
    <language>en</language>
    <item>
      <title>Rust Strings Demystified: Literals, Slices, and Fat Pointers Under the Hood</title>
      <dc:creator>Manish Sah</dc:creator>
      <pubDate>Wed, 22 Jul 2026 03:54:29 +0000</pubDate>
      <link>https://dev.to/csemanish12/rust-strings-demystified-literals-slices-and-fat-pointers-under-the-hood-4o34</link>
      <guid>https://dev.to/csemanish12/rust-strings-demystified-literals-slices-and-fat-pointers-under-the-hood-4o34</guid>
      <description>&lt;p&gt;When developers switch to Rust from high-level languages like Python or JavaScript, one of the first mental speedbumps is the string system.&lt;/p&gt;

&lt;p&gt;Why are there two main string types (&lt;code&gt;String&lt;/code&gt; vs &lt;code&gt;&amp;amp;str&lt;/code&gt;)? What actually happens when a function returns &lt;code&gt;"Yummy!"&lt;/code&gt;? And why is a string slice called a &lt;strong&gt;"fat pointer"&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;In this post we'll look under the hood at how Rust lays out string literals, string slices, and pointers in memory — and why this design makes Rust strings fast and memory-efficient.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Literal vs. Slice
&lt;/h2&gt;

&lt;p&gt;Two terms get conflated a lot, so let's separate them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Literal:&lt;/strong&gt; text hardcoded directly into your &lt;code&gt;.rs&lt;/code&gt; source file (e.g., &lt;code&gt;"Hello, world!"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slice (&lt;code&gt;&amp;amp;str&lt;/code&gt;):&lt;/strong&gt; a &lt;em&gt;view&lt;/em&gt; into a contiguous run of valid UTF-8 bytes somewhere in memory, without owning them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The relationship between the two:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every string literal is a string slice, but not every string slice is a string literal.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. A string literal — embedded in the compiled binary.&lt;/span&gt;
    &lt;span class="c1"&gt;// Type: &amp;amp;'static str&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Hello, world!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. A dynamic String — allocated at runtime on the heap.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;user_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="nf"&gt;.push_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Rustacean"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. A slice into that heap string.&lt;/span&gt;
    &lt;span class="c1"&gt;// Type: &amp;amp;str (points into user_input's heap buffer, not the binary)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;dynamic_slice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// "Rust"&lt;/span&gt;

    &lt;span class="c1"&gt;// Both are &amp;amp;str, so the same function accepts either.&lt;/span&gt;
    &lt;span class="nf"&gt;print_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// "Hello, world!"&lt;/span&gt;
    &lt;span class="nf"&gt;print_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dynamic_slice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "Rust"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;print_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Printing slice: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;literal&lt;/code&gt; is a string literal, so its type is &lt;code&gt;&amp;amp;'static str&lt;/code&gt; — it's embedded directly in the binary.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dynamic_slice&lt;/code&gt; is a valid &lt;code&gt;&amp;amp;str&lt;/code&gt;, but &lt;strong&gt;not&lt;/strong&gt; a literal — it was carved out of &lt;code&gt;user_input&lt;/code&gt; at runtime and didn't exist in your source code.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Normal Pointer vs. Fat Pointer
&lt;/h2&gt;

&lt;p&gt;In C, a string pointer is a plain 8-byte address (on 64-bit). String functions scan forward byte-by-byte until they hit a null terminator (&lt;code&gt;\0&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Rust drops null terminators entirely. Instead, &lt;code&gt;&amp;amp;str&lt;/code&gt; is a &lt;strong&gt;fat pointer&lt;/strong&gt;: two 64-bit fields packed back to back, 16 bytes total on a 64-bit target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────┬─────────────────────────┐
│     Pointer (8 bytes)   │     Length (8 bytes)    │
├─────────────────────────┼─────────────────────────┤
│  0x00007FFF00401050     │            6             │
└─────────────────────────┴─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pointer:&lt;/strong&gt; the address of the first byte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Length:&lt;/strong&gt; the number of bytes in the slice.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: this ptr-then-length picture is the right &lt;em&gt;mental model&lt;/em&gt;, but Rust doesn't guarantee that exact field ordering as part of its stable ABI. What &lt;em&gt;is&lt;/em&gt; guaranteed is that a &lt;code&gt;&amp;amp;str&lt;/code&gt;/&lt;code&gt;&amp;amp;[T]&lt;/code&gt; reference is two machine words wide.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. What Happens When a Function Returns &lt;code&gt;"Yummy!"&lt;/code&gt;?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;picky_eater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;food&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"strawberry"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"Yummy!"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;food&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"potato"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"I guess I can eat that."&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"No thanks!"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step A: The read-only data segment (&lt;code&gt;.rodata&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;At compile time, string literals like &lt;code&gt;"Yummy!"&lt;/code&gt; get written into the binary's read-only data segment (&lt;code&gt;.rodata&lt;/code&gt;). When the OS loads the binary, that data lands at a fixed address in RAM for the entire life of the program — which is exactly why literals get the &lt;code&gt;'static&lt;/code&gt; lifetime.&lt;/p&gt;

&lt;p&gt;Say the OS loads &lt;code&gt;"Yummy!"&lt;/code&gt; at &lt;code&gt;0x00007FFF00401050&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;th&gt;Byte (hex)&lt;/th&gt;
&lt;th&gt;Character&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x00007FFF00401050&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x59&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'Y'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x00007FFF00401051&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x75&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'u'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x00007FFF00401052&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x6D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'m'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x00007FFF00401053&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x6D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'m'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x00007FFF00401054&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x79&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'y'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x00007FFF00401055&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x21&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'!'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step B: Constructing and returning the fat pointer
&lt;/h3&gt;

&lt;p&gt;Rust doesn't copy those 6 bytes onto the stack. It just constructs a 16-byte fat pointer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pointer&lt;/strong&gt; = &lt;code&gt;0x00007FFF00401050&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Length&lt;/strong&gt; = &lt;code&gt;6&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the underlying bytes live in &lt;code&gt;.rodata&lt;/code&gt; for the program's entire lifetime, returning this pointer is completely safe — you're never pointing at stack memory that disappears when the function returns.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Zero-Cost Slicing (and its one sharp edge)
&lt;/h2&gt;

&lt;p&gt;Because length is metadata on the pointer itself, sub-slicing is zero-cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Yummy!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// "ummy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No new allocation, no copy — just a new 16-byte fat pointer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory:     [  'Y'  |  'u'  |  'm'  |  'm'  |  'y'  |  '!'  ]
Address:     ...1050  ...1051  ...1052  ...1053  ...1054  ...1055
             ▲        ▲                             ▲
             │        └──────────────┬──────────────┘
             │                       │
`full`:  ptr = ...1050, len = 6      │
`sub`:   ptr = ...1051, len = 4 ─────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;full&lt;/code&gt;: pointer = &lt;code&gt;0x...1050&lt;/code&gt;, length = &lt;code&gt;6&lt;/code&gt; (&lt;code&gt;"Yummy!"&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt;: pointer = &lt;code&gt;0x...1051&lt;/code&gt; (offset by 1 byte), length = &lt;code&gt;4&lt;/code&gt; (&lt;code&gt;"ummy"&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both slices point into the exact same memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gotcha:&lt;/strong&gt; those slice indices are &lt;em&gt;byte&lt;/em&gt; offsets, and Rust requires them to land on UTF-8 character boundaries. &lt;code&gt;&amp;amp;full[1..5]&lt;/code&gt; works because every character in &lt;code&gt;"Yummy!"&lt;/code&gt; is a single ASCII byte. Slice a multi-byte character (say, an emoji or accented letter) in the middle of its encoding, and Rust panics at runtime rather than handing back corrupted UTF-8. Coming from Python, where &lt;code&gt;str[1:5]&lt;/code&gt; just silently does the "wrong" thing on multi-byte content, this is worth internalizing early.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Summary Cheat Sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Lifetime / Memory&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Literal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text hardcoded in source (&lt;code&gt;"..."&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'static&lt;/code&gt;, lives in &lt;code&gt;.rodata&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Embedded in the binary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Slice (&lt;code&gt;&amp;amp;str&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A view (pointer + length) into valid UTF-8 bytes.&lt;/td&gt;
&lt;td&gt;Depends on what it borrows.&lt;/td&gt;
&lt;td&gt;16 bytes (fat pointer).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;String&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Owned, growable, heap-allocated UTF-8 buffer.&lt;/td&gt;
&lt;td&gt;RAII — dropped when it goes out of scope.&lt;/td&gt;
&lt;td&gt;24 bytes (pointer, length, and capacity fields).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why this design matters
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;O(1) length lookups&lt;/strong&gt; — &lt;code&gt;.len()&lt;/code&gt; costs nothing at runtime; the length is already sitting in the fat pointer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-copy passing&lt;/strong&gt; — handing a 1 GB string slice to a function copies 16 bytes of pointer metadata, not the string.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>computerscience</category>
      <category>performance</category>
      <category>programming</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
