-
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
Merged
scottmcm
merged 3 commits into
rust-lang:master
from
Centril:rfc/safe-unsafe-trait-methods
Aug 10, 2021
Merged
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
- Feature Name: safe_unsafe_trait_methods | ||
- Start Date: 2018-01-30 | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
This RFC allows safe implementations of `unsafe` trait methods. | ||
An `impl` may implement a trait with methods marked as `unsafe` without | ||
marking the methods in the `impl` as `unsafe`. This is referred to as | ||
*overconstraining* the method in the `impl`. When the trait's `unsafe` | ||
method is called on a specific type where the method is known to be safe, | ||
that call does not require an `unsafe` block. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
A trait which includes unsafe methods in its definition permits its impls to | ||
define methods as unsafe. Safe methods may use `unsafe { .. }` blocks inside | ||
them and so both safe and `unsafe` methods may use unsafe code internally. | ||
|
||
The key difference between safe and unsafe methods is the same as that | ||
between safe and unsafe functions. Namely, that calling a safe method with | ||
inputs and state produced by other safe methods never leads to memory | ||
unsafety, while calling a method marked as `unsafe` may lead to such unsafety. | ||
As such, it is up to the caller of the `unsafe` method to fulfill a set of | ||
invariants as defined by the trait's documentation (the contract). | ||
|
||
The safe parts of Rust constitute a language which is a subset of unsafe Rust. | ||
As such, it is always permissible to use the safe subset within unsafe contexts. | ||
This is currently however not fully recognized by the language as `unsafe` trait | ||
methods must be marked as `unsafe` in `impl`s even if the method bodies in such | ||
an `impl` uses no unsafe code. This is can currently be overcome by defining a | ||
safe free function or inherent method somewhere else and then simply delegate | ||
to that function or method. Such a solution, however, has two problems. | ||
|
||
## 1. Needless complexity and poor ergonomics. | ||
|
||
When an `unsafe` method doesn't rely on any unsafe invariants, it still | ||
must be marked `unsafe`. Marking methods as `unsafe` increases the amount of | ||
scrutiny necessary during code-review. Extra care must be given to ensure that | ||
uses of the function are correct. Additionally, usage of `unsafe` functions | ||
inside an `unsafe` method does not require an `unsafe` block, so the method | ||
implementation itself requires extra scrutiny. | ||
|
||
One way to avoid this is to break out the internals of the method into a | ||
separate safe function. Creating a separate function which is only used | ||
at a single place is cumbersome, and does not encourage the keeping of | ||
`unsafe` to a minimum. The edit distance is also somewhat increased. | ||
|
||
## 2. `unsafe` method `impl`s might not require any `unsafe` invariants | ||
|
||
The implemented trait method for that specific type, which you know only has | ||
a safe implementation and does not really need `unsafe`, can't be used in a | ||
safe context. This invites the use of an `unsafe { .. }` block in that context, | ||
which is unfortunate since the compiler could know that the method is really | ||
safe for that specific type. | ||
|
||
## In summation | ||
|
||
The changes proposed in this RFC are intended to increase ergonomics and | ||
encourage keeping `unsafe` to a minimum. By doing so, a small push in favor | ||
of correctness is made. | ||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
Concretely, this RFC will permit scenarios like the following: | ||
|
||
## *Overconstraining* | ||
|
||
First consider a trait with one or more unsafe methods. | ||
For simplicity, we consider the case with one method as in: | ||
|
||
```rust | ||
trait Foo { | ||
unsafe fn foo_computation(&self) -> u8; | ||
} | ||
``` | ||
|
||
You now define a type: | ||
|
||
```rust | ||
struct Bar; | ||
``` | ||
|
||
and you implement `Foo` for `Bar` like so: | ||
|
||
```rust | ||
impl Foo for Bar { | ||
// unsafe <-- Not necessary anymore. | ||
fn foo_computation(&self) -> u8 { 0 } | ||
} | ||
``` | ||
|
||
Before this RFC, you would get the following error message: | ||
|
||
``` | ||
error[E0053]: method `foo_computation` has an incompatible type for trait | ||
--> src/main.rs:11:5 | ||
| | ||
4 | unsafe fn foo_computation(&self) -> u8; | ||
| --------------------------------------- type in trait | ||
... | ||
11 | fn foo_computation(&self) -> u8 { 0 } | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected unsafe fn, found normal fn | ||
| | ||
= note: expected type `unsafe fn(&Bar) -> u8` | ||
found type `fn(&Bar) -> u8` | ||
``` | ||
|
||
But with this RFC implemented, you will no longer get an error in this case. | ||
|
||
This general approach of giving up (restricting) capabilities that a trait | ||
provides to you, such as the ability to rely on caller-upheld invariants | ||
for memory safety, is known as *overconstraining*. | ||
|
||
## Taking advantage of *overconstraining* | ||
|
||
You now want to use `.foo_computation()` for `Bar`, and proceed to do so as in: | ||
|
||
```rust | ||
fn main() { | ||
// unsafe { <-- no unsafe needed! | ||
|
||
let bar = Bar; | ||
let val = bar.foo_computation(); | ||
|
||
// other stuff.. | ||
|
||
// } | ||
} | ||
``` | ||
|
||
This is permitted since although `foo_computation` is an `unsafe` method as | ||
specified by `Foo`, the compiler knows that for the specific concrete type `Bar`, | ||
it is defined as being safe, and may thus be called within a safe context. | ||
|
||
## Regarding API stability and breaking changes | ||
|
||
Note however, that the ability to call *overconstrained* methods with | ||
the absence of `unsafe` in a safe context means that introducing `unsafe` | ||
later is a breaking change if the type is part of a public API. | ||
|
||
## Impls for generic types | ||
|
||
Consider the type `Result<T, E>` in the standard library defined as: | ||
|
||
```rust | ||
pub enum Result<T, E> { | ||
Ok(T), | ||
Err(E), | ||
} | ||
``` | ||
|
||
Let's now implement `Foo` for `Result<T, E>` without using `unsafe`: | ||
|
||
```rust | ||
impl<T, E> Foo for Result<T, E> { | ||
fn foo_computation(&self) -> u8 { | ||
// Let's assume the implementation does something interesting.. | ||
match *self { | ||
Ok(_) => 0, | ||
Err(_) => 1, | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Since `Result<T, E>` did not use `unsafe` in its implementation of `Foo`, you | ||
can still use `my_result.foo_computation()` in a safe context as shown above. | ||
|
||
## Recommendations | ||
|
||
If you do not plan on introducing `unsafe` for a trait implementation of | ||
your specific type that is part of a public API, you should avoid marking | ||
the `fn` as `unsafe`. If the type is internal to your crate, you should | ||
henceforth never mark it as `unsafe` unless you need to. If your needs | ||
change later, you can always mark impls for internal types as `unsafe` then. | ||
|
||
Tools such as `clippy` should preferably lint for use of `unsafe`, | ||
where it is not needed, to promote the reduction of needless `unsafe`. | ||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
Assuming a `trait` which defines some `fn`s marked as `unsafe`, an `impl` | ||
of that trait for a given type may elect to not mark those `fn`s as `unsafe` | ||
in which case the bodies of those `fn`s in that `impl` are type checked as | ||
safe and not as `unsafe`. A Rust compiler will keep track of whether the | ||
methods were implemented as safe or `unsafe`. | ||
|
||
When a trait method is called for a type in a safe context, the type checker | ||
will resolve the `impl` for a specific known and concrete type. If the `impl` | ||
that was resolved implemented the called method without an `unsafe` marker, | ||
the compiler will permit the call. Otherwise, the compiler will emit an error | ||
since it can't guarantee that the implementation was marked as safe. | ||
|
||
With respect to a trait bound on a type parameter `T: Trait` for a trait with | ||
unsafe methods, calling any method of `Trait` marked as `unsafe` for `T` is | ||
only permitted within an `unsafe` context such as an `unsafe fn` or within an | ||
`unsafe { .. }` block. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
While this introduces no additional syntax, it makes the rule-set of the | ||
language a bit more complex for both the compiler and the for users of the | ||
language. The largest additional complexity is probably for the compiler | ||
in this case, as additional state needs to be kept to check if the method | ||
was marked as safe or `unsafe` for an `impl`. | ||
|
||
# Rationale and alternatives | ||
[alternatives]: #alternatives | ||
|
||
[RFC 2237]: https://github.com/rust-lang/rfcs/pull/2237 | ||
|
||
This RFC was designed with the goal of keeping the language compatible | ||
with potential future effects-polymorphism features. In particular, the | ||
discussion and design of [RFC 2237] was considered. No issues were found | ||
with respect to that RFC. | ||
|
||
No other alternatives have been considered. There is always the obvious | ||
alternative of not implementing the changes proposed in any RFC. For this RFC, | ||
the impact of not accepting it would be too keep the problems as explained | ||
in the [motivation] around. | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
There are currently no unresolved questions. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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:
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 typeMyFutureCombinator<_>
":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.