-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add an unsafe fn
for creating a ZST
#292
Comments
I don't think this is neccessary as one can just do Edit: I guess you'd need it for |
But that's still different because it needs an instance. I suppose one could (That said, I'm still considering the reference versions out of scope for this ACP, unless libs-api says otherwise.) |
The value of including reference-returning versions would be to enable creating ZST refs while communicating clearly to readers that no allocation is happening, and also discoverability; searching (But perhaps most of the value would come from documenting these tricks in |
I'm skeptical that this is something we truly want. While I understand that the docs would certainly say that it has to uphold all safety invariants of the produced type, I feel that a nontrivial number of people will either overlook that requirement, ignore it, or not bother to check what the safety requirements actually are. |
I feel like this can be applied to almost every Having a well documented function gives us where to point when we spot UB. |
|
I suspect that anyone who writes |
I've seen several cases of people asking about using transmute or other tricks to produce a ZST. The question they manage to think of is “Is this UB?”, which is a good thing to think about, but having to make that journey distracts them from the more important question — “is this correct for the ZST type?” Having an official function for conjuring ZSTs, whose documentation tells them about the actually important question, will be a usability improvement (as well as a readability improvement as discussed in my previous comment). (On the other hand, I've never seen someone ask about conjuring a ZST where they were intending to do it for a third-party type that it'd be invalid for. So perhaps this isn't important in practice.) |
I think I can mostly sum up my position in a single question. If a library wanted to permit downstream users to create a ZST out of thin air, why wouldn't they expose that behavior themself? Uses for within the same crate aren't a concern, given that they can just |
What I would hope is that the mere fact that it's unsafe helps people have the "wait, why isn't it just safe to do that?", rather than just reaching for With highly-flexible unsafe methods it's easy to think "well of course it's unsafe because of other cases, but not in this one", whereas with a hyper-specific specific method if it's
I wholeheartedly agree that However, I point to things like the linked |
We discussed this ACP in last week's libs-API meeting. Overall, we were positive on an unsafe function in core::mem for obtaining a value of ZST type, and there were no objections to We spent a while debating whether it would be more valuable as a wrapper around My initial impression was that if mem::size_of::<T>() == 0 {
conjure_zst::<T>()
} else {
// something else
} Mara brought up that she has some cases in her codebase where #[derive(Copy, Clone)]
#[non_exhaustive]
pub struct DidInit;
// Call this once at program startup
pub fn init() -> DidInit {
extern "C" {
init(); // expensive
}
unsafe { init(); }
DidInit
}
// Called often in hot code
pub fn do_interesting_thing(_proof: DidInit) {
extern "C" {
fn do_interesting_thing(); // SAFETY: requires init to have been called
}
// No runtime enforcement needed!
unsafe { do_interesting_thing(); }
}
// Most callers will thread a DidInit through from main to all
// the places that need the proof that init has been called. But
// some places (such as, behind certain third-party traits) it's
// just hard to get to and so they want to unsafely assume that
// init has been called. `do_interesting_thing(conjure_zst())` At this point I rethought and leaned toward having a A We didn't end up committed to either approach yet. I think we'll bring it up again in another meeting. After the meeting I read through all the related links:
and still not sure what to think. But supporting One novel alternative was brought up later on Zulip by @the8472. // alloc::vec::IntoIter::next, for example
trait IntoIterImpl {
fn do_next<T>(i: &mut IntoIter<T>) -> Option<T>;
}
struct IsZeroSized<const IS: bool>;
impl IntoIterImpl for IsZeroSized<true> {
fn do_next<T>(i: &mut IntoIter<T>) -> Option<T> { ... }
}
impl IntoIterImpl for IsZeroSized<false> {
fn do_next<T>(i: &mut IntoIter<T>) -> Option<T> { ... }
}
IsZeroSized::<{ mem::size_of::<T>() == 0 }>::do_next(self) If trait impls for Maybe this is "future work" more so than "alternative", because in that snippet, |
This particular case seems to me better served by the library adding an unsafe function
Idea: What if /// If `T` has zero size, then return an instance of `T`.
///
/// This function always returns a fully initialized value and cannot
/// cause undefined behavior solely by being called, but it is still unsafe
/// because the type may have particular invariants. ...
pub unsafe fn conjure_if_zst<T>() -> Option<T> {
if size_of::<T>() == 0 {
Some(unsafe { zeroed::<T>() })
} else {
None
}
} This would serve the run-time-branching cases directly, giving them a useful documented function to use; |
Has there been any development towards this since November? The issue/ACP still gets brought up every so often. |
It's now been more than a year since
so I'm nominating it :) @rustbot label: +I-libs-api-nominated
This is still my biggest thought here. For any case where the specific type is always known to be zero-sized by the compiler (in a path independent manner), other options like writing it I suppose the other answer, though, would be to say that this should instead be handled with a (Though even then I still kinda want a function on which to put the docs, but I guess that function could still exist just implemented with One other nice thing we can do since I originally talked about this: we can use the UB-checks infra to give a nice error in debug while still allowing full optimizability in release, should be so choose. |
Relevant issues for using const asserts: rust-lang/rust#122301 and rust-lang/rfcs#3582 If that approach were sanctioned then a generic caller could, with current rust constructs, avoid triggering the assert with non-ZSTs. What it would not provide is a a path to restricting Relevant const-generics issue: rust-lang/project-const-generics#26 So I think the ideal solution continues to be blocked on language features like generic const exprs. We could pick a less than ideal solution of course. |
The libs-API team discussed this ACP in today's meeting. We are accepting the following variation of this proposal: compile-time checking if instantiated with a concrete type, and runtime checking if instantiated with a generic type. mod concrete {
pub(super) struct Concrete(());
}
fn demo<T>() {
let _ = unsafe { mem::conjure_zst::<concrete::Concrete>() }; // okay
let _ = unsafe { mem::conjure_zst::<std::string::String>() }; // compile error
if mem::size_of::<T>() == 0 {
let _ = unsafe { mem::conjure_zst::<T>() }; // okay
} else {
let _ = unsafe { mem::conjure_zst::<T>() }; // panic
}
} We anticipate that the implementation strategy would involve nominally being always runtime checked, but with something analogous to a deny-by-default lint to recognize the concrete case. |
This is beyond the scope of libs? It seems the implementation would just |
Proposal
Problem statement
People are often unsure about what the rules are for creating a ZST. See, for example, https://internals.rust-lang.org/t/equality-of-zst-values/19815/3?u=scottmcm, where the person made an unsound function. (Thankfully they made the thread to ask about it, rather than just assuming it was fine!)
There's lots of ways that you could make a ZST:
mem::uninitialized::<Z>()
,mem::zeroed::<Z>()
,MaybeUninit::<Z>::uninit().assume_init()
,*NonNull::<Z>::dangling().as_ptr()
,mem::transmute_unchecked::<_, Z>(())
, etc.It would be nice to have one obvious API to point people at that would also serve as a place to put documentation about the safety and validity invariants of ZSTs, and thus the necessary preconditions for calling the method.
Motivating examples or use cases
unsafe
code often has different paths for ZSTs where it wants to make items, such ashttps://github.com/rust-lang/rust/blob/9bd71afb90c2a6e0348cdd4a2b10a3bf39908f19/library/alloc/src/vec/into_iter.rs#L196-L197
This can show up in const generic code making arrays, too, where in an
if N == 0 { return []; }
doesn't work, so a "make a ZST" API could be nice to use there too:https://github.com/rust-lang/rust/blob/4654a910018cf0447db1edb2a46a0cae5f7dff8e/library/core/src/array/mod.rs#L801-L804
Solution sketch
The implementation of which would use (directly or indirectly) things like https://doc.rust-lang.org/nightly/std/intrinsics/fn.assert_inhabited.html to give good error messages for hitting
conjure_zst::<!>()
. And it would probably have a runtime trap (ud2
) for it not actually being a ZST, since that's checkable at monomorphization time, and thus would be cheap in practice.The docs would have text like https://github.com/rust-lang/rust/pull/95385/files#diff-02049689ae3bb7ac98af3418ad8fdca219bfa1227cb6cad31afc72bdbae30e27R681-R717
Alternatives
const { assert!(T::IS_ZST) }
. I didn't pick that one because that would keep it from being used inif T::IS_ZST { ... }
blocks, like the array example.&'static mut Z
and such.Links and related work
A few more threads I found:
https://internals.rust-lang.org/t/is-synthesizing-zero-sized-values-safe/11506?u=scottmcm
https://internals.rust-lang.org/t/static-mutable-references-to-zsts/18330?u=scottmcm
What happens now?
This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):
Second, if there's a concrete solution:
The text was updated successfully, but these errors were encountered: