Handling C++ exceptions

@John_McCall I hear from @gribozavr that you may have some insights into the implementation aspects of this -- could you comment?

I think we probably already generate exception tables that will cause exceptions to fail if you unwind into a Swift frame. Have you tried this?

I personally think C++ exceptions are a failed experiment and would be perfectly happy if Swift/C++ interop only worked with C++ code built with -fno-exceptions. However, I realize that I'm not the only one that matters here.

If you're interested in a similar problem, you might want to check out the Python interop library. Python has the same issue as C++ - functions in general can throw, and neither Python/C++ have widely-used type system features to designate what can through or not.

The approach taken in Python kit is to make thrown exceptions trap by default, but allow one to handle the explicitly (turning them into Swift errors) if you want to. You can play with this in this online notebook if you're curious.

-Chris

4 Likes

FWIW, this is wrong on at least some targets.

Yeah, it isn't generally safe to unwind through Swift frames, both because they don't necessarily maintain sjlj style exception state, and because unwinding a Swift frame even in the best circumstance will skip over cleanups and possibly leave the program in an inconsistent state. I like the Python interop model that @Chris_Lattner3 noted, where you decide whether you care about C++ exceptions or not at the call site for a C++ declaration, but in either case, we would probably want to ensure any exceptions are contained to the call site, either by capturing the exception into some sort of value Swift can digest if opted into, or at least terminating the program immediately before there's a chance of unwinding into Swift frames if the code does not expect to handle exceptions.

2 Likes

Just throwing an idea out there: do the same as for pointers with unknown nullability.

func maybeThisCanThrow() throws!

This is an implicitly trapping throwing function: exceptions thrown from that function will trap unless the caller makes the call within a try expression:

try maybeThisCanThrow() // now you handle the exception
maybeThisCanThrow() // implicitly prefixed with `try!`

The compiler won't emit an error if you fail to use try, it'll just trap as if you had written try!. This is similar to implicitly unwrapped nil pointers.

Note that this behavior can be added later. I don't think there's a conflict with what is proposed here. Implicitly trapping throws! can be a future direction.

9 Likes

Right -- what you are describing is indeed our tentative plan. The question is, what is the best implementation strategy to ensure program termination when it attempts to unwind through a Swift frame -- a strategy that would impose minimal performance and code size overhead on Swift code calling C++. Martin proposed a custom personality function, but maybe there's an easier way.

I think a custom personality would be best on two fronts:

  • First, I think we can come up with a very compact way to express "unwinding is not allowed". If necessary, we can have a personality that always has that effect and just use a different personality for functions that do want to allow some unwinding.

  • Second, if we ever do want to allow unwinding through Swift frames (e.g. to support a second-tier, Rust-like panic unwinding), there are a large number of cleanup cases that we should be able to express without requiring explicit landing pads.

2 Likes

From a type system perspective, C++ (and python) functions would be best modeled with something like a throws! marker. Something that can be ignored in a non-try context but that trap when ignored, or that can be used with try if/when a swift programmer explicitly want to be able to handle the C++ exception, explicitly turning the exception into a Swift error.

-Chris

2 Likes

Thanks everyone for the discussion!

Thoughts on throws!

I really like the throws! marker idea. (I wonder by the way if it should be spelled throws? to indicate that we don't know if there are any circumstances under which the function will actually throw -- thoughts? I'll keep calling it throws! for now to avoid confusion.)

This would give us semantics that are close to C++: If the function throws and you don't have an exception handler, the program terminates; optionally, you can add a handler to catch the exception.

I assume we would disallow throws! in user-written code (i.e. only allow it to be added to functions generated by ClangImporter)? Swift's philosophy is to be disciplined and explicit about exceptions, and it seems like allowing throws! in pure Swift code would encourage programmers to be sloppy about exceptions. (Compiler complaining? Just put a throws! on your function.)

throws! and @dynamicCallable

