WIP: Stabilize #[unwind]

I copied this comment into the "Reference-level explanation" section of the template, filled in a few other details, and left TODOs everywhere. The main reason for opening the PR as a WIP is to move the discussion to a new thread, since it's not strictly relevant to https://github.com/rust-lang/rust/pull/63909.

Thank you all for pushing this forward! Getting this capability is very important to the safety of our Lucet WebAssembly runtime.
One case where an attribute might not suffice is for function pointers. For example:
fn foo(bar_ptr: usize) {
    unsafe {
        let bar = std::mem::transmute::<usize, extern "C" fn()>(bar_ptr);
        bar();
    }
}

In the case of Lucet, where we are grabbing function pointers from a dynamically-loaded shared object, we need to be able to make sure these are not nounwind as well.

One case where an attribute might not suffice is for function pointers. For example:

In case we go the attribute route, my RFC https://github.com/rust-lang/rfcs/pull/2602 would provide the syntactic support allowing us to write type UnwindCFnPtr = #[unwind(allow)] extern "C" fn(). The semantic meaning would need to be provided through a different RFC but it is at least syntactically feasible.

One case where an attribute might not suffice is for function pointers. For example:

In case we go the attribute route, my RFC #2602 would provide the syntactic support allowing us to write type UnwindCFnPtr = #[unwind(allow)] extern "C" fn();. The semantic meaning would need to be provided through a different RFC but it is at least syntactically feasible.

Eesh. That makes me strongly favor deprecating extern and replacing it with something better (which would include the unwind ABI) in the next edition.

Eesh. That makes me strongly favor deprecating extern and replacing it with something better (which would include the unwind ABI) in the next edition.

Do you feel that #[unwind(allow)] extern "C" fn() is somehow more objectionable as compared to extern "C" unwind "allow" or whatnot? The advantage to this approach is that the syntactic support is reusable when we / users need to add their own attributes (even just using #[cfg(...)]).
Personally, I want to limit the additional spec and compiler complexity and still satisfy the common use cases. As such, currently, I'm not positively inclined towards using editions to change things here (well you might not need an edition anyways; it might be backwards compatible to just add something to all editions (e.g. abi "C" fn()) but even so that's a complication).

Let me clarify first that I'm not trying to suggest any breaking changes in this RFC! I just think the extern keyword is confusing enough that it needs to be revisited and hopefully deprecated eventually.

Do you feel that #[unwind(allow)] extern "C" fn() is somehow more objectionable as compared to extern "C" unwind "allow" or whatnot? The advantage to this approach is that the syntactic support is reusable when we / users need to add their own attributes (even just using #[cfg(...)]).

My reaction is mostly "wow, how is that annotation becoming part of the type system?" Whereas with an unwind keyword, it would be clearer that indeed unwind "X" fn is a different type from unwind "Y" fn. So, yes, I do find it more objectionable, though I don't have a strong argument for why annotations should rarely (if ever) be part of the type system (except as a form of sugar or code-generation, where a long-form way of expressing the concept also exists). I just personally find it surprising.

Personally, I want to limit the additional spec and compiler complexity and still satisfy the common use cases. As such, currently, I'm not positively inclined towards using editions to change things here (well you might not need an edition anyways; it might be backwards compatible to just add something to all editions (e.g. abi "C" fn()) but even so that's a complication).

The only change I'm suggesting for a new edition are to introduce abi and/or unwind as keywords and deprecate extern. I would expect the two ways of declaring ABIs to be otherwise identical.

My reaction is mostly "wow, how is that annotation becoming part of the type system?" Whereas with an unwind keyword, it would be clearer that indeed unwind "X" fn is a different type from unwind "Y" fn. So, yes, I do find it more objectionable, though I don't have a strong argument for why annotations should rarely (if ever) be part of the type system (except as a form of sugar or code-generation, where a long-form way of expressing the concept also exists). I just personally find it surprising.

If you find #[unwind(...)] on a function pointer objectionable then we should likewise make a change in this RFC. We either have #[unwind(...)] extern "abi" fn foo() and #[unwind(...)] extern "abi" fn() or we have  extern "abi" unwind "..." fn foo() and extern "abi" unwind "..." fn() but not a mix.
I'd like to note that we do have type-system-affecting attributes already, e.g. repr(packed). More broadly, we have plenty of semantic attributes (more reprs, ..) Of course, as attributes are not allowed on type expressions there's no current expectation that they would affect types, but I don't see why that cannot change. The upside is that attributes scale better syntactically for adhoc concepts.

we do have type-system-affecting attributes already, e.g. repr(packed)

I knew this would be the response to my comment about not liking attributes affecting the type system. 😞 But I'm not sure I understand how repr is actually involved in the type system; it doesn't affect anything like type inference or type-checking, does it?
#[unwind(allowed)] will affect the type of function pointers, because it won't be possible to assign a function pointer with the annotation to one with the abort annotation.

it doesn't affect anything like type inference or type-checking, does it?

It does.
#![forbid(safe_packed_borrows)]
// ^-- until the soundness hole goes away and this becomes on by default.

#[repr(packed)]
struct Foo {
    bar: u8,
    baz: u16,
}

fn stuff() {
    let packed = Foo { bar: 0, baz: 0 };
    &packed.baz;
    //~^ ERROR borrow of packed field is unsafe and requires unsafe function or block
}

That...also surprises me. How can packed structs be used safely?

