<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Compute Bound]]></title><description><![CDATA[A technical log focused on the engineering of BLAS libraries, Rust programming, and the internal mechanics of modern GPU accelerators.]]></description><link>https://vinayakdsci.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 18:39:43 GMT</lastBuildDate><atom:link href="https://vinayakdsci.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Exploring Rust: Lifetimes]]></title><description><![CDATA[The Borrow Checker
I have written mostly C and C++ as long as I have programmed.
However, as I have been experimenting with Rust lately, I have gained the opinion that it is undeniable that the langua]]></description><link>https://vinayakdsci.hashnode.dev/exploring-rust-lifetimes</link><guid isPermaLink="true">https://vinayakdsci.hashnode.dev/exploring-rust-lifetimes</guid><category><![CDATA[Rust]]></category><category><![CDATA[rust lang]]></category><category><![CDATA[RustLang]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Vinayak Dev]]></dc:creator><pubDate>Tue, 24 Mar 2026 03:22:26 GMT</pubDate><content:encoded><![CDATA[<h3>The Borrow Checker</h3>
<p>I have written mostly C and C++ as long as I have programmed.</p>
<p>However, as I have been experimenting with Rust lately, I have gained the opinion that it is undeniable that the language does have some features that are a great help in reducing the developer's cognitive load by offloading some safety enforcements to the compiler. Most of these safety measures are enforced by the compiler through the in-built (and notorious) <strong>Borrow Checker</strong>.</p>
<p>In simple words, the Borrow Checker is an XOR: it enforces that any entity in the program either be mutable or be aliasable. This means, at a very fundamental level, that the compiler now has additional scope of optimization, because it does not have to assume that a block of memory could be aliased under another pointer, like a C or C++ compiler usually would.</p>
<p>The Borrow Checker also enforces the notion of <strong>Lifetimes</strong> of objects. The basic idea is that if I were to construct some object (let's call it <code>A</code>) from a non-owning reference to some other object (which we shall call <code>B</code>) that was live at the time of construction of <code>A</code>, then <code>A</code> should <em>not</em> outlive <code>B</code>, because in that case <code>A</code> would basically have a reference to an object that has been freed from memory.</p>
<p>The thing is, when I first started learning Rust, I had an incredibly hard time with it. Most of the tutorials I looked at, or the videos I saw on YouTube, assumed I was familiar with the whole Rust standard library and would use these super-high-level abstractions that would make it extremely hard to follow what was actually happening.</p>
<p>Coming from a C-based <em>pointers-are-memory</em> and <em>everything-is-unsafe</em> model, I was looking for something that could explain to me how <em>unsafe</em> code could be made safer in Rust, so I worked out a few snippets on my own after going through implementations in the Rust standard library. And <em>that</em>, is what this post is all about.</p>
<p>In the following sections, we will deliberately use unsafe Rust code to create a <code>use-after-free</code> condition, then evolve the faulty code so that the compiler understands the lifetimes of all related objects and compilation fails in case an object on which another object is dependent does not live long enough.</p>
<h3>Unsafe Rust: Raw Pointers and Use-After-Free</h3>
<p>The Rust compiler usually has <em>no</em> notion of what a raw pointer is tethered to. The compiler cannot track its lifetime or perform bounds checking when it is indexed. Basically, a raw pointer is the <em>definition</em> of unsafe in Rust.</p>
<p>To demonstrate a classic <code>use-after-free</code> hazard, let's first construct a struct that holds a pointer, and its length (basically a simple fat-pointer). Then, to overload the <code>[]</code> operator so we can directly index into the memory, we shall implement the <code>Index</code> trait for the struct, and then write a simple assertion just to check if our implementation is correct.</p>
<p>Let's call this struct <code>DataBuffer</code>.</p>
<pre><code class="language-rust">struct DataBuffer {
    data: *const f32,
    length: usize,
}
</code></pre>
<p>The member <code>data</code> is a const raw pointer to memory of type <code>f32</code>.</p>
<p>Let us implement a static method called <code>new(...)</code> on this struct that takes a <strong>non-owning</strong> <strong>reference</strong> to a slice of 1-D data of type <code>f32</code> and constructs a <code>DataBuffer</code> from it.</p>
<pre><code class="language-rust">impl DataBuffer {
    fn new(sl: &amp;[f32]) -&gt; Self {
        Self {
            data: sl.as_ptr(),
            length: sl.len(),
        }
    }
}
</code></pre>
<p>The function, as I mentioned above, takes a reference to a slice, then extracts the pointer by calling the method <code>as_ptr()</code> on it. The method constructs a <code>DataBuffer</code> and then returns it.<br />The thing to note here is, the <em>original</em> vector from which this slice, which the <code>new()</code> method accepted, was constructed, is still owned by its <em>original</em> owner who passed it into the <code>new()</code> method.</p>
<p>If I were to clone the whole thing, the concept of lifetimes would never arise here, because the compiler would <strong>move</strong> the cloned memory into the new <code>DataBuffer</code> object and it would become the owner of that memory. But that would also mean we could kiss performance goodbye, so here we are.</p>
<p>We'll now complete the basics and implement <code>Index</code> for <code>DataBuffer</code> so we can index into it.</p>
<pre><code class="language-rust">impl Index&lt;usize&gt; for DataBuffer {
    type Output = f32;
    fn index(&amp;self, i: usize) -&gt; &amp;Self::Output {
        assert!(i &lt; self.length);
        unsafe { &amp;*self.data.add(i) } 
    } 
}
</code></pre>
<p>There are a couple things to note here. First is the assertion. We noted that the compiler does <em>not</em> do bounds checking for raw pointers, so it is up to us to guarantee that we do not perform an out-of-bounds access on the array. However, this assert is also horrible for performance, and of course, there are much more elegant solutions in production scenarios.</p>
<p>The <em>other</em> thing to note here is the use of <code>unsafe</code> when dereferencing the pointer. Rust treats the <em>dereference</em> as unsafe, but <em>not</em> the creation of the pointer. That means that every time you deref a raw pointer, it has to be wrapped in an <code>unsafe</code> block. We offset the pointer by the required index, dereference it, then borrow and return the value.</p>
<p>It is time to write a main function and compile the code so we can see if it works.</p>
<pre><code class="language-rust">fn main() {
    let buf = vec![0.0, 1.0, 2.0, 3.0];
    let dbuffer = DataBuffer::new(&amp;buf);
    assert_eq!(3.0, dbuffer[3]);
}
</code></pre>
<p>On running, we should see the code compile fine and the assertion pass.</p>
<pre><code class="language-plaintext">Compiling playground v0.0.1 (/playground)
 Finished dev profile [unoptimized + debuginfo] target(s) in 0.71s
  Running target/debug/playground
</code></pre>
<p>Now let's spice things up :)</p>
<p>We rewrite the main function, but now we declare the variable <code>dbuffer</code> as <em>mutable</em> so we can overwrite it. We now overwrite it in a <em>scope</em> inside the main function, in which we also re-declare the original vector we called <code>buf</code> from which <code>dbuffer</code> was created.</p>
<pre><code class="language-rust">fn main() {
    let buf = vec![0.0, 1.0, 2.0, 3.0];

    // dbuffer is now mutable.
    let mut dbuffer = DataBuffer::new(&amp;buf);

    // And here is the scope. Cool thing is, buf
    // will be freed from memory the moment this
    // scope goes OUT of scope.
    {
        // Re-declare buf.
        let buf = vec![0.0, 1.0, 2.0, 3.0];
        // Now overwrite dbuffer to create it
        // from the new buf created in this scope.
        dbuffer = DataBuffer::new(&amp;buf);
    }

    assert_eq!(3.0, dbuffer[3]);
}
</code></pre>
<p>See what happened here? We forced a classic <code>use-after-free</code>! At the end of the scope inside the main function (in which <code>buf</code> was re-declared), the memory associated with <code>buf</code> will be dropped. However, the variable <code>dbuffer</code> which holds a raw pointer to that memory is <strong>still live</strong>. And that means we have now got a <strong>dangling pointer</strong>.</p>
<p>And that is apparent when we run the code and hit the assertion. The safety guarantees of Rust fail, because of course we asked for it by playing with raw pointers.</p>
<pre><code class="language-plaintext">Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/playground`

thread 'main' (13) panicked at src/main.rs:90:5:
assertion `left == right` failed
  left: 3.0
 right: 2.0701483e-19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
</code></pre>
<p>Now here is the thing: How do we use the compiler to fix this? Which brings us to...</p>
<h3>Lifetimes</h3>
<p>Rust allows the developer to syntactically relate the lifetimes of variables in the code. This is a very interesting feature indeed, and we shall now modify our definition of the <code>DataBuffer</code> struct to make use of it.</p>
<p>Lifetimes are usually specified with an identifier following an apostrophe (<code>'</code>) in definitions and function arguments. Let's say that we want our struct to live a lifetime of <code>'a</code>. Note that the letter <code>a</code> can be any other identifier here, it does not really matter.</p>
<pre><code class="language-rust">struct DataBuffer&lt;'a&gt; {
    data: *const f32,
    length: usize,
}
</code></pre>
<p>We introduced the lifetime in the definition of <code>DataBuffer</code>. The same is required for the <code>impl</code> block.</p>
<pre><code class="language-rust">impl&lt;'a&gt; DataBuffer&lt;'a&gt; {
    fn new(sl: &amp;'a [f32]) -&gt; Self {
        Self {
            data: sl.as_ptr(),
            length: sl.len(),
        }
    }
}
</code></pre>
<p><em>This</em> is where the compiler actually <em>enforces</em> the lifetime constraints. We wanted <code>DataBuffer</code> to have a lifetime of <code>'a</code>, but that does not have a significance on its own. To give it meaning, we also tell the compiler that this is <em>also</em> how long the underlying reference from which we construct the struct will live.</p>
<p>This is done by passing in the lifetime in the function argument to <code>new(...)</code> as <code>sl: &amp;'a [f32]</code>. This tells the compiler that <code>DataBuffer</code> <em>cannot</em> outlive the reference, because we explicitly told it to enforce the same lifetimes on both of them.</p>
<p>In the implementation of the <code>Index</code> trait we just introduce an anonymous lifetime and let the compiler figure it out for us.</p>
<pre><code class="language-rust">impl Index&lt;usize&gt; for DataBuffer&lt;'_&gt;
{
    type Output = f32;
    fn index(&amp;self, i: usize) -&gt; &amp;Self::Output {
        assert!(i &lt; self.length);
        unsafe { &amp;*self.data.add(i) }
    }
}
</code></pre>
<p>But here's the catch. When you run the compiler on this code, it throws an error.</p>
<pre><code class="language-plaintext">error[E0392]: lifetime parameter `'a` is never used
  --&gt; src/main.rs:50:19
   |
50 | struct DataBuffer&lt;'a&gt; {
   |                   ^^ unused lifetime parameter
   |
   = help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData`
</code></pre>
<p>The reason for the error is apparent from the error message. We introduced the lifetime parameter in the struct definition, but none of its members make use of the lifetime. The compiler suggests we either remove it or make use of the marker <code>PhantomData</code>. Removing it defeats our purpose, but, as we shall see, <code>PhantomData</code> is exactly what we need.</p>
<p><code>PhantomData</code> is an interesting feature of Rust. It is a zero-sized entity that is optimized away by the compiler and has no effect on runtime. Its only use is that of a <strong>marker</strong>. We insert a member of the <code>PhantomData</code> type into our struct, so that the compiler sees that our struct indeed binds to the lifetime we are specifying.</p>
<p>The definitions change a little bit:</p>
<pre><code class="language-rust">use std::marker::PhantomData;

struct DataBuffer&lt;'a&gt; {
    data: *const f32,
    length: usize,
    _marker: PhantomData&lt;&amp;'a f32&gt;,

}

impl&lt;'a&gt; DataBuffer&lt;'a&gt; {
    fn new(sl: &amp;'a [f32]) -&gt; Self {
        Self {
            data: sl.as_ptr(),
            length: sl.len(),
            _marker: PhantomData,
        }
    }
}
</code></pre>
<p>We just added a new member of type <code>PhantomData</code> into the struct and initialized it as part of the construction of <code>DataBuffer</code>.</p>
<p>The <code>&amp;'a</code> inside the signature of <code>_marker</code> is what does the binding work: it tells the compiler that this struct logically borrows data for lifetime <code>'a</code> even though no member explicitly binds to it.</p>
<p>And that is pretty much all. For the exact same main function we had above, we should now see compilation fail!</p>
<pre><code class="language-plaintext">error[E0597]: `buf` does not live long enough
  --&gt; src/main.rs:90:35
   |
87 |         let buf = vec![0.0, 1.0, 2.0, 3.0];
   |             --- binding `buf` declared here
...
90 |         dbuffer = DataBuffer::new(&amp;buf);
   |                                   ^^^^ borrowed value does not live long enough
91 |     }
   |     - `buf` dropped here while still borrowed
92 |
93 |     assert_eq!(3.0, dbuffer[3]);
   |                     ------- borrow later used here
</code></pre>
<p>This marker pattern is actually a common Rust idiom that is used all over the Rust standard library, and many a times what you see as Rust safety guarantees against <code>use-after-free</code> hazards is this exact idiom in action.</p>
<h3>Conclusion</h3>
<p>The C compiler trusts you. It will very lovingly saw off your fingers if you order it to. The Rust compiler is reluctant to do that - so it gives you some options to free it from the guilt of the crime.</p>
]]></content:encoded></item></channel></rss>