We'd want to be able to use the throws! concept in @dynamicCallable, too, so that Python interop no longer needs the .throwing to indicate that the user wants to handle exceptions. I see two ways of doing this:

  • Allow throws! to be specified on dynamicallyCall() to indicate that the @dynamicCallable should have throws! behavior. This would be a logical extension of what you can do today (where you can either put throws on dynamicallyCall() or not, and that propagates to the @dynamicCallable), but it might be slightly strange to allow throws! in just this one particular place in user code. Nevertheless, I think this is what I would prefer.

  • Alternatively, if dynamicallyCall() is marked throws, mark the @dynamicCallable itself throws! (and not throws, as is the case today). This would eliminate the need for throws! in user code, but it would make it impossible to specify "you must handle exceptions thrown by this @dynamicCallable".

My next steps

It sounds as if most here agree on the general direction, so here's what I'm planning to do:

  • Extend the C++ interop manifesto to capture what we've discussed here (i.e. exceptions trap by default but can optionally be caught).

  • Implement the "trap by default" behavior by installing a suitable personality routine. (It sounds from what @John_McCall says as if this may not work today.)

  • Write up a proposal for how throws! should work. (IIUC this should take the format of a Swift Evolution proposal?)

I think it's likely that I would defer actual implementation of throws! until we have some pieces of the more foundational C++ interop in place, as those seem higher priority. Does that sound acceptable?

1 Like

+1, thanks @michelf very much for suggesting it!

I think it should be throws!. If anything, throws should have been spelled throws? for consistency if we knew about the throws! idea at the time it was being designed. Both throws and throws! functions may throw (but in practice each call might or might not throw). Where they differ is whether an explicit check is needed. Compare it to optionals: T? is an explicitly-checked optional, T! is an implicitly-checked optional.

throws! is probably a bad practice in new code, and we should strongly discourage it, although there is a chance it might find its niche -- like T! has its niche now even in pure Swift code.

I think preventing user-written code from using throws! is going to create friction in interop. For example, when incrementally converting code from C++ to Swift one might want to migrate the implementation separately from changing the API to align it better with Swift API design guidelines.

1 Like

In the context of imported function signatures, T? means the thing definitely can be nil, whereas T! generally mean we don't really know it can be nil. So if we don't really know if it can throw, throws! seems more appropriate. There's also that ! announces trapping behavior.

This is not accurate in my experience. Implicitly unwrapped optional are generally used in cases where deferred initialization is necessary, the value is never used before that happens, and the value is never nil after it is initialized.

That's why I wrote "For imported signatures". (Now reworded to "In the context of imported function signatures" to put more emphasis on this).

This is probably true for all function signatures though.

2 Likes

I've verified this, at least on Linux x64.

Add a test documenting the current state of C++ exception handling by martinboehme · Pull Request #30674 · apple/swift · GitHub adds a test that documents the current behavior. It shows that, currently, C++ exceptions can be thrown and caught across Swift stack frames.

Yeah. It’s just not correct because it doesn’t clean anything up.

You (and others making the same point) have convinced me.

I'm not sure I see the need for that.

Let's say I'm migrating a C++ function void foo().

If the function doesn't actually throw anything, it should become func foo() /* no throws */ in Swift, and none of the existing Swift callsites are presumably calling it with a try, so I don't need throws!.

If the function does actually throw exceptions, then hopefully all of my Swift callsites are already calling it with a try. (If not, shouldn't I do that first?) If all the callsites are using try, then I can convert the function to func foo() throws, and again, I don't need throws!.

Are there use cases I'm not thinking of?

At any rate, I'd prefer to be conservative and disallow throws! in user code until we're sure it's needed.

1 Like

Oh yes, that's understood. I just wanted to have a test that documents the current wrong behavior, and then change the test when I fix it.

Personally I'm on the fence on this. While it would surly be horribly if Swift authors suddenly started using throws! on everything, I really don't see this as a likely case. Using throws! on everything would degrade the static type safety in favor of more runtime trapping, which I don't think authors would find attractive. This would effectively give Swift something like Java's unchecked exceptions. Which while they serve a purpose in the language, are seldom recoverable from in a good way, and program termination is usually not far away.

So I think the main use that throws! would find in user code is for errors that are so bad, that there's probably no safe recovery path, but where the caller could possibly perform some last second cleanup before exit if they want.

1 Like

Sloppy code is sloppy, so I don't expect that every caller would be diligently calling the API with try -- the compiler is not enforcing it, and if the error is not common, software could even reasonably work.