This discussion on my tentative patch for calling C++ constructors has led me to think some more about how C++ exceptions should be handled in the interop. I'd like some feedback on this.
In the discussion linked above, I stated that I would map (potentially) throwing C++ constructors to throwing Swift initializers, but after some more thought and offline discussion with @gribozavr, I don't think this will result in ergonomic imported APIs. (FWIW, the status quo is that we import non-constructor C++ functions, including potentially-throwing ones, as non-throwing Swift functions.)
Background
C++ unfortunately has the opposite default to Swift where exception throwing is concerned. Any C++ function that isn't explicitly marked noexcept
must be assumed to be potentially throwing, even though many of these functions don't throw in practice.
If we imported all of these functions into Swift as throws
functions, the code calling them would be littered with try!
s. Requiring all imported functions (and everything they call transitively) to be marked noexcept
also seems excessively burdensome and is not idiomatic C++.
Proposal
-
By default, import C++ (member) functions (including constructors) as non-throwing Swift functions.
-
If a C++ function called from Swift throws an exception, terminate the program. (I believe it's possible to implement this with zero overhead -- see below).
-
An exception thrown in C++ can be handled in Swift by writing a C++ wrapper that catches the exception and returns a corresponding error object.
-
Optionally, we could provide an attribute that could be applied to a C++ function to indicate that this function should be imported into Swift as a
throws
function. We would generate code that propagates the exception and converts it into an appropriate C++ type.
Implementation
I believe that, at least under the Itanium C++ ABI, the behavior described above (terminate if a C++ exception is thrown) can be implemented at zero cost. In particular, it is not necessary to wrap every C++ function in a synthesized try-catch block.
The Itanium C++ ABI defines a so-called personality routine that is called for each stack frame as the stack is being unwound. The idea is that different languages can have different personality routines, so that different languages can co-exist on the stack and can each define their stack unwinding logic.
The stack unwinding infrastructure finds the correct personality routine by consulting a so-called unwind table, which maps program counter ranges to personality routines.
We would define unwind table entries covering all of the Swift code and have these entries map to a personality routine that simply terminates the program.
I expect other ABIs use similar constructs. For those that don't, we could still fall back to synthesizing try-catch blocks.
I'd appreciate feedback from those more familiar with the Itanium C++ ABI on whether I'm missing anything here.