Note that while extern "ABI" unwind "ABI" would require parser changes, it would not require an edition change to make unwind` a keyword; a positional keyword works fine here.
The position in the non_exhaustive is also towards attributes not effecting "real code semantics"; that's why it's up for getting dedicated syntax rather than the currently implemented attribute.
Longer term, I think a syntax that specifies call/unwind ABI inline and not in an attribute is desirable. Short term, however, we really want to minimally have #[unwind(allowed)] to "just" control the emition of LLVM nounwind. It can then potentially be reimplemented as a (deprecated) proc-macro when dedicated syntax and more detailed semantics are designed and implemented.

For function pointers, another simpler alternative is to just not add nounwind when calling a function pointer. If that turns out to be a performance issue, we can still talk about richer function types.

@acfoltzer,

One case where an attribute might not suffice is for function pointers. [...]
In the case of Lucet, where we are grabbing function pointers from a dynamically-loaded shared object, we need to be able to make sure these are not nounwind as well.

As I understand it, the point of this RFC is to only affect the function generation. So function pointers shall all be considered unwind and no nounwind function pointer shall be introduced. It can be done at later time, preferably by refining the ABI definitions.
This is conservative approach that can be implemented quickly. Discussion about adding the nounwind flag to the ABI shall be postponed to #2699 or other later RFC.

The first sentence discusses "imported" extern "ABI" { fn ... declarations, but the rest of the proposed #[unwind] spec doesn't seem to indicate whether #[unwind] would affect such imports.
I think Rust should assume that imported functions might unwind, and in the future we could consider something like a #[nounwind] annotation that, when applied to imported functions, would indicate that it's not necessary to support unwinding when calling such functions.
When Rust is compiled with unwind = abort, I think the safest thing to do would be to generate a shim function to call the extern function; this shim would have landing pads and would abort if an unwind ever actually occurred. However, I think that's also outside the scope of this RFC; for now, calling an extern function that unwinds from panic = abort Rust code would remain undefined behavior.

@Centril Are there currently any circumstances under which we do consider function pointers nounwind? My quick test indicates "no".
If that is indeed the case, then I think this RFC does not have to concern itself with function pointers at all. The existing behavior works fine for the use-case this is supposed to enable. If someone wants function pointers to be more aggressively optimized than they are right now, the burden would be on them to make that work.

The first sentence discusses "imported" extern "ABI" { fn ... declarations, but the rest of the proposed #[unwind] spec doesn't seem to indicate whether #[unwind] would affect such imports.

Right now, the attribute does affect such imports.

I think Rust should assume that imported functions might unwind,

It is my interpretation of @alexcrichton's concern that this would be considered an undesirable performance regression.
I think the rules for whether nounwind is added should be the same for functions defined in Rust, and for FFI imports. Imports with extern "Rust" should by default be assumed to unwind (this does not reflect the current implementation but that is a bug IMO); imports with extern "C" should by default be assumed not to unwind. #[unwind(allowed)] can be used to change that. Having #[unwind(aborts)] on an import makes no sense though (we cannot add the abort-on-panic shim on something we import), it should not be allowed.

When Rust is compiled with unwind = abort, I think the safest thing to do would be to generate a shim function to call the extern function; this shim would have landing pads and would abort if an unwind ever actually occurred. However, I think that's also outside the scope of this RFC; for now, calling an extern function that unwinds from panic = abort Rust code would remain undefined behavior.

Yeah. The RFC should maybe say that though.
The way I view it, panic = abort changes Rust's unwinding ABI (to not supporting unwinding); so linking that code with code that does unwind constitutes "calling a function with the wrong unwinding ABI".

In the current state, I believe that the drawbacks of landing this as is (the RFC can still change) are much larger than the value this currently adds. I finally had some time this weekend and decided to invest it in seeing why none of the proposed approaches would actually work for mozjpeg. AFAICT, they all work, they all require minimal changes, they don't have any performance drawbacks at least for this library, and they don't have many of the drawbacks of this feature (e.g. they would still work correctly even if we were to make a breaking change to the Rust unwinding implementation).
Could the stakeholders of this RFC, that is, those developing libraries that are relying on undefined behavior for this, please point us to the branches or PRs where they have tried to fix the undefined behavior but weren't able to ? For mozjpeg, the issue was that when we told the maintainer to write some code to automatically generate the bindings, apparently, they couldn't figure out what code to write. Some of us write that kind of code regularly, so what might be obvious to us mgiht not necessarily be obvious to those developing these libraries.

So function pointers shall all be considered unwind

Whether a function can or cannot unwind is parts of its signature. The signature of function pointers like extern "C" fn() matches that of functions that do not unwind. I don't think it would be wise to have function signatures in function types work in the opposite way as they work in function definitions / declarations.
@RalfJung

Are there currently any circumstances under which we do consider function pointers nounwind?

Whether a function is unwind or nounwind is part of the function type, and since currently function pointers do not support specifying that they can unwind, we consider them to be nounwind. This allows significant optimizations: https://rust.godbolt.org/z/Wd5LNq

@gnzlbg I will reach out to stakeholders and encourage them to explain why this feature is important to them.
Personally, although I am not myself currently writing an application that would benefit from this feature, I think that systems languages should provide all system functionality; that is, if a feature is well-supported and well-defined by the platform (as unwinding ABIs in fact are), then any true systems language should expose some way to interact with that platform feature. (This, I would say, is the general logic behind providing a way to write assembly code directly in Rust.)
And, for Rust specifically, I do not want anyone to be forced to write C++ when they would prefer to write Rust; for this to be possible, Rust must enable all runtime features enabled by C++.
Regarding your new ffi_wrapper_nounwind crate, I'm a bit confused by it; the comments indicate that it will "catch" unwinds, but per the discussion above, that's not yet well-defined.  (I took a look and was somewhat confused by the FIXME comments stating that the code doesn't yet work.)
@acfoltzer Do you have any feedback on whether it would be acceptable for Lucet to use error codes to avoid cross-language unwinding?

@gnzlbg It's misleading to suggest that the mozjpeg-sys diff is less than 200 LoC, as that excludes the 500 LoC support crate you wrote to support it. Additionally, you've completely skipped over the throwing part of the problem, which is still not solved there. It's "just"

Rewriting mozjpeg-rust's jpeg_error_mgr management in C++
Translating the libjpeg error code into an owned Rust String (in C++)
Throwing a C++ exception (whose definition is owned by ffi_wrapper_nounwind, not mozjpeg-rust)
Somehow allowing the C++ version of ErrorMgr to be used from Rust somewhat sanely
Figuring out how to handle the 14 functions you skipped in your PR

With all due respect, I'd want to see the mozjpeg-rust patch as well as the mozjpeg-sys before saying the solution definitely works. And that's completely skipping over the fact that it requires writing and maintaining C++ for the Rust developers maintaining mozjpeg-rust.
It really starts to sound like the solution for mozjpeg would be to write a small C++ wrapper around the C library to completely hide the jpeg_error_mgr error handling and just use C++ exceptions, then make -sys bindings around that which translate the C++ exceptions to Rust panics. But again, that means writing and maintaining a C++ library to just use the C library in Rust, when "just" throwing a Rust panic in the error handling function could "just" work.

@BatmanAoD Asked me to chime in about the difficulty in rlua with trying to avoid unwinding past rust frames.
The issue with rlua sounds pretty different from the one with mozjpeg-sys: rlua is NOT relying on rust panics unwinding through C frames, rather rlua is calling into C code which longjmps into other C code across rust frames.  Even more than this, the Lua C API is fundamentally designed around this ability: the primary way you interact with Lua's C API is by passing it function pointers which must call other Lua C API functions (most of which may longjmp) and letting other parts of the Lua C API handle those longjmps as errors.  Lua has an entire error system built into it to implement its own form of exceptions on top of setjmp / longjmp through "linked lists" of setjmp structures throughout the C -> Lua -> C -> Lua call stacks, and the whole API assumes that you can participate in this.
Theoretically you could wrap each individual Lua C function in a different version that always called lua_pcall internally to protect the caller from any longjmps, but this is actually very very hard to do because of the higher order nature of the Lua C API.  You're not just calling functions which may longjmp, you're passing callback function pointers to the Lua C API which the Lua C API expects to call and expects them to longjmp, but indirectly.  Often times in order to wrap this API you would either need a way to provide data to those function calls to do a generic wrapping, or you would need to a special wrapper per callback which is effectively just moving a large portion of Rust code into C.  The Lua C API does provide a way to give data to function types via "closures", but this is too high level for such a wrapper, because the code using the wrapper is also expecting to be able to make closures with its own data tied to the callback functions.  I can't find it right now, but there exists at least one effort to make a "longjmp-free" Lua C API and I believe it was forced to rely on TLS to do the wrapping, and I'm not entirely sure that it was ever even successful.
The other side of this problem is that the only mechanism Lua gives you to produce errors is actually to call longjmp-ing functions, you place the Lua value you wish to throw on the stack and call lua_error (longjmp) as the last call in your function, and that is the only mechanism to generate errors that there is.  You run into errors here as well with callback functions, because you are forced to change the stack API of whatever functions you are wrapping and it can be extremely hard to come up with an API that is close to the Lua C API and doesn't require you to again make several different versions of C callbacks for each version of something that used to exist in Rust.
rlua is not the fastest Lua bindings system in existence (it is as fast as I can humanly make it and be memory safe, I simply don't know how you can make it any faster without either being unsafe or incomplete).  Even though it does not try to be maximum performance at the expense of safety, having to wrap each individual API call for Lua is going to be probably a big downside.  I say probably simply because I think it would be days of work for me to even try it, but it is definitely not the way Lua is intended to be used.    Calling lua_pcall is not even close to free, because there is a lot of error handling setup that Lua does, since "bunching together" potential longjmp functions into blocks of function calls is what is expected.
A lot of these issues are from memory trying to write a C wrapper before the Rust 1.24.1 patch, when I assumed that I would be forced to write such a wrapper.  I spent maybe a day and a half trying a bunch of different strategies before giving up, because I had ended up with about half my low level code in C, and I wasn't done and didn't end up succeeding.  It's possible this could be made easier by generating some of this binding code at build time, but it's also possible that the situation is even worse now that rlua is now "gc-safe".  At the time of the 1.24.1 patch, rlua had some hacks inside it to simply abort during gc metamethod errors or OOM errors, but now actually correctly handles errors that arise solely due to memory allocation, so handling the 'm' API calls in addition to just the 'e' API calls as listed here is much harder than what I was attempting before.
Lua's API is probably the "most C" API I've ever used, it is imo very nearly impossible to use from any other environment.  If I were to write something like Lua, I would NOT ever choose this API, I'm not at all trying to advocate that Lua's C API is a good design, but it does exist.  It's very very hard for me to prove that writing a longjmp-free C wrapper for Lua is hard without actually attempting it, and even then it could just be that I wasn't clever enough to find a good one, but I don't currently see a good way of making one and honestly have no interest in spending the time on one right now.
Also, I'm definitely not trying to make your decision for you regarding rust unwinding behavior, I don't really know what is the best course of action honestly.  I'm not currently even using Lua in a project, and if I did it would probably be via my own Lua interpreter project rather than PUC-Rio's Lua VM.  It could be that the costs of such an unwinding feature outweigh the benefits of being able to "seamlessly" interact with an API like Lua's, certainly Lua's API is the worst example I personally know of by a huge margin.  From what I know it does seem to be worth it, but I could also be wrong and I also know that I don't know the whole situation.  However, if it becomes necessary to make these changes in rlua in order to support future rust versions, I'm probably not going to be the one to write it.

@CAD97

@gnzlbg It's misleading to suggest that the mozjpeg-sys diff is less than 200 LoC,

How so? It literally took me less than one hour and 200 LOC of Rust code to do that: https://github.com/kornelski/mozjpeg-sys/pull/13 .

, as that excludes the 500 LoC support crate you wrote to support it.

Those 500 LOC are for a production quality solution for -sys that pass a certain bar, like actually being a correct FFI wrapper.

(I took a look and was somewhat confused by the FIXME comments stating that the code doesn't yet work.)

mozjpeg-sys at least is not a correct FFI wrapper, neither ABI nor API wise. For example, see https://github.com/kornelski/mozjpeg-sys/issues/15 where the Rust wrappers just call C with a completely different type.
Obviously, if your Rust FFI library is incorrect, automatically-generating wrappers won't make it correct. The reason all those FIXMEs exist is because automatically-generating wrappers has a similar effect that rust-bindgen / ctest API/ABI verification: the wrappers in the C side will fail to compile if the types that they use do not match those used by C.

Additionally, you've completely skipped over the throwing part of the problem, which is still not solved there.

I'm really curious now, how do you know that I skipped this part ?

With all due respect, I'd want to see the mozjpeg-rust patch as well

Sure, as mentioned in the mozjpeg-sys issue, this part is trivial: https://github.com/ImageOptim/mozjpeg-rust/compare/master...gnzlbg:fub?expand=1

@kyren thank you for chiming in and given such a detailed explanation. One question, when unwinding through extern "C" was aborting on nightly, did you see any aborts on rlua ?
If you did, then I think that's probably a bug in the Rust implementation. A longjmp deallocates Rust frames without calling destructors, which is very different from unwinding. For example, if you call a function that unwinds, the caller needs landing pads, cleanup code, etc. in case that the function unwinds, to be able to run destructors. If the function longjmps, none of that is necessary, and if the function is marked as nounwind, none of that might be emitted, but that's ok AFAICT.
You can try to adapt the nounwind miscompilation bug here: https://github.com/rust-lang/rust/issues/63943 to use setjmp/longjmp. With setjmp/longjmp that code should never be miscompiled.
TL;DR: rlua extern "C" functions never unwind, so they should be nounwind, so they don't need #[unwind].

A longjmp deallocates Rust frames without calling destructors, which is very different from unwinding.

I believe longjmp uses SEH on windows.

@bjorn3

I believe longjmp uses SEH on windows.

The C++ standard guarantees that this code:
void foo(JmpBuf& e) noexcept { longjmp(e, 1) }

does not call std::terminate (that is, that it does not abort).
When using the MSVC compiler on windows, this code works correctly.
This same code fails with clang, so AFAICT, clang has a bug. If the similar code does not work with Rust, then Rust has a bug as well.

Whether a function is unwind or nounwind is part of the function type, and since currently function pointers do not support specifying that they can unwind, we consider them to be nounwind. This allows significant optimizations: https://rust.godbolt.org/z/Wd5LNq

I don't even see a function pointer in this example?
When I checked, the LLVM we generated for extern "C" fn function pointers did not involve any nounwind. If LLVM can figure out the actual function that gets called and realizes it is nounwind, that's fine. But what we care about for this RFC is that taking a fn ptr to an #[unwind(allow)]-function and calling that should not in any way introduce a nounwind assumption. I don't think you can demonstrate that on godbolt because https://github.com/rust-lang/rust/pull/63909 has not landed, so the attributes are wrong.

@kornelski, I believe you are the primary person maintaining mozjpeg-sys; have you had a chance to look at gnzlbg's PR yet? (I took a look and was somewhat confused by the FIXME comments stating that the code doesn't yet work.)

Also see their recent response at https://github.com/rust-lang/rust/pull/63909#issuecomment-526557917.

The issue with rlua sounds pretty different from the one with mozjpeg-sys: rlua is NOT relying on rust panics unwinding through C frames, rather rlua is calling into C code which longjmps into other C code across rust frames.

 Specifically, if there are pinned objects in those stack frames, you are violating the pinning drop guarantee. This can lead to UB.

@gnzlbg No, @bjorn3 is correct- longjmp uses SEH on windows. SEH is a layer below C++ exceptions and so does not interact with C++ noexcept the way you expect. All you've done here is prevent some locals from being destructed correctly.
See the docs here: https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/longjmp?view=vs-2019

You are probably aware, but just to be sure: in general, and independent of the entire nounwind story, it is unsafe to longjump across Rust frames you do not control. Specifically, if there are pinned objects in those stack frames, you are violating the pinning drop guarantee. This can lead to UB.

That's completely fine, the only thing rlua requires is to be able to write extern "C" functions that have no Drop or Pin types on the stack at all, and have C longjmp over them.  All such functions are inside rlua, it is a (very bad!) bug if Lua is allowed to jump over user's Rust frames.

@russel im not sure where you are going, but my point is that in C++ at
least the SEH thrown by longjmp does not trigger an abort on noexcept. Are
you saying that they should trigger an abort in Rust extern “C”?

When I checked, the LLVM we generated for extern "C" fn function pointer

@RalfJ LLVM function pointers have no attributes but we do apply the
attributes of the function type when performing a function call via a
function pointer.

we do apply the
attributes of the function type when performing a function call via a
function pointer.

Where do we do that?
I tried to test for this the the resulting LLVM IR does not indicate nounwind anywhere:
; playground::call_C
; Function Attrs: nonlazybind uwtable
define void @_ZN10playground6call_C17h236a503b58319d68E(void ()* nocapture nonnull %f) unnamed_addr #0 {
start:
  tail call void %f()
  ret void
}

Where do we do that?

Apparently we don't, I thought we did. Opened: https://github.com/rust-lang/rust/issues/64090

Just so that we are all on the same page about what ABI guarantees we can rely on, I belive that this list of compatibility issues for Clang with MSVC should apply to rustc as well: https://clang.llvm.org/docs/MSVCCompatibility.html

And, for Rust specifically, I do not want anyone to be forced to write C++ when they would prefer to write Rust; for this to be possible, Rust must enable all runtime features enabled by C++.

I'm in this boat right now, our project is https://github.com/wasmerio/wasmer/ and we JIT WebAssembly to native code and run it. Sometimes a fault happens and we need to unwind the Wasm program. Right now, our mostly-Rust runtime has a C++ component hanging off of it which exists to throw exceptions, and the JIT'd code is called by that same C++ component to catch said exceptions. While we do need to pass a bit of information, we don't need exceptions per se, they're just a convenient way to begin unwinding. It would be awfully nice if we could call our generated code directly from rust, and the generated code could call either a library written in Rust to begin the unwinding.

@acfoltzer Do you have any feedback on whether it would be acceptable for Lucet to use error codes to avoid cross-language unwinding?

(Copying what I wrote in reply to a review comment above, as I forgot it wouldn't thread)
Like many engineering challenges, this is less about other solutions not working out than about trying to avoid undesirable tradeoffs. We don't want the overhead of a setjmp every time we enter generated code, nor do we want the extra complexity and bookkeeping of maintaining a stack of jump buffers. We also don't want to sacrifice late binding of WebAssembly imports in order to fit into C wrappers, or to incur special-case complexity to change our compiler to generate the wrappers in the Lucet shared objects.
Yes, depending on implementation-defined behavior for the unwinding mechanism instead comes with its own drawbacks, but these are mitigated by the fact that we are targeting a specific platform and have control over both our Rust compiler and our code generator. The one piece that's out of our control now is that stable Rust insists on emitting these nounwinds.

I think there are two concerns that have two be distinguished here. The first is the external behaviour of extern "ABI" fns w.r.t. unwinding, be they FFI imports, functions defined in Rust, or function pointers. The second is how Rust panics, in functions defined in Rust, relate to the behaviour specified in the first point.
tl;dr My solution is to split the C ABI depending on whether unwinding is allowed. And if unwinding is part of the ABI, then an #[unwind] attribute is useless.

In my opinion, the unwinding concerns are part of the ABI. Indeed, some want the C ABI to forbid unwinding for the optimisation benefits, while others want to interface with functions that unwind but still follow the same calling convention as theC ABI.
I thus think it necessary to split the C ABI depending on whether unwinding is allowed. For instance, keep C as forbidding unwinding while introducing a new C+unwind ABI that allows unwinding through the system's default mechanism. These two ABIs would be distinct; they just happen to be so close that they can sometimes be compatible:

all functions that follow the C ABI are valid as C+unwind functions as well; and
a C+unwind function that happens not to trigger the unwinding mechanism looks like it is following the C ABI.

This is a consequence of the unwinding mechanisms under discussion not requiring the functions to be invoked differently. Contrast that with this proposal for multi-return functions, which would have to be its own distinct ABI because it changes the way functions are called (e.g. pushing multiple return addresses).
This is also why, unlike this comment on the previous RFC 2699, I don't think it is an N×M problem: because not all unwinding mechanisms make sense for all calling conventions, some might be very specific. Having one ABI string per combination allows us to only expose the ones that make sense, at the expense of having to specify them (and stabilise them) one by one.
Now, if unwinding is part of the ABI, what about the Rust one? I think the answer is that its unwinding mechanism is unspecified or implementation-defined except under panic=abort; the latter completely disables unwinding in the Rust ABI.

Assuming the unwinding behaviour is part of the ABI, the #[unwind] attribute becomes rather useless:

either the function's ABI does not support unwinding, and there is no choice but to abort (or risk UB)
or it does support unwinding and then the panic can be translated accordingly.

With this specification, the Rust ABI is not even special: under panic=abort it does not support unwinding thus panics must abort, and under panic=unwind it does support unwinding but the mechanism is unspecified/implementation-defined.

@gThorondorsen I agree that we should probably end up with a solution like this eventually, but it is going to require much more design work to identify what the ABI choices should be on various platforms, encode the subtyping relations between them soundly, and to decide how Rust's unwinding mechanism should or should not interact with foreign unwinding.
In the meantime, stabilizing #[unwind] gives folks like me a chance to get our code working immediately, even if we're depending on quite a bit of implementation-defined behavior. I don't want this to be held back by seeking a better solution, especially because I agree with @CAD97 that #[unwind] has a smooth transition path to a better solution later:

Short term, however, we really want to minimally have #[unwind(allowed)] to "just" control the emition of LLVM nounwind. It can then potentially be reimplemented as a (deprecated) proc-macro when dedicated syntax and more detailed semantics are designed and implemented.

@gThorondorsen Note that I do not intend for this RFC to entirely supersede #2699, which is intended to provider a more complete longer-term solution.

I am starting to think that Rust should get support for ABI-defined C++ unwinding, but that it should be distinct from panics. Panics were never intended as an exception mechanism, but as a nicer alternative to raise(SIGABRT), and Rust libraries are supposed to still do the right thing when compiled with panic=abort.
In the light of proposals like p1095 (mentioned in #2699), I am starting to think that mechanism should be extern "C++" fn, which always returns something like Result<T, libcpp::exception>.

@kyren, setjmp/longjmp is a different kettle of fish, but note that if PUC-Rio Lua is compiled with a C++ compiler, it uses C++ exceptions instead. This is required for most uses from C++.

@jan-hudec

I am starting to think that Rust should get support for ABI-defined C++ unwinding, but that it should be distinct from panics. Panics were never intended as an exception mechanism, but as a nicer alternative to raise(SIGABRT), and Rust libraries are supposed to still do the right thing when compiled with panic=abort.

This is widely out of scope for the current RFC.
People wanting to solve "all the problems" is exactly what basically killed https://github.com/rust-lang/rfcs/pull/2699. So, please, don't do the same to this RFC. It very deliberately has an xtremely narrow scope.

@kornelski @jan-hudec @acfoltzer Please feel free to fill in some of the "TODOs" (though do not remove the "TODO" itself unless you consider that section "done" after your changes).
(I don't know if you have edit access to the branch, but you can propose PRs against it just as you would against master, and I'll merge them.)

Proposal for handling function pointers: let's specify, in the RFC text, that "we currently assume that calls through extern "C" function pointers may unwind". And then, if we in the future find that tagging extern "C" function as nounwind would open up substantial optimization possibilities, we could implement and specify a mechanism for distinguishing between unwind and nounwind for extern "C" functions. That should be a separate RFC, not this one.
Any objection to adding a sentence to the RFC stating the assumption that extern "C" function pointers may unwind?

@RalfJung, I didn't mean to say to do that now, I was basically saying that

I don't see the clear path to the proper solution and
that the approach of abusing panics for unwinding is rather unfortunate for other reasons.

However, I now realize that a proc macro can of course generate whatever shims will be appropriate for the final solution to provide backward compatibility, so that's OK.
Now the remaining question is, are any of the problems solved much better by this than the shims generated with ffi_wrapper_nounwind?

Now the remaining question is, are any of the problems solved much better by this than the shims generated with ffi_wrapper_nounwind?

For several reasons, unwinding is a better solution for Lucet.

@joshtriplett

Any objection to adding a sentence to the RFC stating the assumption that extern "C" function pointers may unwind?

The conservative assumption is not only to assume that called function pointers may unwind, but also to forbid to pass functions that may unwind as extern "C" callbacks. For instance, consider qsort in the C standard library. What happens if the library has not been compiled with unwinding support but I provide a comparison callback that unwinds? Is that UB?
That's why I advocated for an ABI split instead: it automatically takes care of everything, including variance for function pointers, and is as easy to use. Compare
extern {
    #[unwind(allowed)]
    fn foo();
}

#[unwind(allowed)]
extern fn bar() {
    foo();
}

to
extern "C+unwind" {
    fn foo();
}

extern "C+unwind" fn bar() {
    foo();
}

I understand the attribute is already implemented and easily stabilised, whereas a new ABI would need some implementation work first. But they should be very similar otherwise, unless I am missing something.
@acfoltzer

@gThorondorsen I agree that we should probably end up with a solution like this eventually, but it is going to require much more design work to identify what the ABI choices should be on various platforms, encode the subtyping relations between them soundly, and to decide how Rust's unwinding mechanism should or should not interact with foreign unwinding.

For a first implementation, what I had in mind was to make the new ABI do nothing more than the attribute currently does. Subtyping (or coercions) and interactions between the various mechanisms would just be future possibilities.

What happens if the library has not been compiled with unwinding support but I provide a comparison callback that unwinds? Is that UB?

Yes, but it's fundamentally no different from any other way of unwinding into stack frames that don't support it. (Though your example is pretty strange.)
Your proposal would actually complicate the type system, because it would prohibit forming a pointer to #[unwind]  functions. But I believe it would be out of the question for this proposal even if it were simple, because such callbacks are the only way to cause precompiled C libraries to invoke Rust code that would trigger an unwind.

For a first implementation, what I had in mind was to make the new ABI do nothing more than the attribute currently does.

Any new ABI spec is an entirely new type. If function pointers are assumed to potentially unwind, no new type is needed.
I appreciate your interest in trying to make this feature more future-proof, but I think discussion of enhancements to the ABI syntax and its interaction with the type system should be moved to #2699.

Any objection to adding a sentence to the RFC stating the assumption that extern "C" function pointers may unwind?

This behavior has at least the following drawbacks:

It would be the opposite behavior of that of extern "C" function declarations and extern "C" function definitions . We'll need to teach this inconsistency in behavior, and anyone reading/writing/modifying unsafe code that calls an extern "C" function would need to be aware of it. For example, when calling an extern "C" function (declaration/definition), Drop guards are not required because the function call cannot panic. The naive refactoring of just replacing a function declaration with a function pointer would break this invariant which might be required for safety.

It would limit the optimizations that we can apply on extern "C" function pointers for the case in which they don't unwind, which is a very common case. For example, this code size optimization (https://github.com/rust-lang/rust/issues/64090) which both gcc and clang implement, would become unimplementable for all function pointers in use today.

So this incurs a teaching cost, adds mental overhead for people doing FFI calls, and reduces the optimizations that we can apply for the common case.
The only argument I've seen mentioned so far is that this can be implemented quickly. What are the other arguments in favor of doing this?

@acfoltzer

We don't want the overhead of a setjmp every time we enter generated code, nor do we want the extra complexity and bookkeeping of maintaining a stack of jump buffers.

Can you expand on what the generated code does or point us to some examples, and maybe also explain why Lucet needs to generate non-Rust code ? As in, if you were generating Rust code, or code sticking to the Rust ABI (e.g. writing assembly code), you could use extern "Rust" to interface with it, instead of using extern "C" and doing so wouldn't require #[unwind].

So this incurs a teaching cost, adds mental overhead for all users doing FFI calls (not only those doing unwinding ones), and reduces the optimizations that we can apply for the common cases.
The only argument supporting doing this that I've seen mentioned so far is that this can be implemented quickly. What are the other arguments in favor of doing this?

It sounds like treating function pointers as though they can unwind when called is the current behavior of the compiler, so we are already incurring these downsides today. The choice then is to keep the current behavior until a more comprehensive solution arises from #2699, or to introduce a new transitional behavior which would 1. incur teaching cost and 2. prevent this RFC from adequately addressing the issues we are facing in Lucet.

Can you expand on what the generated code does or point us to some examples, and maybe also explain why Lucet needs to generate non-Rust code (EDIT: and in which language the code is in)?

The code is generated from WebAssembly; you can read further details here: https://github.com/fastly/lucet/wiki

As in, if you were generating Rust code, or code sticking to the Rust ABI (e.g. writing assembly code), you could use extern "Rust" to interface with it (EDIT: if Lucet is willing to rely on the unspecified details of the unwinding ABI, maybe it would make sense for it to also rely on the other unspecified details of the Rust ABI), instead of using extern "C" and doing so wouldn't require #[unwind].

Maintaining a compiler that matches the complete, unspecified Rust ABI would require substantially more effort, the results would be much more prone to unexpected breakage, and either way we also call this generated code from C. Conversely, it's relatively straightforward to look at libpanic_unwind to make sure a new Rust release is still using the system unwinder, and to just let the existing C ABI support handle the rest.

[Assuming function pointers can unwind] would be the opposite behavior of that of the default behavior of extern "C" function declarations and extern "C" function definitions

I'm actually still not yet convinced that the default for import-declarations should be nounwind. It makes sense to me that to be maximally conservative, the defaults for import and export should be opposite each other: the one should not expose the potentially-dangerous unwind behavior, and the other should be safe even if the callee does in fact unwind.
But even if we do decide to treat extern "C" as nounwind by default in both cases, I think giving function pointers the opposite default still makes sense. The problem is one of how much information the compiler and the user have about a function:

for functions defined in Rust, it is known at compile time whether or not the function can unwind.
for extern "C" import-blocks, the actual function definition can't be known until link time (at the earliest).
For function pointers, the actual function isn't known until run time (and may vary from invocation to invocation).

The long-term solution, as previously discussed, is to add the unwinding information to the ABI and thus to the type of the function pointer. But such changes to the type system should be reserved for #2699. For now, we need to enable extern "C" function pointers that can unwind to exist; without making the default be "assume function pointers can unwind", this is only possible by changing the type system.

For example, when calling an `extern "C"` function (declaration/definition), `Drop` guards are not required because the function call cannot panic. The naive refactoring of just replacing a function declaration with a function pointer would break this invariant which might be required for safety.

I don't think I understand this example. A "naive refactoring" that replaces a function that can't unwind with one that can does not make sense to me; and if an author is manually relying on the assumption that a function cannot unwind, then it is their responsibility to ensure that this is true after performing any refactoring. If indeed the function pointer still points to a function that cannot unwind, the invariant is not broken (it just can't be checked by the compiler).

It would limit the optimizations that we can apply on extern "C" function pointers for the case in which they don't unwind, which is a very common case.

...which is one motivating reason to continue work on #2699 after this RFC is resolved.

@acfoltzer

It sounds like treating function pointers as though they can unwind when called is the current behavior of the compiler, so we are already incurring these downsides today.

In Rust as specified today: (1) calling an extern "C" function that unwinds is UB, and (2) creating an extern "C" function pointer that does not point to an extern "C" function is also UB.
That is, no single use of extern "C" is allowed to unwind, which is simple to teach, consistent, and allows all optimizations for interfacing with strict C code.
Maybe you can elaborate on how you arrive at this other different conclusion? The Rust language reference is pretty clear about this, but if it isn't, it would be helpful if you could point us to the specific places that gave you a different impression so that these can be clarified.

Maintaining a compiler that matches the complete, unspecified Rust ABI would require substantially more effort,

If you are interfacing via extern "C", you probably are sticking to repr(C) types and such, so unless you want to start interfacing repr(Rust) types, I don't see why this would be necessary.

prevent this RFC from adequately addressing the issues we are facing in Lucet.
The code is generated from WebAssembly; you can read further details here: https://github.com/fastly/lucet/wiki

I went through the wiki and the open PR and still have no idea why Lucet (and Wasmer) require #[unwind] for. Since these are the only two projects that we still have that would benefit from this attribute, can somebody explain in detail what problem are they precisely trying to solve ?
The only thing I could imagine while reading the wiki and PR of Lucet is that they are compiling WASM to native code using Cranelift, and that native code calls Rust code that panics, and they want to unwind through WASM. But since WASM does not support stack unwinding (or at least didn't last time I checked, did that change?), that can't be it, so it must be something else.
A precise explanation of why these projects need the attribute is needed for the motivation anyways.

@BatmanAoD

, I think giving function pointers the opposite default still makes sense. The problem is one of how much information the compiler and the user have about a function:

How do you conclude from those points that giving function pointers the opposite default makes sense? I'm missing the link here.

I'm actually still not yet convinced that the default for import-declarations should be nounwind. It makes sense to me that to be maximally conservative, the defaults for import and export should be opposite each other: the one should not expose the potentially-dangerous unwind behavior, and the other should be safe even if the callee does in fact unwind.

extern "C" was intended to interface with C, which does not unwind, hence the default. Independently of that was a good or bad idea back then, there is a lot of Rust code using extern "C" like that today.

For now, we need to enable extern "C" function pointers that can unwind to exist;

This isn't something that can be done temporarily. If we somehow word that now function pointers can unwind, people will use them with functions that unwind, and changing that will break their code.
Today, the behavior of extern "C" functions and function pointers that unwind is undefined, so the following safe Rust code never invokes undefined behavior:
fn foo(x: extern "C" fn()) {
    if std::panic::catch_unwind(|| x()).is_err() { /* something that invokes UB */ }
}

If we were to allow extern "C" functions to unwind, then the assumptions made by such code are no longer valid, and the safe Rust API becomes unsound.

I don't think I understand this example.

If one has this code:
extern "C" { fn foo(); } // never unwinds

then this code never invokes UB:
fn bar() {
    if std::panic::catch_unwind(|| foo()).is_err() { /* something that invokes UB */ }
}

If one then were to replace the call to foo with a function pointer:
pub static mut FOO: extern "C" fn() = foo;
fn bar() {
    if std::panic::catch_unwind(|| FOO).is_err() { /* something that invokes UB */ }
}

then if FOO is never allowed to unwind, that code is still correct. If FOO is allowed to unwind, then the implementation of bar now has a subtle bug. What is being proposed here is that this refactoring should, instead, have looked something like this:
pub static mut FOO: #[unwind(abort)] extern "C" fn() = foo;
fn bar() {
    if std::panic::catch_unwind(|| FOO).is_err() { /* something that invokes UB */ }
}

and that now the type of FOO will statically reject assigning to it a function pointer that can unwind. That requires users to know that extern "C" never unwinds on function definitions and function declarations, but that it unwinds in function pointers. That's a cost that this feature would impose on all users, not only on the ones that are actually interested in using this feature. Feels like a huge unnecessary footgun to me.

The only thing I could imagine while reading the wiki and PR of Lucet is that they are compiling WASM to native code using Cranelift, and that native code calls Rust code that panics, and they want to unwind through WASM. But since WASM does not support stack unwinding (or at least didn't last time I checked, did that change?), that can't be it, so it must be something else.

While WASM itself doesnt, any embedder can add support for unwinding through thr generated code. WASM just doesnt have support for catching such exceptions/panics/traps.

While WASM itself doesnt, any embedder can add support for unwinding through thr generated code. WASM just doesnt have support for catching such exceptions/panics/traps.

@bjorn3 that makes sense. @acfoltzer is that correct ?

Maybe you can elaborate on how you arrive at this other different conclusion? The Rust language reference is pretty clear about this, but if it isn't, it would be helpful if you could point us to the specific places that gave you a different impression so that these can be clarified.

As far as I can tell, the reference doesn't say anything about the unwinding characteristics of imported functions, but the nomicon states that a panic crossing the FFI is UB:

It’s important to be mindful of panic!s when working with FFI. A panic! across an FFI boundary is undefined behavior.

I don't think this RFC aims to change that, but in terms of the downsides you mentioned there is currently nothing stopping someone from using an extern "C" fn pointer that unwinds (albeit using unsafe), and the nounwind attribute is not emitted for calls, so the optimization opportunities a no-unwinding guarantee would enable are already unused.

I went through the wiki and the open PR and still have no idea why Lucet (and Wasmer) require #[unwind] for. Since these are the only two projects that we still have that would benefit from this attribute, can somebody explain in detail what problem are they precisely trying to solve ?

I updated some of the text in https://github.com/fastly/lucet/pull/254 to describe what our currently-favored approach is, and the problem statement is laid out at the top, though written in more tentative language from when I didn't understand the moving parts as deeply.
Essentially, we have a runtime system with green threads that happen to be used primarily to run code that was compiled from WebAssembly. Just like with normal Rust threads, we want to limit the scope of those panics to just those threads as you can with a JoinHandle. There are a variety of reasons we might need to panic:

A Rust function we export to Wasm panics for mundane reasons like a bad unwrap somewhere in library code
A Rust function we export to Wasm needs to terminate the green thread due to some unrecoverable Wasm error, like a dynamic borrow error for the Wasm heap
A fault occurs while running Wasm code, such as a division by zero

We also support suspending and resuming these threads, and so if a thread is dropped while in a suspended state, we need to unwind any Rust frames that might have been left on its stack.
For all this unwinding to work in the presence of foreign call frames, we therefore need to be able to cross FFI boundaries with our panics. We've extended Cranelift to generate the right unwinding information for the foreign frames, and I learned enough dark magic to write the DWARF expressions to connect the green thread stack with its caller. All that's missing at this point is the ability to define extern "C" functions in Rust that don't emit nounwind.
I should note that it's also possible that we'll use the system unwinder to implement the Wasm exceptions proposal down the road, but that's a more complicated issue.

Now the remaining question is, are any of the problems solved much better by this than the shims generated with ffi_wrapper_nounwind?

For several reasons (edited: linked to wrong comment), unwinding is a better solution for Lucet.

The wrappers do unwind. They convert Rust panics to C++ exceptions and back again. So there is no setjmp overhead, though there is an extra function call.

The wrappers do unwind. They convert Rust panics to C++ exceptions and back again. So there is no setjmp overhead, though there is an extra function call.

You're right, that particular point doesn't apply in our case (setjmp had come up in previous discussions). However the points about late binding and compiler complexity apply to any wrapper generation solution.

@acfoltzer

As far as I can tell, the reference doesn't say anything about the unwinding characteristics of imported functions,

The relevant part of the reference is https://github.com/rust-lang-nursery/reference/blob/master/src/behavior-considered-undefined.md

there is currently nothing stopping someone from using an extern "C" fn pointer that unwinds
and the nounwind attribute is not emitted for calls

You seem to be trying to make the point that because rustc does not perform an optimization today, the behavior of the language must be defined to make that optimization illegal. That's not how it works.
The reference says that:

Calling a function with the wrong call ABI or wrong unwind ABI

is undefined behavior. That holds independently of whether the call happens via a function pointer or not. The extern "C" Rust ABI today is defined to not allow unwinding. Therefore, if you call a function via an extern "C" function pointer that unwinds, you are violating the "wrong unwind ABI" part, and we can optimize that code under the assumption that this does not happen. We already do this for functions and it is IMO a bug that we don't do it for function pointers like clang does (AFAIK the only reason we don't do this optimization for function pointers is because nobody knew that it was possible to appropriately annotate the LLVM-IR of function pointer calls).

However the points about late binding and compiler complexity apply to any wrapper generation solution.

Could you explain why the wrappers would sacrifice the late binding of WebAssembly imports ?
Also, how much compiler complexity are we talking about here ?
In my experience generating the required shims can be done in a couple of hundred lines of code, and that is peanuts compared to the complexity of what's being proposed here.

@jan-hudec

They convert Rust panics to C++ exceptions and back again. So there is no setjmp overhead, though there is an extra function call.

Note that in the platforms that support it, you can compile the wrappers with the lto linker plugin to avoid the extra function call.

The relevant part of the reference is https://github.com/rust-lang-nursery/reference/blob/master/src/behavior-considered-undefined.md

This is a recent addition that I hadn't seen; the stable version does not have that line.

You seem to be trying to make the point that because rustc does not perform an optimization today, the behavior of the language must be defined to make that optimization illegal. That's not how it works.

If the practical concern is, in part, worry about losing optimizations, it's germane to point out that those optimizations are not happening today. I understand that you see that as a bug. What I'm trying to point out is that if it has not bothered anyone up until now, the potential harm of defining the behavior of the language (pending #2699) to exclude those optimizations is limited.

Could you explain why the wrappers would sacrifice the late binding of WebAssembly imports ?

"Sacrifice" was perhaps too strong a word: the point is that generating wrappers becomes substantially more complicated when you don't have a header file to generate them from, or even know what the APIs you're wrapping will be until runtime.

Also, how much compiler complexity are we talking about here ?

I'm not sure how I'm supposed to quantify this without ending up doing exactly the work I'm trying to avoid by getting this RFC through. It would be much more complex than a couple hundred lines of code.

If the practical concern is, in part, worry about losing optimizations, it's germane to point out that those optimizations are not happening today.

Agreed, that would not make sense, which is why that is not the concern.
The concern is the loss of optimization potential, that is, the ability to even evaluate whether these optimizations are worth it and implement them if they are.

the point is that generating wrappers becomes substantially more complicated when you don't have a header file to generate them from, or even know what the APIs you're wrapping will be until runtime.

When a WASM module calls one of these functions, isn't the function API validated ? Otherwise a WASM module could call some extern "C" function expecting, say, an i8, by passing it an i64, and the behavior would be undefined. I'd expect that to cause a WASM module validation error.

I'm not sure how I'm supposed to quantify this without ending up doing exactly the work I'm trying to avoid by getting this RFC through.

The extern "C" Rust ABI today is defined to not allow unwinding. Therefore, if you call a function via an extern "C" function pointer that unwinds, you are violating the "wrong unwind ABI" part...

I think this may actually be conflating imports and exports.
extern "C" Rust functions are not supposed to unwind; panicking out of such a function is what has always been explicitly undefined and what will hopefully soon be defined as an automatic abort. There is good reason for this: 'rustc` cannot guarantee that the caller will be able to propagate the unwind safely, especially if the Rust unwinding mechanism changes in the future.
But calling an extern "C" fn pointer does not imply anything beyond "this function has a C-like ABI". And the goal of this RFC is explicitly to make it possible for such functions to unwind safely, so "a C-like ABI" does not necessarily imply nounwind.
Since Rust is not yet fully specified, it seems disingenuous to me to disregard the current compiler behavior when trying to establish whether or not a new specification would contradict previously defined behavior. The fact that rustc does not currently treat extern "C" fn pointers as nounwind means that, since the Rust and C++ unwind mechanisms are ABI-compatible, there is not in fact any ABI conflict when calling a C++ function that happens to unwind via extern "C". I don't believe this is merely a matter of a missing optimization; I think it's fundamentally different from the converse case of adding nounwind to Rust functions.

