Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Well-defined unwinding through FFI boundaries #2699

Closed
wants to merge 23 commits into from

Conversation

BatmanAoD
Copy link
Member

@BatmanAoD BatmanAoD commented May 11, 2019

Update 28 October 2019: The effort to provide this language feature has been spun into a working group. See the announcement RFC.


Rendered

@Ixrec
Copy link
Contributor

Ixrec commented May 11, 2019

This text about how the behavior could change at any time sounds like the very definition of an unstable feature, but it also sounds like this is being proposed for stabilization? That opens a big can of worms around our stability guarantees that ought to be discussed in the RFC. In particular, it seems like we should at least consider the option of making crates that depend on this become unstable, or contain that dependency in an unstable feature or something.

I think we're also missing a summary of why any crate would want to rely on this in the first place. I've probably read it somewhere before, but I've completely forgotten it now, and it's obviously critical to this proposal. Is it as simple as "catch_unwind has a performance cost"?

@Ixrec
Copy link
Contributor

Ixrec commented May 11, 2019

Fix link markdown

Co-Authored-By: Joe ST <joe@fbstj.net>
@BatmanAoD
Copy link
Member Author

@lxrec Re: instability, what can change is the implementation of unwinding, not the observable behavior in Rust code. In the guide-level explanation, I mentioned that Rust-to-Rust unwinding will be well-defined. I should probably expand on that paragraph, but there are real use-cases for this. From a comment by @gnzlbg further down in one of those threads:

Doing Rust->Rust via FFI is a reasonable thing to do (e.g., DLLs, hot reloading in games, ...)

As long as the code on both sides of the FFI boundary uses the same compiler, changes to the implementation would not change the observable unwinding behavior.

Regarding the three "on the other hand" quotes you've cited:

  • The issue here is not about allowing people to solve a problem that they can’t solve today....

I found this post pretty confusing at first, but @gnzlbg later clarified:

What works 100% of the time is using an error code in FFI, converting from/to each language error handling mechanism. On functions exposed from Rust to FFI you catch panics and return an error code, and in FFI functions called from rust you raise the error code as a panic. On the other side of FFI, e.g., if it is C++, you catch all exceptions in exposed functions and return error codes, and when calling FFI you raise the error codes as exceptions.

And later acknowledged:

the problem with the C wrappers approach like @joshtriplett mentions is that it is a pain.

  • If you need to "unwind through C code", you either need to compile the C code yourself with a compiler that supports a form of unwinding as an extension, or try to find another way....

Yes, that's why I mentioned -fexceptions (GCC/LLVM) and SEH (MSVC) in the RFC. These are known to be compatible with Rust's current unwinding implementation.

  • I've prototyped a "plan B" of wrapping each FFI call in a C++ try/catch and translating caught errors to C-compatible error codes. It's doable, so if you decide to kill unwinding in 12 weeks, I could reluctantly switch to the workaround.

The problem with this is two-fold. First, I think it's probably not controversial to say that any solution requiring users to add C++ to a Rust/C project would be undesirable. Second, C++ doesn't actually provide strong guarantees than Rust for this; the only reason to use this as a solution would be if Rust implements the unwind-aborts-at-FFI-boundaries feature without providing an opt-out mechanism.

@jcranmer
Copy link

The short summary of the state of the affairs:

  • The desired use case is to throw an exception from Rust -> C/C++ -> Rust.
  • extern "C" functions in Rust are (supposed to be) marked nounwind, which means there's a hole in correctness if such a function throws an exception. The solution is to make all such functions abort if they would throw an exception, which is what is supposed to land already but is being held up because it breaks mozjpeg.
  • The only legal way in standard ISO C to throw an exception is to use setjmp/longjmp. On Windows, setjmp and longjmp are implemented using SEH, which is the same mechanism that Rust presently uses to throw exceptions.
  • This means that Rust cannot use setjmp/longjmp to throw exceptions into FFI code. There is therefore no way to throw exceptions out of Rust, so you have to use various forms of status codes and adjustments, which is possible but suboptimal.

What this RFC is proposing--in terms of implementation--is basically to provide a mechanism to make an extern "C" function not get the nounwind attribute and therefore not need abort semantics for safety. This leads to some complications:

  • It's not clear that the implementation legwork is sufficient for safety. One particular concern that has been brought up is the possibility of a Rust exception escaping via extern "C" into Rust code that comes from a different implementation of Rust with not-quite-compatible ABIs.
  • There is some dispute as to whether or not it is actually legal to catch/propagate exceptions between different languages or if it is truly UB and all working code is merely accidentally working. My contention is that this amounts to an ABI issue, so that if you use specific flags to get the compiler to conform to the ABI, it is legal code, but I will concede that this view is not universal.
  • There are some ideas for changing Rust's ABI for unwinding. I don't have a link handy to the idea, but it does mean that there we have to be careful about imposing constraints on the ABI.

Okay, with that out of the way, here are my personal thoughts on the RFC:

I'm definitely in favor of giving us better tools in Rust for interacting with unwinds and FFI exceptions. This RFC doesn't go all the way there, but it doesn't need to do so to solve the more immediate and pressing problem: the minimal implementation change mentioned above sounds like the right change, so long as we have analyzed to make sure that the different Rust ABI situation can be handled in all cases. But I don't think this gives the right semantics to achieve that change.

The property of unwinding to me is a fundamental component of the ABI--not only is there an exception coming via some unwinding side channel, but the mechanics of how that side channel works. This means that it is effectively part of the calling convention, as function pointers also have to know if their target is a nounwind function call or an unwind(Rust) call. I'm not sure encoding such information into an attribute is the right way to do it, but it's also mostly orthogonal to the calling convention that usually goes in the extern "foo" part.

Also, exposing the ABI of Rust that would happen with marking #[unwind(Rust)] extern "C" feels too constrictive. It would be better to have an #[unwind(native)], whose semantics are "this causes unwinds out of this function to use the appropriate native unwinding implementation" (i.e., SEH, Itanium, ARM EH ABI, SjLj, as appropriate). This would mean that Rust must support such an unwinding implementation in perpetuity, even if it chooses to use a different unwinding implementation internally. On the other hand, this RFC as it stands would force any alternate unwinding implementation to have to try to handle the different-Rust-ABIs problem correctly, and probably to somehow keep the backchannel established if there is C code in the middle (while the RFC prohibits the Rust->C->Rust case explicitly, it does try to backdoor it by telling you when it will actually work).

Copy link
Contributor

@gnzlbg gnzlbg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC proposes a feature to solve propagating panics in a Rust->X->Rust code chain, where X is written in a programming language that is somehow able to propagate Rust panics properly. The feature proposed does a very poor job at solving this problem.

This feature could be useful to solve other problems, e.g., like allowing those that want to link Rust code dynamically (Rust->Rust with the same toolchain), e.g., in a dll, using the C ABI, to be able to use Rust panics.

# Summary
[summary]: #summary

