Recommended resources
The freely available resources this lesson will direct you to.
Good to see you again. In the previous lesson, traits and generic bounds made an API’s behavioral contract explicit: a type parameter such as C: SegmentCatalog tells the compiler what operations are available. Lifetimes express a different kind of contract: when an API returns a borrow, they tell the compiler which input data that borrow may come from and how long it may be used safely.
This matters directly for media parsing. An MP4 or manifest parser often should avoid copying slices of a downloaded buffer. Instead, it can return lightweight views into that buffer. By the end of this lesson, you should be able to read a signature such as fn parse<'src>(input: &'src [u8]) -> BoxView<'src> and explain both its performance benefit and its safety constraint.
Plan for roughly 40 minutes, including the short videos and Rust Book reading.
The question behind every borrowed parser result
A reference is useful only while the data it points to still exists. Rust calls the span in which a reference remains valid its lifetime. Lifetimes prevent a reference from surviving the value it points into, which would otherwise create a dangling reference.
Start with this concise explanation of why a function returning a reference sometimes needs an explicit lifetime relationship.
Rust Lifetimes Finally Explained!
Watch “Rust Lifetimes Finally Explained!” by Let's Get Rusty. It establishes the key idea: annotations describe relationships between borrowed inputs and outputs; they do not prolong data.
Watch the function contract for the longest example and the meaning of one shared lifetime label. Then watch the scope test, where a shorter lived argument restricts how long the returned borrow can be used. Focus on why the compiler must be conservative when a function may return either input.
The classic signature is:
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() > right.len() {
left
} else {
right
}
}
Read this as a contract, not as a description of a clock or a memory-management action:
'ais a generic lifetime parameter, similar in spirit to a generic type parameter such asT.- Both inputs are references valid for at least some common lifetime
'a. - The returned
&stris tied to that same lifetime because it could be a view into eitherleftorright. - At a call site, the usable lifetime of the result is limited by the shorter-lived input.
The annotation does not make either string live longer. It also does not mean the two inputs have identical lexical scopes. Instead, it tells Rust that the returned reference can be used only for the overlap in which both possible sources are valid.