The concern is the loss of optimization potential, that is, the ability to even evaluate whether these optimizations are worth it and implement them if they are, by making them unsound beforehand.

This optimization should be easy to provide as part of #2699, presumably as part of the changes to the ABI-specification mechanism. Something like abi "C" nounwind fn could enable the optimization. So I am not convinced that anything in this proposal truly causes a loss of optimization potential.

This is a recent addition that I hadn't seen; the stable version does not have that line.

https://doc.rust-lang.org/reference/items/functions.html#extern-functions from the stable reference says:

As non-Rust calling conventions do not support unwinding, unwinding past the end of an extern function will cause the process to abort. In LLVM, this is implemented by executing an illegal instruction.

I think it's pretty clear that @gnzlbg is right re. function pointers and I firmly reject the notion that saying otherwise would be "affirming". It would be a change to the language specification, no matter what we tell LLVM today.
@gnzlbg also makes a good point in https://github.com/rust-lang/rfcs/pull/2753#issuecomment-528985377 re. backwards compatibility and how safe Rust today may assume that (non-Rust-ABI) function pointers cannot unwind.
I also agree with @gnzlbg re. having different defaults for function pointers and items would be confusing. I'm trying to reduce inconsistencies in the language overall and that seems to go against that grain. I will happily accept some minor type system adjustments around what coercions are allowed and not. In terms of actual compiler complexity, I expect this will be simple to implement and also to define in the spec as it is not particularly novel. Note that to me, the optimization opportunities are secondary to consistency here.
The notion that having function pointers, or items more broadly, default to unwind, rather than nounwind, would somehow be more conservative is also strange to me. At least from an effects POV, I think "fewer base effects" (nounwind in this case) is more conservative than "more base effects". Similarly, I would say that the behavior in e.g. Haskell (all functions are referentially transparent) and e.g., Agda (all functions are total) is more conservative than unrestricted side-effects / turing-completeness.
(Continuing this effects-POV line of reasoning, I would personally be thrilled to see another modifier added to functions with which you could statically prevent panics in safe Rust and also effect polymorphism around that similar to what is being discussed for const fn.)

(Continuing this effects-POV line of reasoning, I would personally be thrilled to see another modifier added to functions with which you could statically prevent panics in safe Rust and also effect polymorphism around that similar to what is being discussed for const fn.)

I think it would be better to branch a discussion about panic as an effect somewhere else; the whole point of the #[unwind] feature is to try to avoid large language changes. I've opened https://github.com/Centril/rfc-effects/issues/11 to track that in case people want to discuss it.

@Centril It looks like I forgot to actually address the points in @gnzlbg's comment you've linked; I'll try to explain why I think we have a different understanding of what the "conservative" specification would be.
In general, I think that the more assumptions the language and the compiler make, with undefined behavior possible wherever those assumptions are invalid, the less conservative they are. And the more properties the language enforces, preventing undefined behavior by catching errors at compile time, the more conservative it is.
I think this is perhaps the link I failed to make clear in my discussion of "less information" and "more information" above. The less information the compiler has, the fewer assumptions it can or should make; the more information it has, the safer it is to make assumptions.

At least from an effects POV, I think "fewer base effects" (nounwind in this case) is more conservative than "more base effects".

If the compiler can enforce a limitation on effects (e.g. the way it does with mut or lack thereof), then yes, fewer effects are more conservative. But in the case of function pointers, the compiler cannot know whether the pointed-to function will attempt to unwind. So stating that function pointers can't unwind is merely assuming that they won't, not, enforcing that they can't.
Let's look at @gnzlbg's example.

