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
RFC: #[derive(SmartPointer)] #3621
base: master
Are you sure you want to change the base?
Conversation
Co-authored-by: Lukas Wirth <lukastw97@gmail.com>
My main thought is that maybe this should just require repr(transparent), which simplifies a lot of the explanation of the requirements part. |
I don't think there should be a hard requirement that smart pointers be transparent. But since the requirements are very similar, it might make sense to incorporate the requirements by reference, for simplicity of documentation. |
After doing a bit more research to get myself acquainted with the What these traits really are about is ensuring proper variance, which is especially weird since variance is usually only exposed to programmers in Rust through weird mechanisms like
Smart pointers require both of these just by the nature of how references in Rust work in general: because autoderef lets us borrow things mostly automatically, we expect this automatic borrowing to happen even if a smart pointer is between us and our data. However, it's important to realise that this is just about variance and we only tie it specifically to smart pointers because unsized types in Rust today have to be used behind pointers. If we allow unsized locals, whose RFC was accepted, now these types become even more just about variance. You should always be able to pass arrays into functions which accept unsized slices, return unsized trait objects by returning a concrete object, etc., etc. Honestly, thinking of this more specifically as a way of managing variance, I do question whether the current method of manually implementing these traits is the right approach, and whether we should rely more on something like Of course, we can't just use
Actually, the allocator API runs amok with this current proposal, too, since it would require that an allocator be zero-sized. If we reframe the proposal from the perspective of unsized types (treating smart pointers as implicitly always sized), this "single field" instead turns into "last field," like the current unsized requirements. I believe that zero-sized types are also not allowed to be after the unsized field of a struct, but we could probably amend this pretty easily. Maybe we could have some kind of
Basically ensuring that everything works as you'd expect. A few footnotes:
|
@clarfonthey: A few things regarding your last comment-- It's not correct to call the relationship between Their relationship is called unsizing in Rust, which is an explicit coercion operation inserted into the MIR and involves actually changing the type layout of pointer. Also,
Also the last field requirement you mention is a limitation of data being
(disclaimer that i've been too busy to read the actual rfc yet, just wanted to clarify some stuff before people started also responding and a whole thread gets generated based off of false premises)
Footnotes
|
Oh, huh, I was under the impression that I just assumed that allocators were kind of in the same boat, where they could be present like the counts for references but would be peeled away somehow, also like those. In terms of my mention of contravariance: I definitely am jumbling up terms here since generally what people mean by variance is about subtyping and supertyping, whereas I'm using it to mean literally varying the type via the unsize and dynamic dispatch operations you describe. It's a sloppy analogy for sure, but the main reason I used it is to point out that it shouldn't be explicitly derived and instead inferred somehow like variance currently is. I guess in that sense, it's closer to the mathematical definitions of stuff like homomorphisms, where instead of stating that two things have identical properties, you have the existence of operations which can be applied while retaining certain properties. Subtle differences but similar analogies. |
The last-field limitation actually means nothing ABI-wise since the Rust layout allows reordering the fields use std::{ptr::NonNull, mem::offset_of};
struct MySmartPointer<T: ?Sized> {
extra: u16,
interior: NonNull<T>,
}
fn main() {
type P = MySmartPointer<usize>;
assert_eq!(offset_of!(P, interior), 0);
assert_eq!(offset_of!(P, extra), 8);
} So, to call
However, since |
the Rust compiler is what chooses what order the fields are in, which also means the Rust compiler can always leave the unsizable field at the end, as I'm guessing it currently does. This means conversion from/to |
I can see how But I disagree with calling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason to use the fully qualified path of Sized
for U
? For T
the prelude is used.
My example code in #3621 (comment) already showed it does not. We are not talking about maybe-unsized struct tail here, but a pointer to a maybe-unsized type which can appear anywhere. struct X<T: ?Sized> {
extra: u16,
tail: T, // yes, this field is guaranteed to be at the end of X<T>
}
/* layout of X<T>:
+---------------+
0 | extra |
+---------------+
2 |x(padding)x x x|
| x x x x x x x |
4 |x x x x x x x x|
| x x x x x x x |
6 |x x x x x x x x|
+---------------+
8 | tail |
| (*actual |
A | offset |
| depends on |
C | align_of T) |
| |
: :
*/
struct P<T: ?Sized> {
extra: u16,
ptr: *const T, // note that it is a pointer here, there is no guarantee about offset of `ptr` in P<T>
}
/* layout of P<T>:
+---------------+
0 | ptr |
| ~~~~~~~~~~~> +---------------+
2 | | | *ptr |
| | | |
4 | | : :
| |
6 | |
+---------------+
8 | extra |
+---------------+
A |x(padding)x x x|
| x x x x x x x |
C |x x x x x x x x|
| x x x x x x x |
E |x x x x x x x x|
+---------------+
*/
Coercion from |
|
||
Whenever a `self: MySmartPointer<Self>` method is called on a trait object, the | ||
compiler will convert from `MySmartPointer<dyn MyTrait>` to | ||
`MySmartPointer<MyStruct>` using something similar to a transmute. Because of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's actually worse than a transmute of these values, we're effectively transmuting the function pointer. This means the two types do not just have to be layout compatible, they have to be function call ABI compatible. That's also why allowing extra fields in these types is pretty difficult.
We actually currently rely on them being the equivalent of transparent. The only reason DispatchFromDyn does not just check the The way we call arbitrary-self receiver functions via vtables relies on repr(transparent) -- specifically, it relies on ABI compatibility. There's a subtle invariant here that all instances of repr(Rust) types that follow the repr(transparent) rules are indeed ABI-compatible to their one non-1-ZST field even if the repr(transparent) attribute is not present. I hope all our ABI adjustment logic ensures this invariant (and we have tests covering the basics), but this is easy to get wrong. This would be much easier if we could rely on repr(transparent)... |
Is there any plan to make this work with allocators at all, or are we basically stuck with this design for the foreseeable future? Since as I mentioned, the current RFC proposal would also be unable to do anything with non-zero-sized allocators for reasons mentioned. I'd guess that kernel development would at best be dealing with ZSTs anyway since storing an extra pointer or something like it would be undesirable for performance reasons, but it's still a question that feels worth asking. What I'd imagine would happen here is the fields for the allocator would always be after the pointee to ensure that you can still cast the pointer and have it Just Work, but that requires a bit of compiler help. |
Extending DispatchFromDyn to be able to cover more cases (i.e., to handle types that are not just newtyped pointers) is a non-trivial topic on its own, I'd rather not derail this RFC by going there. |
One question I have is about FFI smart pointers, which were one of the key use cases for arbitrary self types. If I understand correctly, these smart pointers cannot easily be e.g. It seems a little awkward to call this derive Is there perhaps a way where the FFI smart pointers could be made compatible with this? By having dyn metadata (manually?) placed in a second field, perhaps? |
@davidhewitt I think you underestimate the extent to which something like pub struct ARef<T: AlwaysRefCounted> {
ptr: NonNull<T>,
_p: PhantomData<T>,
}
pub unsafe trait AlwaysRefCounted {
fn inc_ref(&self);
unsafe fn dec_ref(obj: NonNull<Self>);
} Making this work with All that said, mixing FFI and |
I see, I think you might also be able to do something like For |
I expect you can just store |
I tried typing this up before on my phone but GitHub ate my message. -_- Basically translating what I was proposing into a more concrete proposal. The tl;dr is that I think that instead of
This also converts my nebulous and partially wrong analysis into an actionable proposal. |
Without this adjustment, it would not be possible to use this macro with types such as the `ARef` type that was discussed on the RFC thread [1], since we need `AlwaysRefcounted` to be specified for both T and U. Link: https://www.github.com/rust-lang/rfcs/pull/3621#issuecomment-2094094231 [1]
## Derive macro or not? | ||
|
||
Stabilizing this as a derive macro more or less locks us in with the decision | ||
that the compiler will use traits to specify which types are compatible with | ||
trait objects. However, one could imagine other mechanisms. For example, stable | ||
Rust currently has logic saying that any struct where the last field is `?Sized` | ||
will work with unsizing operations. (E.g., if `Wrapper` is such a struct, then | ||
you can convert from `Box<Wrapper<[u8; 10]>>` to `Box<Wrapper<[u8]>>`.) That | ||
mechanism is not specified using a trait. | ||
|
||
However, using traits for this functionality seems to be the most flexible. To | ||
solve the unresolved questions, we most likely need to constrain the | ||
implementations of these traits for `Pin` with stricter trait bounds than what | ||
is specified on the struct. That will get much more complicated if we use a | ||
mechanism other than traits to specify this logic. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Continuing #3621 (comment) by @clarfonthey in a thread.
I'm not convinced about #[repr(smart_pointer)]
. It's pretty weird for a repr
to implement traits on the type. And like I explained in this new section, I think it is unlikely that we will not end up using traits for this functionality.
I think we can get pretty good errors from the derive macro. The DispatchFromDyn
type already has special built-in checks in the compiler, and we can just adjust the errors that it emits to get reasonable errors.
Other than that, did you see commit f7758256? It's not correct that only smart pointers implement both traits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that them existing as traits is mostly just a quirk of the existing implementation. Since they're unstable, the traits might as well not exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course, but I imagine that #[derive(SmartPointer)]
will not be the end of evolution of this feature. At some point we will hopefully stabilize the underlying traits as well, and I think it is quite likely that we still want them to be traits when we do so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, I'm not quite sure how that would be useful, but I'll ask anyway: what kind of generic code would you expect to be able to use these traits for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not so much that I think it would be useful for generic code. Rather, I think it is useful to use traits because it gives us a lot of control over when the special traits are implemented.
Imagine we go for the #[repr(smart_pointer)]
solution and also have a #[repr(transparent_container)]
for the case of Cell
. Then each type has essentially three possible choices:
- No
repr
annotation. The type is never usable with dynamic dispatch. - The
#[repr(smart_pointer)]
annotation. The type is always usable with dynamic dispatch. - The
#[repr(transparent_container)]
annotation. The type is usable with dynamic dispatch whenever its contents are.
However, these choices are not sufficiently flexible due to the Pin
issue. If we go with the StableDeref
solution, which is the most elegant solution, then we need a fourth option of "the type is usable with dynamic dispatch whenever the inner smart pointer implements StableDeref
".
Using traits, this is no issue. You just add T: StableDeref
as a trait bound to the implementation and that's it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. From that perspective, you're definitely right that having traits would be helpful long-term, even though it's still mostly a compiler detail. I think this analysis would be good to add to that section of the RFC; I think that deriving would make sense here, but that then leads back to the original issue about the combined deriving.
Of course, I guess that derive macros could ultimately be deprecated anyway if we decide to split them up later. Although it's unprecedented for the standard library, proc_macro
s can do whatever they want, so, we already see individual crates doing stuff like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did try to explain this in the section I added yesterday, but I will elaborate in more details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I also hadn't read the section before you mentioned it, so, I'll double check I didn't just miss the details there.)
…=jhpratt Add test for dynamic dispatch + Pin::new soundness While working on [the `#[derive(SmartPointer)]` RFC][1], I realized that the soundness of <code>impl [DispatchFromDyn](https://doc.rust-lang.org/stable/std/ops/trait.DispatchFromDyn.html) for [Pin](https://doc.rust-lang.org/stable/std/pin/struct.Pin.html)</code> relies on the restriction that you can't implement [`Unpin`](https://doc.rust-lang.org/stable/std/marker/trait.Unpin.html) for trait objects. As far as I can tell, the relevant error exists to solve some unrelated issues with coherence. To avoid cases where `Pin` is made unsound due to changes in the coherence-related errors, add a test that verifies that unsound use of `Pin` and `DispatchFromDyn` does not become allowed in the future. [1]: rust-lang/rfcs#3621
…=jhpratt Add test for dynamic dispatch + Pin::new soundness While working on [the `#[derive(SmartPointer)]` RFC][1], I realized that the soundness of <code>impl [DispatchFromDyn](https://doc.rust-lang.org/stable/std/ops/trait.DispatchFromDyn.html) for [Pin](https://doc.rust-lang.org/stable/std/pin/struct.Pin.html)</code> relies on the restriction that you can't implement [`Unpin`](https://doc.rust-lang.org/stable/std/marker/trait.Unpin.html) for trait objects. As far as I can tell, the relevant error exists to solve some unrelated issues with coherence. To avoid cases where `Pin` is made unsound due to changes in the coherence-related errors, add a test that verifies that unsound use of `Pin` and `DispatchFromDyn` does not become allowed in the future. [1]: rust-lang/rfcs#3621
Rollup merge of rust-lang#125072 - Darksonn:pin-dyn-dispatch-sound, r=jhpratt Add test for dynamic dispatch + Pin::new soundness While working on [the `#[derive(SmartPointer)]` RFC][1], I realized that the soundness of <code>impl [DispatchFromDyn](https://doc.rust-lang.org/stable/std/ops/trait.DispatchFromDyn.html) for [Pin](https://doc.rust-lang.org/stable/std/pin/struct.Pin.html)</code> relies on the restriction that you can't implement [`Unpin`](https://doc.rust-lang.org/stable/std/marker/trait.Unpin.html) for trait objects. As far as I can tell, the relevant error exists to solve some unrelated issues with coherence. To avoid cases where `Pin` is made unsound due to changes in the coherence-related errors, add a test that verifies that unsound use of `Pin` and `DispatchFromDyn` does not become allowed in the future. [1]: rust-lang/rfcs#3621
Use custom smart pointers with trait objects.
Rendered
Tracking issue: rust-lang/rust#123430
Co-authored by @Darksonn and @Veykril
Thank you to @compiler-errors for the original idea