|
| 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