A new function annotation, `#[unwind(Rust)]`, will be introduced; it will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use present tense - this RFC introduces...


A new function annotation, `#[unwind(Rust)]`, will be introduced; it will
explicitly permit `extern` functions to unwind (`panic`) without aborting. No
guarantees are made regarding the specific unwinding implementation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do make some guarantees though. For example, when using a particular toolchain to compile Rust code, all extern functions and extern function declarations with this attribute are assumed to use the same panic implementation.

[summary]: #summary

A new function annotation, `#[unwind(Rust)]`, will be introduced; it will
explicitly permit `extern` functions to unwind (`panic`) without aborting. No
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only extern "ABI" functions ? (all ABIs ? or which ones). Shouldn't this also be supported on extern function declarations (extern { fn-decl }) ?

around the `libpng` and `libjpeg` C libraries) that make use of the current
implementation's behavior. The proposed annotation would act as an "opt out" of
the safety guarantee provided by aborting at an FFI boundary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These library authors decided to rely on an implementation detail of undefined behavior, they knew about it, and have been actively blocking the fix of soundness hole in the language without putting in any work towards a solution.

If anything, this is probably the strongest argument against this RFC. It sends the message that it is ok to rely on UB, exploit implementation details, etc. knowingly as long as you complain loud enough when things break because then the language will be bent in your favor.

Calling it an ""opt out" of a safety guarantee" does not help.


Honestly I don't think even mentioning this would do this RFC a favor. From the use cases discussed, the Rust-Rust dynamic linking one felt like the only one that could motivate this. It might be better to focus on that instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize you felt this way about the specific impetus for the request and the proposal itself; I had thought, from you last comments in the issue thread, that you were in agreement that this was a reasonable request and a viable proposal.

I disagree with your perspective on the UB issue and am willing to have that conversation, but I'm now wondering if that discussion should be continued back on the internals thread so that it doesn't make this PR too noisy. If I had understood that we didn't yet have agreement on the way forward, I wouldn't have opened this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(By "the UB issue" I don't mean expecting LLVM not to use nounwind for optimization; I agree that doing so is incorrect.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize you felt this way about the specific impetus for the request and the proposal itself; I had thought, from you last comments in the issue thread, that you were in agreement that this was a reasonable request and a viable proposal.

I thought this comment expressed what I believe would be an appropriate motivation for this attribute.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gnzlbg I agree that FFI Rust->Rust is an appropriate motivation for the attribute, but it felt disingenuous to me to advocate for it on that ground alone since it's not the real motivation for myself or @kornelski to be pushing for an annotation of some sort changing FFI unwind behavior. What I took from your phrasing in that comment about Rust->X->Rust was that you agreed it would be appropriate for @kornelski and anyone else to use the proposed annotation for Rust->X->Rust as long as they realize that they're relying on an implementation detail:

...by accident, it would allow those doing Rust->X->Rust to continue to "work" as long as their assumptions about implementation details still hold.

Knowledge about implementation details doesn't seem like an overly burdensome requirement for unsafe interactions between languages; that's how the computing world managed to function without a formally defined C ABI for several decades.

Copy link
Contributor

@gnzlbg gnzlbg May 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I took from your phrasing in that comment about Rust->X->Rust was that you agreed it would be appropriate for @kornelski and anyone else to use the proposed annotation for Rust->X->Rust as long as they realize that they're relying on an implementation detail:

I don't see where I agreed to that. My comment says that "panics in Rust->Rust FFI" is a simple extension to the language that solves an useful problem, and that such a feature would allow some of the people that are invoking undefined-behavior in Rust today to continue doing so after the soundness fix for panics in Rust FFI lands (which AFAICT is all they wanted).

I never suggested #[unwind(Rust)] as a solution to "panic in Rust->X", nor say that it would be appropriate for anyone to write code with undefined behavior. If anything, I showed multiple times how to write code without UB in stable Rust today, and argued that because that is possible "panics in Rust->X" might not be a problem worth solving.

than zero will cause the program to be aborted. This is the only way the Rust
compiler can ensure that the function is safe, because there is no way for it
to know whether or not the calling code supports the same implementation of
stack-unwinding used for Rust.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is the main issue.

The Rust language guarantees that extern "C" functions do not panic, and these functions are optimized accordingly by LLVM. If they were to actually panic, the optimizations would be incorrect, which results in undefined behavior. Therefore, the language requires these functions to abort if a panic were to escape from them, but due to a bug in the implementation, this does not happen today. This bugs allows safe Rust programs to trigger UB, which makes the implementation unsound.

None of this has anything to do with the unwinding implementation of other languages. In fact, even if Rust code compiled with the exact same toolchain was used to call this function, the unsoundness bug would still be there, because LLVM will still optimize this code under the assumption that it does not panic.

}
assert!(result.is_err());
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above, this example is unsound.

}
```

**PLEASE NOTE:** Using this annotation **does not** provide any guarantees
Copy link
Contributor

@gnzlbg gnzlbg May 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior of extern functions and extern function declarations that unwind (in any way - not only via Rust panics) is undefined. #[unwind(Rust)] allows these functions to let a Rust panic! escape, which is something that they cannot do without this attribute.

Explaining that does not really need mentioning anything about the panic implementation. If you want to mention that, just mentioning that the Rust panic implementation is unspecified, that is, it can change at any time, should be enough.

Since the behavior may be subject to change without notice as the Rust compiler
is updated, it is recommended that all projects that rely on unwinding from
Rust code into C code lock the project's `rustc` version and only update it
after ensuring that the behavior will remain correct.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this feature were aimed at solving this problem, then I don't think that having to do this to be able to use this feature correctly would be good enough.

I don't think this feature can solve this problem well without major changes.


Unwinding for functions marked `#[unwind(Rust)]` is performed as if the
function were not marked `extern`. This is identical to the behavior of `rustc`
for all versions prior to 1.35 except for 1.24.0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads a bit like this feature is an opt-out workaround for some language change. I don't think the RFC should frame that this way.


This annotation has no effect on functions not marked `extern`. It has no
observable effect unless the marked function `panic`s (e.g. it has no
observable effect when a function returns normally or enters an infinite loop).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the attribute does have an effect from the point-of-view of how the generated code can be optimized, even if the marked function never panic!s in practice.

@gnzlbg
Copy link
Contributor

gnzlbg commented May 13, 2019 via email

@BatmanAoD
Copy link
Member Author

@gnzlbg Well...I do think that the specific annotation unwind(Rust) is better for Rust->Rust than Rust->X->Rust. But I also think that the more pressing and important issue really is Rust->X->Rust, and that the RFC ought to solve that problem. I believe there was resistance to the unwind(native) annotation in the other thread(s), but that's actually the one I would personally prefer. It would be a bit strange, though, because the meaning of "native" could depend on the toolchain, linker flags, platform, etc.

