Why does rustc consider self to be borrowed when it hands out a generic ref but not when it hands out a static ref?

I created an API that is effectively a glorified vector that only takes Strings and hands out a &'static mut String to its elements:

struct MemManager {
    ptr: *mut String,
    len: usize,
    cap: usize,
    marker: std::marker::PhantomData<String>,
}

impl MemManager {
    fn new(cap: usize) -> Self {
        unsafe {
            let layout = std::alloc::Layout::array::<String>(cap).unwrap();
            let ptr = std::alloc::alloc(layout) as *mut String;
            Self {
                ptr,
                cap,
                len: 0,
                marker: std::marker::PhantomData,
            }
        }
    }

    pub fn push(&mut self, string: String) -> usize {
        unsafe {
            let index = self.len;
            std::ptr::write(self.ptr.add(index), string);
            self.len += 1;
            index
        }
    }

    pub fn get(&mut self, index: usize) -> &'static mut String {
        unsafe { &mut *self.ptr.add(index) }
    }
}

Very straight forward. Then I created a Wrapper that wraps MemManager:

struct Wrapper(MemManager);

impl Wrapper {
    fn new() -> Self {
        Self(MemManager::new(10))
    }

    fn get_data(&mut self, index: usize) -> StaticData {
        let data = self.0.get(index);
        StaticData { data }
    }

    fn push(&mut self, string: String) -> usize {
        self.0.push(string)
    }

    fn other_mutable(&mut self) {}
}

Notice that get_data() doesn't hadn't out the String references directly but hands out another wrapper that looks like this:

#[derive(Debug)]
struct StaticData<'d> {
    data: &'d mut String,
}

Note that while the API that the string ref comes from (MemManager's API) was &'static mut String this wrapper stores the String ref as a generic lifetime. This leads to an interesting result: Wrapper is considered borrowed while the StaticData instance is still alive.

fn main() {
    let mut wrapper = Wrapper::new();
    let index1 = wrapper.push("Jumbo".to_string());

    let data = wrapper.get_data(index1);
    wrapper.other_mutable();
    println!("{data:?}");
}

This code will NOT compile and rustc gives the following error:

error[E0499]: cannot borrow `wrapper` as mutable more than once at a time
  --> src/main.rs:80:5
   |
79 |     let data = wrapper.get_data(index1);
   |                ------- first mutable borrow occurs here
80 |     wrapper.other_mutable();
   |     ^^^^^^^ second mutable borrow occurs here
81 |     println!("{data:?}");
   |               -------- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` (bin "playground") due to 1 previous error

However, if you change the generic lifetime on StaticData to use a 'static lifetime:

#[derive(Debug)]
struct StaticData {
    data: &'static mut String,
}

all of a sudden Wrapper is no longer mutable borrowed while the StaticData instance is still around and you get the following output:

                                 Standard Error
   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/playground`
                                 Standard Output
StaticData { data: "Jumbo" }

Part of me thinks this might be something to do with elided lifetimes and the generic lifetime is inheriting the &mut self lifetime but I cannot find anything in The Book or the Rust Reference that talks about how the compiler determines generice lifetimes stored in a struct. They all talk about the lifetimes of references returned directly. Also, my understanding was that lifetimes stipulate how long a borrow lives NOT what is being borrowed (Although, yes, what is being borrowed is necessarily intertwined with lifetimes, a lifetime being generice vs. static should change that from my understanding) .

What is going on here??

Lifetime "elision" is the term you'll want to look up.

Here are some pointers:

Don't hide lifetimes - Learning Rust

Lifetime Elision - The Rustonomicon

Lifetime elision - The Rust Reference


Edit: I see you already mention elision in your reply. Indeed, the rules of elision are not specific to reference types but apply to all lifetime parameters in all types appearing in function signatures. Note that the style of elision that your code uses, where the lifetime parameter on StaticData is completely hidden in the Wrapper::get_data function signature is discouraged (unfortunately still not warn-by-default, but you really should avoid it, and there's an opt-in warning, see the first link above).

1 Like

I mentioned lifetime elision near the bottom of my post. I looked over your links. Was my intuition correct?

Part of me thinks this might be something to do with elided lifetimes and the generic lifetime is inheriting the &mut self lifetime

Yes, here's the relevant example from the reference (3rd link in my previous reply):

these three are equivalent

fn new1(buf: &mut [u8]) -> Thing<'_>;                 // elided - preferred
fn new2(buf: &mut [u8]) -> Thing;                     // elided
fn new3<'a>(buf: &'a mut [u8]) -> Thing<'a>;          // expanded
1 Like

Thank you! I see that due to the generic lifetime, by default the generic is considered to be tied to &self unless stated otherwise therefore self gets borrowed. Also, I didn't know leaving out the lifetime was discouraged and unidiomatic. I learned a lot here.

1 Like

This is true and false. True in that the design behind lifetime annotations is kind-of to give names to how long something is borrowed. False in that the nature of lifetime annotations in Rust means that they really mostly serve only to relate different borrows to each other, and thus it's more of a "what can (re-)borrow from what" kind of annotation.

This is because lifetime annotations can only be generic variables or 'static. Admitted, 'static kind-of spells out how long something lives explicitly (see also the next paragraph though); but as long as they're generic variables, all you can say is "this can be any lifetime", which doesn't really say much about "how long" something is borrowed. The only additional information is in making multiple places use the same lifetime, or adding outlives relations between them. It's those relations then that really convey any meaning.

In this context of asking "what" is borrowed, you could think of 'static as saying "this borrows from nothing". For most intents and purposes a static reference like &'static str is just as versatile in use as directly owned data like a i32, and doesn't really contain any (relevant) borrows at all.

5 Likes

Not really related to your question, but I wonder if you could solve your problem without writing new unsafe code by using typed-arena. It doesn't intrinsically promote things to 'static (unless you store the arena in a static variable), but it gets you the “store values and get back &mut” part.

You probably only included the code relevant to the question, but just in case: to prevent UB don't forget to check cap in push and check len in get.

That's probably not enough to prevent ub, you can still get aliased mut refs to each element.

1 Like

I believe typed-arena uses as Vec on the backend. That means that getting &'static mut probably isn't possible. That of course is not an issue if it is not an issue.

If the Arena is made 'static (e.g. with once_cell::sync::Lazy or Box::leak) then it can hand out 'static references.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.