Chapter 11: Borrow and Lifetimes
Chapter 10 introduced the verbs that say what a function does with an argument. This chapter covers the borrow forms underneath them and the lifetime labels that let the compiler check that a borrowed value does not outlive what it borrows from. All of it is compile-time only: lifetime labels are erased before code generation and cost nothing at run time (Language Reference §3-2.4.2).
The chapter covers borrow and excl borrow, lifetime parameters ([lifetime ~name], borrow[~name]), inferred struct lifetimes, closures and noescape fn, unowned[~name], volatile borrow — and, just as important for kernel code, which escapes the 1.25 checker detects and which it does not yet.
borrow and excl borrow
borrow x: T is a shared, read-only borrow — the same thing as reads. excl borrow x: T is an exclusive, mutable borrow of a struct or unique value — the same thing as edits for those types. The verbs are the preferred spelling in new code; the borrow forms remain valid in host.native and freestanding (§3-2.4.9), and they are the forms that take a lifetime label.
@mode(freestanding)
@repr(C)
struct SavedRegs {
var rip: uint64;
var rsp: uint64;
var rflags: uint64;
}
func interruptsWereOn(borrow regs: SavedRegs): bool {
return (regs.rflags & 0x200u64) != 0u64
}
func skipInstruction(excl borrow regs: SavedRegs, length: uint64): void {
regs.rip = regs.rip + length
}
func handleBreakpoint(): bool {
var regs = SavedRegs(rip = 0x1000u64, rsp = 0x8000u64, rflags = 0x202u64)
skipInstruction(regs, 1u64) # an excl borrow parameter is called plainly
return interruptsWereOn(regs)
}
Listing 11-1: Shared and exclusive borrows of a struct
Writing through a plain borrow is the same E_READS_MUTATION you saw in chapter 10. excl borrow is limited to structs and unique values, because a plain class reference can be aliased:
@mode(freestanding)
class IrqStats {
pub var count: uint64 = 0;
}
func recordIrq(excl borrow stats: IrqStats): void {
stats.count = stats.count + 1u64
}
error[E_EXCL_BORROW_NOT_STRUCT]: E_EXCL_BORROW_NOT_STRUCT: `excl borrow` parameter 'stats' has type `IrqStats` — only struct types and `unique` types may be exclusively borrowed; use `edits` to exclusively borrow a class instance
--> src/main.lpfree:7:16
|
7 | func recordIrq(excl borrow stats: IrqStats): void {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
The message names the fix: func recordIrq(edits stats: IrqStats), called as recordIrq(edits: stats). edits extends exclusive borrowing to class instances, which is one reason the verbs are the preferred spelling.
Lifetime parameters
A lifetime is a compile-time name for how long a borrowed value is valid. A function or type introduces one in its bracket list as lifetime ~name, and a borrow tied to it is written borrow[~name]. Labels use ~, not Rust's 'a.
# One line of a boot command line, borrowed from the text it came from.
struct Line[lifetime ~buf] {
borrow[~buf] text: string;
var number: uint32;
}
func firstLine[lifetime ~buf](borrow[~buf] cmdline: string): Line[~buf] {
return Line(text = cmdline, number = 1u32)
}
func lineNumber(cmdline: string): uint32 {
let line = firstLine(cmdline) # Line[~buf] for this cmdline
return line.number
}
Listing 11-2: A lifetime-parameterized struct and a function that returns it
The signature of firstLine states the contract: the returned Line borrows from cmdline, so it is valid only as long as cmdline is. The caller does not write labels; the compiler matches them up at each call (§3-2.4.2, "reborrowing"). ~static is the built-in region that lasts for the whole program.
Inferred struct lifetimes
A struct whose non-trivial fields are all borrow gets an implicit lifetime, so short-lived views need no labels (§3-2.4.4). The inference covers local use; the struct still may not escape the scope of what it borrows, and returning one from a function needs the explicit form:
@mode(freestanding)
# A view of one received frame: every field borrows from the frame.
struct FrameView {
borrow header: string;
borrow payload: string;
}
func split(borrow frame: string): FrameView {
return FrameView(header = frame, payload = frame)
}
error[E_INFERRED_LIFETIME_RETURN]: E_INFERRED_LIFETIME_RETURN: function 'split' returns 'FrameView', a struct with an inferred lifetime, without a lifetime annotation on its return type — give the struct a `[lifetime ~name]` parameter and return 'FrameView[~name]' (§3-2.4.4)
--> src/main.lpfree:9:1
|
9 | func split(borrow frame: string): FrameView {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
Without a label, a caller could not tell what the returned view borrows from. Name the lifetime and tie the parameter to it:
struct FrameView[lifetime ~f] {
borrow[~f] header: string;
borrow[~f] payload: string;
}
func split[lifetime ~f](borrow[~f] frame: string): FrameView[~f] {
return FrameView(header = frame, payload = frame)
}
func headerLength(frame: string): int {
let view = split(frame)
return view.header.length()
}
Listing 11-3: Returning a view with an explicit lifetime
Storing a view into a variable that lives longer than its source is caught too:
@mode(freestanding)
struct FrameView {
borrow header: string;
}
func latestHeader(fresh: bool): uint32 {
var last = FrameView(header = "none")
if (fresh) {
let received = "eth0 frame 1"
let v = FrameView(header = received)
last = v
}
return 0u32
}
error[E_INFERRED_LIFETIME_ESCAPE]: E_INFERRED_LIFETIME_ESCAPE: 'v' has an inferred lifetime (it borrows from 'received') and is stored in 'last', declared in an enclosing block, which outlives it (§3-2.4.4)
--> src/main.lpfree:12:9
|
12 | last = v
| ^^^^^^^^
|
received ends with the if block, so last would outlive what it points into. Either use the view inside the block, or declare the source in the outer scope.
Closures that capture borrows
A closure that captures a borrow[~name] parameter may only be handed to a noescape fn parameter — a plain fn value could be stored and called after the borrow is gone (§3-2.4.2 rule 3):
@mode(freestanding)
func checksumLater[lifetime ~pkt](borrow[~pkt] payload: string): uint32 {
let job: fn(): uint32 = func(): uint32 {
return payload.length() as uint32
}
return job()
}
error[E_BORROW_CLOSURE_ESCAPE]: E_BORROW_CLOSURE_ESCAPE: a closure capturing 'payload: borrow[~pkt]' is bound to 'job', a plain `fn` binding — a lifetime-capturing closure may only be passed to a `noescape fn` parameter (§3-2.4.2 rule 3)
--> src/main.lpfree:4:5
|
4 | let job: fn(): uint32 = func(): uint32 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
Pass the closure straight to a function whose parameter is noescape fn:
func sumOver(n: int, visit: noescape fn(int): uint32): uint32 {
var total: uint32 = 0
var i = 0
while (i < n) {
total = total + visit(i)
i = i + 1
}
return total
}
func checksumNow[lifetime ~pkt](borrow[~pkt] payload: string): uint32 {
return sumOver(1, func(i: int): uint32 {
return payload.length() as uint32
})
}
Listing 11-4: A borrow-capturing closure passed to a noescape fn parameter
The noescape promise is checked on the callee's side as well: a noescape fn parameter cannot be stored in a longer-lived fn variable.
@mode(freestanding)
var onTick: fn(uint64): void = func(t: uint64): void { }
func everyTick(handler: noescape fn(uint64): void): void {
onTick = handler
}
error[E0001]: cannot assign 'noescape fn(uint64): void' to 'fn(uint64): void'
--> src/main.lpfree:6:5
|
6 | onTick = handler
| ^^^^^^^^^^^^^^^^
|
A callback that is meant to be stored — a timer hook, an interrupt dispatch entry — takes a plain fn parameter, and is usually a named module-level function rather than a capturing closure:
var onTick: fn(uint64): void = func(t: uint64): void { }
func setTickHandler(handler: fn(uint64): void): void {
onTick = handler
}
var ticks: uint64 = 0
func countTick(now: uint64): void {
ticks = now
}
func initTimer(): void {
setTickHandler(countTick)
}
Listing 11-5: Storing a plain function as a callback
unowned[~name] back-references
unowned[~name] declares a non-owning field whose referent the compiler proves outlives the holder, using scope nesting (§3-2.4.3). The class must declare the lifetime it names:
class TreeNode[lifetime ~owner] {
pub let value: int;
pub unowned[~owner] var parent: TreeNode[~owner]?;
new(value: int, parent: TreeNode[~owner]?) {
this.value = value
this.parent = parent
}
}
safe func build[lifetime ~tree](): int {
let root = TreeNode[~tree](value = 0, parent = None)
let child = TreeNode[~tree](value = 1, parent = Some(root))
return child.value
}
Listing 11-6: A child that points back to its parent
Assigning a referent declared in a narrower scope — a node created inside an if block, stored as the parent of one created outside it — is E_UNOWNED_LIFETIME_MISMATCH; leaving [lifetime ~owner] off the class is E_UNOWNED_CLASS_NO_PARAM. In freestanding, where class objects are staged as chapter 7 describes, keep such trees small and prefer index handles into a fixed table for anything long-lived.
What the 1.25 checker does not catch yet
The region checks above are a conservative, syntactic subset (§3-2.4.2, "What is statically detected"). Two gaps matter for kernel code:
- Storing a borrowed parameter into a module variable. The Language Reference makes returning or storing a
reads/borrowparameter beyond the callE_BORROW_ESCAPE. That check is not implemented:saved = nameinsidefunc stash[lifetime ~user](borrow[~user] name: string)compiles. Do not cache pointers from a system call's arguments in kernel state; copy the data you need. - Outlives constraints. There is no
where ~a: ~bsyntax; the parser rejects it. Express "this result lives no longer than that input" by giving both the same label.
Treat lifetime labels as checked documentation of these contracts, and keep the pointers that cross a boundary — a system call, an interrupt, a DMA completion — in values whose lifetime the checker does follow: parameters, locals and the structs built from them.
volatile borrow — device registers
For memory-mapped registers, the borrow form takes one more keyword: volatile borrow tells the compiler that every read and write through the reference is an observable device access that must not be merged, reordered or removed (§3-3.8). Chapter 15 covers it in depth. A frequent mistake is to write it as an annotation, the way some older material did:
@mode(freestanding)
@repr(C)
struct UartRegs {
var data: uint8;
var lsr: uint8;
}
func uartWrite(@volatile borrow uart: UartRegs, b: uint8): void {
uart.data = b
}
error[E0001]: expected identifier
--> src/main.lpfree:9:16
|
9 | func uartWrite(@volatile borrow uart: UartRegs, b: uint8): void {
| ^
|
error[E0002]: expected `{`
--> src/main.lpfree:9:16
|
9 | func uartWrite(@volatile borrow uart: UartRegs, b: uint8): void {
| ^
|
error[E0001]: expected identifier
--> src/main.lpfree:9:17
|
9 | func uartWrite(@volatile borrow uart: UartRegs, b: uint8): void {
| ^^^^^^^^
|
error[E0001]: expected expression
--> src/main.lpfree:11:1
|
11 | }
| ^
|
There is no @volatile annotation; annotations cannot appear inside a parameter list at all, which is why the parser stops at the @. volatile is a keyword in front of borrow:
@repr(C)
struct UartRegs {
var data: uint8;
var lsr: uint8;
}
func uartWrite(volatile borrow uart: UartRegs, b: uint8): void {
while ((uart.lsr & 0x20u8) == 0u8) { } # every read of lsr reaches the device
uart.data = b # a volatile store
}
Listing 11-7: A volatile borrow parameter
A volatile borrow is still a borrow: the lifetime rules apply to it like to any other.
Summary
borrow=readsandexcl borrow=editsfor structs anduniquevalues;excl borrowon a class isE_EXCL_BORROW_NOT_STRUCT— useedits.[lifetime ~name]introduces a lifetime;borrow[~name] Tties a borrow to it; labels are checked, then erased.- All-
borrowstructs get an inferred lifetime for local use; returning one needs an explicit label (E_INFERRED_LIFETIME_RETURN), and storing one where it outlives its source isE_INFERRED_LIFETIME_ESCAPE. - A closure capturing a lifetime-labelled borrow may only go to a
noescape fnparameter (E_BORROW_CLOSURE_ESCAPE), and anoescape fncannot be stored; stored callbacks are plainfnvalues. unowned[~name]back-references are checked by scope nesting (E_UNOWNED_LIFETIME_MISMATCH).- Not yet checked: storing a borrowed parameter in a module variable (
E_BORROW_ESCAPE); there are nowhere ~a: ~bconstraints. volatile borrowmarks device-register parameters;@volatiledoes not exist.
Next: unique ownership.