@Centril Centril added A-panic Panics related proposals & ideas A-machine Proposals relating to Rust's abstract machine. A-ffi FFI related proposals. T-lang Relevant to the language team, which will review and decide on the RFC. A-attributes Proposals relating to attributes labels May 16, 2019
@gnzlbg
Copy link
Contributor

gnzlbg commented May 20, 2019

But I also think that the more pressing and important issue really is Rust->X->Rust, and that the RFC ought to solve that problem.

Do you think that the feature proposed here is a good solution to the Rust->X->Rust problem?

I believe there was resistance to the unwind(native) annotation in the other thread(s), but that's actually the one I would personally prefer.

Something like that is IMO a better solution for Rust->X->Rust than this.

@BatmanAoD
Copy link
Member Author

Do you think that the feature proposed here is a good solution to the Rust->X->Rust problem?

"Good" is ambiguous, but I do think it would be sufficient, at least for now. But #[unwind(native)] would be a better solution, I. believe.

Something like that is IMO a better solution for Rust->X->Rust than this.

It seems to me that #[unwind(native)] would also meet the needs of Rust->Rust; would you agree?

Looking again at the internals post and previous GitHub discussion, I don't actually see specific objections to #[unwind(native)]. Would you be more inclined to support a proposal for #[unwind(native)]? Or perhaps would you prefer to see both #[unwind(Rust)] and #[unwind(native)] as separate RFCs?

@gnzlbg
Copy link
Contributor

gnzlbg commented May 20, 2019

"Good" is ambiguous, but I do think it would be sufficient, at least for now.

Usually (always?) RFCs start by specifying the problem they intend to solve, and then they set out to find the "best" solution to that problem, where by "best" one should understand that the "best" solution often involves many trade-offs and constraints. So while this RFC might start at "good"/"sufficient", for it to be merged it will need to be the "best" solution to the problem it intends to solve, given the constraints.

It seems to me that #[unwind(native)] would also meet the needs of Rust->Rust; would you agree?

#[unwind(native)] would unwind with (probably) whatever the native C++ implementation does, which isn't necessarily a Rust panic. That is, Rust->Rust FFI code that wants to unwind using a "Rust panic", cannot do that with #[unwind(native)].

Would you be more inclined to support a proposal for #[unwind(native)]? Or perhaps would you prefer to see both #[unwind(Rust)] and #[unwind(native)] as separate RFCs?

I think both features solve slightly different problems, but these problems are related. I don't really mind much about whether features to solve these problems are proposed in one or multiple RFCs, but I think that each of these problems should be worth solving on its own, and that each feature that solves them should solve their problem well.

@jan-hudec
Copy link

It seems to me that #[unwind(native)] would also meet the needs of Rust->Rust; would you agree?

#[unwind(native)] would unwind with (probably) whatever the native C++ implementation does, which isn't necessarily a Rust panic. That is, Rust->Rust FFI code that wants to unwind using a "Rust panic", cannot do that with #[unwind(native)].

I think #[unwind(native)] is, actually, the most appropriate solution for Rust->Rust FFI too, because of

  • There are some ideas for changing Rust's ABI for unwinding. I don't have a link handy to the idea, but it does mean that there we have to be careful about imposing constraints on the ABI.

The C++ unwinding is defined for all the target platforms, already supported by LLVM (because it is also a C++ compiler backend) and is not likely to change. Or at least less likely than Rust's per above, and if it does, the libraries will probably become incompatible for more reasons.

An #[unwind(native)] annotation would insert suitable conversions so that a Rust panic would traverse through C++ (but not be caught by anything other than catch(...)) and be decoded back in Rust, and any other exception would be seen by rust as a panic with generic “unexpected panic” message.

In many cases the conversion would be trivial, but it would leave most flexibility on the Rust side while covering the use-cases. At the cost of having to define the conversions as part of the unwinding definition for each platform.

@jan-hudec
Copy link

needs of Rust->Rust

Also in the long term it would be preferable to define a Rust interface for shared libraries that would be restricted to things that can be fully compiled in the library (i.e. mainly no generic parameters except lifetimes), but otherwise use Rust symbol mangling and not need extern "C", so the functions would not become #[nounwind] in the first place.

@BatmanAoD
Copy link
Member Author

The C++ unwinding is defined for all the target platforms, already supported by LLVM (because it is also a C++ compiler backend) and is not likely to change. Or at least less likely than Rust's per above, and if it does, the libraries will probably become incompatible for more reasons.

I agree that this makes #[unwind(native)] preferable for FFI. Another consideration is that theoretically, a shared-object boundary should ideally be language-agnostic (though it obviously cannot be platform-agnostic). A hot-swapping Rust app could in theory become a hybrid Rust/C++ app. (Or, since going from Rust to C++ sounds crazy to me personally, a Rust/FutureLang app.)

I suspect, though, that if the Rust exception ABI does change, the transformation to and from the native exception mechanism could be expensive, and so anyone using #[unwind(native)] might just opt out of the new exception mechanism entirely. This, of course, would make #[unwind(native)] equivalent to #[unwind(Rust)], which is part of why I thought the latter might be a sufficient solution.

@BatmanAoD
Copy link
Member Author

I am now wondering whether it's worth permitting individual function annotations to specify unwinding mechanisms at all, rather than introducing rustc/cargo settings that would specify the unwinding mechanism globally. Then, if/when the default unwinding mechanism changes, users can opt-out (which I suspect will be a necessary feature anyway), and the only per-function annotation we'd introduce would be #[unwind(allow)] for FFI functions. (I thought there was an existing RFC to introduce that annotation, but I can't find it.)

@joshtriplett
Copy link
Member

joshtriplett commented May 29, 2019 via email

@gnzlbg
Copy link
Contributor

gnzlbg commented May 29, 2019

Note that for Rust->Rust FFI translating Rust panics to C++ exceptions and re-translate C++ exceptions back into Rust panics isn't a zero-cost abstraction.

@BatmanAoD
Copy link
Member Author

Normal Rust functions have type extern "Rust" fn() and changing this would be a breaking change.

@gnzlbg As I said above, unwinding is not part of Rust's type system, so the documentation's incorrect implication that extern "Rust" fn() has the same type as fn() is irrelevant.

But just because non-extern Rust functions have the same type in the type system as normal functions does not mean that "Rust functions are also extern functions". extern "Rust" fn is externally linkable (an aspect of the ABI, not of the type system); fn is not.

Unwinding through an extern "Rust" fn is currently undefined behavior that "just happens" to work, and changing such functions to abort is not a tacit acknowledgment that "no Rust functions can unwind". Rather, it is already the case that no externally linkable Rust functions can safely unwind (i.e. without UB).

I don't believe that extern "Rust" functions are currently marked nounwind, but part of the reason the Release team held off on stabilizing the abort logic was because we believed that this code (courtesy of @Centril ) has undefined behavior, even though unsafe is not used:

extern "Rust" fn foo() { panic!("unwind"); }

