-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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: Overconstraining and omitting unsafe
in impls of unsafe
trait methods
#2316
RFC: Overconstraining and omitting unsafe
in impls of unsafe
trait methods
#2316
Conversation
method is called on a specific type where the method is known to be safe, | ||
that call does not require an `unsafe` block. | ||
|
||
# Motivation |
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.
Do you have some real world examples of traits and implementations that would take advantage of 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.
Here's one example: https://doc.rust-lang.org/std/slice/trait.SliceIndex.html#tymethod.get_unchecked
with impls:
- https://doc.rust-lang.org/src/core/slice/mod.rs.html#967-999
- https://doc.rust-lang.org/src/core/str/mod.rs.html#1827-1853
While this falls under "trivial example", avoiding unsafe
where possible is still nice =)
...stay tuned for more
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.
Does there exist any code that has ever called slice.get_unchecked_mut(..)
?
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.
Oh; you were referring to the calling side of things and not the motivation, I see. then slice.get_unchecked_mut(..)
does not apply (I think). My bad ;)
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 am trying to figure out what problem that currently exists this RFC is proposing to solve. "If this feature existed it would enable me to make a thing that was previously impossible", or "If this feature existed I could make this thing I wrote better".
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 think I agree with you that specialization is a better long-term solution.
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.
Why is adding a new unstable language feature a shorter term solution than using another unstable language feature?
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.
@sfackler Perhaps I'm using it wrong, but I don't believe the current version of specialization allows this specific impl. The following errors with "conflicting implementations of trait ImmovableFuture
for type MyFutureCombinator<_>
":
#![feature(specialization)]
trait Future {}
trait ImmovableFuture {}
default impl<T> ImmovableFuture for T where T: Future {}
struct MyFutureCombinator<T>(T);
impl<T> Future for MyFutureCombinator<T> where T: Future { }
impl<T> ImmovableFuture for MyFutureCombinator<T> where T: ImmovableFuture { }
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.
Either way, I'd expect this feature to be much quicker and easier to implement and stabilize than even the most minimal version of specialization.
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.
Then this can provide some motivation for finishing specialization! IIRC that kind of implementation was covered in at least some revisions of the specialization RFC.
Are there any more motivating examples for this feature? I am kind of concerned with adding a new language feature that will be used for a single trait.
@nikomatsakis Didn't we used to have bugs because of this? |
I remember something like this, but can't find where the change was done. |
What is the story on APIs that currently use |
Isn't the point of
In particular, you might want safe blocks inside an Also, if you really need an
|
Hopefully we can get the inverse as well, though I would understand if that was a separate RFC. |
@burdges If you're already writing an unsafe function then having to put an unsafe block inside of it is silly to say the least. |
The
Searching for "trusted iterator length" problem points to https://internals.rust-lang.org/t/the-trusted-iterator-length-problem/1302, where one of the solutions is about I think "unsafe to implement" stuff should be performed through |
Actually, I would like to dig a bit deeper here. If I have a trait: trait FooTrait {
unsafe fn foo();
} The
I guess this RFC is proposing that it does not mean the former and may or may not mean the latter... Fundamentally, I don't think it even makes sense to declare an abstract fn I agree with @kennytm: I think this will answer one of the questions I had when reading the RFC: what should the rustdocs for |
@mark-i-m I wrote a blog post about what I believe is the correct way to interpret unsafe in this context: https://boats.gitlab.io/blog/post/2018-01-04-unsafe-abstractions/
There's a subtle but important distinction that I think you're missing: being marked unsafe doesn't mean you can do any unsafe anything in that function. An So its not at all tied to having a default impl; the point is that every impl has to rely on the same invariants that the trait declares. |
So, could we summarize that whole thing with "Any time |
@Lokathor Yes, that's exactly how I interpret it. |
I think this is the right interpretation, though in the past I've had another POV (roughly: "unsafe means read the comments; somebody has an extra job here"). To be honest I think we should revisit the design of unsafe to help us make this clearer (who is imposing what obligations on whom), but I'm not 100% sure yet what I think. |
Hmm... @withoutboats's blog post was very thought provoking! It bothered me that the use of My proposal (hopefully this doesn't have too many holes):
For an explanation of how I came to this definition of unsafe see the following long-winded details... Details
I write all of this because I think that if we are to move forward with a feature like this RFC, we need to nail down the definition of |
That's, like, way complicated my friend. Let's try going back to the simple version and building from there. Also, let's try to avoid introducing any new special terms.
|
cc @eternaleye wrt. |
Copying a portion of my message elsewhere:
I negated the definitions of some of these specifically to make them effects: Something that grants the body/callee additional powers, while constraining the user/caller. Rust has a rudimentary effect system, but discussing it is complicated by several things:
EDIT: With these reframings in place, I'd argue that:
It would also seem to argue that:
|
@Lokathor The question is who does
I argue that
|
@mark-i-m That's not what masking means. Masking is stopping the outward (resp. inward) propagation of an effect (resp. restriction) marker.
|
@eternaleye I think we are talking about the same thing. "stopping the outward propagation of an effect" is isomorphic to promising that you fulfill all preconditions. Not fulfilling all preconditions forces you to assume a precondition, which someone else must fulfill (which is propagation)... |
@mark-i-m: Mm, I disagree. I'm going to show by comparison with So, with trait Foo {
// Here, in the trait-side signature
unsafe fn foo();
}
impl Foo for () {
// Here, in the impl-side signature
unsafe fn foo() {
// Here, in the impl-side body
}
} It's clear that In addition, Now, let's look at trait Foo {
// Here, in the trait-side signature
const fn foo();
}
impl Foo for () {
// Here, in the impl-side signature
const fn foo() {
// Here, in the impl-side body
}
} Here, the infection moves inwards - if it appears in the trait signature, it must appear in the impl signature, and if it appears in the impl signature, one must only use Now for // Here, in the trait-side signature
unsafe trait Foo {
fn foo();
}
// Here, in the impl-side signature
unsafe impl<B: Bar> Foo for () {
fn foo() {
// Here, in the impl-side body
<B as Bar>::bar()
}
} Note how, compared to However, the impl-side body calls This is where Anyway, in none of these examples does masking occur between trait-side signature and impl-side signature - in both cases where it's possible, it occurs between impl-side signature and impl-side body; explicitly in one case, and implicitly in the other. EDIT: Note that this actually affects |
Perhaps I was somehow unclear in my explanation?
That's not a proposal by me for some future version of rust, that's a statement of the current rules of rust as it exists today. Personally, I can't even envision another way for it to work. When you say "the user, rather than the implementor", I don't know what you mean. If you impl |
@eternaleye Thanks for the clarification :) I think I don't quite follow you here:
But the My goal was to come up with a (proposed) general formulation so we can see what we is possible in the future and consistent with current rust. I think the formulation I came up with does that, and I still believe it is consistent with both your/@withoutboats's model and @eternaleye's... but I seem to be doing a bad job of explaining it 😛
In the case of a |
@mark-i-m This calls back to the first post I made in this thread - specifically, the quoted part:
In particular, However, there is no way for Considering By contrast, if This problem is further magnified if instead of |
I still do not understand why |
@burdges Firstly, static mut foo: u32 = 0;
fn safe1() {
println!("Just starting");
}
// The signature must have `unsafe` because...
unsafe fn not_safe(x: u32) {
// ... the body uses `unsafe` and infected the signature
foo += x;
}
fn safe2() {
println!("Almost done");
}
// The signature must have `unsafe` because...
unsafe fn not_safe2() {
// ... the body uses `unsafe` and infected the signature
foo -= 1;
}
// The signature isn't infected because...
fn example() {
// ...safe functions do not infect...
safe1();
// ...and masking stops the spread...
unsafe {
// ...of any infection present.
not_safe(1);
}
safe2();
unsafe {
not_safe2();
}
} EDIT: We lack a "block-scoped effectless context" construct, but calling a safe Note that infection refers to the marker - the keyword |
I have learnability concern here. The semantics of unsafe traits and unsafe methods in traits is complicated, and new users are constantly confused about this. I feel that adding an extra "you can, but not required, put an |
I object to the idea that it's complicated. I think it is just very poorly explained most of the time. We just need to make our docs better. |
I wonder if the trusted/unsafe distinction would make this distinction easier. I am increasingly in favor of such a move. It could definitely be managed with an edition with relative ease. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
I saw a few comments (#2316 (comment), #2316 (comment)) about the dangers of accidentally omitting |
Specifically the concern, @Aaron1011, is that people might omit |
@Aaron1011 We discussed this in the lang team meeting today, but the consensus in the meeting was that it was a known and tolerable danger. For me personally, it was my biggest worry with this. From a type system and semantic perspective on correct code it's absolutely fine, basically just a form of variance. I was eventually convinced that the risk is tolerable because it's already the case for functions and non-trait methods that one can leave off a necessary Of course, that just makes it yet another thing that makes me want |
So this is referring to this problem? |
@RalfJung No, referring to the "footgun" mentioned in #2316 (comment) My understanding is that the lang team is currently in agreement that the way to impose requirements on a trait implementer is to make the trait an unsafe trait, whereas an |
I don't understand the footgun from that comment, unless the footgun is what is described in #2316 (comment). As in, I don't even see what could possibly go wrong when one writes a safe The rest of what you say sounds like this is indeed about #2316 (comment). |
What is the lang team's view regarding this issue? I've used this in my own code, and found it a convenient way to indicate that an implementor of a method must fulfill obligations or else safety invariants may be violated. I've only used it a small handful of times, so I wouldn't be incredibly sad if it went away, although I have found it useful. |
I think the argument is essentially that if you didn't put unsafe on a function (whether it's a trait method or otherwise), but use something unsafe internally, it is possible that you didn't prove the obligation that the use of an unsafe operation created, having intended to push it up to the caller, without noticing that the function is not unsafe. When writing trait methods that are unsafe in the trait definition, it is plausibly somewhat easier to do this by accident, as you may be reading the documentation on the trait definition which indicates what the caller must uphold and believe that the caller must uphold those things in your method, forgetting that you need to also mark the method as unsafe in the implementation. Anyone methodically making sure that safety obligations are upheld should easily get this right, perhaps even with compiler/lint assistance, but this is my understanding of this concern.
I cannot speak for the lang team, but my understanding from discussions has been that the position is that defining an unsafe method in a trait -- like with any other unsafe function -- does not allow callers to make any assumptions about the implementation of that function. The only way for a trait author to guarantee that implementations uphold some constraints is to make the trait itself unsafe. |
I cannot speak for the lang team, but my understanding from discussions
has been that the position is that defining an unsafe method in a trait --
like with any other unsafe function -- does not allow callers to make any
assumptions about the implementation of that function. The only way for a
trait author to guarantee that implementations uphold some constraints is
to make the trait itself unsafe.
+1000
Unsafe functions impose requirements on their callers. Unsafe traits impose
requirements on their implementors. This has long been the meaning of
unsafe in these contexts and should not be muddied.
|
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. The RFC will be merged soon. |
... are we going to merge this? |
I'm reviewing my lang team action items and I see that I was supposed to summarize details of learnability our meeting from 2021-05-25. In the meantime, many of those same points have been made, but I thought I'd drop it in here for reference... In short, it's true that the meaning of
This particular RFC doesn't seem to make that situation worse; it could even help to clarify, as otherwise it makes adding |
Speaking personally, I have definite interest in exploring a |
@nikomatsakis you wrote "Marking a trait unsafe" twice in your list. presumably one of those bullet points is for trait methods. |
Huzzah! The @rust-lang/lang team has decided to accept this RFC. To track further discussion, subscribe to the tracking issue here: rust-lang/rust#87919 |
Rendered
This RFC allows safe implementations of
unsafe
trait methods. Animpl
may implement a trait with methods marked asunsafe
without marking the methods in theimpl
asunsafe
. This is referred to asoverconstraining the method in the
impl
. When the trait'sunsafe
method is called on a specific type where the method is known to be safe, that call does not require anunsafe
block.A simple example:
This RFC was written in collaboration with @cramertj whom it was my pleasure to work with.
cc @withoutboats @aturon