41

Things Rust doesn’t let you do

 5 years ago
source link: https://www.tuicool.com/articles/hit/bEFz6bb
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

TL;DR: A survey of things that Rust — and especially the mutability system and the borrow checker — doesn’t let you do, while arguably safe in some circumstances. Justifications for the current behaviour are discussed and possible workarounds and future improvements are explained.

Contents:

About references and lifetimes

The design decisions to live by

The shortcomings with answers

1. Doing control flow aware stuff

2. Postponing mutability of a lifetime

3. Skipping trivial bounds in data types

4. Splitting up mutable references

5. Having multiple aliasing mutable references

6. Being able to point inside Cell types

7. Having self-referencing structs

8. Capturing only disjoint fields in closures

9. Having associated types that are generic over lifetimes

Open problems: from here on there be dragons

10. Downgrading a mutable lifetime to a shared one

11. Calling mutable methods that don’t access overlapping fields

12. Hiding mutable lifetimes in data types

13. Using “ambient” lifetimes

14. Moving the owner of a heap-allocated object that has an inbound reference

Closing words

The borrow checker is undisputedly the weirdest and most novel feature of Rust the programming language. It’s what makes Rust the what it is — a memory safe language without a garbage collector that strives for zero-overhead abstractions. Rust manages its memory using compile-time static analysis: checking for dangling pointers, ensuring mutability constraints and inserting calls for freeing memory as a part of type-checking. This analysis is not always perfect and sometimes it requires jumping through the hoops to get a Rust program to compile. Understanding the lifetime and borrow system thoroughly gets you quite far, but there are still some cases you can’t convince the compiler to accept the code, even if you can convince yourself that it is indeed safe. In this article I’ll list such cases: limitations of the borrow checker, the reasons why it doesn’t let the code pass and how the situation could possibly improve in the future. I hope that the borrow checker keeps evolving and some day this list becomes redundant.

About references and lifetimes

This is not meant to be a tutorial for Rust but I’ll briefly introduce the reference system a bit for starters. You can skip this part if you are already familiar with the concepts. Rust has the concept of “owned” values. Owning a value means that you have the single right and responsibility to dispose of it once you are done with it. Because there is no shared ownership built into the language, the compiler always knows when you are done with a value, so it inserts the call to the destructor automatically.

Other than using a value directly, you can take a reference to it — this is called borrowing in Rust parlance. References are like pointers in C, but they are checked for correctness. The lifetime system in Rust ensures that a reference can’t outlive the value it points to. You can’t also construct bogus references such as null references; you can only take a reference of an existing object. References are thus, always valid.

There are two kinds of references: shared references that look like this: &MyType and mutable references that look like this &mut MyType . You can have many shared references to a value, but you can’t mutate the value they point to. (There are some exceptions though, elaborated later.) If you want to mutate values through references, you can do that using a mutable reference, but you can have only one of those at a time. As long as a mutable reference to a value exists, that value can only be accessed (read or written) through that reference. No one else— including the owner — are allowed to access the value while the mutable reference exists.

The design decisions to live by

