Hey all! Happy New Year! I'd like to pitch to you some initial exception-handling functionality in Swift in the form of an Error
type that represents a thrown-and-caught C++ or Objective-C exception.
Proof-of-Concept PR: #40765
Related Bugs: SR-12470, ...
Problem
Let's say you have a function like this one, implemented in C++:
extern "C" int getBiggestInteger(void) {
if (INFINITY <= INT_MAX) {
return INFINITY;
} else {
throw std::range_error("Infinity does not fit in an int.");
}
}
When called by a C++ caller, this function's thrown exception can be caught:
try {
int biggestInteger = getBiggestInteger();
std::cout << "The biggest integer is " << biggestInteger << std::endl;
} catch (const std::exception& e) {
std::cerr << "Couldn't get the biggest integer: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Couldn't get the biggest integer." << std::endl;
}
However, when called from Swift, this function's behaviour is undefined because Swift has no knowledge of C++ exceptions and no way to catch them. On some platforms, the program immediately aborts when an exception is thrown through a Swift stack frame. On other platforms, the program may continue for some time before crashing.
A partial solution
I'm proposing that, as part of the larger effort to add C++ interop to Swift, we add a type to the standard library that represents any thrown C++ or Objective-C exception:
public struct CaughtException: Error {
/// The exception that is currently caught, if any.
///
/// If the current code is executing within a C++ `catch` or Objective-C
/// `@catch` clause, the value of this property equals the exception that
/// has been caught. Otherwise, the value of this property is `nil`.
public static var current: Self? { get }
#if _runtime(_ObjC)
/// The Objective-C object that was thrown, if any.
///
/// If the represented exception is not an Objective-C object, the value of
/// this property is `nil`.
///
/// - Note: Objective-C allows throwing objects of any class, not just
/// instances of `NSException`.
public var thrownObjectiveCObject: Any? { get }
// OR:
public func `as`(_ type: T.Type) -> T? where T: AnyObject
#endif
#if os(Windows)
/// The Windows structured exception code that was thrown, if any.
///
/// If the represented exception is not a Windows structured exception, the
/// value of this property is `nil`.
public var thrownExceptionRecord: EXCEPTION_RECORD? { get }
// OR:
public func `as`(_: EXCEPTION_RECORD.Type) -> EXCEPTION_RECORD?
#endif
}
As for actually bridging exceptions to errors… there's a few different ways we can support bridging between caught exceptions and Error
. Because it's still undefined behaviour to throw an exception through a Swift stack frame, we'd probably need to pursue a solution on the C++ side. Ideally the solution we implement will also be usable as part of the C++ interop's runtime implementation, but stopgaps are worth exploring as well.
I have ideas™ here, but they're beyond the scope of this pitch. I'll explore them in detail in a future pitch.
The magic of std::exception_ptr
For folks who don't have the entire C++ Standard Library memorized (and who can blame you?), the canonical exception type in C++ is std::exception
, but any value of any type can be thrown and caught in C++. That includes things like int
, double
, std::string
, FILE *
, void (* fp)(int, char, const std::string&)
, etc. If it can be represented in the C++ type system and can be copied, then it can be thrown.
So how do you inspect an exception that was thrown without knowing the type in advance? Well, you can catch (const std::exception& e)
for any canonical exception of course. And for the cases you don't know about, you can catch (...)
. But there's not much you can do with the caught exception in that case.
Enter std::exception_ptr
: a type that boxes any thrown value in a refcounted smart pointer. You can get the currently-caught exception from a catch (...)
clause using std::current_exception()
which returns a value of type std::exception_ptr
. You can hold onto that value, copy it, or pass it to another function as often as you like. You can even throw it again using std::rethrow_exception()
.
Catching Objective-C exceptions (including NSException
)
On platforms where Swift supports Objective-C interop, Objective-C exceptions are thrown and caught using the same mechanism as C++ exceptions. They are, therefore, able to be handled using the same std::exception_ptr
-based mechanism.
To get the thrown Objective-C object back from a CaughtException
, you'd use the thrownObjectiveCObject
property described above.
Catching Windows structured/vectored exceptions (SEH/VEH)
Windows has the concept of "structured exception handling." Structured exceptions unwind the stack similarly to C++ exceptions, so if one reaches a Swift stack frame, the same undefined behaviour will ensue.
Some structured exceptions are intended to be fatal; if one reaches Swift, we should terminate the process rather than rethrow it as a CaughtException
. Other structured exceptions are reporting recoverable failures, which is Error
's job. So, in contexts where Swift needs to call a C++ function on Windows, we'd probably want to handle structured exceptions too.
We can do so by calling _set_se_translator()
before said C++ function and having it rethrow the structured exception as an instance of EXCEPTION_RECORD
. The proposed thrownExceptionRecord
property on CaughtException
would then know how to unbox the EXCEPTION_RECORD
value when called.
For more information on bridging structured exceptions, see here.
Notes
-
Naming is hard. The name of
CaughtException
as well as its members are subject to review, of course. If you have suggestions for better names, let me know! -
Instead of
thrownObjectiveCObject
andthrownExceptionRecord
, it would be nice to be able to implement cast operators to any supported underlying exception types, but the language doesn't support that at this time. We would probably also run into trouble casting tostd::exception
due to object slicing. -
My proof-of-concept branch currently creates a new implicitly-imported
_ExceptionHandling
module (Ă la_Concurrency
) because the exception-handling code needs to specify-fexceptions
and-frtti
at compile-time so thattry
,catch
, anddecltype
work. Ideally, any new API here would simply be part of the runtime and standard library (as Objective-C interop support is.) This is doable—I just haven't done it. -
std::exception_ptr
itself could be extended to conform toError
, but we run into two problems:-
std::exception_ptr
is a C++-ism and an implementation detail. Its relation to other types, especially non-C++ types likeNSException
, is unclear to a lot of developers: if we're trying to catch anNSException
, why would we catchstd::exception_ptr
instead? It seems to me that it would be better to provide a Swift-native, language-agnostic abstraction over exceptions that looks at home in Swift rather than in the C++ STL. - To convert a
std::exception_ptr
to its underlying exception type, you must rethrow it and catch it as said type. In Objective-C, you can throw any object of any class. In C++, you can throw any value of any type. Even with fully implemented C++ interop support, not all types are going to be catchable in Swift. A Swift extension likefunc
as<T>(_ type: T.Type) -> T?
probably can't help us either except for specific known types because C++'s type resolution is static. How do we saycatch (const T&)
in a stdlib helper function ifT
is not known at stdlib's compile-time?
-