fn main() { foo(); }

The proposal, of course, would change the behavior of this program; but it would change it from undefined behavior that happens to propagate the panic into main into well defined behavior that aborts the program, with an opt-in annotation to preserve the original behavior in a well-defined way.

@gnzlbg
Copy link
Contributor

gnzlbg commented Aug 15, 2019

@BatmanAoD you are conflating a couple of orthogonal issues.

First, you claim that non-extern Rust functions are not exactly equivalent to extern "Rust" fns. This is incorrect, there is no difference between fn foo() and extern "Rust" fn foo(): they are exactly identical. The reference does not explain this as clearly as it should, there is a PR to the reference addressing that: rust-lang/reference#652

As a consequence, the other claims being made: "Unwinding through an extern "Rust" fn is currently undefined behavior", "Rust functions without an extern qualifier are not externally linkable" (we link fns across crates all the time!), etc. aren't true either.

I don't believe that extern "Rust" functions are currently marked nounwind,

They aren't, as opposed to, e.g., extern "C" functions.

but part of the reason the Release team held off on stabilizing the abort logic was because we believed that this code (courtesy of @Centril ) has undefined behavior, even though unsafe is not used:

Do you have a link to that discussion?


You also make the orthogonal claim that unwinding is not part of the Rust type system, because it is not part of a function call ABI. Do you have an example of a calling convention specification that does not consider unwinding to be part of the calling convention?

@eddyb
Copy link
Member

eddyb commented Aug 15, 2019

In case there are any doubts left, let's take it from the horserustc's mouth:

$ echo 'extern "Rust" fn main() {}' | rustc - --pretty=normal -Z unstable-options
fn main() { }

The parser itself treats fn and extern "Rust" fn identically.
Internally, all functions have an "ABI string", which can be "Rust".

