Skip to content

Commit 7ddde5a

Browse files
committed
Add design notes for function-type Default implementation discussion
1 parent 8157ddc commit 7ddde5a

File tree

2 files changed

+351
-0
lines changed

2 files changed

+351
-0
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
- [Design notes](./design_notes.md)
1111
- [Allowing integer literals like `1` to be inferred to floating point](./design_notes/int_literal_as_float.md)
1212
- [Generalizing coroutines](./design_notes/general_coroutines.md)
13+
- [Extending the capabilities of compiler-generated function types](./design_notes/fn_type_trait_impls.md)
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
# Extending the capabilities of compiler-generated function types
2+
3+
## Background
4+
5+
Both standalone functions and closures have unique compiler-generated types.
6+
The rest of this document will refer to both categories as simply "function
7+
types", and will use the phrase "function types without upvars" to refer to
8+
standalone functions _and_ closures without upvars.
9+
10+
Today, these function types have a small set of capabilities, which are
11+
exposed via trait implementations and implicit conversions.
12+
13+
- The `Fn`, `FnMut` and `FnOnce` traits are implemented based on the way
14+
in which upvars are used.
15+
16+
- `Copy` and `Clone` traits are implemented when all upvars implement the
17+
same trait (trivially true for function types without upvars).
18+
19+
- `auto` traits are implemented when all upvars implement the same trait.
20+
21+
- Function types without upvars have an implicit conversion to the
22+
corresponding _function pointer_ type.
23+
24+
This design note attempts to capture a range of proposed solutions, and some
25+
of the feedback regarding those solutions. This design note does not intend
26+
to favor any specific solutions, just reflect past discussions. The presence
27+
or absence of any particular feedback in this document does not necessarily
28+
serve to favor or disfavor any particular solution.
29+
30+
## Motivation
31+
32+
There are several cases where it is necessary to write a [trampoline]. A trampoline
33+
is a (usually short) generic function that is used to adapt another function
34+
in some way.
35+
36+
Trampolines have the caveat that they must be standalone functions. They cannot
37+
capture any environment, as it is often necessary to convert them into a
38+
function pointer.
39+
40+
Trampolines are most commonly used by compilers themselves. For example, when a
41+
`dyn Trait` method is called, the corresponding vtable pointer might refer
42+
to a trampoline rather than the original method in order to first down-cast
43+
the `self` type to a concrete type.
44+
45+
However, trampolines can also be useful in low-level code that needs to interface
46+
with C libraries, or even in higher level libraries that can use trampolines in
47+
order to simplify their public-facing API without incurring a performance
48+
penalty.
49+
50+
By expanding the capabilities of compiler-generated function types it would
51+
be possible to write trampolines using only safe code.
52+
53+
[trampoline]: https://en.wikipedia.org/wiki/Trampoline_(computing)
54+
55+
## History
56+
57+
Several mechanisms have been proposed to allow trampolines to be written in safe
58+
code. These have been discussed at length in the following places.
59+
60+
PR adding `Default` implementation to function types:
61+
62+
- https://github.com/rust-lang/rust/pull/77688
63+
64+
Lang team triage meeting discussions:
65+
66+
- https://youtu.be/NDeAH3woda8?t=2224
67+
- https://youtu.be/64_cy5BayLo?t=2028
68+
- https://youtu.be/t3-tF6cRZWw?t=1186
69+
70+
## Example
71+
72+
### An adaptor which prevents unwinding into C code
73+
74+
In this example, we are building a crate which provies a safe wrapper around
75+
an underlying C API. The C API contains at least one function which accepts
76+
a function pointer to be used as a callback:
77+
78+
```rust
79+
mod c_api {
80+
extern {
81+
pub fn call_me_back(f: extern "C" fn());
82+
}
83+
}
84+
```
85+
86+
We would like to allow users of our crate to safely use their own callbacks.
87+
The problem is that if the callback panics, we would unwind into C code and this
88+
would be undefined behaviour.
89+
90+
To avoid this, we would like to interpose between the user-provided callback and
91+
the C API, by wrapping it in a call to `catch_unwind`. Unfortunately, the C API
92+
offers no way to pass an additional "custom data" field that we could use to
93+
store the original function pointer.
94+
95+
Instead, we could write a generic function like this:
96+
97+
```rust
98+
use std::{panic, process};
99+
100+
pub fn call_me_back_safely<F: Fn() + Default>(_f: F) {
101+
extern "C" fn catch_unwind_wrapper<F: Fn() + Default>() {
102+
if panic::catch_unwind(|| {
103+
let f = F::default();
104+
f()
105+
}).is_err() {
106+
process::abort();
107+
}
108+
}
109+
unsafe {
110+
c_api::call_me_back(catch_unwind_wrapper::<F>);
111+
}
112+
}
113+
```
114+
115+
This compiles, and is intended to be used like so:
116+
117+
```rust
118+
fn my_callback() {
119+
println!("I was called!")
120+
}
121+
122+
fn main() {
123+
call_me_back_safely(my_callback);
124+
}
125+
```
126+
127+
However, this will fail to compile with the following error:
128+
129+
> error[E0277]: the trait bound `fn() {my_callback}: Default` is not satisfied
130+
131+
## Implementing the `Default` trait
132+
133+
The solution initially proposed was to implement `Default` for function types
134+
without upvars. Safe trampolines would be written like so:
135+
136+
```rust
137+
fn add_one_adapter<F: Fn(i32) + Default>(arg: i32) {
138+
let f = F::default();
139+
f(arg + 1);
140+
}
141+
```
142+
143+
Discussions of this design had a few central themes.
144+
145+
### When should `Default` be implemented?
146+
147+
Unlike `Clone`, it intuitively does not make sense for a closure to implement
148+
`Default` just because its upvars are themselves `Default`. A closure like
149+
the following might not expect to ever observe an ID of zero:
150+
151+
```rust
152+
fn do_thing() -> impl FnOnce() {
153+
let id: i32 = generate_id();
154+
|| {
155+
do_something_with_id(id)
156+
}
157+
}
158+
```
159+
160+
The closure may have certain pre-conditions on its upvars that are violated
161+
by code using the `Default` implementation. That said, if a function type has
162+
no upvars, then there are no pre-conditions to be violated.
163+
164+
The general consensus was that if function types are to implement `Default`,
165+
it should only be for those without upvars.
166+
167+
However, this point was also used as an argument against implementing
168+
`Default`: traits like `Clone` are implemented structurally based on the
169+
upvars, whereas this would be a deviation from that norm.
170+
171+
### Leaking details / weakening privacy concerns
172+
173+
Anyone who can observe a function type, and can also make use of the `Default`
174+
bound, would be able to safely call that function. The concern is that this
175+
may go against the intention of the function author, who did not explicitly
176+
opt-in to the `Default` trait implementation for their function type.
177+
178+
Points against this argument:
179+
180+
- We already leak this kind of capability with the `Clone` trait implementation.
181+
A function author may write a `FnOnce` closure and rely on it only being callable once. However, if the upvars are all `Clone` then the function itself can be
182+
cloned and called multiple times.
183+
184+
- It is difficult to construct practical examples of this happening. The leakage
185+
happens in the wrong direction (upstream) to be easily exploited whereas we
186+
usually care about what is public to downstream crates.
187+
188+
Without specialization, the `Default` bound would have to be explicitly listed
189+
which would then be readily visible to consumers of the upstream code.
190+
191+
- Features like `impl Trait` make it relatively easy to avoid leaking this
192+
capability when it's not wanted.
193+
194+
Points for this argument:
195+
196+
- The `Clone` trait requires an existing instance of the function in order to be
197+
exploited. The fact that the `Default` trait gives this capability to types
198+
directly makes it sufficiently different from `Clone` to warrant a different
199+
decision.
200+
201+
These discussions also raise the question of whether the `Clone` trait itself
202+
should be implemented automatically. It is convenient, but it leaves a very
203+
grey area concerning which traits ought to be implemented for compiler-generated
204+
types, and the most conservative option would be to require an opt-in for all
205+
traits beyond the basic `Fn` traits (in the case of function types).
206+
207+
### Unnatural-ness of using `Default` trait
208+
209+
Several people objected on the grounds that `Default` was the wrong trait,
210+
or that the resulting code seemed unnatural or confusing. This lead to
211+
proposals involving other traits which will be described in their own
212+
sections.
213+
214+
- Some people do not see `Default` as being equivalent to the
215+
default-constructible concept from C++, and instead see it as something
216+
more specialized.
217+
218+
To avoid putting words in people's mouths I'll quote @Mark-Simulacrum
219+
directly:
220+
221+
> I think the main reason I'm not a fan of adding a Default impl here is
222+
> because you (probably) would never actually use it really as a "default";
223+
> e.g. Vec::resize'ing with it is super unlikely. It's also not really a
224+
> Default but more just "the only value." Certainly the error message telling
225+
> me that Default is not implemented for &fn() {foo} is likely to be pretty
226+
> confusing since that does have a natural default too, like any pointer to
227+
> ZST). That's in some sense just more broadly true though.
228+
229+
- There were objections on the grounds that `Default` is not sufficient to
230+
guarantee _uniqueness_ of the function value. Code could be written today that
231+
exposes a public API with a `Default + Fn()` bound, expecting all types
232+
meeting that bound to have a single unique value.
233+
234+
If we expanded the set of types which could implement `Default + Fn()` (such
235+
as by stabilizing `Fn` trait implementations or by making more function
236+
types implement `Default`) then the assumptions of such code would be
237+
broken.
238+
239+
On the other hand, we really can't stop people from writing faulty code and
240+
this does not seem like a footgun people are going to accidentally use, in
241+
part because it's so obscure.
242+
243+
### New lang-item
244+
245+
This was a relatively minor consideration, but it is worth noting that this
246+
solution would require making `Default` a lang item.
247+
248+
## Safe transmute
249+
250+
This proposal was to materialize the closure using the machinery being
251+
added with the "safe transmute" RFC to transmute from the unit `()` type.
252+
253+
The details of how this would work in practice were not discussed in detail,
254+
but there were some salient points:
255+
256+
- This solves the "uniqueness" problem, in that ZSTs are by definition unique.
257+
- It does not help with the "privacy leakage" concerns.
258+
- It opens up a new can of worms relating to the fact that ZST closure types
259+
may still have upvars.
260+
- Several people expressed something along the lines of:
261+
262+
> if we were going to have a trait that allows this, it might as well be
263+
> Default, because telling people "no, you need the special default" doesn't
264+
> really help anything.
265+
266+
Or, that if it's possible to do this one way with safe code, it should be
267+
possible to do it in every way that makes sense.
268+
269+
## `Singleton` or `ZST` trait
270+
271+
New traits were proposed to avoid using `Default` to materialize the function
272+
values. The considerations here are mostly the same as for the "safe
273+
transmute" propsal. One note is that if we _were_ to add a `Singleton` trait,
274+
it would probably make sense for that trait to inherit from the `Default`
275+
trait anyway, and so a `Default` implementation now would be
276+
backwards-compatible.
277+
278+
## `FnStatic` trait
279+
280+
This would be a new addition to the set of `Fn` traits which would allow
281+
calling the function without any `self` argument at all. As the most
282+
restrictive (for the callee) and least restrictive (for the caller) it
283+
would sit at the bottom of the `Fn` trait hierarchy and inherit from `Fn`.
284+
285+
- Would be easy to understand for users already familiar with the `Fn` trait hierarchy.
286+
- More unambiguously describes a closure with no upvars rather than one which is a ZST.
287+
- Doesn't solve the problem of accidentally leaking capabilities.
288+
- Does not force a decision on whether closures should implement `Default`.
289+
290+
This approach would also generalize the existing closure -> function pointer
291+
conversion for closures which have no upvars. Instead of being special-cased
292+
in the compiler, the conversion can apply to all types implementing `FnStatic`.
293+
Furthermore, the conversion could be implemented by simply returning a pointer
294+
to the `FnStatic::call_static` function, which makes this very elegant.
295+
296+
### Example
297+
298+
With this trait, we can implement `call_me_back_safely` from the prior example
299+
like this:
300+
301+
```rust
302+
use std::{panic, process};
303+
304+
pub fn call_me_back_safely<F: FnStatic()>(_f: F) {
305+
extern "C" fn catch_unwind_wrapper<F: FnStatic()>() {
306+
if panic::catch_unwind(F::call_static).is_err() {
307+
process::abort();
308+
}
309+
}
310+
unsafe {
311+
c_api::call_me_back(catch_unwind_wrapper::<F>);
312+
}
313+
}
314+
```
315+
316+
## Const-eval
317+
318+
Initially proposed by @scalexm, this solution uses the existing implicit
319+
conversion from function types to function pointers, but in a const-eval
320+
context:
321+
322+
```rust
323+
fn add_one_adapter<const F: fn(i32)>(arg: i32) {
324+
F(arg + 1);
325+
}
326+
327+
fn get_adapted_function_ptr<const F: fn(i32)>() -> fn(i32) {
328+
add_one_adapter::<F>
329+
}
330+
```
331+
332+
- Avoids many of the pitfalls with implementing `Default`.
333+
- Requires giving up the original function type. There could be cases where
334+
you still need the original type but the conversion to function pointer
335+
is irreversible.
336+
- It's not yet clear if const-evaluation will be extended to support this
337+
use-case.
338+
- Const evaluation has its own complexities, and given that we already have
339+
unique function types, it seems like the original problem should be solvable
340+
using the tools we already have available.
341+
342+
## Opt-in trait implementations
343+
344+
This was barely touched on during the discussions, but one option would be to
345+
have traits be opted-in via a `#[derive(...)]`-like attribute on functions and
346+
closures.
347+
348+
- Gives a lot of power to the user.
349+
- Quite verbose.
350+
- Solves the problem of leaking capabilities.

0 commit comments

Comments
 (0)