The labels themselves are arbitrary. Rust programmers often write 'a, 'b, and 'c; in a parser API, names such as 'src and 'input are often clearer because they reveal what data is being borrowed.
A zero-copy parser contract
Suppose the player has downloaded an initialization segment into a Vec<u8>. A parser can either allocate separate byte arrays for every parsed item, or it can return references into the original buffer.
Here is a deliberately small representation of a parsed ISO-BMFF-like box:
#[derive(Debug)]
struct BoxView<'src> {
kind: &'src [u8],
payload: &'src [u8],
}
#[derive(Debug)]
enum ParseError {
TooShort,
}
fn parse_box<'src>(source: &'src [u8]) -> Result<BoxView<'src>, ParseError> {
if source.len() < 8 {
return Err(ParseError::TooShort);
}
Ok(BoxView {
kind: &source[4..8],
payload: &source[8..],
})
}
The central relationship is:
source: &'src [u8]
BoxView<'src> {
kind: &'src [u8],
payload: &'src [u8],
}
BoxView owns neither kind nor payload. Each field is a view into the caller-owned byte buffer. Consequently, creating a BoxView is cheap: it copies reference metadata, not the payload bytes.
| Value | Owns the media bytes? | Role |
|---|---|---|
Vec<u8> |
Yes | The downloaded initialization or fragment buffer |
&'src [u8] |
No | A borrowed view into that buffer |
BoxView<'src> |
No | A structured collection of borrowed views |
The lifetime parameter on the struct is essential:
struct BoxView<'src> {
payload: &'src [u8],
}
It means: an instance of BoxView<'src> may contain references valid for 'src, so the instance cannot be used after its borrowed source becomes invalid.
A typical safe use looks like this:
let bytes = vec![0, 0, 0, 16, b'm', b'o', b'o', b'f', 1, 2, 3, 4];
let parsed = parse_box(&bytes).expect("valid box header");
println!("box kind: {:?}", parsed.kind);
println!("payload bytes: {:?}", parsed.payload);
Here, bytes owns the data and remains alive while parsed is used. Rust permits this.
By contrast, this arrangement must fail:
let parsed;
{
let bytes = vec![0, 0, 0, 16, b'm', b'o', b'o', b'f', 1, 2, 3, 4];
parsed = parse_box(&bytes).expect("valid box header");
}
println!("{:?}", parsed.payload);
The inner bytes vector is dropped at the end of its block. Since parsed.payload would point into freed storage afterward, Rust rejects the assignment before the program can run.
This is the practical interpretation of a parser type like ParsedMpd<'src>, Moof<'src>, or SampleTable<'src>: keep the original source buffer alive for as long as you retain parsed views into it.
Annotations describe a relationship; they do not repair invalid ownership
A common misconception is that adding a lifetime label somehow persuades Rust to accept a reference. It does not. The compiler still verifies that the implementation actually honors the declared contract.
For example, this function promises to return a reference into input:
fn first_component<'src>(
input: &'src str,
separator: &str,
) -> &'src str {
input.split(separator).next().unwrap_or("")
}
The output is marked 'src, while separator has an independent, elided lifetime. That is correct: the returned string slice comes from input, not from separator.
This API can therefore accept a short-lived separator without unnecessarily shortening the returned view:
let manifest = "audio|video";
let first = first_component(manifest, &String::from("|"));
println!("{first}");
The temporary String only has to remain valid during the call. The returned first points into manifest, so it is governed by the lifetime of manifest.
Now consider an impossible promise:
fn wrong<'src>(input: &'src str, separator: &str) -> &'src str {
separator
}
Rust rejects this function. The output contract says the result is valid for 'src, but separator is not guaranteed to live that long. The compiler checks the function body against the signature’s claim.
The same principle rules out returning a reference to a local allocation:
fn uppercase_label(input: &str) -> &str {
let label = input.to_uppercase();
&label
}
label is dropped when uppercase_label returns, so a borrow of it cannot escape. If the parser must create new data, it must return an owned value:
fn uppercase_label(input: &str) -> String {
input.to_uppercase()
}
This gives a useful design rule:
Return a borrowed value when it is a view into caller-provided or otherwise long-lived data. Return an owned value when parsing or transformation creates new data.
Reading common lifetime syntax in library APIs
The Rust Book’s treatment is worth studying because it separates the core safety problem, function signatures, borrowed structs, and the compiler’s elision rules.
Validating References with Lifetimes
Read the relevant portions of “Validating References with Lifetimes” in The Rust Programming Language. The goal is not to memorize punctuation, but to learn how a signature states where a returned borrow can originate.
In “Generic Lifetimes in Functions,” read the call site reasoning. Relate the overlap of x and y to the possible source of the returned reference. Then, in “Lifetime Annotation Syntax,” read the annotation syntax. Notice where a label is declared and where it is attached to a reference. In “In Struct Definitions,” read the borrowed struct example. Treat ImportantExcerpt<'a> as the same basic pattern as BoxView<'src>. Finally, in “Lifetime Elision,” read the three rules. Focus on when omitted annotations are predictable and when ambiguity remains.
Lifetime elision: annotations that Rust writes mentally
Not every lifetime relationship appears in source code. Rust applies three deterministic elision rules to many function and method signatures:
- Each reference parameter initially receives its own lifetime.
- If a function has exactly one input lifetime, Rust assigns it to borrowed outputs.
- For a method with
&selfor&mut self, Rust normally assigns the lifetime of thatselfborrow to borrowed outputs.
That is why this compiles without visible annotations:
fn first_box(input: &[u8]) -> &[u8] {
&input[..4]
}
Rust can interpret it as though it were written:
fn first_box<'src>(input: &'src [u8]) -> &'src [u8] {
&input[..4]
}
But with two possible source references, elision cannot choose for you:
fn choose_box(left: &[u8], right: &[u8]) -> &[u8] {
// Which input does the result borrow from?
todo!()
}
A public parser API should generally make non-obvious source relationships explicit. It helps both the compiler and the human reader understand which retained buffer owns the bytes.
Parser objects and methods: source lifetime versus borrow of the parser
Parsers frequently maintain a cursor while returning views into a fixed input buffer. This is an important API shape to recognize:
struct FragmentParser<'src> {
remaining: &'src [u8],
}
impl<'src> FragmentParser<'src> {
fn new(source: &'src [u8]) -> Self {
Self { remaining: source }
}
fn next_chunk(&mut self) -> Option<&'src [u8]> {
if self.remaining.len() < 4 {
return None;
}
let (chunk, rest) = self.remaining.split_at(4);
self.remaining = rest;
Some(chunk)
}
}
next_chunk takes &mut self, but returns &'src [u8], not simply &[u8].
That difference is meaningful:
&mut selfgives the method temporary permission to advance the parser cursor.&'src [u8]says the returned chunk points into the original source buffer.- The returned chunk is not tied to the temporary borrow of
self; it is tied to the buffer supplied when the parser was constructed.
Therefore, a caller can retain a parsed chunk and continue advancing the cursor:
let bytes = [10, 11, 12, 13, 20, 21, 22, 23];
let mut parser = FragmentParser::new(&bytes);
let first = parser.next_chunk().unwrap();
let second = parser.next_chunk().unwrap();
assert_eq!(first, &[10, 11, 12, 13]);
assert_eq!(second, &[20, 21, 22, 23]);
The original bytes array is immutable and remains alive, so retaining both views is safe.
A subtle accessor design choice
Consider a parsed value with a borrowed field:
impl<'src> BoxView<'src> {
fn payload(&self) -> &[u8] {
self.payload
}
fn payload_from_source(&self) -> &'src [u8] {
self.payload
}
}
The first method relies on elision. Its returned reference is associated with the borrow of self. This is usually the right ergonomic choice for an ordinary accessor.
The second method explicitly says the returned data is tied to the original source lifetime, rather than the particular temporary borrow of self. This is useful when an API deliberately yields source-backed views that may be retained independently of a parser cursor or short-lived wrapper.
When interpreting an unfamiliar parser crate, ask:
- What owns the bytes? Look for a
Vec<u8>,Bytes, memory map, string, or caller-provided slice. - Which references share a lifetime label? Those references participate in one stated validity relationship.
- What does the return type borrow from? Follow the output label back to an input or a field of a borrowed type.
- Can the parser wrapper be dropped while output survives? An explicit source lifetime on the output often signals that this is intended.
- Does the parser transform data? If it allocates or synthesizes content, expect an owned output such as
String,Vec<u8>, or a struct that owns those values.
When two lifetime parameters are genuinely needed
Most parser APIs need just one source lifetime. Multiple lifetimes become useful when an object stores multiple references that serve different roles and may live for different durations.
For example, a string splitter may borrow both an input and a delimiter, but yield slices from the input only:
struct SplitView<'input, 'delimiter> {
remaining: &'input str,
delimiter: &'delimiter str,
}
impl<'input, 'delimiter> SplitView<'input, 'delimiter> {
fn next(&mut self) -> Option<&'input str> {
let position = self.remaining.find(self.delimiter)?;
let item = &self.remaining[..position];
self.remaining = &self.remaining[position + self.delimiter.len()..];
Some(item)
}
}
The distinct labels express two facts:
remainingborrows the input text.delimiterborrows a separate value.nextreturns a slice tied to the input, not to the delimiter.
If both fields were unnecessarily assigned one shared lifetime, a short-lived delimiter could artificially limit how long the returned input slice may be used. Separate lifetimes prevent the API from making a stronger restriction than its implementation actually needs.
The following excerpt from Jon Gjengset’s parser-focused lifetime discussion develops exactly this distinction.
Crust of Rust: Lifetime Annotations
Watch selected portions of Jon Gjengset’s “Crust of Rust: Lifetime Annotations.” The example is a custom string splitter, but its structure is directly applicable to parsers that retain input slices and return zero-copy views.
Watch the source relationship to see why a returned item can be tied to the original input rather than to the splitter object itself. Then watch separate lifetimes, which shows why an input and a delimiter may need distinct labels when only the input is returned.
You will not often need multiple lifetime parameters when writing ordinary application code. But when reading a parser, they are a precise map of where each returned slice can originate.
A note on 'static
'static is a special lifetime meaning that a reference can remain valid for the entire program run. String literals are a familiar example:
let codec_name: &'static str = "avc1";
This does not mean “make the compiler stop complaining” or “put any temporary value in static memory.” A parsed slice of a network response is ordinarily tied to that response buffer, not 'static.
In later async work, you will encounter bounds involving 'static on tasks. For now, keep the distinction clear:
&'static stris a reference to data that lasts for the program’s lifetime.BoxView<'src>borrows a particular source buffer and is valid only while that source remains available.
Key takeaways
- A lifetime annotation states a borrowing relationship; it does not extend the life of any value.
fn parse<'src>(input: &'src [u8]) -> BoxView<'src>means the result may contain views intoinput.- A struct such as
BoxView<'src>does not own the source bytes, so the original buffer must outlive every view derived from it. - A function cannot return a reference to a local value. Return an owned type when the function creates new data.
- Rust often elides straightforward lifetimes, but explicit annotations are necessary when an output’s source would otherwise be ambiguous.
- A parser method can mutate its cursor through
&mut selfwhile returning a slice tied to the original input lifetime. - Multiple lifetime parameters are useful when a type borrows several independent inputs but its outputs are tied to only one of them.
'staticis a specific promise about program-long validity, not a general escape hatch.
Next, you will build on ownership, traits, and lifetimes by constructing move closures that capture owned state safely for callbacks and, soon afterward, asynchronous tasks.