@@ -16,6 +16,10 @@ Additionally, we define the behavior for a limited number of
16
16
previously-undefined cases when an unwind operation reaches a Rust function
17
17
boundary with a non-` "Rust" ` , non-` "C unwind" ` ABI.
18
18
19
+ As part of this specification, we introduce the term [ "Plain Old Frame"
20
+ (POF)] [ POF-definition ] . These are frames that may be safely deallocated with
21
+ ` longjmp ` .
22
+
19
23
This RFC does not define the behavior of ` catch_unwind ` in a Rust frame being
20
24
unwound by a foreign exception. This is something the [ project
21
25
group] [ project-group ] would like to specify in a future RFC; as such, it is
@@ -31,13 +35,16 @@ the Lucet and Wasmer projects.
31
35
There are also existing Rust crates (notably, wrappers around the ` libpng ` and
32
36
` libjpeg ` C libraries) that ` panic ` across C frames. The safety of such
33
37
unwinding relies on compatibility between Rust's unwinding mechanism and the
34
- native exception mechanisms in GCC, LLVM, and MSVC.
38
+ native exception mechanisms in GCC, LLVM, and MSVC. Despite using a compatible
39
+ unwinding mechanism, the current ` rustc ` implementation assumes that `extern
40
+ "C"` functions cannot unwind, which permits LLVM to optimize with the
41
+ assumption that such unwinding constitutes undefined behavior.
35
42
36
43
Additionally, there are libraries such as ` rlua ` that rely on ` longjmp ` across
37
44
Rust frames; on Windows, ` longjmp ` is implemented via [ forced
38
45
unwinding] [ forced-unwinding ] . The current ` rustc ` implementation makes it safe
39
- to ` longjmp ` across Rust frames without ` Drop ` types, but this is not formally
40
- specified in an RFC or by the Reference.
46
+ to ` longjmp ` across Rust [ POFs ] [ POF-definition ] ( frames without ` Drop ` types),
47
+ but this is not formally specified in an RFC or by the Reference.
41
48
42
49
The desire for this feature has been previously discussed on other RFCs,
43
50
including [ #2699 ] [ rfc-2699 ] and [ #2753 ] [ rfc-2753 ] .
@@ -74,8 +81,8 @@ how well the current design satisfies these constraints.
74
81
* ** Enable error handling with ` longjmp ` :** As mentioned above, some existing
75
82
Rust libraries use ` longjmp ` . Despite the fact that ` longjmp ` on Windows is
76
83
[ technically a form of unwinding] [ forced-unwinding ] , using ` longjmp ` across
77
- Rust frames [ is safe] [ longjmp-pr ] with the current implementation of ` rustc ` ,
78
- and we want to specify that this will remain safe.
84
+ Rust [ POFs ] [ POF-definition ] [ is safe] [ longjmp-pr ] with the current
85
+ implementation of ` rustc ` , and we want to specify that this will remain safe.
79
86
* ** Do not change the ABI of functions in the ` libc ` crate:** Some ` libc `
80
87
functions may invoke ` pthread_exit ` , which uses [ a form of
81
88
unwinding] [ forced-unwinding ] in the GNU libc implementation. Such functions
@@ -126,6 +133,28 @@ may be "sandwiched" between Rust frames, so that Rust `panic`s may safely
126
133
unwind the C++ frames, if the Rust code declares both the C++ entrypoint and
127
134
the Rust entrypoint using ` "C unwind" ` .
128
135
136
+ ## "Plain Old Frames"
137
+ [ POF-definition ] : #plain-old-frames
138
+
139
+ A "POF", or "Plain Old Frame", is defined as a frame that can be trivially
140
+ deallocated: returning from or unwinding a POF cannot cause any
141
+ observable effects. This means that POFs do not contain any pending destructors
142
+ (live ` Drop ` objects) or ` catch_unwind ` calls.
143
+
144
+ The terminology is intentionally akin to [ C++'s "Plain Old Data"
145
+ types] [ cpp-POD-definition ] , which are types that, among other requirements, are
146
+ trivially destructible (their destructors do not cause any observable effects,
147
+ and may be elided as an optimization).
148
+
149
+ Rust frames that do contain pending destructors or ` catch_unwind ` calls are
150
+ called non-POFs.
151
+
152
+ Note that a non-POF may _ become_ a POF, for instance if all ` Drop ` objects are
153
+ moved out of scope, or if its only ` catch_unwind ` call is in a code path that
154
+ will not be executed. The next section provides an example.
155
+
156
+ [ cpp-POD-definition ] : https://en.cppreference.com/w/cpp/named_req/PODType
157
+
129
158
## Forced unwinding
130
159
[ forced-unwinding ] : #forced-unwinding
131
160
@@ -143,20 +172,44 @@ deallocate Rust frames without true unwinding on other platforms.
143
172
This RFC specifies that, regardless of the platform or the ABI string (` "C" ` or
144
173
` "C unwind" ` ), any platform features that may rely on forced unwinding are:
145
174
146
- * _ undefined behavior_ if they cross frames with destructors or a
147
- ` catch_unwind ` call
148
- * _ defined behavior_ otherwise
175
+ * _ undefined behavior_ if they cross non-[ POFs] [ POF-definition ]
176
+ * _ defined behavior_ when all unwound frames are POFs
149
177
150
- As an example, this means that Rust code can (indirectly, via C) invoke
151
- ` longjmp ` using the "C" ABI, and that ` longjmp ` can unwind or otherwise cross
152
- Rust frames, so long as those frames do not contain any pending destructors or
153
- make use of ` catch_unwind ` . If those Rust frames do contain pending
154
- destructors, then invoking ` longjmp ` would be undefined behavior (and hence a
155
- bug).
178
+ As an example:
179
+
180
+ ``` rust
181
+ fn foo <D : Drop >(c : bool , d : D ) {
182
+ if c {
183
+ drop (d );
184
+ }
185
+ longjmp_if_true (c );
186
+ }
187
+
188
+ /// Calls `longjmp` if `c` is true; otherwise returns normally.
189
+ extern " C" fn longjmp_if_true (c : bool );
190
+ ```
191
+
192
+ If a ` longjmp ` occurs, it can safely traverse the ` foo ` frame, which will be a
193
+ POF because ` d ` has already been dropped.
194
+
195
+ Since ` longjmp_if_true ` function is using the ` "C" ` rather than the `"C
196
+ unwind"` ABI, the optimizer may assume that it cannot unwind; on LLVM, this is
197
+ represented by the ` nounwind ` attribute. On most platforms, ` longjmp ` is not a
198
+ form of unwinding: the ` foo ` frame is simply discarded. On Windows, ` longjmp `
199
+ is implemented as a forced unwind, which is permitted to traverse ` nounwind `
200
+ frames. Since ` foo ` contains a ` Drop ` type the forced unwind will include a
201
+ call to the frame's cleanup logic, but that logic will not produce any
202
+ observable effect; in particular, ` D::drop() ` will not be called again. The
203
+ observable behavior should therefore be the same on all platforms.
204
+
205
+ Conversely, if, due to a bug, ` longjmp ` were called unconditionally, then this
206
+ code would have undefined behavior on all platforms when ` c ` is false, because
207
+ ` foo ` would not be a POF.
156
208
157
209
[ inside-rust-forced ] : https://blog.rust-lang.org/inside-rust/2020/02/27/ffi-unwind-design-meeting.html#forced-unwinding
158
210
159
211
## Changes to ` extern "C" ` behavior
212
+ [ extern-c-behavior ] : #changes-to-extern-c-behavior
160
213
161
214
Prior to this RFC, any unwinding operation that crossed an ` extern "C" `
162
215
boundary, either from a ` panic! ` "escaping" from a Rust function defined with
@@ -167,8 +220,8 @@ This RFC retains most of that undefined behavior, with two exceptions:
167
220
168
221
* With the ` panic=unwind ` runtime, ` panic! ` will cause an ` abort ` if it would
169
222
otherwise "escape" from a function defined with ` extern "C" ` .
170
- * Forced unwinding is safe with ` extern "C" ` as long as no frames with
171
- destructors (i.e. ` Drop ` types) are unwound. This is to keep behavior of
223
+ * Forced unwinding is safe with ` extern "C" ` as long as only
224
+ * [ POFs ] [ POF-definition ] are unwound. This is to keep behavior of
172
225
` pthread_exit ` and ` longjmp ` consistent across platforms.
173
226
174
227
## Interaction with ` panic=abort `
@@ -182,7 +235,7 @@ to abort with `panic=abort`, though:
182
235
183
236
* Forced unwinding: Rust provides no mechanism to catch this type of unwinding.
184
237
This is safe with either the ` "C" ` ABI or the new ` "C unwind" ` ABI, as long
185
- as none of the unwound Rust frames contain destructors .
238
+ as only [ POFs ] [ POF-definition ] are unwound.
186
239
* Unwinding from another language into Rust if the entrypoint to that language
187
240
is declared with ` extern "C" ` (contrary to the guidelines above): this is
188
241
always undefined behavior.
@@ -206,24 +259,25 @@ Regardless of the panic runtime, ABI, or platform, the interaction of Rust
206
259
frames with C functions that deallocate frames (i.e. functions that may use
207
260
forced unwinding on specific platforms) is specified as follows:
208
261
209
- * ** When deallocating Rust frames without destructors :** frames are safely
262
+ * ** When deallocating Rust [ POFs ] [ POF-definition ] :** frames are safely
210
263
deallocated; no undefined behavior
211
- * ** When deallocating Rust frames with destructors :** undefined behavior
264
+ * ** When deallocating Rust non-POFs :** undefined behavior
212
265
213
266
No subtype relationship is defined between functions or function pointers using
214
267
different ABIs. This RFC also does not define coercions between ` "C" ` and
215
268
` "C unwind" ` .
216
269
217
- As noted in the [ summary] [ summary ] , if a Rust frame containing ` catch_unwind ` is unwound by a
218
- foreign exception, the behavior is undefined for now.
270
+ As noted in the [ summary] [ summary ] , if a Rust frame containing a pending
271
+ ` catch_unwind ` call is unwound by a foreign exception, the behavior is
272
+ undefined for now.
219
273
220
274
# Drawbacks
221
275
[ drawbacks ] : #drawbacks
222
276
223
- Forced unwinding is treated as universally unsafe across frames with
224
- destructors , but on some platforms it could theoretically be well-defined. As
225
- noted [ above] ( forced-unwind ) , however, this would make the UB inconsistent
226
- across platforms, which is not desirable.
277
+ Forced unwinding is treated as universally unsafe across
278
+ [ non-POFs ] [ POF-definition ] , but on some platforms it could theoretically be
279
+ well-defined. As noted [ above] ( forced-unwind ) , however, this would make the UB
280
+ inconsistent across platforms, which is not desirable.
227
281
228
282
This design imposes some burden on existing codebases (mentioned
229
283
[ above] [ motivation ] ) to change their ` extern ` annotations to use the new ABI.
@@ -249,12 +303,12 @@ RFC is referred to as "option 2" in that post.
249
303
behavior of a forced unwind across a ` "C unwind" ` boundary under ` panic=abort ` .
250
304
Under the current proposal, this type of unwind is permitted, allowing
251
305
` longjmp ` and ` pthread_exit ` to behave "normally" with both the ` "C" ` and the
252
- ` "C unwind" ` ABI across all platforms regardless of panic runtime. If there are
253
- destructors in the unwound frames , this results in undefined behavior. Under
254
- "option 1", however, all foreign unwinding, forced or unforced, is caught at
255
- ` "C unwind" ` boundaries under ` panic=abort ` , and the process is aborted. This
256
- gives ` longjmp ` and ` pthread_exit ` surprising behavior on some platforms, but
257
- avoids that cause of undefined behavior in the current proposal.
306
+ ` "C unwind" ` ABI across all platforms regardless of panic runtime. If
307
+ [ non-POFs ] [ POF-definition ] are unwound, this results in undefined behavior.
308
+ Under "option 1", however, all foreign unwinding, forced or unforced, is caught
309
+ at ` "C unwind" ` boundaries under ` panic=abort ` , and the process is aborted.
310
+ This gives ` longjmp ` and ` pthread_exit ` surprising behavior on some platforms,
311
+ but avoids that cause of undefined behavior in the current proposal.
258
312
259
313
The other proposal in the blog post, "option 3", is dramatically different. In
260
314
that proposal, foreign exceptions are permitted to cross ` extern "C" `
@@ -277,11 +331,12 @@ Our reasons for preferring the current proposal are:
277
331
Rust to have undefined behavior under ` panic=abort ` , whereas the current
278
332
proposal does not permit the ` panic=abort ` runtime to introduce undefined
279
333
behavior to a program that is well-defined under ` panic=unwind ` .
280
- * This optimization could be made available with a single ABI by means of an
281
- annotation indicating that a function cannot unwind (similar to C++'s
282
- ` noexcept ` ). However, Rust does not yet support annotations for function
283
- pointers, so until that feature is added, such an annotation could not be
284
- applied to function pointers.
334
+ * This optimization could be made available with a single ABI by means of a
335
+ function attribute indicating that a function cannot unwind (similar to C++'s
336
+ ` noexcept ` ). Such attributes [ are already available in nightly
337
+ Rust] [ nightly-attributes ] . However, Rust does not yet support attributes
338
+ for function pointers, so until that feature is added, there would be no
339
+ way to indicate whether function pointers unwind using an attribute.
285
340
* This design has simpler forward compatibility with alternate ` panic! `
286
341
implementations. Any well-defined cross-language unwinding will require shims
287
342
to translate between the Rust unwinding mechanism and the natively provided
@@ -335,15 +390,13 @@ future work.
335
390
### Enable error handling with ` longjmp `
336
391
337
392
This constraint is met: ` longjmp ` is treated the same across all platforms, and
338
- is safe as long as the deallocated Rust frames do not contain pending ` drop ` or
339
- ` catch_unwind ` calls.
393
+ is safe as long as only [ POFs] [ POF-definition ] are deallocated.
340
394
341
395
### Do not change the ABI of functions in the ` libc ` crate
342
396
343
397
This constraint is met: ` libc ` functions will continue to use the ` "C" ` ABI.
344
398
` pthread_exit ` will be treated the same across all platforms, and will be safe
345
- as long as the deallocated Rust frames do not contain pending ` drop ` or
346
- ` catch_unwind ` calls.
399
+ as long as only [ POFs] [ POF-definition ] are deallocated.
347
400
348
401
# Prior art
349
402
[ prior-art ] : #prior-art
@@ -366,31 +419,62 @@ foreign exceptions as well. In the current proposal, though, such foreign
366
419
exception support is not enabled by default with ` panic=unwind ` but requires
367
420
the new ` "C unwind" ` ABI.
368
421
369
- ## Attributes on nightly Rust
422
+ ## Attributes on nightly Rust and prior RFCs
423
+ [ nightly-attributes ] : #attributes-on-nightly-rust-and-prior-rfcs
370
424
371
425
Currently, nightly Rust provides attributes, ` #[unwind(allowed)] ` and
372
- ` #[unwind(abort)] ` , for making the behavior of ` panic ` crossing a ` "C" ` ABI
373
- boundary well defined.
374
- <!-- TODO explain why new ABI string is preferable to attributes -->
375
-
376
- ## Prior RFCs and other discussions
377
-
378
- There were two previous RFCs, [ #2699 ] [ rfc-2699 ] and [ #2753 ] [ rfc-2753 ] , that
379
- attempted to introduce a well-defined way for uwnding to cross FFI boundaries.
380
-
381
- <!-- TODO other discussions:
382
- Tickets:
383
- * https://github.com/rust-lang/rust/issues/58794
384
- * https://github.com/rust-lang/rust/issues/52652
385
- * https://github.com/rust-lang/rust/issues/58760
386
- * https://github.com/rust-lang/rust/pull/55982
387
-
388
- Discourse:
389
- https://internals.rust-lang.org/t/unwinding-through-ffi-after-rust-1-33/9521?u=batmanaod
390
- -->
391
-
426
+ ` #[unwind(abort)] ` , that permit users to select a well-defined behavior when a
427
+ ` panic ` reaches an ` extern "C" ` function boundary. Stabilization of these
428
+ attributes has [ a tracking issue] [ attributes-tracking-issue ] , but most
429
+ of the discussion about whether this was the best approach took place in two
430
+ RFC PR threads, [ #2699 ] [ rfc-2699 ] and [ #2753 ] [ rfc-2753 ] .
431
+
432
+ The attribute approach was deemed insufficient for the following reasons:
433
+
434
+ * Currently, Rust does not support attributes on function pointers. This may
435
+ change in the future, but until then, attributes cannot provide any way to
436
+ differentiate function pointers that may unwind from those that are
437
+ guaranteed not to. Assuming that no function pointers may unwind is not
438
+ viable, because that severly limits the utility of cross-FFI unwinding.
439
+ Conversely, assuming that all ` extern "C" ` function pointers may unwind is
440
+ inconsistent with the no-unwind default for ` extern "C" ` functions.
441
+ * The existence of a compatible unwind mechanism on both sides of a function
442
+ invocation boundary is part of the binary interface for that invocation, so
443
+ the ABI string is a more appropriate part of the language syntax than
444
+ function attributes to indicate that unwinding may occur.
445
+ * The ability of a function to unwind must be part of the type system to ensure
446
+ that callers that cannot unwind don't invoke functions that can unwind.
447
+ Although attributes are sometimes part of a function's type, a function's ABI
448
+ string is always part of its type, so we are not introducing any new elements
449
+ to the type system.
450
+
451
+ [ attributes-tracking-issue ] : https://github.com/rust-lang/rust/issues/58760
392
452
[ rfc-2699 ] : https://github.com/rust-lang/rfcs/pull/2699
393
- [ rfc-2753 ] : https://github.com/rust-lang/rfcs/pull/2573
453
+ [ rfc-2753 ] : https://github.com/rust-lang/rfcs/pull/2753
454
+
455
+ ## Older discussions about unwinding through ` extern "C" ` boundaries
456
+
457
+ As mentioned [ above] [ motivation ] , it is currently undefined behavior for
458
+ ` extern "C" ` functions to unwind. As documented in [ this
459
+ issue] [ abort-unwind-issue ] , the lang team has long intended to make ` panic! `
460
+ cause the runtime to abort rather than unwind through an ` extern "C" ` boundary
461
+ (which the current proposal [ also specifies] [ extern-c-behavior ] ).
462
+
463
+ The abort-on-unwind behavior was [ stabilized in 1.24] [ 1.24-release ] and
464
+ [ reverted in 1.24.1] [ 1.24.1-release ] ; the team originally planned to [ stabilize
465
+ it again] [ 1.33-stabilization ] in 1.33, but ultimately [ decided not
466
+ to] [ 1.33-discussion ] . Community discussion [ on discourse] [ discourse-thread ] was
467
+ largely concerned with the lack of any stable language feature to permit
468
+ unwinding across FFI boundaries, and this contributed to the decision to block
469
+ the re-stabilization of the abort-on-unwind behavior until such a feature could
470
+ be introduced.
471
+
472
+ [ abort-unwind-issue ] : https://github.com/rust-lang/rust/issues/52652
473
+ [ 1.24-release ] : https://blog.rust-lang.org/2018/02/15/Rust-1.24.html#other-good-stuff
474
+ [ 1.24.1-release ] : https://blog.rust-lang.org/2018/03/01/Rust-1.24.1.html#do-not-abort-when-unwinding-through-ffi
475
+ [ 1.33-stabilization ] : https://github.com/rust-lang/rust/pull/55982
476
+ [ 1.33-discussion ] : https://github.com/rust-lang/rust/issues/58794
477
+ [ discourse-thread ] : https://internals.rust-lang.org/t/unwinding-through-ffi-after-rust-1-33/9521?u=batmanaod
394
478
395
479
# Unresolved questions
396
480
[ unresolved-questions ] : #unresolved-questions
0 commit comments