There are some good reasons why the Rust reference system is so restrictive. First of all, having single ownership ensures that it’s not easy to leak values and it’s impossible to “double-free” them — calling the destructor twice. Rust ensures the single ownership principle by being move-by-default . That means that if you pass a value somewhere, you lose the ownership over it and can’t use it anymore. This is also called affine typing . (Often confused with linear typing ; see here for further discussion: https://gankro.github.io/blah/linear-rust/ ) Rust also supports types that are copy-by-default ; many fundamental types such as integers are defined such and you can define your own but move-by-default makes sense as a conservative default.

As for the mutable references, the principle of a single mutable reference may feel overly restrictive from the perspective of a C programmer who can have multiple mutable pointers to a value. However, there are valid reasons for that. For a convincing practical reason from software development perspective, see this blog post by Manish Goregaokar: https://manishearth.github.io/blog/2015/05/17/the-problem-with-shared-mutability/ .

Other than that, there is some additional reasons: one of them has to do with compiler optimizations around aliasing and another has to do with thread safety. To quickly explain the gist of these two: single mutability ensures that you can safely keep the value in processor register without fear that some other piece of code invalidates the version that resides in RAM — this can enable nice optimisation speedups. It also ensures that if you send a mutable reference to another thread, there aren’t any other mutable references left that could cause race conditions when writing through them in an unsynchronized manner from multiple threads. Mutable references in Rust are guaranteed to be unique , and it would cause undefined behaviour to somehow being able to clone one. Fortunately the type checker protects you from that.

There is still one general principle that affects the design decisions of Rust: all analysis should be local. No whole-program stuff. Rust programs must be type checkable function-by-function. The function signatures have to contain enough of lifetime and type information that the body can be checked. This also means that some situations where the borrow checker might seem stupid (“This function only mutates only field A, so why can’t I call also that function that mutates field B, they are different fields! It should allow that much!”), but the point is that it isn’t allowed to “peek into” functions other than the one it is currently checking. All it gets to know are the function signatures.

The shortcomings with answers

So, here is the meat of this article. I’d like to review some cases that are certainly safe, but for one reason or another, the borrow checker isn’t sophisticated enough to see that. I’ve ordered the cases by whether there exists an upcoming solution for the problem or if the problem is still unsolved. The first part consists of problems that are about to get fixed. That’s exciting, so let’s get started!

1. Doing control flow aware stuff

At the moment, the borrow checker thinks of the code as a bunch of hierarchically nested scopes, or blocks. The outer scopes outlive the inner ones and the borrow lifetimes behave accordingly. This is a very simple way to think of the borrows — but a rather unsophisticated one. It breaks down when the control flow doesn’t match the block structure. Here’s an example borrowed (ha!) from Niko Matsaki’s excellent introduction to the problem. ( http://smallcultfollowing.com/babysteps/blog/2016/04/27/non-lexical-lifetimes-introduction/ ) As you can see, the borrow checker is being overly conservative; even in the branch None where value , derived from map , doesn’t exist, it considers map as borrowed:

fn process_or_default<K,V:Default>(map: &mut HashMap<K,V>,
                                   key: K) {
    match map.get_mut(&key) { // -------------+ 'lifetime
        Some(value) => process(value),     // |
        None => {                          // |
            map.insert(key, V::default()); // |
            //  ^~~~~~ ERROR.              // |
        }                                  // |
    } // <------------------------------------+
}

There’s some other juicy examples too in the linked blog post; highly recommended reading!

Remedy: Non-lexical lifetimes

There are ongoing efforts to land improvements to the borrow checker that allow it to reason about the borrows with finer granularity. The RFC describing the proposal in detail can be found here: https://github.com/rust-lang/rfcs/blob/master/text/2094-nll.md . The improved borrow checker is currently available on the beta version of the compiler and will be available stabilised on release 1.31, as a part of the new 2018 edition. Only some weeks to go! It’s not panacea, though; as mentioned before, it can only reason about things local to the current function, so however simple, no “intra-procedural analysis” is done.

2. Postponing mutability of a lifetime

An oft-recurring pattern:

items.mutate_n(items.len());

At glance, this looks fine — first the length of the container items is measured by the method len which takes a shared, immutable reference to items . After the value has returned, it is passed to the method mutate_n along with a mutable reference to items . No mutable and shared lifetimes are supposed to overlap. However, there’s a complication due to Rust’s evaluation order. Here’s a desugared version of the method calls:

let receiver_of_mutate_n = &mut items; // A mutable (unique) borrow!
let receiver_of_len = &items; // A shared borrow!
let result_of_len = Collection::len(receiver_of_len);
let result_of_mutate_n = Collection::mutate_n(
    receiver_of_mutate_n,
    result_of_len
);

As you can see, the receiver is resolved before the expressions inside the parentheses! This means that there exists shared references at the same time there exists a mutable reference, which isn’t allowed! Admittedly, if the nested call would mutate items there would be some fertile ground for nasty and hard to notice bugs, but in this case we’d want to allow this, as len is only a read-only method that can’t cause any harm.

Remedy: Enabling nested method calls

Granted, “postponing” the mutability of a lifetime in presence of nested method calls feels kind of a special case but it’s nice for ergonomics since it’s an often recurring pattern. There has been an approved RFC around this case ( https://github.com/rust-lang/rfcs/pull/2025 ), and it too is going to land on 1.31, edition 2018!

3. Skipping trivial bounds in data types

When defining data types with generics and lifetimes, the compiler can be a bit pedantic:

struct MyGenericDataType<'a, T: 'a> {
    foo: &'a T,
}

See the <'a, T: 'a> part there? It’s needed. What we have here is a generic struct that holds a reference to any type T . First of all, Rust requires you to spell out the lifetime of the reference. Since it’s a data type that can be instantiated at any point of our program, there is no one and true lifetime that our struct will have — after all, it depends on the reference we store in there! That’s why the type is generic over lifetimes. < > is the Rust syntax for declaring generic types, and by specifying a lifetime annotation 'a there, we declare that the struct is valid for any lifetime the reference it contains is valid for. (Note that the type of field foo contains the lifetime 'a .) However, our struct is also generic over the actual type the reference points to! That’s what T refers to. It stands for “any type”.

Enter the pedantic part: we need one more annotation, T: 'a , which means that the type T outlives lifetime 'a . What does that mean? It means that the value the reference points to must live longer than the reference itself. Makes sense! If it wouldn’t, we would have a dangling pointer!

Except that this is totally trivial . Of course it has to live longer! Why do we have to spell that out? There’s no way that the pointer could soundly live longer than the pointee! It’s a model example of boilerplate code — it’s the only sensible choice and yet we have to spell it out.

Remedy: Inferring outlives requirements on structs

There is an accepted RFC ( https://github.com/rust-lang/rfcs/pull/2093 ) that says that the trivial bounds such as introduced above can be elided. That would allow us to write just <'a, T> instead of the verbose <'a, T: 'a> . This feature, too, will land on 1.31, as a part of edition 2018.

4. Splitting up mutable references

A commonly expressed concern about Rust is that it’s hard to get from one mutable reference to many. If you have a HashMap of items you can get a mutable reference to a single item inside it, but you can’t get many! Why is that? The get_mut method of HashMap receives a mutable reference to the hash map itself: &mut self . The method then returns a reference to an item contained in the hash map: &mut Item . Because the item reference points inside the hash map, it must have the same or shorter lifetime as the &mut self reference — it can’t outlive that, because that would allow us to dispose of the hash map, and the item reference would become a dangling pointer. Since we are talking about mutable references here, that also means that we can’t have many of them! We can only call get_mut again after the lifetime of the last borrow has ended!

But, you say, obviously calling get_mut multiple times should be allowed here, since the call doesn’t actually do anything bad! And being able to get multiple mutable references out of a hash map sounds so elementary that of course it should be allowed. However, imagine what we could do without that limitation: we’d call get_mut to get a mutable reference to item A. Then we’d call get_mut to get another mutable reference to item A and find ourselves from the world of undefined behaviour.

There’s a similar, but even more obviously “stupid” limitation:

let mut array = [1, 2, 3, 4];

let ref_a = &mut array[1];
let ref_b = &mut array[2]; // This isn't allowed!
*ref_a = 9;
*ref_b = 9;

I’ve seen people new to Rust complain about this many times. Obviously the indexes 1 and 2 do not overlap, so having a mutable references to them should be allowed. However, the compiler doesn’t have any specialised knowledge about array indexing to understand this! It just plays its game with lifetimes, borrows and mutability. The end result of having two non-overlapping mutable references is fine from this perspective, but if the means to achieve that are against the rules, the compiler is not going to give in.

Remedy: Helper APIs

In the Rust standard library, there is the method split_mut on slices that is the model example of helper API that improves the situation. Using split_mut we can split a mutable slice into two non-overlapping subslices. For example, we can split &mut [1, 2, 3, 4] to &mut [1, 2] and &mut [3, 4] or to &mut [1] and &mut [2, 3, 4] . These subslices can be accessed separately and of course, split even further. Another example is iterators: you can mutably iterate over a vector, and as a result get a mutable reference to each of the elements.

So the borrow checker doesn’t actually need to be super smart. Using a bit of unsafe code and wrapping that behind a safe interface, helper APIs can be defined to save the day. In the future I would like to see more these kind of APIs in the standard library.

For example, HashMap could have today a method get_pair_mut that returns two mutable references to two separate items. Of course it would have to perform a run-time check that the items are actually separate, but that’s the price of a safe API. It would also be possible to have facades on top of existing containers that would keep track dynamically which items are borrowed out and which aren’t. Actually, I did a bit of experimenting with such APIs a year back: https://github.com/golddranks/multi_mut

There aren’t any proposals to expand the current set of helper APIs that I know of. We need a hero that champions an RFC for that! The upcoming language features such as const generics and stack-allocated dynamically sized types are likely to help defining better helper APIs too. (I’m imagining a facade over containers that uses dynamically sized types on stack to keep track of the borrows without needing to heap allocate a buffer for that.)

5. Having multiple aliasing mutable references

From a semi-seasoned Rustacean, this sounds like an oxymoron. “Rust isn’t supposed to have these! They were supposed to be awful!” Yet it sometimes helps to be able to have multiple mutable pointers to the same location. Every C programmer knows that aliasing pointers is definitely possible technically, so why doesn’t Rust cut us some slack?

The aliasing restrictions of Rust have good reasons I already spelled out in the above chapter The design decisions to live by. On the other hand, if C gets away with mutable aliased pointers, why should we constrain ourselves to the spartan asceticism of the current borrow checker? Fortunately Rust provides us an escape hatch (other than using raw pointers and unsafe code): the Cell type. Cell is a wrapper type that can be mutated even through shared — an thus normally immutable — references. Using Cell requires the compiler to be a bit more cautious. It must be more careful with aliasing and it mustn’t allow any references to a Cell -wrapped type across threads, as that would lead to data races.

Here’s an example — from the viewpoint of a seasoned Rustacean, this might feel abhorrent; the value of fuga just changes under your feet. However, it shows that this kind of code is possible in Rust too.

use std::cell::Cell;
fn borrow_add_one(val: &Cell<u32>) -> &Cell<u32> {
    val.set(val.get() + 1);
    val
}
fn main() {
    let hoge = Cell::new(4);
    let fuga = borrow_add_one(&hoge);
// Prints "fuga == 5"
    println!("fuga == {:?}", fuga);
hoge.set(10); // Mutating hoge but fuga changes too!
// Prints "hoge == 10, fuga == 10"
    println!("hoge =={:?}, fuga == {:?}", hoge, fuga);
}

Anyway, here’s the actual problem: Cell allows us to bend the curve when we need it, but it also requires us to define our types as Cell beforehands! It’s a wrapper type , after all. What if we have a huge program with established data types? If we want to use Cell , it would be awful to refactor the whole program to use this pattern if we need to.

Remedy: Conversions from &mut T to &Cell<T>

There is an accepted RFC ( https://github.com/rust-lang/rfcs/pull/1789 ) that basically states that the actual byte representation of T and Cell<T> is the same. That means that converting between them, and even converting between references to them is a no-op procedure from runtime viewpoint. The only difference between the two is what the compile-time type system allows. It then becomes possible to convert and use &mut T to &Cell<T> even if the codebase T originates from doesn’t have any Cell types to begin with. Once you have a unique, mutable reference to some type T , you can “fan out” that reference out to multiple shared references Cell<T> do what you must, and then give the references up — everything’s back to normal. The conversion is currently implemented, but not stabilised yet.

There is also already existing pattern for the other direction: since the memory representations of T and Cell<T> were defined to be equivalent, it is possible to go from &mut Cell<T> to &mut T ! The mutable reference to Cell ensures that no other references pointing to the Cell exist. That means that the compiler can safely relax a bit, and consider the inner type as a non- Cell type for the lifetime of the mutable reference. It is helpful for passing Cell -wrapped types to APIs that don’t expect Cell types. The API for that was stabilised in Rust 1.11.

6. Being able to point inside Cell types

Being able to convert &mut T to &Cell<T> allows for great flexibility, but it only goes so far. The greatest flaw in &Cell<T> , besides the caveats mentioned this far, is that you can’t have any references pointing to the insides of it. Let’s think for a moment why.

In Rust, there are basically two kinds of data types: structs and enums. Structs are familiar to many, but enums are a rarer feature; they are not like enums in C; they are basically tagged unions: a pair that consists of a union — a memory area that can represent any one of the many declared types or variants  — and a tag that is used to tell which one it currently is. Let’s suppose that we have the following enum:

enum JsonValue {
    String(String),
    Number(f64),
    Boolean(bool),
    Object(Map),
}

Let’s build a JsonValue that is inside a Cell : Cell::new(JsonValue::Boolean(false)) Then, suppose that we take a reference to the inner boolean. Our reference of type &bool points to the value false . Let’s suppose that we would change the value of our enum, using another &Cell<JsonValue> reference, to Number(2) . That would break everything! Why? Because that would make our first reference that is supposed to be a &bool to point something that is definitely not a bool . We have just broken our type system! Beware of the nasal demons. ( http://catb.org/jargon/html/N/nasal-demons.html )

So, there’s a good reason why the Cell types prevent inner references. Especially the type of mutability that can change the memory layout of the type can easily cause UB, but as mentioned before, there are subtler reasons around aliasing and threading too. When the type is wrapped in Cell , the compiler knows to be careful, but if we could get a normal reference to the inner value, we would be able to “cheat”, having just a normal &T reference that the compiler doesn’t know to be careful of. The compiler would have a false sense of security that the pointed value couldn’t possibly change, while we could actually change it through another &Cell<T> reference. But what if we really really want to access the insides of Cell<T> with a finer granularity?

Remedy: Conversion between &Cell<[T]> &[Cell<T>] , conversion between &Cell<T> (&Cell<field of T>, ...) .

Actually, there is a pattern that allows referencing the insides of a Cell<T> safely: splitting it up to non-overlapping parts and having each of the constituents live behind another Cell reference. The point is to never allow “bare” references point in. References with Cell are safe to modify, because the compiler knows to be careful.

The same RFC mentioned in the last item also allows conversions between &Cell<[T]> and &[Cell<T>] . That basically means that you can have just a normal slice of values of type T and go from &mut [T] to &Cell[T] to &[Cell<T>] and end up with a sliceful of mutably aliasable values! As said, the RFC is accepted and implemented, but not stabilised yet.

Similarly, it would be possible to convert a &Cell<MyStruct> to a tuple that contains Cell references to the fields of that struct: (&Cell<type of field 1>, &Cell<type of field 2>, …) . However, there is no plausible mechanism in the language at the moment to specify a general pattern like that. There has been some design discussion circling around the topic. ( https://internals.rust-lang.org/t/idea-derefpin-derefcell/7292 ) Maybe we’ll see a Cell field projection in the future? That would require, again, someone to think about the design and write an RFC.

7. Having self-referencing structs

Sometimes there is a need for a struct to have a reference pointing to itself. This has become especially important recently with the work on generators. Generators need to represent their suspended stack frames as first-class values. One can take a reference to a value in the same stack frame inside a generator and then suspend. This makes the the generators self-referencing.

Why self-references are a problem? Because stuff moves and references stay valid only as long as the objects they point to stay put, so allowing self-references while not forbidding moving, we are going to have dangling pointers. Rust is actually able to do some reasoning around self-referential structs:

#[derive(Debug)]
struct Game<'p> {
    player_a: u32,
    player_b: u32,
    current_player: Option<&'p u32>,
}
fn main() {
let mut g = Game {
player_a: 10,
player_b: 20,
current_player: None
};
g.current_player = Some(&g.player_a);
println!("{:?}", g.current_player); // prints Some(10)
g.current_player = Some(&g.player_b);
println!("{:?}", g.current_player); // prints Some(20)
}

If we try to move g , it complains that there is a reference to it. However, problems start right away with examples any more complicated than this — with mutability, to begin with. For example, if you store the struct in heap using a Box and then try to access it mutably, initialising the field current_player with a self reference, the whole lifetime of the struct gets “tainted” with the mutable borrow, because by storing a reference derived from that borrow in the struct itself, we accidentally set the lifetime of the mutable borrow equal to the lifetime of the struct itself. The struct “locks up” — we lose the ability to access it using any other reference until it’s dropped.

We would need some way to signal the borrow checker that after mutating the struct, we have “downgraded” the lifetime (see also the the item 10 about downgrading mutable lifetimes) to a shared one. But even if we would be able to do that, since the struct still contains a reference derived from a borrow of the struct itself, the struct would stay in a “borrowed mode” until it gets dropped — we could never mutate it again.

Wrapping current_player into a Cell cuts us some slack with mutability. But even then we are in troubles with lifetimes:

use std::cell::Cell;
#[derive(Debug)]
struct Game<'p> {
    player_a: u32,
    player_b: u32,
    current_player: Cell<Option<&'p u32>>,
}
fn init_game<'???>() -> Box<Game<'???>> {
    let g = Box::new(Game {
        player_a: 10,
        player_b: 20,
        current_player: Cell::new(None)
    });
    g.current_player.set(Some(&g.player_a));
    g // Doesn't work!
}
fn main() {
    let game = init_game();
    println!("{:?}", game);
}

Note the lifetime '??? ! There is no lifetime that we could name that fits there. The lifetime clearly isn’t something that the caller of the function can decide as a parameter, since it’s simply the lifetime of how long the heap-allocated Game happens to live — that might depend on anything, including the runtime control flow. On the other hand, the self-reference, originally borrowed from g lives only as long as the variable g does — the borrow checker doesn’t understand that the reference actually points to a heap allocation that is going to live longer than the variable g . This means that we can’t meaningfully pass the self-referential types down or up the stack, even if it would be safe in the sense that the heap allocation doesn’t move. (See the item 14 for further discussion!)

No, it seems that we have to use unsafe code and raw pointers. The borrow checker doesn’t check raw pointers, so they provide us all the flexibility we need. But then we face another problem: if nobody checks for the correctness, do we lose the ability to abstract the unsafety away, behind a safe interface? What if we release a library that uses self-referencing types, but our users shoot themselves in foot because they accidentally move the value without realising that isn’t allowed?

Remedy: Pin references

There was a recent RFC that addresses this problem: Pin references. ( https://github.com/rust-lang/rfcs/blob/master/text/2349-pin.md ) Having an object behind a pin reference provides an important guarantee: either the object is safe to move or it is not safe but will not move until it’s dropped. So using pin, one is able to require the users of a type not to move it. Actually getting a mutable reference to the insides of a pin that contains a self-referencing type requires unsafe code; anybody getting a mutable reference will do so knowing that they must not move the value. Pin references are going to be in the standard library soonish—the stabilisation has been a proposed but not decided upon yet.

8. Capturing only disjoint fields in closures

There is a slight ergonomics problem when using closures: they tend to capture values too eagerly:

fn update(&mut self) {
    // borrowing self.list mutably
    self.list.retain(
        |i| self.filter.allowed(i) // can't borrow self!
    );
}

The problem here is that although self.list and self.filter are separate fields and can normally be borrowed separately, the closure tries to borrow self as a whole! There is a simple workaround: manually borrow the field and then let the closure capture that:

fn update(&mut self) {
    let filter = &self.filter;
self.list.retain(|i| filter.allowed(i));
}

Having to do this is annoying; it’s not a show stopper, but certainly it would be nice if would be a bit smarter automatically.

Remedy: Capture disjoint fields

There is a merged RFC that makes the closure capture smarter by default: https://github.com/rust-lang/rfcs/pull/2229 It’s not stabilised yet though.

9. Having associated types that are generic over lifetimes

When processing data from a stream, it’s not uncommon to have a buffer that holds a “chunkful” of the data being processed. You can reuse the buffer when you are done with processing the current data. This helps to avoid allocations during the operation. Of course this means that references to the buffer are valid only for the lifetime of the current chunk — once we start overwriting the buffer with new data, all the references pointing the previous data in the buffer must be gone.

It would be nice to express this as an iterator pattern! Think of it: looping over the contents of the stream like we usually loop over data containers. However, there’s a problem with the iterator interface that blocks us from expressing this:

trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}

The main workhorse of the Iterator trait is the next method that returns the items of the iterator. Item is an associated type — every implementation of the trait can decide what that iterator spits out. As we are iterating over a stream, we would like to spit out a reference to the buffer that holds the current chunk of the data. However, if we set type Item = &'a Chunk , we soon stumble into expressiveness problems. What is the lifetime 'a ? It should be the same lifetime as of the &mut self of the next method, but as Self::Item doesn’t have any lifetime parameters, we are unable to express that!

It turns out that iterators are able to return only 'static items (items that don’t contain lifetimes at all or items that contain only lifetime 'static ) such as String or u32 , or items with lifetimes that are nameable by the type that implements Iterator . Here’s an example of the latter:

struct StrVecIter<'a> {
    index: usize,
    inner_vec: &'a Vec<String>,
}
impl<'a> Iterator for StrVecIter<'a> {
    type Item=&'a str; // We can use StrVecIter's 'a here
    fn next(&mut self) -> Option<&'a str> {
        let s = self.inner_vec[self.index].as_str();
        self.i += 1;
        Some(s)
    }
}

Turns out that Rust’s iterators don’t support so-called streaming iterator pattern. They can only return references that live as long as the container they refer to, lives. We want to return references with a shorter lifetime — by the next call to next the previous reference should already be gone!

Remedy: generic associated types

The problem was with the associated Item type — it should be generic over lifetimes so that we could use it in the next method to equate it with the lifetime of &mut self each call. As it turns out, Rust doesn’t at the moment support generics in the associated types of traits. Fortunately that is subject to change: an RFC that enables that feature has been accepted: https://github.com/rust-lang/rfcs/blob/master/text/1598-generic_associated_types.md

The implementation is not done yet and the feature is not considered high priority before 2018 edition, but I’m hopeful that we’ll hear more about it early next year and possibly see it stabilised at some point of the year. This feature doesn’t only provide added expressiveness around streaming iterators, it should enable a whole lot of other nice patterns too.

Open problems: from here on there be dragons

At this point, we have exhausted stuff that are agreed upon via the RFC process. For the rest of the items in this article, there are going to problems without ready-made remedies. I think each of these problems can be solved, but doing so will require a significant amount of design and consensus work.

10. Downgrading a mutable lifetime to a shared one

It’s not uncommon to call methods that mutate some fields but return just a shared reference:

impl Request {
   fn get_header(&mut self) -> &Header {
      if let Some(cached) = self.cache.retrieve() {
         cached
      } else {
         let parsed = self.parse_header();
         let cached = self.cache.store(&parsed); // Needs &mut self
         cached
      }
   }
}
...
let header = request.get_header();

However, as long as we grab to our header , we aren’t allowed to call any other methods on request ! It’s completely locked up. Why? Because we originally borrowed request with a mutable lifetime and no additional borrows are possible until that lifetime has ended.

But, you say, the returned header is only a shared reference! Surely calling other methods that take &self , a shared reference, is okay. But it’s actually not. For what the borrow checker knows, get_header could have stashed a mutable reference to request somewhere. Maybe send it to another thread? Maybe store it in the local thread storage? Maybe hide it in a Cell<Option<&mut Cache>> field in the returned header ? There isn’t any ironclad guarantee that the mutability of the lifetime would be actually over until the end of the borrow. Here’s another great example of this: https://internals.rust-lang.org/t/blog-post-nested-method-calls-via-two-phase-borrowing/4886/33

Anyway, the request “locking up” is annoying. In some cases, it’s sensible to wrap the cache field in a Cell and be done it. But wouldn’t it be nice if the API itself could declare that it really is done with mutating things?

Remedy: Read-write lifetimes? Downgrade declarations?

There have been some ideas around having two lifetimes when taking a reference: a read lifetime and a write lifetime. Strawman syntax:

fn mut_then_share<'w, 'r: 'w>(&{'w -> 'r} mut self) -> &'r u32

The idea behind this is that the write lifetime is a subset of the read lifetime. The concerns are separated, so the write lifetime may be dropped earlier, leaving only the shared lifetime. Another plausible syntax would be some kind of “downgrade declaration”:

fn mut_then_share<'a, 'b>(&'a mut self) -> &'b u32
    where 'b: 'a + const

There seems to be some support behind these ideas, but they are effectively postponed after the work on non-lexical lifetimes has finished. There are also going to be some design issues about the conditions where downgrading can be regarded safe but I’m hopeful we’ll see this feature in some form in the future!

11. Calling mutable methods that don’t access overlapping fields

I remember being frustrated with this one when I started Rust:

struct Brute {
    name: String,
    cry: String,
}
impl Brute {
    fn new() -> Self {
        Self { name: "Pochi".into(), cry: "WOOF!".into() }
    }
fn get_name(&self) -> &str { &self.name }
fn set_cry(&mut self, cry: &str) -> bool {
        self.cry = cry.to_ascii_uppercase();
    }
}
fn main() {
    let mut pochi = Brute::new();
    
    let pochis_name = pochi.get_name();
    
    pochi.set_cry(&pochis_name); // Can't borrow!
}

This pattern often emerges with getter/setter style accessors. The problem is that the methods take references to self as a whole and — as mentioned earlier — by design, the borrow checker can’t peek into the method bodies and see which fields are actually accessed.

If a similar access pattern is done locally, there is no problem; the borrow checker can see that the name and cry fields are non-overlapping and can be borrowed separately. That means that as a workaround with structs, you can set the fields public and access them directly, but this isn’t good if your type has invariants you want to protect. For example, here we want to ensure that cry is always uppercase!

This is especially troubling when working with traits and generic code. Traits are collections of interface methods; even if the underlying type that implements a trait has non-overlapping fields, the trait hides that as an implementation detail, so you are forced to access self without finer granularity.

Remedy: Fields in traits? Read-only fields? Partial borrows?

To address the problem with traits, there has been an RFC (https://github.com/rust-lang/rfcs/pull/1546 ) that allows defining field in traits that each implementer maps to the corresponding fields it has. Fields are different from setter and getter methods in the sense that the compiler can verify that they actually map to non-overlapping memory, allowing safe access. The RFC was postponed for now— but there is still demand for a feature like this and I’m quite sure it will be revisited after the 2018 edition has shipped.

Part of the design space has also got to do with mutability of fields. There are quite often patterns where you can show the contents of a field, but not allow it to be modified safely. As getters have the granularity problem described above, it would be desirable to expose the field itself publicly, while restricting the access to it to immutable only.

There has been also some ideas floating around about refining the granularity of self : methods would declare the fields they access in the function signature:

fn get_name(self { &name }) -> &str { &self.name }
fn set_cry(self { &mut cry }, cry: &str) -> bool {
    self.cry = cry.to_ascii_uppercase();
}

This would allow the borrow checker to conclude that the method calls access non-overlapping fields without peeking into the method body.

All of these ideas have some drawbacks in the sense that they expose things that are currently thought of implementation details but we will see what comes out of them.

12. Hiding mutable lifetimes in data types

Some time ago there was a blog post by Aleksey Kladov ( https://matklad.github.io/2018/05/04/encapsulating-lifetime-of-the-field.html ) that highlighted a problem with lifetime annotations in data types. The problem raises its head when nesting types with mutable lifetimes:

struct Foo<'s> {
    string: &'s mut String,	
}

struct Bar<'f, 's: 'f> {
    foo: &'f mut Foo<'s>
}

struct Hoge<'b, 'f: 'b, 's: 'f> {
    bar: &'b mut Bar<'f, 's>
}

// As you see, the declarations are getting longer and longer!
struct Piyo<'p, 'b: 'p, 'f: 'b, 's: 'f> {
    hoge: &'p mut Hoge<'b, 'f, 's>
}

