Proposal to add 'sandboxed' compile-time enforcement for element access

There’s an alternative implementation possible for the same basic idea: instead of using something like sandboxed_scope to create a reference from an existing vector, instead you allocate a MaybeUninit<Vec<…>> and then have a constructor for SandboxMut that takes the MaybeUninit as an argument, initialises it and returns a reference into it. I hadn’t seen the version that takes an existing vector before, but they complement each other well (and are probably both special cases of the same general idea: you have a “bounding type” – Vecin your case and MaybeUninit in mine – and can have references to it that treat it as having any subtype of that bounding type).

I’ve been meaning to write a lot on this topic for a while, but haven’t managed to find the time. The underlying technique, though, is a) very powerful and b) very different from the way things are normally done in Rust – it effectively lets you implement strong updates, but at the cost of objects being harder to create than normal and references acting a bit differently

Some other applications for this pattern which would be hard to implement using normal Rust techniques:

  • implementing objects that can’t be moved, like futures (Pin is an existing workaround for the issue, but you can use this sandboxing technique instead; given how confusing Pin is, it might even be more ergonomic – it also statically guarantees that you don’t get panics from polling a future after it returns);
  • implementing memory allocators in safe Rust that allocate from a buffer on the stack (if you try to do it naively, you discover that the allocator becomes a cyclic structure, because it needs a slice of “memory I haven’t used yet” but that points to inside the allocator itself – this patterns olves it because the slice gets stored in the reference to the allocator rather than the allocator);
  • using padding bytes of enum fields as though they were niches, in order to store the tags of the other variants (this doesn’t work in current Rust because if you took a reference to the field, a write through that reference might write the padding bytes in a way that changed the enum variant – but as long as the bounding type is MaybeUninit, it works in a sandbox because the reference type can fix the padding bytes when it’s dropped and there’s no way to observe the bytes of the enum while it’s mutably borrowed)

One of the biggest changes is that if you mem::forget a regular Rust reference, the value it was borrowed or reborrowed from will always be a valid value of the type of the reference, whereas if you mem::forget one of these sandbox references, the value it was borrowed from ends up as a value of the bounding type (which is often a much more general type like MaybeUninit). In general, I think this is an improvement (it makes panic-safety easier to define and understand), but it’s very different from how Rust currently works.

The other really big change is with respect to reborrowing: Rust is very reliant on reborrowing at present for working with mutable references, but sandbox mutable references hardly use it. I wasn’t previously aware that reborrowing was even possible with this sort of reference, although this post made me realise that you could implement it using nested sandboxes.

I guess the real question here is “this sort of thing is clearly useful in some cases – but to what extent do we change the Rust libraries to use this pattern rather than more normal Rust patterns?“ You could probably rewrite the entire standard library in this form, and it might even be an improvement, but doing that would break all the existing Rust code. Rewriting just part of it in this form, alongside the existing APIs, would be confusing and might cause an ecosystem split, so it would probably be best reserved for cases where it were particularly helpful.

Perhaps all this is an argument for trying to implement this sort of thing as a new language feature rather than a library: that would probably make it easier to integrate with existing code. But it’s currently unclear to me what such a feature would look like (although there are some thoughts in the strong updates post I linked above, they probably aren’t directly applicable to the Vec situation).

1 Like