Probably Simple Lifetime Issue

Hi all,

I feel like lifetimes are going to be the last thing I understand properly in Rust. I came across the following issue:

pub struct Item<'a> {
    zip_archive: ZipArchive<File>,
    buf_reader: BufReader<ZipFile<'a>>,
}

impl<'a> Item<'a> {
    pub fn new(filename: &str) -> Self {
        let file = File::open(filename).unwrap();
        let mut zip_archive = ZipArchive::new(file).unwrap();
        let npy: ZipFile = zip_archive.by_index(0).unwrap();
        let mut buf_reader = BufReader::new(npy);
        Self {
            zip_archive,
            buf_reader,
        }
    }
}

In my mind this should work. buf_reader contains a reference to a ZipFile tied to the lifetime of Item. In new zip_file is borrowed from zip_archive and the compiler states that zip_archive is dropped at the end of new, but how can I tell the compiler that Item will own zip_archive meaning that the reference to zip_file will be alive for the same scope (as it's tied to the lifetime of Item)?

This line:

pub fn new(filename: &str) -> Self {

Should be read as saying this:

pub fn new(filename: &'a str) -> Item<'a> {

That is, your new function is saying that the item must not outlive the filename you've given it. If you want an Item that owns its ZipFile, you need to construct an Item<'static>, or an item that is not borrowing anything.

Another problem you probably have is that your ZipFile appears to borrow from the ZipArchive, which means Item is a self-borrowing struct, and that's not allowed in safe Rust. (If Item is moved, your buf_reader could become invalid.)

An Item<'a> is necesarrily only valid within the scope of some object with lifetime 'a that it borrows from. If you're borrowing from a ZipArchive, you need to pass a reference to the ZipArchive into Item::new when you construct the item.

Thanks very much. I thought that the reference to filename might have been an issue and I refactored it to take a owned string at one point but couldn't get it to compile.

I think this is the issue, a self borrowing struct. I guess I will need change it so that I'm holding a reference to a ZipArchive.

Thanks again.

I maybe wrong, but isn't the whole point of introducing Pin solving this problem?

But how can this be explained? Don't structs own their fields? If structs do own the fields, then the newly formed struct Item should have the ownership of the zip_archive field and the zip_archive variable declared in new function should no longer be the owner and the value shouldn't be dropped.

Pin doesn't magically solve the problem. You still need unsafe code to create a self-borrowing struct, Pin just makes it easier to encapsulate the unsafe code.

The real issue is that ZipFile borrows from ZipArchive, and moving the local variable into a returned struct is a move of that ZipArchive, which is not allowed while ZipFile borrows from it.

1 Like

This is not quite correct: an elided lifetime will not unify with a named lifetime from an outer scope (see the next post by Yandros). The actual unelided version of new is

pub fn new<'b>(filename: &'b str) -> Item<'a> { /* ... */ }

which means the 'a actually comes from nowhere; the caller may choose any lifetime it pleases, even 'static. The only way to satisfy that constraint inside new is to return an Item<'static>.

In this case it doesn't make a huge difference to the implementor of new because 'a and 'b are both chosen by the caller, and equally unsatisfiable.

2 Likes

Indeed! Minor nitpick / disambiguation though: by named you mean a lifetime that is fixed / introduced by an outer scope.

1 Like

So, if I have a ZipArchive with a single ZipFile in it is it even possible to create a struct that iterates over the ZipFile? This is basically what I would like to do, maybe I am approaching the problem in the wrong way.

The ZipFile should be in a separate struct from the ZipArchive.

Hmm, ok I see. Thanks very much.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.