The lifetime annotations don’t compose well! This isn’t a problem with shared lifetimes, which stay clean:

struct Foo<'s> {
    string: &'s String,	
}

struct Bar<'f> {
    foo: &'f Foo<'f>
}

struct Hoge<'b> {
    bar: &'b Bar<'b>
}
struct Piyo<'p> {
    hoge: &'p Hoge<'p>
}

The difference here is that shared lifetimes — due to their restrictions with mutation — can be subtypes of other shared lifetimes. Mutable lifetimes, on the other hand don’t “mix and match”. This is a quite fundamental difference between the two kinds, and it must be respected to avoid soundness issues. However, the proliferating lifetime annotations get icky rather quick.

Remedy: Hiding mutable lifetimes from type signature?

Inspired by Aleksey’s blog post and the trick explained there that lifetimes can be hidden with trait objects, I started thinking about reifying this hiding mechanism as a language feature. I started writing an RFC, but it’s essentially just a draft at the moment: https://internals.rust-lang.org/t/pre-rfc-encapsulating-private-lifetimes/7500 Getting busy with other things in life, I haven’t been polishing it, but hopefully I’ll manage to return into it some day.

13. Using “ambient” lifetimes

One of the important concerns for library code is modularity. A big part of application programming is composing available libraries to achieve higher-level goals, but if the libraries don’t play nicely together, this gets troublesome. Libraries should be either as simple or as generic as possible; or preferably if there exists a way to be simple and generic, do that. Especially requirements of specific “ambient” features in the runtime environment limit the generality of libraries. A great example of this is that the libraries with no_std capabilities have greater composability because they don’t depend on the standard library.