If we change the semantics of function pointers to allow them to unwind, then the implementation of bar now has a subtle bug.

In this case, I do not see any "subtle bug"; whether or not the compiler assumes that the function pointer can't unwind, the function it points to will in fact not unwind, so there is no risk of undefined behavior.
If the function were to unwind, then the behavior would be undefined regardless of what the compiler assumes or what the language spec says.

What is being proposed here is that this refactoring should, instead, have looked something like this:
pub static mut FOO: #[unwind(abort)] extern "C" fn() = foo;

In the long run, yes, I think that we need to extend the syntax for function pointer types, though I would prefer a new syntax without extern and without an annotation. But I don't think that's in scope for this RFC; if the default is "assume that function pointers may unwind", it's not necessary.

@BatmanAoD

If the compiler can enforce a limitation on effects

We could trivially forbid assigning a function with the #[unwind] attribute to function pointer, but as @acfoltzer mentioned above, Lucet needs to be able to call functions that #[unwind] via function types.

In general, I think that the more assumptions the language and the compiler make, with undefined behavior possible wherever those assumptions are invalid, the less conservative they are. And the more properties the language enforces, preventing undefined behavior by catching errors at compile time, the more conservative it is.

I see. So instead of debating what the meaning of "conservative" is here, I'll just note that this definition is not useful to my aims as a language designer. What I usually want is more degrees of freedom and UB in more cases typically gives me that (well, declaring things UB can ostensibly also make things permanently so if users can assume it is UB, so what is most helpful is really "this is UB but don't rely on it"). Defaults for function pointer is a good example of giving up degrees of freedom.

But in the case of function pointers, the compiler cannot know whether the pointed-to function will attempt to unwind. So stating that function pointers can't unwind is merely assuming that they won't, not, enforcing that they can't.

As @gnzlbg points out, we can rather easily prevent such coercions and the type system is already equipped to do the reasoning based on "not the same types" necessary for soundness. What the compiler cannot check is unsafe casts, but that's true in lots of other cases and this case is not special.

In this case, I do not see any "subtle bug"; whether or not the compiler assumes that the function pointer can't unwind, the function it points to will in fact not unwind, so there is no risk of undefined behavior.

But it is unsound for a safe function that takes the function pointer as input to make such reasoning about specific inputs.

If the function were to unwind, then the behavior would be undefined regardless of what the compiler assumes or what the language spec says.

Which means that the safe function @gnzlbg encoded would be unsound, but it is in fact not according to our current spec and hence if we change the default it is a breaking change.

But I don't think that's in scope for this RFC; if the default is "assume that function pointers may unwind", it's not necessary.

To me, changing the spec such that function pointers are, by default, assumed to unwind is a widening of the scope, not a narrowing.

We could trivially forbid assigning a function with the #[unwind] attribute to function pointer, but as @acfoltzer mentioned above, Lucet needs to be able to call functions that #[unwind] via function pointers.

Lucet's full use-case will need to be satisfied later in that case.

Right. For that reason alone, our options are limited to either "let the unwind annotation extend the type system" or "assume function pointers can always unwind."

@acfoltzer @BatmanAoD FYI I think we are already optimizing function pointers under the assumption that they cannot unwind on some cases: https://github.com/rust-lang/rust/issues/64090#issuecomment-529090518 =/

Lucet's full use-case will need to be satisfied later in that case.

Without the ability to unwind through function pointers, I don't see what use-cases would be satisfied by this RFC. C libraries such as mozjpeg with callbacks in their API that are designed to be invoked by any language can only invoke Rust callbacks via function pointers.

more degrees of freedom...

If that's your primary aim as a language designer, then I'm afraid we have different language design principle. But this surprises me; one of the goals of Rust's design, it seems, is specifically limiting UB. Your characterization of your language design goals sounds very C-ish or C++-ish. I get that compiler design benefits from more assumptions of UB; but language design should, I believe, be for users first and for the compiler second.

Which means that the safe function @gnzlbg encoded would be unsound, but it is in fact not according to our current spec...

This does not make sense to me. If nothing else, this RFC will introduce a way to make unwinding through extern "C" functions sound. So with that change to the current spec, the function would certainly be unsound.

If that's your primary aim as a language designer, then I'm afraid we have different language design principle. But this surprises me; one of the goals of Rust's design, it seems, is specifically limiting UB. Your characterization of your language design goals sounds very C-ish or C++-ish. I get that compiler design benefits from more assumptions of UB; but language design should, I believe, be for users first and for the compiler second.

A language is no good if you can't write a compiler for it. ;)
On a more serious note, this is not about users vs lang/compiler devs, and I think trying to paint those as opposing camps is not helpful. Compiler devs most of the time want UB because it allows them to write more optimizations (those optimizations are frequently very hard to implement well, so it is not at all like adding UB makes compilers simpler). And users want the performance that those optimizations enable.
So, the actual trade-off here is between "how well can the compiler locally reason about code with the goal of optimizing it" and "how easy is it for the (unsafe-code-writing) user to understand whether their program has UB". Both sides of this trade-off benefit the user, but in different ways, and the hard bit about specifying UB is getting this trade-off right.
In the case at hand, some functions do unwind but knowing that a function does not unwind is indeed helpful for optimizations (and, as is sometimes the case, even for unsafe code authors reasoning about their code -- not coincidental at all, because the compiler and the programmer both do a similar job here, that of locally analyzing code). So it seems fairly clear that we want to have both "kinds of functions" as things in the language. If we want to get the optimization and ease of analysis even when function pointers are involved, the function pointer type has to encode the information whether the function can unwind. Now that this has been brought up, that seems much more natural than making this an attribute. ;)
With all that said, the most simple (most incremental) solution to me indeed seems to be @gThorondorsen's C+unwind ABI. This actually simplifies a bunch of code in the compiler by making "can this function unwind" purely a (mathematical) function of the ABI, not involving attributes or such things. Of course behavior should be consistent for FFI imports, exports, and function pointers.
The main issues first coming to my mind are (a) we are kind-of abusing strings here, but OTOH given the unpredictable amount of ABIs we might want to support, maybe that's okay (reminds me a bit of target feature flags, maybe we can take some ideas from there), and (b) what about panic=abort builds? Seems like calling C+unwind functions could not be supported, or maybe we can fold this into the Rust unwind ABI specification? Maybe it should be C+panic, indicating the only kind of unwinding we allow is panics caused by Rust code, which as a consequence means with panic=abort there cannot be any unwinding?
(I still have to catch up with some of the discussion here, but this comment seemed relevant given the last few posts here.)
@gnzlbg

FYI I think we are already optimizing function pointers under the assumption that they cannot unwind

I don't think so, see https://github.com/rust-lang/rust/issues/64090#issuecomment-529383502.

@gnzlbg

The concern is the loss of optimization potential, that is, the ability to even evaluate whether these optimizations are worth it and implement them if they are, by making them unsound beforehand.

Yes, I am trying to be explicit as possible in saying that I hear and understand this concern. However, that concern must not be viewed in isolation, but as a tradeoff to make against other concerns. The fact that the optimization potential is currently unexploited means that temporarily blocking that potential is not a regression, instead it would be an additional motivator for #2699 as @BatmanAoD says. It's still worth noting as a disadvantage for this RFC, but weighed appropriately.

When a WASM module calls one of these functions, isn't the function API validated ? Otherwise a WASM module could call some extern "C" function expecting, say, an i8, by passing it an i64, and the behavior would be undefined. I'd expect that to cause a WASM module validation error.

Uses of imported functions within a Wasm module are of course typechecked, but these are just signatures written down at the import site. It is up to the embedder to provide a compatible function when the module is instantiated.

This would not show the complexity, because it would be for a single module, with a single exported function and a single imported function, all of which are known at the same time. The complexity emerges from the fact that the runtime system must be compiled potentially before the Wasm modules have even been written, let alone compiled, and from the fact that real programs have call chains that involve more than just the simple case of Rust -> Wasm -> Rust. We must have an interface that can accommodate any number of functions, with any number of types, and with call chains that are limited only by the size of the stack.
Aside from the technical merits of this exercise, though, this approach to debating the merits of a proposal is exhausting, and is making me feel as though my input and expertise is not trusted or valued. I would more readily accept a higher burden of proof if I was arguing that an alternative is impossible, but I'm not. I'm only stating that given the multiple available alternatives, the constraints of our application, and the resource budget our team has to spend, that allowing unwinding is the best solution for us. I understand wanting quantitative data to better understand the sides of an argument, but in the end this is not a quantitative decision—we can't compare lines of code in a demo based on a single project to postponed optimization potential or our aesthetic preferences around language consistency. I suggest we instead note all of these factors as we consider the overall worth of the RFC.

@Centril

https://doc.rust-lang.org/reference/items/functions.html#extern-functions from the stable reference says:

As non-Rust calling conventions do not support unwinding, unwinding past the end of an extern function will cause the process to abort. In LLVM, this is implemented by executing an illegal instruction.

I now agree with this interpretation, but since this is on the Items page, it read to me at first like a discussion of definitions (as opposed to function pointers), particularly since it discusses the LLVM emitted to implement this behavior.

I think it's pretty clear that @gnzlbg is right re. function pointers and I firmly reject the notion that saying otherwise would be "affirming". It would be a change to the language specification, no matter what we tell LLVM today.

If we're agreed in the interpretation that the above quote from the reference was meant to apply to function pointers as well, then I agree. It seems then that the attribute approach is insufficient, as we don't have a syntactic way of attaching them to function pointers.

I will happily accept some minor type system adjustments around what coercions are allowed and not. In terms of actual compiler complexity, I expect this will be simple to implement and also to define in the spec as it is not particularly novel.

Just to be concrete, are you referring to adding a new ABI string like "C+unwind" instead of an annotation? If so, it's good to hear that it doesn't sound too complex to implement; I haven't hacked on the Rust compiler itself too much, so I don't have a good sense of the impact of these changes. In particular, when @joshtriplett and I were discussing this, we were concerned about introducing a new subtyping relationship and having to deal with variance as the types appeared in different contexts.

The notion that having function pointers, or items more broadly, default to unwind, rather than nounwind, would somehow be more conservative is also strange to me.

In the short term, if we have something like a new ABI string that can denote the unwindability of individual function pointers, then I agree with this. In the longer term, if we develop actual compatibility (or just non-interference) with foreign unwinding, I start to favor allowing unwinding by default, particularly when Rust is the caller, but that's more of a discussion for #2699.

