After spending a bit of time getting to know Rust, I think I’ve finally come to an understanding with the borrow checker, at least in terms of ownership and borrowing. The one thing I still have trouble getting my head around is lifetimes. I’ve written this post as my attempt to internalise a proper understanding of lifetimes.
According to the Rust Book, a lifetime is the scope for which a reference is valid. To explore lifetimes I will implement a simple event handler project which will call a specific handler function dependent on the name of an event. The project is made up of the following main parts:
- a Handler is a function that takes a string slice as a parameter.
- a Dispatcher maintains a map of event names to Handlers.
- the Dispatcher calls the relevant Handler, passing in the parameter, when the
call
method is invoked with an event name and parameter.
The first version of the code looks like this:
use std::collections::HashMap;
type Handler = Fn(&str);
struct Dispatcher {
handlers: HashMap<&str, &Handler>,
}
impl Dispatcher {
// create a new dispatcher
fn new() -> Dispatcher {
Dispatcher {
handlers: HashMap::new(),
}
}
// add a new handler
fn add(&mut self, event_name: &str, handler_function: &Handler) {
self.handlers.insert(event_name, handler_function);
}
// call a handler, passing parm as a parameter
fn call(&self, event_name: &str, parm: &str) {
if let Some(h) = self.handlers.get(event_name) {
h(parm);
}
}
}
// a test handler
fn handler_1(value: &str) {
println!("handler_1 called with value '{}'", value);
}
fn main() {
// create the dispatcher
let disp = Dispatcher::new();
// add a handler to the dispatcher
disp.add("handler_1", &handler_1);
// call the dispatcher, passing "parm 1" as a parameter
disp.call("handler_1", "parm 1");
}
When run the code it should return:
$ cargo run
handler_1 called with value 'parm 1'
but instead, the result is:
$ cargo run
Compiling dispatcher v0.1.0 (C:\rust\dispatcher)
error[E0106]: missing lifetime specifier
--> src\main.rs:7:23
|
7 | handlers: HashMap<&str, &Handler>,
| ^ expected lifetime parameter
error[E0106]: missing lifetime specifier
--> src\main.rs:7:29
|
7 | handlers: HashMap<&str, &Handler>,
| ^ expected lifetime parameter
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0106`.
error: Could not compile `dispatcher`.
To learn more, run the command again with --verbose.
Because the Dispatcher
struct contains references in the HashMap, the rust compiler does not know how the lifetimes of the references relate to the lifetime of the struct itself. In order for it to be safe to use the Dispatcher
stuct, the struct cannot out live it’s references, otherwise it could be left with dangling references that point to invalid memory.
To tell the compiler how the lifetimes relate to each other I need to add lifetime annotations to the declarations. It is important to note that these annotations don’t actually affect the lifetimes of the things they annotate, they just tell the compiler how long references need to remain valid. If the compiler decides that the code cannot meet the specified lifetime requirements, the compilation will fail.
Here I’ve added a lifetime annotation of 'a
to the struct and the references in the HashMap. This tells the compiler that the references in the HashMap must remain valid for as long as the struct remains valid.
struct Dispatcher<'a> {
// maps an event name to a function
handlers: HashMap<&'a str, &'a Handler>,
}
Ok, with those changes in place, lets try and build it again:
$ cargo build
Compiling dispatcher v0.1.0 (C:\rust\dispatcher)
error[E0106]: missing lifetime specifier
--> src\main.rs:10:6
|
10 | impl Dispatcher {
| ^^^^^^^^^^ expected lifetime parameter
error[E0106]: missing lifetime specifier
--> src\main.rs:12:17
|
12 | fn new() -> Dispatcher {
| ^^^^^^^^^^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
= help: consider giving it a 'static lifetime
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0106`.
error: Could not compile `dispatcher`.
To learn more, run the command again with --verbose.
The compilation got a bit further this time, but now I have a problem in the implementation block for the Dispatcher
. The first error
10 | impl Dispatcher {
| ^^^^^^^^^^ expected lifetime parameter
tells me that I need to add a lifetime annotation to the Dispatcher
part of the implementation declaration. This is because the lifetime annotation that I added to the Dispatcher
struct is now considered a part of the struct’s signature.
The second error
12 | fn new() -> Dispatcher {
| ^^^^^^^^^^ expected lifetime parameter
tells me that I need to add a lifetime annotation to the return value of the new
method. This is because the new method is returning a borrowed value and the compiler doesn’t know how long the borrowed value needs to live.
I fixed those 2 errors and tried again
impl Dispatcher<'a> {
// create a new dispatcher
fn new() -> Dispatcher<'a> {
Dispatcher {
handlers: HashMap::new(),
}
}
which results in
$ cargo build
Compiling dispatcher v0.1.0 (C:\rust\dispatcher)
error[E0261]: use of undeclared lifetime name `'a`
--> src\main.rs:11:17
|
11 | impl Dispatcher<'a> {
| ^^ undeclared lifetime
error[E0261]: use of undeclared lifetime name `'a`
--> src\main.rs:13:28
|
13 | fn new() -> Dispatcher<'a> {
| ^^ undeclared lifetime
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0261`.
error: Could not compile `dispatcher`.
To learn more, run the command again with --verbose.
These latest two errors have the same cause. If an implementation block contains any lifetime annotations, they need to be specified immediately after the impl
keyword in order to be valid in the implentation block, like the following.
impl<'a> Dispatcher<'a> {
// create a new dispatcher
fn new() -> Dispatcher<'a> {
Dispatcher {
handlers: HashMap::new(),
}
}
With that change made, it is time to try compiling again
$ cargo build
Compiling dispatcher v0.1.0 (C:\rust\dispatcher)
error[E0621]: explicit lifetime required in the type of `event_name`
--> src\main.rs:21:30
|
20 | fn add(&mut self, event_name: &str, handler_function: &Handler) {
| ---- help: add explicit lifetime `'a` to the type of `event_name`: `&'a str`
21 | self.handlers.insert(event_name, handler_function);
| ^^^^^^^^^^ lifetime `'a` required
error[E0621]: explicit lifetime required in the type of `handler_function`
--> src\main.rs:21:42
|
20 | fn add(&mut self, event_name: &str, handler_function: &Handler) {
| -------- help: add explicit lifetime `'a` to the type of `handler_function`: `&'a (dyn for<'r> std::ops::Fn(&'r str) + 'static)`
21 | self.handlers.insert(event_name, handler_function);
| ^^^^^^^^^^^^^^^^ lifetime `'a` required
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0621`.
error: Could not compile `dispatcher`.
To learn more, run the command again with --verbose.
This time, with help from the compilers extremely useful messages, I can see that I need to add lifetime annotations to the parameters on the add
method. This is because I am inserting the event_name
and the handler_function
into the handlers
vector, which already has a lifetime defined. Adding the lifetime annotations to the parameters indicates that the parameters need to live as long as the vector.
// add a new handler
fn add(&mut self, event_name: &'a str, handler_function: &'a Handler) {
self.handlers.insert(event_name, handler_function);
}
Giving it another compile, finally gives us a good build
$ cargo build
Compiling dispatcher v0.1.0 (C:\rust\dispatcher)
Finished dev [unoptimized + debuginfo] target(s) in 2.39s
and I can run the program and finally get the expected output.
$ cargo run
handler_1 called with value 'parm 1'
As I said at the top of this post, my purpose in writing this was to internalise the way lifetimes work in Rust, so that I don’t keep bumping up against the kind of compilation failiures seen here. I think that the understanding of lifetimes that I’ve worked through here is correct, but if I’m making any mistakes, please add a comment below.