This is why I often think of 'static lifetime bounds as undesirable things in APIs. It limits what the user can pass in. If possible, it’s always better to be generic with regards to lifetimes to allow the user pass values with lifetimes that suit themselves.

What makes “forced” 'static even worse is the fact that statics are so hard to initialise. The crate lazy_static helps and there is also pattern where you can “forget” a heap-allocated value to protect it from deallocation for the rest of the program lifetime and turn it into a reference to 'static . But these are essentially hacks: if you are unable to clean up after finishing your business with a library, that library can’t be said to be composable. Rust doesn’t have “life before main”, which is a great design decision—besides the other problems it prevents, it also reifies the fact that one should avoid “hard-coding” lifetimes. But requiring ‘static is essentially that: hard-coding lifetimes.

However, there’s an understandable reason why one would like to use the 'static lifetime for things: it’s the only globally nameable lifetime. An ambient lifetime, so to say. Because it’s a concrete lifetime that’s available everywhere, it doesn’t proliferate in the type declarations like generic lifetimes do. It’s more ergonomic to use and easier to understand.

Is there any way we could prevent the hard-coding problem and still have the ease of using 'static ?

Remedy: ambient lifetimes/module-level lifetimes

Imagine if there would be a lifetime like 'static in the sense that you don’t have to write it into the signature of your types? A lifetime that would say: I live longer than this struct could ever possibly live, so you don’t have to care what I am.