(Continuing this effects-POV line of reasoning, I would personally be thrilled to see another modifier added to functions with which you could statically prevent panics in safe Rust and also effect polymorphism around that similar to what is being discussed for const fn.)

:heavy_plus_sign: :100: :tada: I would love to see this!

@RalfJung

(b) what about panic=abort builds? Seems like calling C+unwind functions could not be supported, or maybe we can fold this into the Rust unwind ABI specification? Maybe it should be C+panic, indicating the only kind of unwinding we allow is panics caused by Rust code, which as a consequence means with panic=abort there cannot be any unwinding?

My first-glance expectation for this would be that C+unwind functions would just be treated as C functions (with aborts). Disallowing it seems overly restrictive; the closest comparison I can think of is that we don't reject programs with catch_unwind when panic=abort.

The fact that the optimization potential is currently unexploited means that temporarily blocking that potential is not a regression,

I agree. The proposed solution of recovering this optimization potential by inc-consistently making function pointers opt-out of #[unwind] while making definitions and declarations opt-into #[unwind] has severe drawbacks though.

Aside from the technical merits of this exercise, though, this approach to debating the merits of a proposal is exhausting, and is making me feel as though my input and expertise is not trusted or valued. I would more readily accept a higher burden of proof if I was arguing that an alternative is impossible, but I'm not. I'm only stating that given the multiple available alternatives, the constraints of our application, and the resource budget our team has to spend, that allowing unwinding is the best solution for us.

I understand. The RFC doesn't have a motivation yet. My recommendation here would be for Lucet and Wasmer to explain in the motivation what the precise requirements are.

In particular, when @joshtriplett and I were discussing this, we were concerned about introducing a new subtyping relationship and having to deal with variance as the types appeared in different contexts.

If we make unwinding part of the type I think this kind of needs to be dealt with. E.g. I expect that one can call an extern "C" function that never unwinds from an extern "C+unwind" function pointer type because it is ok for an extern "C+unwind" function not to unwind.
Since we are dealing with function types, you probably can just copy the coercion rules from unsafe fn and (safe) fn, where unsafe fn is extern "C+unwind" and (safe) fn is extern "C".

@RalfJung

(b) what about panic=abort builds? Seems like calling C+unwind functions could not be supported, or maybe we can fold this into the Rust unwind ABI specification? Maybe it should be C+panic, indicating the only kind of unwinding we allow is panics caused by Rust code, which as a consequence means with panic=abort there cannot be any unwinding?

IIUC the RFC only allows throwing Rust panics compiled with the exact same toolchain, so I guess the answer is yes.
Today, for example, libstd is always compiled with panic=unwind. When something in libstd panics, it calls into the panic run-time. When a downstream crate is compiled with panic=abort, the abort run-time is chosen, and that run-time will just abort on a panic. That is, it isn't wrong for libstd to have been compiled with cleanup code, etc. All that code would just not be used if the panic run-time is abort.
This would be the same, if panic=abort, extern "C+unwind" code never unwinds with a Rust panic, because the panic runtime never unwinds.
Lifting the restriction of "only Rust panics" would be very tricky. For example, when we double panic from Rust, the second panic checks if a panic is in flight, prints the String message of the first panic, and aborts. If we were to allow that first panic to be any unwind system, e.g., a C++ exception, our code today would try to print its String, and probably fail, because the exception object has a different layout from a Rust panic (C++ exceptions use virtual functions, have arbitrary payloads, etc.). We'd need a way to tell Rust panics from other unwinding thingies. In other words, panicking and unwinding are not the exact same thing, so using "C+panic" here feels clearer to me. That's an extern "C" declaration that can unwind with a Rust panic.

A quick aside: I saw a few comments in this thread asking about the goals of this RFC, and in particular asking about whether this RFC has value if it doesn't specify a complete solution for the case of calls from Rust through extern function pointers to non-Rust code. (It would, however, still specify enough about function pointers to make forward progress, namely that they don't currently use nounwind.)
I think it's worth pointing out that the value of this RFC isn't just that it specifies #[unwind] as an attribute to not emit nounwind to LLVM. This RFC also provides definition/specification that makes this no longer UB, which has been a major concern for several folks participating in this discussion. That has a great deal of value quite aside from the specification of #[unwind].

Would it be reasonable to scale back this specific RFC further to just about

Allow #[unwind] extern "C"; declarations and definitions with the annotation allow panics to escape to the caller, and those without are expected to not. Calling an #[unwind] extern "C" definition from anything that is not a #[unwind] extern "C" declaration compiled by the same toolchain is explicitly unstable, implementation defined behavior. Function pointers are explicitly not changed by this RFC.

somewhat reasonable? It seems like that is the minimum patch to the language specification to allow non-#[unwind] extern fn to abort on unwinds, plug the soundness hole, but not remove functionality. The implementation defined behavior remains but is opt-in; it's forward compatible (as a proc-macro) with a more in depth solution, and avoids committing us to any one direction, other than to continue looking for a better, more permanent solution.
If that's already the content of the RFC, then I honestly don't see the problem with accepting it as that. The motivation could even be as simple as "we want to plug the long-existing soundness hole of allowing panics from extern "C" fn but people were using it, so we provide an interim solution to allow them to keep relying on this implementation defined behavior."

After a fair bit of discussion with @Centril, I find myself warming up to the idea of extern "C+unwind". In particular, I'm comfortable with that instead of #[unwind] if we pointedly do not define any kind of subtyping relationship, and just define it as a separate ABI from extern "C". (We could optionally allow non-transmute coercion, using as, from extern "C" fn to extern "C+unwind" fn, as long as we don't allow any more complex coercions.)
I don't think this would change the reasoning or justification here, and it would nicely handle function pointers as well.

That's pretty close to the direction #2699 was taking, so I will need to
read through the discussion there again.
On Mon, Sep 9, 2019, 7:03 PM Josh Triplett<EMAIL_ADDRESS>wrote:

After a fair bit of discussion with @Centril https://github.com/Centril,
I find myself warming up to the idea of extern "C+unwind". In particular,
I'm comfortable with that instead of #[unwind] if we pointedly do not
define any kind of subtyping relationship, and just define it as a separate
ABI from extern "C". (We could optionally allow non-transmute coercion,
using as, from extern "C" fn to extern "C+unwind" fn, as long as we don't
allow any more complex coercions.)
I don't think this would change the reasoning or justification here, and
it would nicely handle function pointers as well.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rfcs/pull/2753?email_source=notifications&email_token=AARU4TZA3VFMPRW22BYEI6LQI3W6JA5CNFSM4ISGM2FKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD6JPGYQ#issuecomment-529724258,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AARU4TZVGJ7EZMSLMNRU56DQI3W6JANCNFSM4ISGM2FA
.

@joshtriplett @Centril I am in favor of changing the syntax in this RFC from an annotation to extern "C<sep>unwind", where <sep> is something other than +.
In #2699, there was a bit of discussion about providing some way to specify the unwinding mechanism separately from the rest of the ABI, but there was a concern that this would lead to an unmanageable proliferation ("N x M") of ABI fn types.
extern "C+unwind" is a bit different in that it does not specify the unwinding mechanism, but this seems to be a large part of @gnzlbg's concern above: extern "C+unwind" is merely implementation-defined (if even that), and its ABI semantics are subject to change without warning. I think this is a reasonable compromise for now, with the expectation that we will eventually provide a way to specify the unwinding mechanism of an ABI if Rust ever introduces another mechanism.
The use of + as the separator seems nicely analogous to its use in trait bounds, but I think the similarity between "C+" and "C++" outweighs that benefit, so I would like to further bikeshed the exact ABI string. C +unwind (with one mandatory space) would be an improvement, but an entirely different symbol might be better. & seems to indicate the semantics of "with an extra characteristic" fairly well. At the risk of opening up the same can of worms that plagued the await syntax discussion, though, I really think just about any ASCII punctuation character would be okay. Even a simple space might do: extern "C unwind".

Another open question about extern "C+unwind is what mechanism it would represent if Rust's default panic implementation changes. Based on the name, I would expect "C+unwind" to always match the platform-native unwinding mechanism, i.e., C++ exceptions. But that would commit us to a potentially tricky implementation wrinkle if the panic mechanism changes.
If we want the new ABI string to mean "this is just a Rust panic, however that is implemented", perhaps we should use C+panic.

@BatmanAoD
The unwinding implementation laid out in this Cranelift issue is completely incompatible with any C calling convention that I know of (few, admittedly). If we want the freedom to implement the Rust ABI that way, we cannot have an ABI which is "C calling convention, but with Rust panics". So yes, I think the "C unwind" ABI should mean platform-native unwinding, the same way it means a platform-native calling convention.
As for the implementation complexities that would arise when changing the Rust ABI, I think they are inevitable as long as at least one of the "Rust" -> "C unwind" -> "Rust" or "C unwind" -> "Rust" -> "C unwind" scenario is supported without programmer intervention.

@gnzlbg

We'd need a way to tell Rust panics from other unwinding thingies.

I know less about how it'd work on Windows, but at least with Sys V ABIs this can be straightforwardly done in the eh_personality function. The fact that it currently doesn't would be a bug, except it's currently UB for this situation to arise by a foreign exception reaching Rust's eh_personality. This should be fixed up as part of #2699.
@joshtriplett

In particular, I'm comfortable with that instead of #[unwind] if we pointedly do not define any kind of subtyping relationship, and just define it as a separate ABI from extern "C".

Despite me bringing up subtyping and variance issues earlier, I'm comfortable with this as well for Lucet's immediate needs.

(@gThorondorsen) The unwinding implementation laid out in this Cranelift issue is completely incompatible with any C calling convention that I know of (few, admittedly).

How so? I see there's a mention of having a dynamically-selected return address, which wouldn't match the C ABI, but the last comment on that proposal states that it's unlikely to be an efficient implementation. I don't see any fundamental reason why the proposal wouldn't be compatible with a C ABI, other than that part of the C function signature would be "hidden" from the Rust signature.

(@gThorondorsen) The unwinding implementation laid out in this Cranelift issue is completely incompatible with any C calling convention that I know of (few, admittedly).

How so? I see there's a mention of having a dynamically-selected return address, which wouldn't match the C ABI [...]

That's the part I am talking about. Even though it may not be the most efficient implementation on current hardware, that still is a valid implementation of the Rust ABI.
More generally, my point with that example is to show that unwinding mechanisms are not necessarily separated from the calling convention, they can also be woven together. Therefore, my opinion is that it's better to view the ABI as a whole, not try to mix and match. But maybe I'm too focused on that specific example.

No objection to using "C+panic" instead of "C+unwind". Also no objection to "C panic", to avoid writing "C+" and having any confusion with C++.
@gThorondorsen You're right, that implementation wouldn't necessarily be compatible with C unwinding, and that's OK. A Rust compiler using that unwinding scheme might just reject extern "C panic" fn entirely. But whether it does or not, this language applies:

The Rust unwind mechanism is intentionally not specified here.
Propagating a Rust panic through non-Rust code is unspecified;
implementations that define the behavior may require target-specific options
for the non-Rust code, or this feature may not be supported at all.

So it's entirely possible that that modified panic scheme just doesn't work with this, and that's completely alright.

A Rust compiler using that unwinding scheme might just reject extern "C panic" fn entirely.

How do you imagine this would happen?
We support 3 ABIs on stable Rust: "system", "C", and "Rust". I don't think I ever heard anywhere that it would be possible for a future Rust compiler to reject compiling code that uses any of these stable ABIs.
If you are suggesting that "C panic" would be special, in that it would be ok for future Rust compiler versions to not support it,  then AFAICT this new ABI wouldn't be "stable" but something else, and an RFC proposing it would need to work out what that something else is.

(@gThorondorsen) More generally, my point with that example is to show that unwinding mechanisms are not necessarily separated from the calling convention, they can also be woven together.

No disagreement there; all I was trying to say is that many mechanisms would be compatible with any ABI, so there really would be an extensive "grid" of possible combinations. But since I also don't think we necessarily need to support every possible combination (even in the future), I don't think that needs to be too much of a concern here.

@gnzlbg If we ever supported, say, "stdcall", or "fastcall", which we do need to support at some point, then those would only be supported on targets where those ABIs exist.
And it's absolutely possible for a feature to exist in stable Rust but not exist on all targets.

A Rust compiler using that unwinding scheme might just reject extern "C panic" fn entirely.

I agree with @gnzlbg that this wouldn't be a valid change. So perhaps we should go with extern "C unwind" and, if panic changes, "C unwind" would still refer to the "native C++" unwinding mechanism (modulo the personality function, etc.; i.e. the "classic" panic implementation).

