Skip to content

Commit 9c65b4a

Browse files
authored
Merge pull request #29 from BatmanAoD/Rfc-POF-terminology
POF terminology, links & summary for older discussions
2 parents 86b4697 + a727b49 commit 9c65b4a

File tree

1 file changed

+146
-62
lines changed

1 file changed

+146
-62
lines changed

rfcs/0000-c-unwind-abi.md

Lines changed: 146 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Additionally, we define the behavior for a limited number of
1616
previously-undefined cases when an unwind operation reaches a Rust function
1717
boundary with a non-`"Rust"`, non-`"C unwind"` ABI.
1818

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+
1923
This RFC does not define the behavior of `catch_unwind` in a Rust frame being
2024
unwound by a foreign exception. This is something the [project
2125
group][project-group] would like to specify in a future RFC; as such, it is
@@ -31,13 +35,16 @@ the Lucet and Wasmer projects.
3135
There are also existing Rust crates (notably, wrappers around the `libpng` and
3236
`libjpeg` C libraries) that `panic` across C frames. The safety of such
3337
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.
3542

3643
Additionally, there are libraries such as `rlua` that rely on `longjmp` across
3744
Rust frames; on Windows, `longjmp` is implemented via [forced
3845
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.
4148

4249
The desire for this feature has been previously discussed on other RFCs,
4350
including [#2699][rfc-2699] and [#2753][rfc-2753].
@@ -74,8 +81,8 @@ how well the current design satisfies these constraints.
7481
* **Enable error handling with `longjmp`:** As mentioned above, some existing
7582
Rust libraries use `longjmp`. Despite the fact that `longjmp` on Windows is
7683
[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.
7986
* **Do not change the ABI of functions in the `libc` crate:** Some `libc`
8087
functions may invoke `pthread_exit`, which uses [a form of
8188
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
126133
unwind the C++ frames, if the Rust code declares both the C++ entrypoint and
127134
the Rust entrypoint using `"C unwind"`.
128135

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+
129158
## Forced unwinding
130159
[forced-unwinding]: #forced-unwinding
131160

@@ -143,20 +172,44 @@ deallocate Rust frames without true unwinding on other platforms.
143172
This RFC specifies that, regardless of the platform or the ABI string (`"C"` or
144173
`"C unwind"`), any platform features that may rely on forced unwinding are:
145174

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
149177

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

157209
[inside-rust-forced]: https://blog.rust-lang.org/inside-rust/2020/02/27/ffi-unwind-design-meeting.html#forced-unwinding
158210

159211
## Changes to `extern "C"` behavior
212+
[extern-c-behavior]: #changes-to-extern-c-behavior
160213

161214
Prior to this RFC, any unwinding operation that crossed an `extern "C"`
162215
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:
167220

168221
* With the `panic=unwind` runtime, `panic!` will cause an `abort` if it would
169222
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
172225
`pthread_exit` and `longjmp` consistent across platforms.
173226

174227
## Interaction with `panic=abort`
@@ -182,7 +235,7 @@ to abort with `panic=abort`, though:
182235

183236
* Forced unwinding: Rust provides no mechanism to catch this type of unwinding.
184237
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.
186239
* Unwinding from another language into Rust if the entrypoint to that language
187240
is declared with `extern "C"` (contrary to the guidelines above): this is
188241
always undefined behavior.
@@ -206,24 +259,25 @@ Regardless of the panic runtime, ABI, or platform, the interaction of Rust
206259
frames with C functions that deallocate frames (i.e. functions that may use
207260
forced unwinding on specific platforms) is specified as follows:
208261

209-
* **When deallocating Rust frames without destructors:** frames are safely
262+
* **When deallocating Rust [POFs][POF-definition]:** frames are safely
210263
deallocated; no undefined behavior
211-
* **When deallocating Rust frames with destructors:** undefined behavior
264+
* **When deallocating Rust non-POFs:** undefined behavior
212265

213266
No subtype relationship is defined between functions or function pointers using
214267
different ABIs. This RFC also does not define coercions between `"C"` and
215268
`"C unwind"`.
216269

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

220274
# Drawbacks
221275
[drawbacks]: #drawbacks
222276

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

228282
This design imposes some burden on existing codebases (mentioned
229283
[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.
249303
behavior of a forced unwind across a `"C unwind"` boundary under `panic=abort`.
250304
Under the current proposal, this type of unwind is permitted, allowing
251305
`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.
258312

259313
The other proposal in the blog post, "option 3", is dramatically different. In
260314
that proposal, foreign exceptions are permitted to cross `extern "C"`
@@ -277,11 +331,12 @@ Our reasons for preferring the current proposal are:
277331
Rust to have undefined behavior under `panic=abort`, whereas the current
278332
proposal does not permit the `panic=abort` runtime to introduce undefined
279333
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.
285340
* This design has simpler forward compatibility with alternate `panic!`
286341
implementations. Any well-defined cross-language unwinding will require shims
287342
to translate between the Rust unwinding mechanism and the natively provided
@@ -335,15 +390,13 @@ future work.
335390
### Enable error handling with `longjmp`
336391

337392
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.
340394

341395
### Do not change the ABI of functions in the `libc` crate
342396

343397
This constraint is met: `libc` functions will continue to use the `"C"` ABI.
344398
`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.
347400

348401
# Prior art
349402
[prior-art]: #prior-art
@@ -366,31 +419,62 @@ foreign exceptions as well. In the current proposal, though, such foreign
366419
exception support is not enabled by default with `panic=unwind` but requires
367420
the new `"C unwind"` ABI.
368421

369-
## Attributes on nightly Rust
422+
## Attributes on nightly Rust and prior RFCs
423+
[nightly-attributes]: #attributes-on-nightly-rust-and-prior-rfcs
370424

371425
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
392452
[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
394478

395479
# Unresolved questions
396480
[unresolved-questions]: #unresolved-questions

0 commit comments

Comments
 (0)