Rust: Borrowed Views and Useful Interfaces
Connect lifetimes, typed offsets, From and Into, Display, derived traits, generic capabilities, and validated builders.
Cheatsheet for Lesson 3.4
A one-page summary of “Rust: Borrowed Views and Useful Interfaces” you can keep beside your notes. Plain Markdown.
Lesson 3.4 — Rust: Borrowed Views and Useful Interfaces: cheatsheet
- Remember: Use types to distinguish roles, traits to name capabilities, and builders to gather choices. Each serves a different purpose.
Quick reference
- Return a borrowed view:
&'a [u8]— View cannot outlive its input owner. - Distinguish integer roles:
struct Rva(u32);— A new type, not a renamed integer. - Generate diagnostics/equality:
#[derive(Debug, PartialEq, Eq)]— Implements supported capabilities from fields. - Permit cheap implicit copies:
CopyplusClone— Appropriate only when copying does not duplicate ownership duties. - Convert infallibly:
From<T>/.into()— Destination type defines the conversion. - Convert with validation:
TryFrom<T>/.try_into()— Caller handles a possible failure. - Write a user-facing label:
impl Display— Used by{}and.to_string(). - Ask for a capability:
&impl GoldReader— Borrows a concrete trait implementation. - Gather optional settings:
Builder::default().choice(...).build()— Validate before producing the ready object.
Review
- Q: A returned slice points into the caller’s buffer. What does a matching lifetime annotation express? A: The returned borrow is tied to the input borrow.
- Q: A function creates a temporary local array and returns a reference to it. Can naming a longer lifetime fix the problem? A: No; the local storage still ends when the call returns.
The previous lesson used checked slices and iterator queries. Those functions also made promises about access: some borrowed an existing collection, while others returned a newly owned result. Here we name those relationships and give functions interfaces that say which kinds of values they can accept.
A lifetime describes how borrows are related
Section titled “A lifetime describes how borrows are related”Suppose a function returns the first count bytes of its input. The
returned slice points into the caller’s buffer; it does not own a copy.
fn prefix<'a>(bytes: &'a [u8], count: usize) -> Option<&'a [u8]> { bytes.get(..count)}
fn main() { let buffer = [10_u8, 20, 30, 40]; if let Some(view) = prefix(&buffer, 2) { println!("{view:?}"); }}'a is a lifetime parameter. The repeated name says the returned
borrow is tied to the input borrow. It does not keep the buffer alive
longer. The compiler rejects using the view after the buffer is gone or
changing the borrowed elements while that shared view is still in use.
Here the view ends after its print, and the caller retains the buffer.
The caller’s buffer and what prefix returns
buffer: [u8; 4], owned by the caller
prefix(&buffer, 2) returns
no bytes copied
What the view holdsget(..2) fits inside the 4 bytes, so prefix returns a view of 2 bytes. The view stores where they start and how many there are; no byte is copied.
With one borrowed input, Rust can infer this relationship. The shorter
signature fn prefix(bytes: &[u8], count: usize) -> Option<&[u8]>
expresses the same borrowing relationship. Explicit names become useful
when several borrowed inputs make the relationship ambiguous, or when a
struct keeps a borrow. Lifetimes describe validity, not a number of seconds.
A returned view stays tied to its owner. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
fn prefix<'a>(bytes: &'a [u8], count: usize) -> Option<&'a [u8]> {
bytes.get(..count)
}
let buffer = [10_u8, 20, 30, 40];
let view = prefix(&buffer, count);A valid view can be used only while its owner is alive and the borrow permits it. The compiler checks this relationship.
Final values: owner = buffer in caller; count = 2; view = Some(&[10, 20]); copied_bytes = 0; lifetime = view cannot outlive buffer.
Read every step of the lesson’s example
Step 1, line 4. The caller owns buffer. No lifetime annotation extends how long it lives.
owner = buffer in caller; count = 2.
Memory: +0 = 0A; +1 = 14; +2 = 1E; +3 = 28.
Step 2, line 5. The function receives a temporary shared borrow, plus the requested count.
owner = buffer in caller; count = 2.
Memory: +0 = 0A; +1 = 14; +2 = 1E; +3 = 28.
Step 3, line 2. get lends a view into the same allocation. It does not copy the elements.
owner = buffer in caller; count = 2; view = Some(&[10, 20]); copied_bytes = 0.
Memory: +0 = 0A; +1 = 14; +2 = 1E; +3 = 28.
Step 4, line 5. A valid view can be used only while its owner is alive and the borrow permits it. The compiler checks this relationship.
owner = buffer in caller; count = 2; view = Some(&[10, 20]); copied_bytes = 0; lifetime = view cannot outlive buffer.
Memory: +0 = 0A; +1 = 14; +2 = 1E; +3 = 28.
Put behavior next to a struct with impl
Section titled “Put behavior next to a struct with impl”The Primer’s Runner record supplies the receiver’s fields:
struct Runner { name: String, stamina: u32, health: u32, max_health: u32,}This is record context for the method below, not a second record definition
to paste into the complete Mira/Sol program.
Replace the Primer’s brief forwarding method with this direct-check
alternative; keep one try_dash definition.
An impl block attaches behavior to Runner. This alternative method
performs the same guarded subtraction as the Primer’s dash function:
impl Runner { fn try_dash(&mut self, cost: u32) -> bool { if self.stamina < cost { return false; } self.stamina = self.stamina - cost; return true; }}self means the runner receiving the method call. &mut self gives
the method temporary editable access to that runner, like the earlier
&mut Runner parameter. Place this alternative impl block below the
two-runner program, replacing the forwarding method, and use
runner.try_dash(4). The result stays Mira 8 and Sol 3. The method
organizes the code differently; it keeps the same stamina check and subtraction. In Rust, a struct holds fields and an impl gives it methods;
class inheritance is not required.
Give numbers different types when they mean different things
Section titled “Give numbers different types when they mean different things”An RVA is an offset within a loaded module. A virtual address identifies a location in a process. Both can be represented with integers, but interchanging them produces a different question. A newtype wraps an existing value in a distinct type:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]struct Rva(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]struct VirtualAddress(usize);
fn resolve(base: VirtualAddress, offset: Rva) -> Option<VirtualAddress> { base.0.checked_add(offset.0 as usize).map(VirtualAddress)}Rva(512) and VirtualAddress(512) have different types even though their
numbers agree. resolve cannot receive them in the opposite order. .0
reads a tuple struct’s first field. The check rejects overflow; a real
reader must still validate the address against the intended process and
memory region.
Handles benefit from the same distinction. A ProcessHandle and a
ModuleHandle should not become interchangeable just because both are
machine-sized values. An owned process handle must also close exactly
once, so it must not derive Copy: a numeric copy would duplicate the
cleanup duty. Handle ownership develops that wrapper and
its Drop implementation. A borrowed handle view has a different duty
from an owned handle.
Let derive generate ordinary trait implementations
Section titled “Let derive generate ordinary trait implementations”#[derive(...)] asks the compiler to generate the listed capabilities
from the fields. Debug supports diagnostic {:?} output; PartialEq
supports equality comparisons; Eq marks equality without exceptional
cases such as floating-point NaN. Clone provides explicit .clone();
Copy permits an implicit value copy and requires Clone.
For Rva, copying its plain integer is harmless. For String, an
explicit clone allocates a separate owned string, so String does not
implement Copy. Derived capabilities are not permission checks or
address validation. If a field cannot implement a requested trait, the
derive fails to compile.
Convert with From and Into; format with Display
Section titled “Convert with From and Into; format with Display”From describes an infallible conversion into your type. Implementing
From<u32> for Rva also makes u32 implement Into<Rva>.
impl From<u32> for Rva { fn from(value: u32) -> Self { Self(value) }}
impl std::fmt::Display for Rva { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "RVA 0x{:X}", self.0) }}
fn main() { let offset: Rva = 512_u32.into(); println!("{offset}"); // RVA 0x200}The destination annotation chooses what .into() constructs. This
conversion simply names a number’s role; it does not check a module’s
size. Use TryFrom and a Result when a conversion may fail.
Display controls user-facing {} formatting and .to_string().
Debug answers a different question: what should a diagnostic view of
this value look like? You implement Display yourself because the best
label cannot be inferred from a struct’s fields. write! writes into
the supplied formatter and reports a formatting error if its destination
fails; it does not print directly to the terminal.
Give one number a type and a readable spelling. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Rva(u32);
impl From<u32> for Rva { fn from(n: u32) -> Self { Self(n) } }
impl std::fmt::Display for Rva {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "RVA 0x{:X}", self.0)
}
}
let offset: Rva = raw.into();
let label = offset.to_string();to_string uses Display. The number has a clearer role, but its validity still depends on the target module.
Final values: raw = 512; offset = Rva(512); traits = Debug, Clone, Copy, PartialEq, Eq; label = RVA 0x200; validated = false.
Read every step of the lesson’s example
Step 1, line 2. Rva is a distinct type, so a function taking Rva cannot accidentally receive a different newtype.
raw = 512; offset = not constructed.
Step 2, line 1. derive generates the named trait implementations from the field. Copy fits this plain numeric wrapper; it would be wrong for an owned handle.
raw = 512; offset = not constructed; traits = Debug, Clone, Copy, PartialEq, Eq.
Step 3, line 9. Implementing From<u32> also supplies Into<Rva>. The destination annotation chooses the conversion.
raw = 512; offset = Rva(512); traits = Debug, Clone, Copy, PartialEq, Eq.
Step 4, line 6. Display writes an intended user-facing label into the formatter.
raw = 512; offset = Rva(512); traits = Debug, Clone, Copy, PartialEq, Eq; label = RVA 0x200.
Step 5, line 10. to_string uses Display. The number has a clearer role, but its validity still depends on the target module.
raw = 512; offset = Rva(512); traits = Debug, Clone, Copy, PartialEq, Eq; label = RVA 0x200; validated = false.
A trait describes a capability
Section titled “A trait describes a capability”A trait names behavior that several types can provide. A fixture can supply invented gold values; another implementation can obtain a checked snapshot. A caller that only needs the gold does not need to know which storage system provided it.
trait GoldReader { fn gold(&self) -> Option<u32>;}
struct Fixture(u32);impl GoldReader for Fixture { fn gold(&self) -> Option<u32> { Some(self.0) }}
struct Missing;impl GoldReader for Missing { fn gold(&self) -> Option<u32> { None }}
fn affordable(reader: &impl GoldReader, price: u32) -> bool { reader.gold().is_some_and(|gold| gold >= price)}The &self method borrows its implementation for reading. In this
argument position, impl GoldReader means “one concrete type that
implements this trait.” Each call can use a different concrete type;
the function’s body uses the promised method. A longer spelling uses a
generic parameter: fn affordable<R: GoldReader>(reader: &R, price: u32).
Both express the same capability requirement here.
For Fixture(20) and price 12, the answer is true. For Missing,
is_some_and returns false without calling its closure. A trait makes
the interface reusable; it does not ensure that every implementation
returns accurate data or has permission to read a process.
One capability lets a function use different readers. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
trait GoldReader { fn gold(&self) -> Option<u32>; }
struct Fixture(u32);
impl GoldReader for Fixture {
fn gold(&self) -> Option<u32> { Some(self.0) }
}
fn affordable(reader: &impl GoldReader, price: u32) -> bool {
reader.gold().is_some_and(|gold| gold >= price)
}Both readers use the same function. The trait does not grant operating-system access or turn missing data into a valid value.
Final values: reader = fixture; price = 12; access = shared borrow; gold = Some(20); affordable = true.
Read every step of the lesson’s example
Step 1, line 1. The trait defines the capability, not where the data comes from.
reader = fixture; price = 12.
Step 2, line 6. The argument borrows one concrete implementation. impl Trait here asks for any type with this capability.
reader = fixture; price = 12; access = shared borrow.
Step 3, line 7. This implementation returns Some(20). The closure compares its number with the price.
reader = fixture; price = 12; access = shared borrow; gold = Some(20); affordable = true.
Step 4, line 8. Both readers use the same function. The trait does not grant operating-system access or turn missing data into a valid value.
reader = fixture; price = 12; access = shared borrow; gold = Some(20); affordable = true.
Accumulate choices with a builder
Section titled “Accumulate choices with a builder”A builder stores construction choices, then checks them before producing the finished object. It is useful when a tool has several optional settings. A simple object with two obvious required fields usually needs only a constructor.
#[derive(Debug)]struct Sampler { limit: usize, label: String }
struct SamplerBuilder { limit: usize, label: String }
impl Default for SamplerBuilder { fn default() -> Self { Self { limit: 1, label: "sample".into() } }}
impl SamplerBuilder { fn limit(mut self, value: usize) -> Self { self.limit = value; self } fn label(mut self, value: impl Into<String>) -> Self { self.label = value.into(); self } fn build(self) -> Result<Sampler, &'static str> { if self.limit == 0 { return Err("limit must be positive"); } Ok(Sampler { limit: self.limit, label: self.label }) }}Each setting method takes ownership of the draft and returns the updated
draft. That enables the chain below. impl Into<String> accepts a string
literal or an owned String and stores an owned label. build validates
the limit once; the object is not ready before it succeeds.
let sampler = SamplerBuilder::default().limit(3).label("gold").build();The builder itself does not start a worker or read memory. It only constructs a validated configuration. A zero limit returns an error that the caller can handle.
A builder accumulates choices before validating. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
let draft = SamplerBuilder::default();
let draft = draft.limit(limit).label("gold");
let sampler = draft.build();build checks the limit once and returns a ready sampler.
Final values: limit = 3; label = gold; validated = true; result = Ok(Sampler { limit: 3, label: gold }).
Read every step of the lesson’s example
Step 1, line 1. Default creates an ordinary draft with a limit of one and a label of sample.
limit = 1; label = sample; validated = false.
Step 2, line 2. Each consuming method returns the updated draft so the next method can use it.
limit = 3; label = gold; validated = false.
Step 3, line 3. build checks the limit once and returns a ready sampler.
limit = 3; label = gold; validated = true; result = Ok(Sampler { limit: 3, label: gold }).
Cheatsheet: access and interfaces
Section titled “Cheatsheet: access and interfaces”| Need | Rust expression | Meaning |
|---|---|---|
| Return a borrowed view | &'a [u8] |
View cannot outlive its input owner. |
| Distinguish integer roles | struct Rva(u32); |
A new type, not a renamed integer. |
| Generate diagnostics/equality | #[derive(Debug, PartialEq, Eq)] |
Implements supported capabilities from fields. |
| Permit cheap implicit copies | Copy plus Clone |
Appropriate only when copying does not duplicate ownership duties. |
| Convert infallibly | From<T> / .into() |
Destination type defines the conversion. |
| Convert with validation | TryFrom<T> / .try_into() |
Caller handles a possible failure. |
| Write a user-facing label | impl Display |
Used by {} and .to_string(). |
| Ask for a capability | &impl GoldReader |
Borrows a concrete trait implementation. |
| Gather optional settings | Builder::default().choice(...).build() |
Validate before producing the ready object. |
See the official From, Display, and lifetime references for details. Next, cleanup and error layers connect an owned resource to the scope responsible for releasing it.
Reason about the concept
A returned slice points into the caller’s buffer. What does a matching lifetime annotation express?
A lifetime describes validity relationships; it does not allocate or extend storage.
Finished Lesson 3.4?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.