Was the use of the keyword extern to specify an "ABI string" a mistake?
Probably! There is some precedent in C++ also doing it (but there it actually changes the mangling which in Rust you need #[no_mangle] for).


I propose we completely stop using "extern function", as a meaningless term and talk about two different things, differently:

  • the ABI of a function (specified by an unfortunate keyword followed by a string literal)
  • FFI imports (found in blocks prefixed with the same unfortunate keyword)

Yes, you can have Rust-ABI FFI imports. And yes we can probably give them slightly different semantics than regular Rust functions, but they still have to be able to coerce to "Rust"-ABI fn() pointers just like regular Rust functions.

@acfoltzer
Copy link

Sorry that I'm joining this thread late; I wish I'd found it sooner because it's quite relevant to things we're doing in Lucet, but hopefully this is still helpful input.

To provide a bit of context, in Lucet, we compile WebAssembly into native shared objects using Cranelift, and then use a runtime system implemented in Rust to run functions exported from those shared objects. Because those WebAssembly programs can call functions imported from their environment, some of which must call back into WebAssembly, we fairly regularly end up with call chains that look like Rust -> Cranelift -> Rust -> Cranelift -> Rust.

Given the features that need to be implemented in those import functions, we can't just impose the Copy-only stack variable requirement that helps rlua in the C -> Rust -> C longjump case, so we're developing Cranelift to allow System V unwinding across our generated code. This means we're quite invested in stabilizing some feature that would result in emitting Rust extern "C" functions without the nounwind LLVM attribute. Additionally, we need to be able to call extern "C" functions, including function pointers that don't have a standalone declaration (e.g., from dlsym()), without Rust assuming they will not unwind.


So, while I agree that it's essential as a first step, I believe we need just a little bit more than what @joshtriplett described above:

Right now, many people are looking for the simplest possible solution that fixes the undefined behavior currently invoked by unwinding through a function called from outside Rust. That solution just needs to make a function not use the nounwind annotation, nothing more.

In addition, we need to be able to mark unsafe calls to extern "C" functions as calls that may potentially unwind. I'm not sure if Rust currently emits any LLVM that precludes this, but the talk in this thread about unwinds from foreign code being UB makes me unsure.


I have some longer term thoughts as well, with the caveat that I've almost exclusively been thinking about System V ABI platforms. I wonder if we should look to the C/C++ compilers' -fexceptions flag behavior as inspiration, at least for platforms where unwinding is part of the ABI, as it is on System V. This flag is on by default for C++, and off by default for C. The expectation is that if you want C code that can interoperate with exception-throwing C++, you must compile that code with -fexceptions. By analogy, three cases then arise for Rust/FFI projects:

  1. All foreign code is under the control of project, i.e., it can be compiled with -fexceptions.
  2. The foreign code is provided as a binary compiled without exception support, or we can't know how it will eventually be used (staticlib or cdylib).
  3. A mix of 1 and 2; some foreign code supports exceptions, and some does not.

In case 1, it would be nice to have an option to mark all extern "C" Rust functions as unwind(allowed), and all called extern "C" functions as potentially-unwinding. It sounds like the latter part would cause some problems today because the Rust eh_personality function does not distinguish between Rust panics and foreign exceptions, but I don't believe there's a fundamental reason it couldn't—System V unwinding is designed for multi-language compatibility.

In case 2, conversely, it would make sense to do the opposite, causing all panics to abort when they reach the entrypoint of any extern "C" function. Whether or not the Rust program uses panic_unwind or panic_abort is orthogonal, though; one might still want to raise and catch panics entirely within Rust in this scenario.

Finally, case 3 gets us back to per-function attributes, so that we can pick and choose whether unwinding is allowed in each place where it might occur. Though this scenario could also work without a new attribute by allowing unwinding by default, and encouraging the use of catch_panic to call abort() or translate the panic into a C-compatible error value, as is already recommended today.


Where all of these options collide with the choice of panic runtime is if we develop choices other than abort or native unwinding. For a novel unwinding runtime to be compatible with #[unwind(allowed)] extern "C" fns, we will need to be able to translate between runtimes at both the C->Rust and the Rust->C boundaries. This probably needs a lot more thought; I'm not sure it's possible in the general case, and it will be so platform-dependent that the current #![panic_runtime] API will probably have to become a lot more complex.

@gnzlbg
Copy link
Contributor

gnzlbg commented Aug 16, 2019

This might be a crazy idea, but we have three stable ABIs that are guaranteed to be supported on all platforms that Rust supports: "Rust", "C", and "system".

What if we make "system" actually map to the system ABI ? In a sense, it is a bug that it doesn't. That is, if the system ABI supports unwinding, then this ABI would also support it. Rust function definitions using "system" would catch Rust panics and translate them to the system unwinding, and extern declarations with the "system" ABI would automatically catch system unwinding and translate that into a Rust panic.

Iff the Rust panics are implemented using the exact same mechanism as the system unwinding, then this might be fairly low cost or zero cost. If we ever implement a different panicking mechanism for Rust, then some translation might need to happen here that might add overhead, but the code should still work, and improving that is a problem that we'll only need to solve if that ever happens - and that we could solve by offering some way of preserving the old behavior.


EDIT: right now, "system" maps to some other ABI, like "sysv64", but the spec of those ABIs actually does support unwinding, so the fact that in Rust they do not, is also kind of a bug.

@acfoltzer
Copy link

In this model, then, we wouldn't have an attribute, the unwinding behavior would just be dependent on the ABI string. I believe this was mentioned upthread, and I think it's a good idea, but we'll have to add new ABI strings to account for this.

For example, the System V ABI supports unwinding but does not require it, so you can have a perfectly compliant System V ABI binary that nonetheless would have UB if you unwound into it. So we would need "sysv64-nounwind" in addition to "sysv64" (or "sysv64-unwind" depending on which we prefer as a default; I think there's arguments for either).

@CAD97
Copy link

CAD97 commented Aug 16, 2019

@acfoltzer said... [link]

I believe this was mentioned upthread

It was, here's a brief overview

(Apologies, but copy/pasting GitHub comments like this removes markup and there's way to much here to fill it back in)

@jcranmer said... [link]

The property of unwinding to me is a fundamental component of the ABI--not only is there an exception coming via some unwinding side channel, but the mechanics of how that side channel works.

@jcranmer said... [link]

The "right" answer here, I think, is to define that the runtime is part of the ABI of the function. Allowing the function declaration to define what panic runtime it's using would solve the dynamic linking problem safely, at the cost of forcing us to stabilize the details of the ABI.

@jcranmer said... [link]

The property of unwinding to me is a fundamental component of the ABI--not only is there an exception coming via some unwinding side channel, but the mechanics of how that side channel works. This means that it is effectively part of the calling convention, as function pointers also have to know if their target is a nounwind function call or an unwind(Rust) call. I'm not sure encoding such information into an attribute is the right way to do it, but it's also mostly orthogonal to the calling convention that usually goes in the extern "foo" part.

@BatmanAoD said... [link]

...my current thought is: what if we extended the set of extern ABI strings to include multiple space-separated items, where extern " unwind " would mean the same as extern "" but with unwind allowed using the unwind-mechanism?

If that's acceptable, then I would suggest we start with these, which would indicate the "native" exception mechanism would be used for unwinding:

@BatmanAoD said... [link]

So I think it's safer and more consistent to make the default be "never unwind through extern" and explicitly permit unwinding when desired (either with an annotation or a change to the extern "<ABI"> spec, as described above

@BatmanAoD said... [link]

In any case, I'll try to explain my position as comprehensively as I can here.

  • As @jcranmer said above, the #[unwind(allow|deny)] attribute is "mostly
    orthogonal" to the ABI specifier, which tradionally only specifies the
    calling convention. (By "traditionally", I am primarily relying on the
    precedent set by C++; I think this is important not because Rust should take
    any design inspiration from C++ but because the prevalence of C++ largely
    defines how the systems programming community understands the maning of
    extern specifiers.) I believe that this alone would make an implicit
    relationship between them surprising.
  • [More great points not reproduced here, click through!]

Here's my opinion: Exception ABI should be specified separately from calling ABI to avoid an N×M problem for ABI strings. However, it would make sense to have a "two-part" ABI string, e.g. extern "Rust" "unwind(Rust)" fn and extern "C" "unwind(abort)" fn. The exception ABI, if not present, would be implied by the calling ABI's minimum requirement. (As such, sysv64, which doesn't require unwinding, would default to non-unwinding, but could opt in with e.g. unwind(sysv64).)

When defining an extern fn (extern "C" "unwind(abort)" fn foo() { foo_impl() }), the postlude of the generated function would catch Rust-native panics and transform them into the correct format, such as aborting or doing any adjusting to declared ABI. This could even include unwind(unsafe), which emits no such landing pads and is UB to unwind out of.

When declaring an extern fn (extern "C" "unwind(abort)" { fn foo; }), the Rust code calling the symbol would wrap it in the necessary glue to transform any unwind into a Rust-native panic. This is of course a no-op if the unwinding ABI is compatible with unwind(Rust), but that is an unspecified implementation detail of the compiler.

This should probably include linting on but allowing "not guaranteed" combinations such as extern "C" "unwind(Rust)" fn foo() { .. }. These would still be allowed, but in using it you are promising that the combination will work on pain of UB.

The minimal way forward would be to allow specifying the "exception ABI string" in addition to the "calling ABI string" and allow the combinations "Rust" "unwind(Rust)" (regular fn), "Rust" "unwind(abort)", "C" "unwind(abort)", and "C" "unwind(Rust)". All other ABI strings would of course get an appropriate default exception ABI string, but specifying them could be left to a later change.

@acfoltzer
Copy link

@CAD97, thank you so much for collecting the previous context about this idea. I also completely agree about the mechanisms for translating between unwinding formats.

The N×M problem hadn't occurred to me because we're so focused on Linux, but I could see that getting out of hand quickly. If we have separate specifiers for the unwinding side of the ABI, though, I strongly support making the default choice be to use the ABI-specified unwinding mechanism rather than the minimal (abort) choice.

First of all, if Rust is using the ABI unwinding mechanism (which I believe it is, at least on Tier 1 platforms?), calling an extern fn defined in Rust from Rust should not abort by default, whether the ABI is "system" or "Rust":

extern "$ABI" fn foo() {
    panic!()
}
foo();

Second, while it seems at first like the conservative default is to disallow unwinding across FFI boundaries, this is only true for the case where the FFI is calling into Rust, not the other way around. In the Rust -> FFI case, if Rust is compiled expecting an exception that never comes, no harm is done, but if Rust isn't expecting any exceptions but gets one, we're in UB.

Postel's law suggests we might then want to abort by default for FFI -> Rust definitions, but support ABI unwinding for Rust -> FFI declarations. I think this asymmetric behavior would be confusing, and also would require C++ -> Rust -> C++ or Rust -> C/C++ -> Rust applications to use non-default settings to allow panics/exceptions to cross the intermediate foreign code.

Finally, C programs are expected to use -fexceptions when calling into C++ that was compiled with default settings; expecting the same when calling into Rust compiled with default settings seems reasonable to me.


As for how users would specify an unwinding mechanism, let's take the example of a program targeting x86_64-pc-windows-gnu. On that platform there are two exception mechanisms available, SEH and SJLJ. SEH is the one specified by the ABI, is less expensive at runtime than SJLJ, and is compatible with system libraries. So, for the majority of users, SEH is going to be what they want to use, and indeed Rust panics on that platform are implemented using SEH as well.

But suppose someone needs to link against a library compiled with SJLJ. For a C++ program, this would mean downloading the SJLJ version of MinGW and recompiling an SJLJ-specific version of the program. As far as I know, there's no way to specify exception mechanism at a more granular level for C++.

In Rust we have to at least be slightly more precise than this, because "C" is not the only ABI we support. I could imagine two complementary solutions:

  1. Allow specifying the unwinding mechanism for a particular ABI when building the final binary, similarly to how we can specify the panic runtime. Strawman: -C $ABI-unwind=$RUNTIME, e.g., -C C-unwind=SJLJ or -C C-unwind=abort.

  2. Allow specifying the unwind mechanism per-function. I think I prefer an attribute here to an extension of extern in order to be less disruptive to the syntax, particularly since I expect this would be the rarest case.

Having a link-time mechanism like 1 would reduce cases where a library client would need to fork a dependency just to change the unwind mechanism for their particular use case, or where a library author would need to expose an abundance of feature flags.

Solution 2 would be for more specialized cases, and could coexist with the link-wide option. For example in Lucet, we have calls between our runtime library and Cranelift-generated code that would need to be precisely specified as System V with DWARF unwinding, but the the mechanism used by the C API we expose for our users need not be fixed.

@CAD97
Copy link

CAD97 commented Aug 16, 2019

Postel's law [Be liberal in what you accept, and conservative in what you send.] suggests we might then want to abort by default for FFI -> Rust definitions, but support ABI unwinding for Rust -> FFI declarations.

I'd argue that this means we would want to abort panics by default going from Rust to FFI on any ABI that doesn't require unwinding support, and collect any reasonable kind of unwind from FFI and convert it to a Rust panic, or the opposite of what you suggest here.

C programs are expected to use -fexceptions when calling into C++ that was compiled with default settings; expecting the same when calling into Rust compiled with default settings seems reasonable to me.

C++ also requires a lot of things for safety by default that Rust doesn't, because it's a good idea for the default direction of the gun to be downrange, not at your foot.

Allow specifying the unwinding mechanism for a particular ABI when building the final binary, similarly to how we can specify the panic runtime.

Isn't this just the panic runtime?

I think being able to set the unwind implementation used by Rust native panics can be useful, but shifting the default for other ABI strings feels absurdly footgunny. If I write #[no_mangle] extern "ABI" fn foo() { .. } a dylib crate and link it with extern "ABI" { #[no_mangle] fn foo(); } I expect it should work soundly, no matter the compiler settings or compiler versions, for any ABI other than `"Rust".

I think I prefer an attribute here to an extension of extern in order to be less disruptive to the syntax, particularly since I expect this would be the rarest case.

Personally, I'm partial to the argument that the exception ABI is too important to the function to be relegated to "just" an attribute.

library client would need to fork a dependency just to change the unwind mechanism for their particular use case, or where a library author would need to expose an abundance of feature flags.

99.9% of Rust crates are statically linked anyway, so it doesn't matter. I'd (maybe naively?) expect the ABI shuffling involved in calling a statically linked function to be optimized out. I think a dylib having a good reason to support multiple unwinding mechanisms unlikely.

@jcranmer
Copy link

It is undefined behavior if the panic is not caught by a catch_unwind: if a
Rust function is intended to be called from C (with -fexceptions)/C++ code
and let a panic propagate into the C/C++ code, the user must arrange for
there to be a Rust function higher up the stack that will catch the unwind.

I don't really understand this limitation. Certainly I think the stipulation
below that "foreign code cannot do anything except propagate the exception" is
reasonable. But since, as noted above, the semantics of panic typically
correspond to abort, I would expect it to always be well-defined to let a
panic unwind the entire thread and potentially terminate the app.

If there is no handler on the thread, I believe all the unwind implementations in question will turn it into a terminate-the-application, and I don't think there's any issue. I haven't verified all the mechanisms yet to see what happens, though, and so erred on the side of caution. Catching the unwind in FFI code is problematic under the current implementation (for Windows MSVC ABI), and needs to remain undefined behavior.

@acfoltzer
Copy link

I'd argue that this means we would want to abort panics by default going from Rust to FFI on any ABI that doesn't require unwinding support, and collect any reasonable kind of unwind from FFI and convert it to a Rust panic, or the opposite of what you suggest here.

Sorry, this was a notation confusion; I was using Rust -> FFI to mean Rust calling into FFI code, rather than a panic unwinding from Rust into FFI code. We agree exactly on what Postel's Law recommends here.

C++ also requires a lot of things for safety by default that Rust doesn't, because it's a good idea for the default direction of the gun to be downrange, not at your foot.

For the purposes of unwinding, I see the safety obligation being on the caller of the foreign code (modulo contravariance for cases involving callbacks). If we default to allow ABI unwinding, it would be the C compiler that has an unsafe default for calling into Rust, and the obligation of the C side to use the correct flag. Perhaps this is too narrow a view of safety and blame, though?

Allow specifying the unwinding mechanism for a particular ABI when building the final binary, similarly to how we can specify the panic runtime.

Isn't this just the panic runtime?

No, the panic runtime is what determines the behavior of panics within a Rust program. The option I was trying to describe here would determine what would happen to those panics once they reach an FFI boundary, as well as what unwinding mechanism to expect when Rust calls into an FFI function.

I think being able to set the unwind implementation used by Rust native panics can be useful, but shifting the default for other ABI strings feels absurdly footgunny. If I write #[no_mangle] extern "ABI" fn foo() { .. } a dylib crate and link it with extern "ABI" { #[no_mangle] fn foo(); } I expect it should work soundly, no matter the compiler settings or compiler versions, for any ABI other than `"Rust".

99.9% of Rust crates are statically linked anyway, so it doesn't matter.

If we require the extern to specify the unwinding mechanism, even to use the ABI-specified mechanism, libraries will end up in situations where this very much does matter.

// libfoo/src/lib.rs
#[cfg(libfoo_unwind = seh)]
extern "C" "seh" fn foo() { ... }
#[cfg(libfoo_unwind = sjlj)]
extern "C" "sjlj" fn foo() { ... }
#[cfg(libfoo_unwind = sysv-dwarf)]
extern "C" "sysv-dwarf" fn foo() { ... }
...

I'm sympathetic to the argument that compiler versions and flags shouldn't matter for non-Rust ABIs, but this would be a composability nightmare in practice.

@CAD97
Copy link

CAD97 commented Aug 19, 2019

What I don't see is any reason a library would need to expose the same symbol with different unwind mechanisms, and not want to expose multiple at the same time. And it would only result in minimal and very macro-by-example-friendly repetition:

extern "C" "unwind(seh)" fn foo_seh() { foo() }
extern "C" "unwind(sysv-dwarf)" fn foo_sysv() { foo() }
fn foo() { ... }

And don't forget that my draft includes being able to specify what unwinding mechanism "unwind(Rust)" is. If you're going to marshal all/many Rust-native unwinds to a FFI-friendly representation, then it's probably best to spawn them that way to begin with to minimize the inter-language cost. extern "ABI" "unwind(Rust)" would have a specified unwinding ABI if built with the compiler flag to specify the unwinding API.

@acfoltzer
Copy link

What I don't see is any reason a library would need to expose the same symbol with different unwind mechanisms, and not want to expose multiple at the same time.

This is the case that would arise when a library wants to export a potentially-unwinding function across multiple platforms, but that doesn't depend on a specific mechanism. This is a similar motivation as that of the "C" ABI string—I don't care which C ABI is used ("fastcall", "sysv64", "win64", etc), as long as it's the right one for the platform I'm targeting.

I think we should have something similar for "C + unwinding"; I would prefer it just be "C" for the reasons mentioned above, but I recognize there are tradeoffs with that choice.

And don't forget that my draft includes being able to specify what unwinding mechanism "unwind(Rust)" is.

This is what the -C panic_runtime option does currently, though it denotes it by crate rather than by name.

@jcranmer
Copy link

// libfoo/src/lib.rs
#[cfg(libfoo_unwind = seh)]
extern "C" "seh" fn foo() { ... }
#[cfg(libfoo_unwind = sjlj)]
extern "C" "sjlj" fn foo() { ... }
#[cfg(libfoo_unwind = sysv-dwarf)]
extern "C" "sysv-dwarf" fn foo() { ... }
...

That level of granularity for describing unwind is very unwarranted: the unwind in each case should be designated as system or native (with nounwind or none as the other available unwind I guess?). For LLVM, the implementation of invoke and the generation of .gcc_except_table is driven by a module-wide target and global compilation flags, so you can't really mix Itanium-like, SjLj, or SEH-based exception handling within the same compilation unit. You'd have to go through a nounwind external function trampoline anyways.

While there is discussion of potentially other unwind systems, it does seem that the current trend for proposals (say, exceptions-lite) is closer to a syntactic sugar for multiple return value ABIs (or returning something like Rust's Result<T, E>) than one that requires ancillary unwind tables. The interaction of such a third-way unwind system with the legacy systems is still a WIP, and it may be that it's easy to describe it as part of the calling convention ABI than the unwinding ABI.

@jan-hudec
Copy link

it may be that it's easy to describe it as part of the calling convention ABI than the unwinding ABI.

Unwinding ABI is part of the calling convention in any case. The compiler needs to know whether a function can unwind when generating the call to it. The calling convention including whether the function can unwind must also be part of the type, because the information has to be available when calling the function through pointers—that can also enforce that you can't pass a function that can unwind as callback to foreign code that is compiled without support for it.

@BatmanAoD
Copy link
Member Author

@gnzlbg

First, you claim that non-extern Rust functions are not exactly equivalent to extern "Rust" fns. This is incorrect, there is no difference between fn foo() and extern "Rust" fn foo(): they are exactly identical. The reference does not explain this as clearly as it should, there is a PR to the reference addressing that: rust-lang/reference#652

Thank you for explaining this. I had no idea it was the case and am actually quite surprised.

@eddyb Thanks for confirming this and demonstrating that the distinction is lost at parse time.

I propose we completely stop using "extern function", as a meaningless term and talk about two different things, differently:

  • the ABI of a function (specified by an unfortunate keyword followed by a string literal)
  • FFI imports (found in blocks prefixed with the same unfortunate keyword)

Makes sense. I had thought that these were converses of each other; i.e., that extern as a function definition qualifier behaved as a sort of "export", but it appears that's not the case.

Now that I understand this, I think the rule I would ideally have is "by default, any entry point in the global symbol table will not unwind, unless otherwise annotated." I'm sure this would be a breaking change, though, and in any case it wouldn't solve the issue at hand, so for now I'll proceed with the assumption that that's not on the table for this RFC.

@gnzlbg

You also make the orthogonal claim that unwinding is not part of the Rust type system, because it is not part of a function call ABI. Do you have an example of a calling convention specification that does not consider unwinding to be part of the calling convention?

I really wasn't considering the function call ABI to be part of the "type system" as such at all. But I suppose it is, insofar as function pointers must maintain their associated ABI information. I kind of wish there were a way around this; for instance, taking a pointer to a non-Rust ABI function would implicitly and transparently generate a wrapper-function using the Rust ABI, then provide a pointer to that. Again, though, that seems well outside the scope of this RFC.

Do you have a link to that discussion?

I was mistaken. The example I was misremembering used extern "C", not extern "Rust": rust-lang/rust#52652 (comment)

What if we make "system"actually map to the system ABI ? In a sense, it is a bug that it doesn't. That is, if the system ABI supports unwinding, then this ABI would also support it. Rust function definitions using"system"would catch Rust panics and translate them to the system unwinding, and extern declarations with the"system"` ABI would automatically catch system unwinding and translate that into a Rust panic.

I actually like this idea at first blush, but I'm not sure what it implies about the other ABIs. I assume extern "C" would have no unwinding, and extern "Rust" would (of course) have unwinding? Also, how would this interact with panic = abort? Should landing pads be generated automatically for any function that calls an extern "system" import?

@gnzlbg
Copy link
Contributor

gnzlbg commented Aug 25, 2019

@CAD97 I'm not fully convinced yet that we could end up with an NxM problem. That would mean that all "unwinding ABIs" can be used with all "non-unwinding-part of call ABIs". Is this the case?

I think that making the unwinding ABI part of the function is a good idea, whether we use a single string or multiple strings for that, is more like a detail.

One thing that we should think about is how this impact the type system. For example (leaving bikeshedding of the precise syntax and unwinding ABI names apart):

extern "Rust" "unwind(abort)" fn foo() { ... }
extern "Rust" "unwind(allow)" fn bar() { ... }

let a: extern "Rust" "unwind(abort)" fn() = foo; // OK
let b: extern "Rust" "unwind(allow)" fn() = foo; // OK abort->unwind coercion ?
let c: extern "Rust" "unwind(abort)" fn() = bar; // ERROR
let d: extern "Rust" "unwind(allow)" fn() = bar; // OK

In particular, if we were to compile the crate with -C panic=abort, should c should still fail to compile? Violating the contract expressed by the types by changing some implementation detail feels like a bad idea..

@CAD97
Copy link

CAD97 commented Aug 25, 2019

@gnzlbg
The reason I'm cautious of N×M is that "most" unwinding mechanisms are "separate" from the "standard" ABI of how you call a function and get its return value. Something like SEH at least theoretically works no matter how you call the actual functions.

"unwind(abort)" would coerce to *almost any "unwind(ABI)", as "unwind(abort)" fulfills their requirements (*except unwinding ABIs that have some requirement of the callee). "unwind(Rust)" wouldn't coerce to any other unwinding ABI ever, as it could be incompatible. (Of course, manually linking to it and controlling the unwinding behavior with flags would be acceptable but unsafe.)

I think it'd be nicer if we could have some decently thin way to wrap function pointers between ABIs, but that's something we don't have for regular ABIs, and would use the same mechanism for unwinding threading.

bors added a commit to rust-lang/rust that referenced this pull request Aug 26, 2019
Permit unwinding through FFI by default

This repeats #62505 for master (Rust 1.38+), as #58794 is not yet resolved. This is a stopgap until a stable alternative is available, like [RFC 2699](rust-lang/rfcs#2699), as long as progress is being made to that end.

r? @joshtriplett
@BatmanAoD
Copy link
Member Author

I met an ex-LLVM developer who told me that the unwinding mechanism is indeed part of the ABI and that "you don't have to do anything special to be able to propagate an unwind safely though there are things you can do to break it." I'm now wondering if the stated design intent to abort by default for any ABI would in fact be a mistake; the dev I've quoted suggested that it would be preferable to keep the language feature for aborting at an API boundary separate from the ABI specification.

So, in that case, the best path would be to stop inserting nounwind on functions with non-"Rust" ABIs, but permit #[unwind(abort)] on any function. If and when the Rust exception mechanism changes, extern "ABI" (where ABI is not Rust) would imply a translation of Rust panics into the exception type expected of that ABI. We could then say that the non-Rust ABIs all have implementation-defined exception mechanisms that will match those used by any other language targeting those ABIs if those languages are compiled with any exception support in the first place (e.g. GCC would require -fexceptions).

The problem of introducing new, custom ABIs with nonstandard exception specifications could then be left for another RFC. And in the long run, I think it may be worth discussing deprecating the extern fn syntax in the next edition and replacing it with something like abi "ABI" unwind "SEH".

@BatmanAoD BatmanAoD changed the title Unwinding through FFI boundaries Well-defined unwinding through FFI boundaries Aug 29, 2019
@BatmanAoD
Copy link
Member Author

@CAD97 @jan-hudec Here is a simpler RFC that doesn't attempt to tackle as much: #2753

@BatmanAoD
Copy link
Member Author

(@gnzlbg) I'm not fully convinced yet that we could end up with an NxM problem. That would mean that all "unwinding ABIs" can be used with all "non-unwinding-part of call ABIs". Is this the case?

I believe there is an existing proposal for an unwind implementation that would be essentially equivalent to syntax sugar over variadic return types (a sort of "invisible Error", I guess). Clearly such a mechanism would be compatible with any ABI. Similarly, abort is compatible with any ABI, and for most platforms (and indeed in the LLVM IR) there are separate SJLJ and SEH mechanisms that are largely independent of the ABI. So that is at least an "N x 4" proliferation.

With that said, personally, I think that's acceptable; and in fact just because the syntax and the target platforms would support certain combinations does not mean that rustc must necessarily support them.

As I mentioned in #2753, I think the extern keyword for definitions and function-pointer types should be replaced, so my proposal for a syntax for both is:

abi "C" unwind "SEH" fn( ....

The unwind "impl" bit would be optional; without it, fn definitions with an explicit abi would abort on panic. (For consistency with other ABIs and with panic=abort, I would want this to be true even of the "Rust" ABI, but I realize there will probably be disagreement on that point.) fn pointers, however, would simply be treated as nounwind (rather than wrapped with some kind of auto-abort logic). I believe this is the behavior @Centril desires for extern "C".

As an alternative to the unwind positional keyword, we could use a separator character, e.g. "C" + "SEH". I don't think & would work well for this, though, even though it's what I currently favor for #2753.

Other unwind specifications we could support initially:

  • native - equivalent to extern "C+unwind" (or whatever that syntax becomes in WIP: "C panic" ABI specifier #2753); this would always be equivalent to C++ exceptions on the target platform with the given backend switches, etc
  • panic - This would represent Rust's default panic mechanism and would be subject to change without warning on updating rustc
  • abort - only permissible on fn definitions; would explicitly add the auto-abort logic
  • nounwind - only permissible on unsafe fns; equivalent to the current (1.37) extern "C" behavior: no auto-abort logic, but it is undefined behavior if the function unwinds (i.e. for LLVM the function is marked nounwind)

For imports, I think we should keep the extern keyword, since it refers to external linkage, but put ABI specifications with their associated fn declarations:

extern {
    abi "C" unwind "SEH" fn( ...

In particular, if we were to compile the crate with -C panic=abort, should c should still fail to compile? Violating the contract expressed by the types by changing some implementation detail feels like a bad idea..

This, to me, seems like the thorniest question.

For imported extern functions, I think the two most reasonable choices are (1) to declare it UB if an imported function actually unwinds, or (2) to automatically generate wrapper-functions with landing pads that provide the auto-abort behavior of unwind "abort". I think I would prefer the latter, but that would make it a bit strange to forbid assignment of a unwind function to an unwind "abort" function pointer, since the wrapped version would really have an identical ABI. I think it's still reasonable to generate the wrapper but forbid such a conversion, though.

One thing that we should think about is how this impact the type system. [Examples of fn pointer assignments]

For an initial implementation, I don't think differently-annotated functions and function pointers should ever be implicitly convertible to each other; even my "equivalent to extern ..." comments above should not be taken to mean that extern "foo" fn would be convertible to or from abi "foo" fn.

Some conversions do seem reasonable enough to provide in the long run, e.g. unwind "abort" -> unwind "nounwind".

But perhaps it would be preferable to provide something like Derive that could auto-generate wrappers performing arbitrary conversions; then the wrapper functions would (if necessary) be truly separate functions with different ABIs, and no casting from one ABI to another would ever be necessary.

@nikomatsakis
Copy link
Contributor

Dear world,

I'm writing with a quick update. As I mentioned in my update comment, the plan of record here is as follows:

  • create a "C unwind" ABI whose details are largely to be determined to start
    • the important point here is that "C unwind" provides something that existing users can migrate to where panics are "expected", even if the current details are not yet specified
    • this in turn enables us to modify extern "C" so as to abort on unwinding
    • this does not in and of itself enable any new use cases on stable; it simply allows existing, working code to continue working in an unstable state
    • like any feature, this ABI would initially be available only on nightly though it would be expected to be stabilized
  • create a project group ('working group') that can iteratively specify the many details at play here
    • once we feel we've settled enough details to make a "useful" guarantee, that would be either a separate RFC or some sort of FCP
    • these guarantees take the form of specifying details of the "C unwind" ABI for particular platforms, or of specifying use patterns that ought to work
    • you can think of this as a more evolved version of the "eRFC" concept

In fact, we've already created a repository for tracking the progress of this group. Right now that's a personal repository of mine, but it will move to rust-lang once the group is "official". The details there are still a "WIP" but hopefully it gives you some sense of the roadmap and the steps we plan to take. We're also chatting in #wg-ffi-unwind over on Zulip.

For the time being, I'm going to go ahead and close this RFC because this discussion thread has gotten long and is quite distracting. Our intention is to open a fresh RFC that declares the above steps. To be clear, I expect that RFC to move fairly quickly through the process -- I think we've had plenty of discussion on the topic and the tradeoffs involved in different strategies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-attributes Proposals relating to attributes A-ffi FFI related proposals. A-machine Proposals relating to Rust's abstract machine. A-panic Panics related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet