Course contentsShow
← Rust
Lesson 427 of 1,9104. Lifetimes and Advanced OwnershipFree lesson

State Machines with Lifetime Bounds

Use state-specific structs to transition between valid and invalid lifetime states.

In this lesson, you will learn how to encode valid state transitions into your type system by using struct-specific lifetimes to restrict when data can be accessed.

The Problem: Temporal Validity

In complex systems—like network protocol parsers or hardware drivers—data is often only valid during specific stages of a lifecycle. For example, a PacketBuffer might hold raw bytes that are only safe to read after an Authenticated state has been reached. If we aren't careful, we might accidentally read garbage memory from a "pre-authenticated" state.

Using State-Specific Structs

Instead of using a generic Buffer struct with a runtime is_authenticated flag, we can define distinct types. By tying these types to the lifetime of the underlying data source, we ensure that a state cannot exist longer than the buffer it depends on.

Consider this pattern:

struct RawBuffer<'a>(&'a [u8]);

struct AuthenticatedBuffer<'a> {
    data: &'a [u8],
    // The lifetime 'a ensures this struct cannot outlive the original buffer
}

impl<'a> RawBuffer<'a> {
    fn authenticate(self) -> AuthenticatedBuffer<'a> {
        // Logic to verify data goes here
        AuthenticatedBuffer { data: self.0 }
    }
}

Enforcing Transitions

By consuming RawBuffer in the authenticate method, we ensure that the "raw" state cannot be used again once the transition to "authenticated" occurs. The lifetime 'a acts as a guardrail: if the original data source (e.g., a network socket buffer) is dropped, the compiler prevents both the RawBuffer and the AuthenticatedBuffer from existing.

Because the AuthenticatedBuffer holds a reference with the same lifetime 'a, it is physically impossible to access the data once the underlying resource has been reclaimed. This moves the validation from a runtime if check to a compile-time guarantee. You are effectively "pinning" the state of your machine to the lifetime of the data it processes. This eliminates entire classes of bugs where state logic runs on stale memory buffers.

Key Takeaway: By binding state-specific structs to the lifetime of their input data, you force the compiler to prove that your state machine only operates on valid, non-stale data.