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.
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.
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 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
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.
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
panicunwinding), there are a large number of cleanup cases that we should be able to express without requiring explicit landing pads.
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.
Thanks everyone for the discussion!
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.)
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:
throws!to be specified on
dynamicallyCall()to indicate that the
throws!behavior. This would be a logical extension of what you can do today (where you can either put
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.
throws, mark the
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
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, 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! 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.
In the context of imported function signatures,
T? means the thing definitely can be
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.
I've verified this, at least on Linux x64.
https://github.com/apple/swift/pull/30674 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
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
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
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.
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.
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.