mod library<'ambient> {
    struct StringRefs {
        ref_a: &'ambient str,
        ref_b: &'ambient str,
    }
fn take_refs(refs: StringRefs) {
        println!("{}", refs.ref_a);
    }
}

Here, 'ambient would live longer than any type defined in module library . It’s like a local version of 'static ! Then, repurposing the use import statement a bit:

fn main() {
    let string_a = String::from("No life");
    let string_b = String::from("Before main!");
use library as lib { // 'ambient gets assigned to this scope
        let r = lib::StringRefs {
            ref_a: string_a.as_ref(),
            ref_b: string_b.as_ref(),
        };
        lib::take_refs(r);
    }
}

Of course, the compiler would prevent any type with 'ambient leaving the scope. This allows initializing everything in main but after that initialization is done, the lifetime of the state that lives there can now be “freely” referred by the types, without the need to carry the lifetime information around in the type signatures.

Another interesting idea would have scopes that “repurpose” what 'static means for the code inside that scope: the code thinks that it has references to static things, but the caller has actually redefined it to be a narrower lifetime. I haven’t thought much about the soundness implications though; it might prove to be an outrageously unsafe idea.

14. Moving the owner of a heap-allocated object that has an inbound reference

I think of this problem as the granddaddy of all lifetime problems and that’s why I left it in the end. Lifetimes in Rust are essentially subject to stack discipline. An “outlives” relationship between two lifetimes means that the longer-living one originates from an outer scope or an earlier (shallower) stack frame.

