Announcing Rental 0.4

I've just published a new, vastly improved version of rental. This crate allows you to define arbitrarily self-referential structs, and even chain such structs together transitively.

Taken from the readme, here's the classic libloading example, where we want to store a dylib and a symbol from it in the same struct:

rental! {
    pub mod rent_libloading {
        use libloading;

        #[rental(deref_suffix)] // This struct will deref to the target of Symbol.
        pub struct RentSymbol<S: 'static> {
            lib: Box<libloading::Library>, // Library is boxed for stable deref.
            sym: libloading::Symbol<'lib, S>, // The 'lib lifetime borrows lib.
        }
    }
}

fn main() {
    let lib = libloading::Library::new("my_lib.so").unwrap(); // Open our dylib.
    if let Ok(rs) = rent_libloading::RentSymbol::try_new(
        Box::new(lib),
        |lib| unsafe { lib.get::<extern "C" fn()>(b"my_symbol") }) // Loading Symbols is unsafe.
    {
        (*rs)(); // Call our function
    };
}

Here, instead of the old 'rental lifetime, that nonsense is gone and each field has a lifetime named after it. This allows for far more flexible arrangements. This example was simple, but let's take a look at one for alto which was impossible in the previous version:

rental! {
    pub mod rent_alto {
        use alto;

        #[rental]
        pub struct RentContext {
            alto: Box<alto::Alto>,
            dev: Box<alto::Device<'alto>>,
            ctx: alto::Context<'dev>,
        }
    }
}

fn main() {
    let alto = alto::Alto::load_default().unwrap(); // Load the default OpenAL impl.
    if let Ok(rent_ctx) = rent_alto::RentContext::try_new(
        Box::new(alto),
        |alto| alto.open(None).map(|dev| Box::new(dev)), // Open the default device.
        |dev, _alto| dev.new_context(None), // Create a new context for our device.
    ) {
        rent_ctx.rent(|ctx| {
            // Do stuff with our context
        });
    };
}

The old version didn't support 3 or more layers of self-reference. It always chafed that I was the author of two crates that couldn't even be used together, so I'm glad to finally have this resolved. You can go as many levels deep as you want (well, up to 32 right now, but I can easily raise the limit if necessary). Here's a more contrived example from the test suite:

rental! {
	mod rentals {
		use super::*;

		#[rental]
		pub struct ComplexRent {
			foo: Box<Foo>,
			bar: Box<Bar<'foo>>,
			baz: Box<Baz<'foo, 'bar>>,
			qux: Box<Qux<'foo, 'bar, 'baz>>,
			xyzzy: Xyzzy<'foo, 'bar, 'baz, 'qux>,
		}
	}
}

There are still some limitations as described in the readme, but this version is far, far more capable than the old one was. After taking a break for a little while I'll update alto to use it.

Let me know if you have any questions or suggestions or what have you. I'm eager to start putting this to use.

6 Likes

Thanks for the update! The API looks really nice.

But I have a little problem, I've tried to create an OwnedSlice:

rental! {
    pub mod foo {
        #[rental]
        pub struct OwnedSlice {
            buffer: Vec<u8>,
            slice: &'buffer [u8],
        }
    }
}

but I hit an error:

   = note: expected type `&u8`
              found type `&[u8]`

Note: if I change Vec to Box, it works. Is this usecase supported by rental? Am doing it right?

Internally, rental uses the single type parameter of a prefix field as the implied deref target, u8 in this case. This is a hack to work around bugs in HRTB associated item unification. More importantly though, Vec is not guaranteed to deref to a stable memory location, so it can't be used as a prefix field. EDIT: I'm dumb, Vec is StableDeref, I forgot about that.

Now, naturally, the address of a Vec's data does not change unless you mutate it, so it's possible this scenario could be supported with a finer grained trait that described that property. I'll ponder this some more, but for now, yeah, I'd recommend just boxing it.

1 Like

Pardon me for not knowing your internals, but if you're allowing mutation, doesn't that make all types have unstable deref memory locations? Even Box<T> will move if I can mem::replace it.

Good observation; this is a subtle but important point. It would be unsafe if I allowed access to the top level type, but I don't. Once you provide the Box or Arc or whatever, the API will internally deref it for you and give you the result. You can't mem::swap it because you never have access to it again.

2 Likes

OK, in that case, Vec is just as stable if you can't access the outer type to push or anything, no? If you can only access the dereferenced &mut [T], then it's not going to move anywhere. And FWIW, Vec does implement StableDeref.

You're right, it would. Unfortunately this doesn't work yet as a result of the syntactic hack I had to use to determine the deref target. Now that you mention it though, I could perhaps add a special case for Vec, since it derefs to a slice of its argument instead of just the argument itself. I'll look into that.

Ah, if that limitation is just about macro hacking, I totally sympathize. I ran into a similar issue creating some internal rayon macros, and ended up forking into one version with an implicit item type picked from the parameters, and another that makes it explicit.

In that case, I didn't want any part of the public API to leak details about the inner types. But in your case, maybe you could just use the actual deref types? Something like <$ty as Deref>::Target?

1 Like

That's actually what I did originally, and what I'd like to do, but the the type checker failed to unify the types since HRTB lifetimes are involved. There are open bugs surrounding this, and it might work after the trait system refactor goes through, and it will for sure work once ATCs land. The syntax hack was just a workaround in the meantime to get it to compile. I still use the associated type to ensure that the type is correct, so it won't compile if you get it wrong, but I can't use the associated type in all positions yet.

2 Likes

I've just published a new version that fixes this; Vec is now a valid prefix field. You can even use the deref_suffix option and your rental struct will deref straight into the slice.

EDIT: Also added a similar workaround for String, although that's probably less useful.

2 Likes

Published version 0.4.3 with a few more quality-of-life improvements. You can use the debug_borrow option on your rental attrib to gain a Debug impl if all of your fields support that. Also, if you're defining a custom StableDeref container or using some other type that rental can't properly guess the Deref::Target of, then you can use the target_ty_hack = "ty" attribute on the field to tell it the correct type. This will probably be particularly useful in no_std scenarios.

Also fixed a potential soundness hole where an improper Deref target could make it through, but you'd have to really go out of your way to craft such a malicious type. Still, I've tightened that down and such types won't be able to sneak through anymore.

1 Like

This crate does fill a useful gap that rust generally misses, so thanks for all the hard work you've put into it! Usually when I need a self-referencing struct it's for performance reasons (e.g. I need to avoid an extra layer of indirection by using references into vector items instead of indices).

Looking at the implementation of rental, it looks like it wraps all my struct's fields in an Option type and then unwraps them whenever obtaining a context. If I'm understanding this right, it might be a good idea to document that since it does limit the applicability to certain applications.

Yeah, that's an unfortunate consequence of drop order being unspecified currently. Fortunately, the stabilize drop order RFC has been proposed for merge, so once that happens and it makes it past FCP I'll remove the Option wrappers and all accesses will be direct.

1 Like

Now that stable drop order has passed FCP, I've removed the Option wrappers and all accesses are now direct.

You realize the RFC hasn't actually been merged yet, right? It's very likely to be, considering the lack of further discussion during final comment period, but if you were going to wait, I'd have waited slightly longer :slight_smile:

1 Like