Static mutable references to ZSTs

Context: It should be possible to get a mutable reference to a ZST literal · Issue #103821 · rust-lang/rust · GitHub

It currently isn't possible to get a 'static dangling ref to a ZST without resorting to Box::leak or NonNull::dangling(), which is unsafe. While it does work for mutable slices, the produced assembly for &mut [] is less than ideal since it loads a pointer to the data section of the binary. (Compiler Explorer)

Also, the Box::leak method does not work without core::alloc.

A possible solution is std::mem::dangling, defined as:

pub fn dangling<'a, T: 'a>(zst: T) -> &'a mut T {
  assert!(std::mem::size_of_val(&zst) == 0);
  unsafe { &mut *std::ptr::NonNull::dangling().as_mut() }
}

This may need a different solution for str, since going from [u8] -> str with std::str::from_utf8_mut generates a lot of code. &mut str could impl From<&mut [u8; 0]>, for example.

Note that leaking heap memory with Box::leak() is not unsafe. But yes, by its very nature it requires heap allocation capabilities to work.

What's the use for it?

Mutation of ZST doesn't do anything, so why should libstd have special support no-op code that tries to do this?

The code appears to be a missing core::mem::forget(zst) (or equivalent). You can't copy the value with a Copy bound and as written it will be dropped at the end, which makes the accessible value afterwards an invalid copy. The intended effect is to pretend such a value to have beeen ''written-to'' the returned memory location. An alternative would indeed be ptr::write(_, zst) which is another no-op that simply forgets the value.

2 Likes

Creating a (reference to) an instance of an arbitrary ZST has to stay unsafe, because the ZST might be some kind of significant token (e.g. providing unique access to mutable static state). Just because a type has no data bytes doesn't mean you should get to construct it if the language doesn't already allow that.

I agree it can and should be possible in the case where you can construct an instance, though. (I just recently was disappointed to find out that I couldn't declare a const that was a mutable reference to a function item ZST, in order to make an easy "null value" of type &mut dyn FnMut().)

9 Likes

Note the the signature as written can be sound. A safe re-implementation is:

pub fn dangling<'a, T: 'a>(zst: T) -> &'a mut T {
    Box::leak(Box::new(zst))
}

However, one of the author's goals is for this operation to be available in no-std and without mention an allocator in the type system. The static code path is indeed pretty similar to the implementation above except for the drop omission mention in my other comment. It doesn't even allocate.

Oops, I missed that the function took a parameter of type T, and was thinking of the case

fn dangling<'a, T: 'a>() -> &'a mut T

which would have the problem I described. Sorry for the noise.

I don't think we'll get a runtime-checked version in std, but if/when it's possible to write where const { size_of::<T> == 0 } (or equivalent), then it seems reasonable to add.

1 Like

I had a PR for an API for "conjuring" ZSTs (Add `mem::conjure_zst` for creating ZSTs out of nothing by scottmcm · Pull Request #95385 · rust-lang/rust · GitHub), but it didn't go anywhere. It could have had conjure_zst_mut and such too.

1 Like

A potential problem with conjure_zst is that it allows creating ZSTs from outside crates. This may break API contracts in those crates.

For dangling, the ZST must be provided as an argument.

1 Like

That's why it's unsafe, yes. And the doc-comment says this is why it's unsafe :slightly_smiling_face:

A nice example of why conjuring ZST is unsafe is the token type of GhostCell. Duplicating the token would allow mutable aliasing of the data.

But I agree that a way to leak an already-constructed ZST without Box would be useful.

1 Like

(NOT A CONTRIBUTION)

The first post should use size_of instead of size_of_val; the compiler should then be able to optimize away the assert pretty easily.

Seems like a strange requirement to me.

I would want to put the ZST requirement in the signature such that it can be made to always succeed, rather than panicking for non-ZST. Box::leak works fine for all types; the point of a new way of doing it is that it can work without alloc, but only for ZST.

I don't think we have any publicly exposed and stable functionality which panics based on ZST or non-ZST (though I could just be forgetting some); rather, there's a significant amount of effort to ensure everything doesn't break for ZST. The closest is the unstable slice::array_chunks which panics if the const N is 0, but there's documented intent to make that into a compile error before stabilization. To me, the static information of ZST-ness falls into that same category.

(NOT A CONTRIBUTION)

I don't think array_chunks should be blocked on that either.

The Rust standard library has plenty of APIs that document that they can panic if called with invalid arguments, because the type system can't easily express the constraint. In this case (and the array chunks case), it seems like a much more trivial burden to put on users to ensure their programs don't panic than in many others, such as bounds checking. I don't think useful APIs (here I am thinking more of array_chunks than this API) should be blocked on an language feature that hasn't even been properly designed yet unless the lack of that feature has a clear, major negative impact on usability.

4 Likes

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