The docs for NonNull::dangling
warn against using it as a sentinel value because it may point to a valid T. As I understand it the dangling value is just the alignment, so if T is 4 byte aligned than it turns a pointer set to address 4. For the vast majority of systems and vast majority of T this is going to be in the 0 page which will never back allocations. Is the warning just because of tiny embedded systems or is there some other reason not to use it as a sentinel? I think the doc could use more context.
The reasoning is that it may point to a valid value, thus you aren't guaranteed to be able to distinguish it from the address of an existing value. Even without any more context, this is a good enough reason not to treat it as never pointing to a valid value.
That's an impractical position for people interested in writing low level code. It's a pointer method obviously targeting low level use cases. You can't deref pointers without using unsafe, so a user reading this doc is most likely doing something low level.
If the type is zero sized, then you can easily end up with valid references to them whose address is exactly what NonNull::dangling
returns, and which are considered to point at a valid value of the zero-sized type. Similarly, a slice of length zero is also allowed to use NonNull::dangling
for its address.
The canonical way to obtain a pointer that is valid for zero-sized accesses is
NonNull::dangling
.
This has nothing to do with how "low-level" or unsafe the code is. There's no unsafe in the following code:
use std::ptr::NonNull;
struct Foo { … }
let x = Foo { … };
let x_ptr = &x as *const Foo;
let x_dangling = NonNull::dangling();
assert_ne!(x_ptr, x_dangling.as_ptr());
yet, the assertion is allowed to fail. This can easily lead to logical errors, not only memory safety.
Since ZSTs by definition can't have any state so you're less likely to care about identity, I think this is usually fine, but I think I see the danger: you could write a container that tries to use dangling as a sentinel to detect if the container is empty, and then it would break for ZSTs. But you can protect against this by panicking on ZSTs, in most cases you're not building containers of ZSTs on purpose.
Ah so slice uses it as a sentinel value? I guess maybe it doesn't count as a sentinel since it's not compared against. But this is exactly the kind of scenario where dangling is useful, if I were implementing something like slice/Vec myself.
No, it is exactly a non-sentinel value. It's a valid pointer that happens to point to a memory region of length 0. But a slice with length zero is not fundamentally different from a slice that has non-zero length.
Slices use the length for that purpose instead. They never compare the address with NonNull::dangling
to determine if it is empty.
Yes it does. If I'm going to the trouble of trying to use sentinel pointer values I am clearly willing to make platform specific assumptions, ESPECIALLY when on every modern non-embedded platform the 0 page is not mapped deliberately in order to ensure null dereferences crash. It is extremely common for low level memory allocator code to use nonzero addresses into this page to express invalid values, and if Rust were not able to deal with it it would be a serious failing for something marketed as a "systems language."
Is your objection that I would want to do this common thing or is it that I am going about it by trying to use the standard library provided function? I can go the gamedev route and ignore std lib and roll my own identically behaving function and spread the word on how it's better to use my one function crate instead of dangling
because it comes with the additional guarantee that I promise not to return a stack or heap address on my supported platforms provided that if you use a custom allocator it doesn't use the zero page (so nearly all platforms and allocators) but that seems silly.
The warning is there because any code using NonNull::dangling
as a special value is making a lot of assumptions that are far from obvious, e.g. that the type is not zero sized or that the pointer doesn't come from a length-zero array. If you know that you're dealing with a valid pointer to a non-ZST, whose alignment is smaller than the page size, and that your platform treats the zero page in this way, then there's no problem with doing what you want to do.
Rust is doing something very good here: It is helping with making non-obvious assumptions behind your code more explicit. There's nothing wrong with making the above assumptions and using them to justify the use of NonNull::dangling
as a sentinel value. You should just be aware that by writing code that does this, you are making those assumptions that I mentioned above — both the ZST one and the alignment one.
Neither. My problem is that you are trying to do something that the standard library explicitly tells you not to. That's asking for trouble. Regardless of whether or not you are on an embedded platform, doing things that you are specifically not supposed to do is asking for trouble.
Unless you find a guarantee of this (I see none), I wouldn't assume it's true. Make your own sentinel
function that returns something you know can't point to a valid object on the platforms you support and document that you don't support any others if it's not otherwise inherent.
Alternatively, file an issue to add the guarantee that it's always the alignment and see if the lib team agrees.
The other alternative to being "silly" is to occasionally break in subtle or not-so-subtle ways when the non-guarantee changes.
Also, if the goal is to make a sentinel, then make a function that gives you a definitely-doesn't-overlap-with-anything-else (though might share an address with a ZST) sentinel. Don't use a method that says it's not a sentinel.
For example, you could do something like
fn sentinel<T>() -> NonNull<T> {
static SENTINEL: AtomicU8 = AtomicU8::new();
NonNull::from(&SENTINEL).cast()
}
Or if you're willing to have alignment requirements, an unaligned pointer is a great sentinel:
fn sentinel<T>() -> NonNull<T> {
assert!(align_of::<T>() > 1);
NonNull::new(usize::MAX as _).unwrap()
}
Keep dangling
for its "I just want something aligned for zero-size access" use, and use something else for other uses.
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.