@BatmanAoD Going down that path turns this back into the previous RFC which tries to specify the panic mechanism. This is not a mechanism for changing how Rust panics; this is a mechanism for saying "this function might unwind with a Rust panic".

Sure, and that's ok because those ABIs only work on some targets, but they would work forever on those targets.
This would initially work on all targets, and then it would be broken on all targets if we change the ABI. That would be a completely different situation.

I think I would just prefer a "C+native" ABI that converts Rust panics to the C unwinding mechanism, by using shims when necessary. Right now, with the current implementation, no shims would be necessary. But if the implementation changes, then we could automatically add shims, and if that turns out to be to slow, we could come up with other features to allow controlling the global panic run-time.

@gnzlbg

But if the implementation changes, then we could automatically add shims, and if that turns out to be to slow, we could come up with other features to allow controlling the global panic run-time.

That sounds like a proposal for a different, substantially more complex RFC. The point of this separate RFC was just to specify a mechanism for declaring that a function might unwind with a Rust panic (concretely, not emitting a nounwind attribute on it), specifying enough of the behavior around such unwinding that it can be done without UB, and leaving it implementation-specific how (or if) you can actually unwind across C.

Thanks, @acfoltzer!

That sounds like a proposal for a different, substantially more complex RFC.

I understand.

The point of this separate RFC was just to specify a mechanism for declaring that a function might unwind with a Rust panic (concretely, not emitting a nounwind attribute on it), specifying enough of the behavior around such unwinding that it can be done without UB, and leaving it implementation-specific how (or if) you can actually unwind across C.

As @gThorondorsen mentioned above (paraphrasing): the unspecified Rust panic ABI is incompatible with extern "C". That is, either a function has the C ABI or it unwinds with an unspecified Rust panic, but no function can be guaranteed to support both at the same time.
To make the approach of this RFC work, we'd need to find a solution to this issue.
AFAICT: we can't break the C ABI, because otherwise all code these functions interface with would be broken. We could try to specify the Rust ABI to something that's forever guaranteed to work with extern "C" or we could try to word how Rust will handle the day when "C+panic" breaks (compilation error, aborting if a panic actually happens, ...), but these options all sound more complex than the "C+native" solution to me.

@gnzlbg There's another option: we could continue to think (in parallel) about how we might handle a future Rust ABI that doesn't panic in a C-with-exceptions-compatible way, but not specify that mechanism, and require that any future implementation of a new Rust ABI will have to specify how it handles that. Because that may well depend on the nature of that new Rust ABI, as well as whether that new toolchain might define an appropriate C ABI for interfacing to such exceptions. We can't necessarily predict the needs of a future toolchain and future ABI.
(Also worth pointing out: some targets may not even fully support C, such as future targets that define std without linking to libc, using syscalls directly. Consider the proposals for x86_64-pc-windows-rust, which would talk to winapi directly with no C runtime.)
If we specify that "C with native Rust panics" (whatever that gets called) must always work on every possible target, then we're predicting and nailing down exactly how some future implementer of a new Rust ABI (whose requirements we don't yet know) must implement this. I don't think we have enough information to do that.
The goal here is to allow people currently invoking UB to stop using UB. That doesn't mean they can't work with implementation-defined behavior, such as "you have to remember to pass such-and-such option to the C compiler". C ABIs that don't define an unwind mechanism or include the appropriate stack-frame-unwinding information are already not compatible with extern "C panic" fn, for instance.

There's another option: we could continue to think (in parallel) about how we might handle a future Rust ABI that doesn't panic in a C-with-exceptions-compatible way, but not specify that mechanism, and require that any future implementation of a new Rust ABI will have to specify how it handles that. Because that may well depend on the nature of that new Rust ABI, as well as whether that new toolchain might define an appropriate C ABI for interfacing to such exceptions. We can't necessarily predict the needs of a future toolchain and future ABI.

If we specify that "C with native Rust panics" (whatever that gets called) must always work on every possible target, then we're predicting and nailing down exactly how some future implementer of a new Rust ABI (whose requirements we don't yet know) must implement this.

That's the option I mentioned when I said:

We could try to specify the Rust ABI to something that's forever guaranteed to work with extern "C"

which is essentially changing the Rust panic ABI from "unspecified" to "somewhat specified". We could do that, but that would be a big chunk of the RFC.

Also worth pointing out: some targets may not even fully support C,

Since Rust is specified as a function of the target C ABI (usize/isize/bool, repr(C), extern "C", etc.) we need a C ABI for all targets that Rust will ever support. If a target does not have a C toolchain at all, we'd need to invent a C ABI for it to be able to support it.

such as future targets that define std without linking to libc, using syscalls directly. Consider the proposals for x86_64-pc-windows-rust, which would talk to winapi directly with no C runtime.)

Those proposals are about building a standard library that does not link against a C standard library, or against a C standard library written in C. That's a long shot from C not being supported on the target.

The goal here is to allow people currently invoking UB to stop using UB.

IIUC the goal of this particular RFC is to allow them to stop invoking UB temporarily as opposed to forever. I don't think that's a good idea, but this would mean that, from the options that we have, we could just write down that if the Rust panic ABI ever becomes incompatible with the C ABI all the ABI "C+panic" / #[unwind] would abort, and that would be fine I guess.

IIUC the goal of this particular RFC is to allow them to stop invoking UB temporarily as opposed to forever.

I'm not sure I understand this; certainly the RFC isn't proposing that extern "C panic" would become UB in some future Rust version. I've mentioned above that I'd personally like to deprecate extern fn in the far future, but that's independent of this RFC.

Since Rust is specified as a function of the target C ABI (usize/isize/bool, repr(C), extern "C", etc.) we need a C ABI for all targets that Rust will ever support.

AFAIK that is not correct. usize, isize, and bool are all defined independently of the C language (and I believe Rust's bool is not ABI-compatible with C's bool, since (bool)2 == (bool)1 is well-defined in C). I don't know if there's anything that would prevent Rust from targeting a platform where extern "C" does not make sense.

...we're predicting and nailing down exactly how some future implementer of a new Rust ABI (whose requirements we don't yet know) must implement this.

We could do that, but that would be a big chunk of the RFC.

I believe @joshtriplett's point was that such "nailing down" would not be desirable for this RFC.

It seems like the biggest currently-unresolved question is "can Rust specify language features that may not be available on future platforms or with future compilation options?" Or, more generally, "how future-proof must Rust's specification be?"
I don't know how to answer this question (though I know which way I personally lean on it). Should the core team or language team be asked to make an "official" decision on this?

@gnzlbg

We could do that, but that would be a big chunk of the RFC.

I think we may have talked past each other here. I was saying I didn't think we should do that. I don't think we should nail down the panic ABI, or add the constraint that we must always support C interoperability with panics.

That's a long shot from C not being supported on the target.

I didn't say "targets that can't run C code at all". I'm talking about targets that might not support combining C and Rust, or at least not without a great deal of care. (Also see @BatmanAoD's response regarding the definitions and sizes of types.)

IIUC the goal of this particular RFC is to allow them to stop invoking UB temporarily as opposed to forever.

I don't know where you got that idea from, but no. The goal is to specify a non-UB way for Rust code to panic-unwind through C to more Rust code on the other side, on platforms/targets where that makes sense. There's no "temporarily" there.

It seems like the biggest currently-unresolved question is "can Rust specify language features that may not be available on future platforms or with future compilation options?"

Rust already does have such features, today. We have ABIs today that aren't supported on all targets. (And in fact, oddly enough, we don't currently give an error for using an ABI your platform can't support; see https://github.com/rust-lang/rust/issues/57182 for an issue to fix that.) We have linker-related attributes that don't work on all platforms (such as link_section or link_ordinal). We have WASM-specific options. We have the windows_subsystem attribute.
I would suggest that we specify extern "C panic" in the reference under the heading "There are also some platform-specific ABI strings:", not under the heading "There are three ABI strings which are cross-platform, and which all compilers are guaranteed to support:".

I don't know where you got that idea from

This RFC proposes to let Rust panics escape extern "C" functions as Rust panics. If we change the panic implementation to something that C does not support, then all code using #[unwind(allows)]/"C+panic" stops working correctly, at least for the intent discussed here of unwinding through external code. According to this RFC, that's an ok outcome, and according to you, we don't want to change that here.
I think that a solution that is allowed to stop working at any time is a temporary solution. If it weren't temporary, it would always work. Other RFCs, like https://github.com/rust-lang/rfcs/pull/2699, propose solutions that are not temporary.
You seem to disagree, so maybe you could explain how a solution that is allowed to break at any time by design isn't temporary ?

I didn't say "targets that can't run C code at all". I'm talking about targets that might not support combining C and Rust, or at least not without a great deal of care.

Do you have a link to a discussion about such a use case ? I've seen many discussions about this, and some implementations, but never heard of such a use case and I can't imagine why interfacing with C would require any particular care there.

(Also see @BatmanAoD's response regarding the definitions and sizes of types.)

What in particular should I see there ? The language and compiler teams FCP'ed here that Rust's bool type is defined to be exactly the same as C _Bool type. The code that @BatmanAoD shows ((bool)1 == (bool)2)) is just casting 2 integers to a boolean, and is equivalent to 1 as bool == 2 as bool in Rust which in both languages is equivalent to true == true.
All I mentioned is properly documented in the reference and summarized, e.g., here https://rust-lang.github.io/unsafe-code-guidelines/layout/scalars.html

@BatmanAoD

It seems like the biggest currently-unresolved question is "can Rust specify language features that may not be available on future platforms or with future compilation options?" Or, more generally, "how future-proof must Rust's specification be?"

I'm not sure what future platforms or compiler options have to do with anything.
According to this RFC and what josh mentions, we can change the Rust panic ABI today to set some register to 1 if a panic happens and to zero otherwise on all currently existing targets. If the C ABI needs to use this same register for the return value of a function on some platform (e.g. on all of them), then "C+panic" would need to choose what to put in that register: the C value, breaking the Rust panic ABI, or the Rust panic information, breaking the C ABI.
Either way, "C+panic" would stop working correctly for all use cases it is intended to work. According to this RFC and the discussion, AFAICT, for this feature, stopping to work would be ok.
The only thing unclear is whether the code using "C+panic" should just start invoking undefined behavior silently, fail to compile, abort at runtime, etc.

@gnzlbg

I think that a solution that is allowed to stop working at any time is a temporary solution. If it weren't temporary, it would always work.

I'm not sure how you got from "if future work wants to use a different panic ABI it'll need to specify how that ABI interacts with this feature" to "this solution is allowed to stop working at any time". We're not going to suddenly throw away the Rust backend that supports C-compatible panics, just because someone comes up with a new backend with a different approach to panics.
Did the definition of panic=abort mean that panics can stop working at any time, so panics are a temporary solution? No, it just means that if you use panics (and especially if you catch them) you might care about whether your code is compiled with panic=abort or not.
This feature still depends on the target, and compiler options. We're trying to eliminate undefined behavior here. That doesn't mean this feature has to work identically on every possible target with every possible set of compiler options. You can use it on targets that support it, and with a C compiler that supports it. The future creation of new targets that don't support it does not mean the feature is broken.
(Also worth noting: if you have a different panic-unwind mechanism, you could potentially still support that if your C compiler supported that same mechanism.)
I think it would suffice if the RFC included a sentence along the lines of "Any future change to the Rust panic unwind ABI will need to specify how it interacts with this feature, such as by making extern "C panic" an unsupported ABI with a compile error, or by defining how the new unwind mechanism interacts with C functions."

Can a change to the current panic=unwind implementation break code using this feature that used to work before the change ?

We're not going to suddenly throw away the Rust backend that supports C-compatible panics, just because someone comes up with a new backend with a different approach to panics.

Sure, we can put it behind -C panic=c-compat, but all code using -C panic=unwind would still break when the default unwinding ABI changes.

Did the definition of panic=abort mean that panics can stop working at any time, so panics are a temporary solution?

I'm talking about changes to the -C panic=unwind ABI that would be OK to do today, but would break a "C+panic" ABI.

I think it would suffice if the RFC included a sentence along the lines of "Any future change to the Rust panic unwind ABI will need to specify how it interacts with this feature, such as by making extern "C panic" an unsupported ABI with a compile error, or by defining how the new unwind mechanism interacts with C functions."

This would mean that a PR changing the panic=unwind ABI could break all code using "C+panic" at any time.

@gnzlbg

Can a change to the current panic=unwind implementation break code using this feature that used to work before the change ?

One would hope we catch that with tests. Or if you mean is it allowed to: not without due consideration.