A reference such as &'a Foo<'b> can be thought as two values: the &'a part, which is essentially just a pointer, and the value being pointed at, here Foo<'b> . The basic rule, of course, is that the value being pointed at must live longer than the pointer—but to elaborate, it must live longer at the location being pointed at . You see, there’s another distinction to be made: we can think of the value as pure information that we can copy and move around — or we can think of the memory slot the value resides in. That can’t move around. Pointers point at memory slots, so Rust ensures that slot stays valid by freezing the value, keeping it there.

Here the stack discipline kicks in: we can call other functions and pass the pointer deeper in the stack, while the pointed value stays put. But once we return, at some point, the memory location where the value resides must be given up. That’s the maximum extent the reference can live. (The minimum extent is of course, up to us, because we can just drop the reference anytime we want.)

This principle works wonderfully with the call stack. But it’s too restrictive with the heap. The lifetimes are not aware of the heap — there can be references to the heap, but they act as if the heap would be just a nice extension to the stack that allows dynamically sized allocations. The lifetime of a reference to a heap allocation is still constrained by the stack frame where the lifetime of the reference originated from.

That’s not how heap works though: unlike with stack, it’s possible to do a heap allocation and return that allocation from a function—so it’s unordered! And here’s the problem, hinted in the item 7 about self-referential structs: the lifetime system doesn’t understand that the lifetime of a heap-pointing reference is not constrained to the extent where the stack-allocated owner of the heap allocation  —  such as a Box —is located at the moment of the borrow. It’s constrained to the extent the heap allocation lives , and the heap allocation in many cases lives as long as the value of its owner lives. Note that here I don’t mean the memory slot of its owner but the actual value ; the piece of information that can be moved around until it’s destructed.

