Skip to content

Commit 281e27f

Browse files
authored
Merge pull request #2316 from Centril/rfc/safe-unsafe-trait-methods
RFC: Overconstraining and omitting `unsafe` in impls of `unsafe` trait methods
2 parents c5c80d5 + ef4dd08 commit 281e27f

File tree

1 file changed

+232
-0
lines changed

1 file changed

+232
-0
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
- Feature Name: safe_unsafe_trait_methods
2+
- Start Date: 2018-01-30
3+
- RFC PR: [rust-lang/rfcs#2316](https://github.com/rust-lang/rfcs/pull/2316)
4+
- Rust Issue: [rust-lang/rust#87919](https://github.com/rust-lang/rust/issues/87919)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
This RFC allows safe implementations of `unsafe` trait methods.
10+
An `impl` may implement a trait with methods marked as `unsafe` without
11+
marking the methods in the `impl` as `unsafe`. This is referred to as
12+
*overconstraining* the method in the `impl`. When the trait's `unsafe`
13+
method is called on a specific type where the method is known to be safe,
14+
that call does not require an `unsafe` block.
15+
16+
# Motivation
17+
[motivation]: #motivation
18+
19+
A trait which includes unsafe methods in its definition permits its impls to
20+
define methods as unsafe. Safe methods may use `unsafe { .. }` blocks inside
21+
them and so both safe and `unsafe` methods may use unsafe code internally.
22+
23+
The key difference between safe and unsafe methods is the same as that
24+
between safe and unsafe functions. Namely, that calling a safe method with
25+
inputs and state produced by other safe methods never leads to memory
26+
unsafety, while calling a method marked as `unsafe` may lead to such unsafety.
27+
As such, it is up to the caller of the `unsafe` method to fulfill a set of
28+
invariants as defined by the trait's documentation (the contract).
29+
30+
The safe parts of Rust constitute a language which is a subset of unsafe Rust.
31+
As such, it is always permissible to use the safe subset within unsafe contexts.
32+
This is currently however not fully recognized by the language as `unsafe` trait
33+
methods must be marked as `unsafe` in `impl`s even if the method bodies in such
34+
an `impl` uses no unsafe code. This is can currently be overcome by defining a
35+
safe free function or inherent method somewhere else and then simply delegate
36+
to that function or method. Such a solution, however, has two problems.
37+
38+
## 1. Needless complexity and poor ergonomics.
39+
40+
When an `unsafe` method doesn't rely on any unsafe invariants, it still
41+
must be marked `unsafe`. Marking methods as `unsafe` increases the amount of
42+
scrutiny necessary during code-review. Extra care must be given to ensure that
43+
uses of the function are correct. Additionally, usage of `unsafe` functions
44+
inside an `unsafe` method does not require an `unsafe` block, so the method
45+
implementation itself requires extra scrutiny.
46+
47+
One way to avoid this is to break out the internals of the method into a
48+
separate safe function. Creating a separate function which is only used
49+
at a single place is cumbersome, and does not encourage the keeping of
50+
`unsafe` to a minimum. The edit distance is also somewhat increased.
51+
52+
## 2. `unsafe` method `impl`s might not require any `unsafe` invariants
53+
54+
The implemented trait method for that specific type, which you know only has
55+
a safe implementation and does not really need `unsafe`, can't be used in a
56+
safe context. This invites the use of an `unsafe { .. }` block in that context,
57+
which is unfortunate since the compiler could know that the method is really
58+
safe for that specific type.
59+
60+
## In summation
61+
62+
The changes proposed in this RFC are intended to increase ergonomics and
63+
encourage keeping `unsafe` to a minimum. By doing so, a small push in favor
64+
of correctness is made.
65+
66+
# Guide-level explanation
67+
[guide-level-explanation]: #guide-level-explanation
68+
69+
Concretely, this RFC will permit scenarios like the following:
70+
71+
## *Overconstraining*
72+
73+
First consider a trait with one or more unsafe methods.
74+
For simplicity, we consider the case with one method as in:
75+
76+
```rust
77+
trait Foo {
78+
unsafe fn foo_computation(&self) -> u8;
79+
}
80+
```
81+
82+
You now define a type:
83+
84+
```rust
85+
struct Bar;
86+
```
87+
88+
and you implement `Foo` for `Bar` like so:
89+
90+
```rust
91+
impl Foo for Bar {
92+
// unsafe <-- Not necessary anymore.
93+
fn foo_computation(&self) -> u8 { 0 }
94+
}
95+
```
96+
97+
Before this RFC, you would get the following error message:
98+
99+
```
100+
error[E0053]: method `foo_computation` has an incompatible type for trait
101+
--> src/main.rs:11:5
102+
|
103+
4 | unsafe fn foo_computation(&self) -> u8;
104+
| --------------------------------------- type in trait
105+
...
106+
11 | fn foo_computation(&self) -> u8 { 0 }
107+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected unsafe fn, found normal fn
108+
|
109+
= note: expected type `unsafe fn(&Bar) -> u8`
110+
found type `fn(&Bar) -> u8`
111+
```
112+
113+
But with this RFC implemented, you will no longer get an error in this case.
114+
115+
This general approach of giving up (restricting) capabilities that a trait
116+
provides to you, such as the ability to rely on caller-upheld invariants
117+
for memory safety, is known as *overconstraining*.
118+
119+
## Taking advantage of *overconstraining*
120+
121+
You now want to use `.foo_computation()` for `Bar`, and proceed to do so as in:
122+
123+
```rust
124+
fn main() {
125+
// unsafe { <-- no unsafe needed!
126+
127+
let bar = Bar;
128+
let val = bar.foo_computation();
129+
130+
// other stuff..
131+
132+
// }
133+
}
134+
```
135+
136+
This is permitted since although `foo_computation` is an `unsafe` method as
137+
specified by `Foo`, the compiler knows that for the specific concrete type `Bar`,
138+
it is defined as being safe, and may thus be called within a safe context.
139+
140+
## Regarding API stability and breaking changes
141+
142+
Note however, that the ability to call *overconstrained* methods with
143+
the absence of `unsafe` in a safe context means that introducing `unsafe`
144+
later is a breaking change if the type is part of a public API.
145+
146+
## Impls for generic types
147+
148+
Consider the type `Result<T, E>` in the standard library defined as:
149+
150+
```rust
151+
pub enum Result<T, E> {
152+
Ok(T),
153+
Err(E),
154+
}
155+
```
156+
157+
Let's now implement `Foo` for `Result<T, E>` without using `unsafe`:
158+
159+
```rust
160+
impl<T, E> Foo for Result<T, E> {
161+
fn foo_computation(&self) -> u8 {
162+
// Let's assume the implementation does something interesting..
163+
match *self {
164+
Ok(_) => 0,
165+
Err(_) => 1,
166+
}
167+
}
168+
}
169+
```
170+
171+
Since `Result<T, E>` did not use `unsafe` in its implementation of `Foo`, you
172+
can still use `my_result.foo_computation()` in a safe context as shown above.
173+
174+
## Recommendations
175+
176+
If you do not plan on introducing `unsafe` for a trait implementation of
177+
your specific type that is part of a public API, you should avoid marking
178+
the `fn` as `unsafe`. If the type is internal to your crate, you should
179+
henceforth never mark it as `unsafe` unless you need to. If your needs
180+
change later, you can always mark impls for internal types as `unsafe` then.
181+
182+
Tools such as `clippy` should preferably lint for use of `unsafe`,
183+
where it is not needed, to promote the reduction of needless `unsafe`.
184+
185+
# Reference-level explanation
186+
[reference-level-explanation]: #reference-level-explanation
187+
188+
Assuming a `trait` which defines some `fn`s marked as `unsafe`, an `impl`
189+
of that trait for a given type may elect to not mark those `fn`s as `unsafe`
190+
in which case the bodies of those `fn`s in that `impl` are type checked as
191+
safe and not as `unsafe`. A Rust compiler will keep track of whether the
192+
methods were implemented as safe or `unsafe`.
193+
194+
When a trait method is called for a type in a safe context, the type checker
195+
will resolve the `impl` for a specific known and concrete type. If the `impl`
196+
that was resolved implemented the called method without an `unsafe` marker,
197+
the compiler will permit the call. Otherwise, the compiler will emit an error
198+
since it can't guarantee that the implementation was marked as safe.
199+
200+
With respect to a trait bound on a type parameter `T: Trait` for a trait with
201+
unsafe methods, calling any method of `Trait` marked as `unsafe` for `T` is
202+
only permitted within an `unsafe` context such as an `unsafe fn` or within an
203+
`unsafe { .. }` block.
204+
205+
# Drawbacks
206+
[drawbacks]: #drawbacks
207+
208+
While this introduces no additional syntax, it makes the rule-set of the
209+
language a bit more complex for both the compiler and the for users of the
210+
language. The largest additional complexity is probably for the compiler
211+
in this case, as additional state needs to be kept to check if the method
212+
was marked as safe or `unsafe` for an `impl`.
213+
214+
# Rationale and alternatives
215+
[alternatives]: #alternatives
216+
217+
[RFC 2237]: https://github.com/rust-lang/rfcs/pull/2237
218+
219+
This RFC was designed with the goal of keeping the language compatible
220+
with potential future effects-polymorphism features. In particular, the
221+
discussion and design of [RFC 2237] was considered. No issues were found
222+
with respect to that RFC.
223+
224+
No other alternatives have been considered. There is always the obvious
225+
alternative of not implementing the changes proposed in any RFC. For this RFC,
226+
the impact of not accepting it would be too keep the problems as explained
227+
in the [motivation] around.
228+
229+
# Unresolved questions
230+
[unresolved]: #unresolved-questions
231+
232+
There are currently no unresolved questions.

0 commit comments

Comments
 (0)