Sure, we can put it behind -C panic=c-compat, but all code using -C panic=unwind would still break when the default unwinding ABI changes.

Or we could create a new panic=shiny-new-abi that doesn't break panic=unwind. Or we can have a different target that uses the new ABI by default. Or we can find a way for the new ABI to interoperate with C compilers (possibly involving different compiler options). We have quite a few options, and when such an ABI exists we can give those options due consideration, taking compatibility with extern "C panic" into account at that time.

I'm talking about changes to the -C panic=unwind ABI that would be OK to do today, but would break a "C+panic" ABI.

I repeat:
"Any future change to the Rust panic unwind ABI will need to specify how it interacts with this feature, such as by making extern "C panic" an unsupported ABI with a compile error, or by defining how the new unwind mechanism interacts with C functions."

This would mean that a PR changing the panic=unwind ABI could break all code using "C+panic" at any time.

No, it means that a PR (or more to the point an RFC, I should hope) changing the panic=unwind ABI would need to specify how it interacts with this feature, and we'll need to take this feature into account when evaluating such a PR.

Can a change to the current panic=unwind implementation break code using this feature that used to work before the change ?

One would hope we catch that with tests. Or if you mean is it allowed to: not without due consideration.

I mean if it is allowed, and I think this RFC must answer this question with yes or no.

I think this RFC must answer this question with yes or no.

I don't believe it needs to do so, no. I believe it suffices to specify behavior, and specify that decisions on future changes need to take that behavior into account.
That's not punting rationale for this change to a future RFC. That's stating that a future RFC has to provide rationale for making a change to what at that time will be existing behavior. And that rationale will depend on the nature of the change.

Then the answer to whether this RFC solves the problem it is supposed to solve is: maybe.

Also, the answer to whether "Is this feature zero-cost" is also "maybe", because we don't know whether this RFC will restrict the evolution of the unwinding implementation for all Rust code, and not only for code that uses this feature.

Personally, I would not be opposed to adding a requirement that any unwinding mechanism that breaks extern "C panic" be opt-in until the next edition. I believe that would leave a sufficient amount of flexibility in the unwinding specification, especially because it is still not yet clear to me that the unwinding mechanism discussed above could not coexist with extern "C panic".
With that said, I believe @JoshTriplett is correct that such verbiage is not necessary in this RFC. Breaking changes are already not generally permitted within an edition, and if a breaking change is deemed necessary in a future RFC, it will occur whether or not this RFC tried to prevent it.

That is, code that does not use this feature might end up paying a price for it.

I do not understand this claim. Nothing in the proposal would forbid compiling with an unwinding mechanism that isn't compatible with extern "C panic".

... we are not committed right now to declare that this feature isn't temporary and future RFCs cannot break it.

Is this ever a requirement for RFCs? Doesn't the prohibition against breaking changes make every RFC essentially permanent?

Is this ever a requirement for RFCs?

This is a requirement for all RFCs, or at least, all that we have merged until now. I do not recall any RFC for which the answer to the question: "Can a future RFC break all code using this feature?" has ever been "maybe" or "yes", it has always been a resounding "No, Rust stability guarantees require us to support this forever.".

I do not understand this claim. Nothing in the proposal would forbid compiling with an unwinding mechanism that isn't compatible with extern "C panic".

Adding an opt-out does not make a feature a zero-cost abstraction. Having to add an opt-out is a consequence of having made the mistake of adding a language feature that was not a zero-cost abstraction.

I think both these issues are red flags. Like is the case for all other RFCs, a new feature should be a zero-cost abstraction and work forever. This design direction appears to me to be fundamentally incompatible with that, but the alternative design direction proposed in https://github.com/rust-lang/rfcs/pull/2699 is just that.

Here is my summary of the issues w.r.t. forward compatibility.

extern "C panic" puts some restrictions on future implementations of panics and the Rust ABI. (At least, one potential design is excluded.)
extern "C unwind" does not put such restrictions, but may add overhead because of the requirement to translate between panics and native unwinding.
However, this translation overhead is implementation-defined and can be made zero by having panics use the native unwinding mechanism. Which, if I am not mistaken, is currently the case.

In this situation, I find it better for the specification to allow as much implementation freedom as possible. After all, it can be made zero-overhead, but it does not have to be zero-overhead in all implementations. Nonetheless, implementation may independently guarantee that they implement the zero-overhead solution.
Maybe some future implementation will have a good reason to make panics use an unwinding mechanism that is not compatible with the native calling convention. With extern "C panic", it just cannot happen. With extern "C unwind", it may happen if the benefits outweigh the translation overhead.

(@gnzlbg) I do not recall any RFC for which the answer to the question: "Can a future RFC break all code using this feature?" has ever been "maybe" or "yes", it has always been a resounding "No, Rust stability guarantees require us to support this forever.".

Okay, maybe Josh and I are not being clear; yes, extern "C panic" will be subject to the same stability guarantees as every other Rust feature. (Since the question I asked in the comment you're responding to was "Doesn't the prohibition against breaking changes make every RFC essentially permanent", I'm not sure why this would be in doubt.)

Adding an opt-out does not make a feature a zero-cost abstraction.

"Zero cost abstraction" is fully compatible with "you only pay for what you use" (in fact, that's arguably a core principle of zero cost abstractions). So I stand by my statement that nothing in the proposal violates the principle that our abstractions should be zero-cost.
I also wouldn't characterize what I said as a suggestion that future RFCs will add an "opt out" for this feature.

Like is the case for all other RFCs, a new feature should be a zero-cost abstraction and work forever. This design direction appears to me to be fundamentally incompatible with that, but the alternative design direction proposed in #2699 is just that.

Which proposal, specifically, in the #2699 discussion are you referring to? Could you please explain why you see that proposal as a zero cost abstraction, but don't see extern "C panic" as one?

(@gThorondorsen) extern "C panic" puts some restrictions on future implementations of panics and the Rust ABI. (At least, one potential design is excluded.)

As long as the alternate implementation is opt-in, and there is no guarantee that "C panic" is one of the "ABI strings which are cross-platform, and which all compilers are guaranteed to support", I don't believe that's true.
(I also don't yet discount the possibility that the design discussed above could be implemented in such a way as to make it possible to still support extern "C panic", but I don't believe it's necessary to guarantee this before stabilizing extern "C panic".)

(@BatmanAoD)

(@gThorondorsen) extern "C panic" puts some restrictions on future implementations of panics and the Rust ABI. (At least, one potential design is excluded.)

As long as the alternate implementation is opt-in, and there is no guarantee that "C panic" is one of the "ABI strings which are cross-platform, and which all compilers are guaranteed to support", I don't believe that's true.

My point is that all implementations of the Rust ABI can support extern "C unwind" (with maybe some overhead for the translation), but not all of them can support extern "C panic". They make no difference for the current implementation, but the former is more forward compatible than the latter: it is supported by more implementations.

Okay, maybe Josh and I are not being clear; yes, extern "C panic" will be subject to the same stability guarantees as every other Rust feature. (Since the question I asked in the comment you're responding to was "Doesn't the prohibition against breaking changes make every RFC essentially permanent", I'm not sure why this would be in doubt.)

I see, this is not what I understood from the thread with @joshtriplett before. It would be good to be on the same page here.

"Zero cost abstraction" is fully compatible with "you only pay for what you use" (in fact, that's arguably a core principle of zero cost abstractions). So I stand by my statement that nothing in the proposal violates the principle that our abstractions should be zero-cost.

It has been pointed out multiple times now that this proposal restricts the degrees-of-freedom of the Rust panic=unwind implementation for all Rust code, independently of whether that Rust code uses it or not. That is, code that does not use this, pays for it.
So can you explain why you think that this is not a violation of the "you only pay for what you use" principle ?

Which proposal, specifically, in the #2699 discussion are you referring to?

The "C+native" ABI proposal.

Could you please explain why you see that proposal as a zero cost abstraction, but don't see extern "C panic" as one?

Only codes that uses "C+native" pays for it. Code that uses it pays zero initially, but at some point this code might pay a conversion from a Rust panic to a native unwind. Code that does not use "C+native" is completely unaffected by it.

On Wed, Sep 11, 2019 at 09:58:01AM -0700, gnzlbg wrote:

Okay, maybe Josh and I are not being clear; yes, extern "C panic" will be subject to the same stability guarantees as every other Rust feature. (Since the question I asked in the comment you're responding to was "Doesn't the prohibition against breaking changes make every RFC essentially permanent", I'm not sure why this would be in doubt.)

I see, this is not what I understood from the thread with @joshtriplett before. It would be good to be on the same page here.

I'm in complete agreement with the summary you're replying to. All of
our stability guarantees remain in place. Any evaluation we do of future
RFCs will, as always, take that into account.

"Zero cost abstraction" is fully compatible with "you only pay for what you use" (in fact, that's arguably a core principle of zero cost abstractions). So I stand by my statement that nothing in the proposal violates the principle that our abstractions should be zero-cost.

It has been pointed out multiple times now that this proposal restricts the degrees-of-freedom of the Rust panic=unwind implementation for all Rust code, independently of whether that Rust code uses it or not. That is, code that does not use this, pays for it.
So can you explain why you think that this is not a violation of the "you only pay for what you use" principle ?

"zero-cost abstraction" is a description of how the language should
behave, not how language-development works. A zero-cost abstraction
still requires maintenance and development work for language
developers. We're going to have to do work to maintain the language,
including existing abstractions. In this case, that may mean that we
need to have a panic conversion mechanism, or to define a new panic
mechanism that people can opt into, or that we need an edition
transition, or any number of other options. Those are all decisions we
can make in the context of a future RFC that interacts with this one.
None of that makes this abstraction a problem for people who don't use
it.

@joshtriplett @BatmanAoD Would you be able to explain the issue that has been raised in your own words ?
Because the proposals that you are making do not make any sense to me.

We discussed this RFC in the Sep 05 meeting
of @rust-lang/lang. The major topic we discussed was the interaction with fn pointers and I'm leaving this post to summarize our conversation. (Note that I've not been following the thread, so I apologize for interjecting.)
In short, function pointer types (e.g., extern "C" fn()) do not carry any information about
whether the target function may unwind or not. Under the current design of the RFC,
a function can be tagged as #[unwind(allowed)] (modulo precise syntax) but this
does not affect the function pointer types it can be coerced to. Therefore, we must assume
that if you invoke a C function indirectly, it may unwind, just as we must assume the same
about Rust functions. (Modulo the use of panic=abort.)
There are pros and cons to the situation.
The pro is that it limits the impact of these attributes on the overall language complex.
If we wish to reflect "unwind status" in the type of a function pointer, that will make
the types of function pointers more complex in some way. Moreover, it means that #[unwind(allowed)]
functions would require special status to manipulate via pointer.
The con is that we lose "peak efficiency", in that we must conservatively prepare for unwindings
that may never occur. (In some cases, we may be able to infer the possible targets for a fn pointer
and avoid such edges, but that could not be guaranteed.)
One possible alternative design would be to reflect the "unwind-allowed" nature of a function
in its ABI. Hence there would be extern "C" and extern "C-unwind", or something similar.
This would have the con that it would create a kind of schism between unwind and not unwinding
functions; it would also raise questions of coercions from one to the other that must be dealt
with. Overall, it feels like it makes the language unambiguously more complicated.
In contrast, the efficiency loss of an unwind edge is minimal. In fact, LLVM may not even have
a way to express the idea of a function call by pointer whose target is no unwind (this was
stated in the meeting, but I did not verify -ed.). In this case, the efficiency loss is actually zero:
all that is lost is possible future gains, were we to adopt a target that could take advantage of this.
The overall sense of the meeting, I believe, was that we would prefer to just leave function types
the way they are and not attempt to reflect the unwind-allowed nature of a function in its
type. In short, the added complexity to the language as a whole is just not worth the incremental
improvement in runtime efficiency.

In contrast, the efficiency loss of an unwind edge is minimal. In fact, LLVM may not even have
a way to express the idea of a function call by pointer whose target is no unwind (this was
stated in the meeting, but I did not verify -ed.).

It does.  You can put nounwind on an LLVM call instruction similarly to other attributes like noreturn, readnone, etc.

This would have the con that it would create a kind of schism between unwind and not unwinding
functions; it would also raise questions of coercions from one to the other that must be dealt
with. Overall, it feels like it makes the language unambiguously more complicated.

From my perspective, treating functions and function pointers differently is its own form of complexity... A rule that "extern "C" is unwind-safe for function pointers but not for functions" is a lot harder to remember than "use extern "C-unwind" if you want to unwind".