It would be sound in principle to have a reference to a heap-allocated value and return that reference alongside of the owner of the heap allocation from a function, down the stack. Likewise, it would be sound to store them in a struct and encapsulate all the lifetimes involved. One could pass the struct along without any lifetime restrictions, because the lifetimes would be implementation details. This would be a boon for zero-copy parsers, graphics API wrappers ( https://github.com/vulkano-rs/vulkano/blob/master/TROUBLES.md ) and basically everyone using the crates Rental and Owning-Ref at the moment.

Remedy: Dependent/existential lifetimes?

There is a significant challenge in designing a lifetime system that would ensure the soundness in safe code while allowing greater flexibility with references to heap allocations. Such a system must adhere to all the design principles of Rust mentioned earlier: it should be locally analysable and statically checked. It should be combatible with the current lifetime system and preferably a minimal extension. I’m not aware of any serious attempts at trying to come up with a working solution yet.

We could think of the pointer and owner of the pointed allocation as a pair that are connected by the guarantee that at no point the pointer is “lower” in the call stack or in the local scope than the pointee — either the pointer is deeper in the stack or they are being passed down together. Imagine something like this:

// Note the for syntax!
fn init() -> for<'base: 'ref> (BorrowedBox<u32, 'base>, &'ref u32) {
    let heap_box = Box::new(10);
    
    // get_existential takes the box as a value
    // and returns a pair with an existential lifetime
    let (borrowed_box, heap_ref) = heap_box.get_existential();
    
    // The borrow checker locally checks that any value
    // containing 'ref doesn't outlive a value with 'base
    
    // stored in a single data type whose soundness is ensured
    // by the for<'base: 'ref> annotation of the function signature
    (borrowed_box, heap_ref)
}

And this:

struct Encapsulated { // No lifetime here!
    for 'base: 'ref,
    heap_box: BorrowedBox<u32, 'base>,
    heap_ref: &'ref u32,
}

I’m thinking of continuing sketching around this.

Closing words

Rust is marching towards the 2018 edition release and a huge amount of work has been done to stabilise some long-awaited features and polish the ergonomics story. Most improvements mentioned in items 1–9 are landing really soon, which is incredibly exciting.

However, when considering the future of Rust in the long term, I think the story around lifetimes and mutability isn’t done yet. Lifetimes are Rust’s flagship feature and they have shown the world that memory management without garbage collector is possible in a sound and a practical way.

In the future I’d like Rust to go all the way and prove to the world that not only memory management with lifetimes is possible , it can be also ergonomic and expressive . The items 10–14 are things that I consider problems that continue hindering users of lifetimes even well after the 2018 edition has shipped. They are also problems that I think are worth solving especially because lifetimes are such a central feature of Rust and Rust has spearheaded their use in real-life code. It frustrates me to think that the problems we still have with granularity, leaking abstractions, unpolished ergonomics and lacking expressiveness might lead some people to think that lifetimes are not worth the hassle. I want the world to see the ultimate form of lifetime-based memory management!

P.S. If you think that I’ve missed some obvious problem with the current lifetime/mutability system, please let me know!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK