[Pitch] A Swift representation for thrown-and-caught exceptions

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 and thrownExceptionRecord, 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 to std::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 that try, catch, and decltype 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. :upside_down_face:

  • std::exception_ptr itself could be extended to conform to Error, but we run into two problems:

    1. std::exception_ptr is a C++-ism and an implementation detail. Its relation to other types, especially non-C++ types like NSException, is unclear to a lot of developers: if we're trying to catch an NSException, why would we catch std::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.
    2. 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 like func 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 say catch (const T&) in a stdlib helper function if T is not known at stdlib's compile-time?
9 Likes

The following catches C++ and Obj-C exceptions for me today:

// ---------
// Objc.h

#ifndef ObjcH
#define ObjcH

#import <Foundation/Foundation.h>

#define noEscape __attribute__((noescape))

@interface ObjC : NSObject
+ (BOOL)catchException:(noEscape void(^)(void))tryBlock error:(__autoreleasing NSError **)error;
@end

#endif

// ---------
// Objc.mm

#import "Objc.h"
#include <exception>

@implementation ObjC
+ (BOOL)catchException:(noEscape void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
    try {
        tryBlock();
        return YES;
    }
    catch(NSException* e) {
        NSLog(@"%@", e.reason);
        *error = [[NSError alloc] initWithDomain:e.name code:-1 userInfo:e.userInfo];
        return NO;
    }
    catch (std::exception& e) {
        NSString* what = [NSString stringWithUTF8String: e.what()];
        NSDictionary* userInfo = @{NSLocalizedDescriptionKey : what};
        *error = [[NSError alloc] initWithDomain:@"cpp_exception" code:-2 userInfo:userInfo];
        return NO;
    }
    catch(...) {
        NSDictionary* userInfo = @{NSLocalizedDescriptionKey:@"Other C++ exception"};
        *error = [[NSError alloc] initWithDomain:@"cpp_exception" code:-3 userInfo:userInfo];
        return NO;
    }
}
@end

Note that you can also use obj-c's @try / @catch but as the above works - there is no need. (Although I'd probably use @try / @catch in a pure Obj-c code base).

Here is a usage example:

// -------
// test.mm

extern "C" void foobar(void) {
//    throw std::exception();
//    throw "Hello World!";
    @throw [NSException exceptionWithName:@"domain" reason:@"reason" userInfo:nil];
}

// ---------------
// some swift file

func usageExample() {
    do {
        try ObjC.catchException {
            foobar()
        }
    } catch {
        print(error)
    }
}

Here catch correctly catches C++ and Obj-C exceptions exemplified in the above "foobar".

The non ideal thing here is that the call site is not a pure try foobar() but something more complex. Also the "code" parameter is hardcoded above.

As to std::current_exception() it looks useless IRT to the task at hand as there is no way to use it to determine C++ exception details for exception caught by catch (...).

Note that that isn't safe today. Exceptions cannot reliably pass through Swift frames, and on many platforms this will corrupt state and/or crash. If you need to catch an ObjC exception, the call to the throwing method must be done from ObjC. As part of C++ interop, we would implement a way to safely call a C++ or ObjC method that's expected to throw recoverable exceptions, and emit the necessary EH info around those calls to be able to bridge the exception over into a Swift error. What Jonathan is proposing here is the type that would be used to represent such exceptions, at least in the most general cases where there isn't a better bridging story available.

11 Likes

In the above code both throw and catch are in ObjC, or I misunderstood you.

You're calling foobar() from a Swift closure you passed into ObjC. If the call to foobar() from Swift causes an exception to be thrown, it can't be reliably passed through the Swift frame and might be corrupt before it reaches the @catch.

I see. AFAIK, it works on mac and iOS (the platforms I care about).

Thanks for your feedback @tera!

While the code you've shown may appear to work correctly, it is in fact invoking undefined behaviour. Ignoring that, the code is still not generalizable. If an exception is thrown through a Swift frame on macOS and iOS, the program will fail to clean up any locally-allocated memory (including retained objects located on the heap.) The stack pointer should get adjusted correctly so "plain old data" types like Int won't pose a problem, but larger values like String or UIWindow will leak.

I'd recommend you take a look at my proof-of-concept branch to see how std::exception_ptr, std::current_exception(), and std::rethrow_exception() are in fact useful for implementing this type. :slight_smile:

8 Likes

I don’t see the utility of CaughtException without a way to actually catch C++ exceptions. I think this pitch is inseparable from actually solving the exception handling issue.

2 Likes

We're tackling the problem one step at a time here. :upside_down_face:

It works by coincidence in the simplest case, but it'll fail to update reference counts, call defer blocks, and other such cleanup while unwinding the stack. We have some tests which throw exceptions over Swift frames (to test that we're reporting the correct "fatal" error) and it takes a decent amount of care to ensure that we only leak objects which are okay to leak, and we've had to adjust or remove tests a few times in the past when Swift has made changes like adding exclusivity checking that turned some things from UB that happens to work into UB that doesn't work.

If you're doing it in non-test code I am actually surprised that you haven't run into any problems.

1 Like

I'm a bit lost as to the context CaughtException.current can be called and return something non-nil? An example on how to use CaughtException would be welcome.

(To me it seems like CaughtException.current would have to be called within a catch on the C++ side, like std::current_exception(), but it's a Swift API so that must not be it.)


Just checking: Swift has no support for macOS 32-bit, right? (The legacy Objective-C runtime does not handle exceptions that way.)

It may turn out we don't need to expose current at all for the reasons you cite. (current or something very similar will eventually be needed within the runtime in order to bridge the languages.)

That's correct—32-bit macOS is obsolete and is not targeted by Swift.

Seems reasonable at first glance. This potential API strikes me as a bit weird:

would it be possible to have this CaughtException type interface with native type casting directly? E.g.

if let thrownFromObjc = caughtException as? SomeObjcThing {
  // ...
}

Is there any sort of C++ interop manifesto/roadmap? It would be great to know what the major missing pieces are and how everything is expected to fit together.

I'd like that to be the case! Right now I don't believe it's something that's been scoped out but @zoecarver might have more info.

Yes, there is! Note the section about exceptions.

2 Likes

I would be interested in a straightforward way to catch Obj-C exceptions in Swift. Foundation throws them and any arbitrary third party Obj-C library may also do so. Dealing with that is awkward at best currently. This is definitely a pain point and a Swift missing feature.

However, I don't think there's a proposed way to catch these exceptions in this pitch. AFAICT this pitch only declares the exception type. Without that next step I don't see the value.

Anyway, ExternalException might be a better name.

I'm glad it works for us in our simple cases (albeit by coincidence). Once we'll have problems with it (if ever) I guess we'll implement a few obj-c wrappers (not the end of the world, just inconvenience).

I guess what helps us to not see the problems are these factors:

  • we are the app, not a library
  • obj-c exceptions are not triggered often, only in some edge cases.
  • we don't use defer statements
  • we can tolerate leaking objects in those rare cases where obj-c exceptions are thrown
  • we caught obj-c exceptions very early in our Objc.catchException wrapper which is very small.
  • I did a minimal test of it on macOS but the app is iOS, probably that also limits the number of things that can go wrong.
  • there are only 7 places in code that does it and that's in regards to only 5 different areas.

Thanks for your feedback! As stated previously, this pitch only covers a Swift-native exception type. Catching exceptions (or rather, rethrowing exceptions as errors) should come later.

I'm with @ksluder and @phoneyDev: as much work as you've put into this, it's only interesting if exceptions can be caught in Swift and/or thrown through Swift stack frames, and I don't think that's been discussed yet at all. If we decide to continue not supporting that, it's not worth having dedicated, Swift-Project-endorsed APIs for manipulating C++ exceptions.

2 Likes

(Sorry that the following isn't a more formulated thought.)

For interop, I think we want to basically wrap every C++ function call in a landing pad that converts the C++ exception into a Swift error such as CxxError<SomeCxxException> (I understand that there might be cases or platforms where we can't do this, but we should try pretty hard to do this as much as possible). And we also want to do the reverse for C++ code that calls throwing Swift functions.

If this is too much of a performance issue, people can either mark their APIs as noexcept or decide not to use exceptions whatsoever (which is what the Swift compiler does for example).

One side effect of this is that calling non-noexcept C++ function will probably require a try. But, I think that this is the only way we can ensure safety in all cases on the Swift side.

Anyway, I think this proposal is a really great step in that direction, and provides us some tools which we can build off of when the time comes to implement the C++ interop side of things :)

That being said, one thing that I worry about here is breaking API when we inevitably introduce some kind of Swift error type for wrapping C++ exceptions. If people get used to using CaughtException which is essentially opaque (at leas on macOS) then they might have to update their code when we introduce proper exception handling support.

In this code:

func usageExample() {
    do {
        try ObjC.catchException(foobar)
    } catch {
        print(error)
    }
}

I don't see (much) swift in between catchException (objc) and foobar (C). See above for details on those. Yet this is what I see in debugger:

(I presume lines 1, 2 & 3 are swift). Is that alright? And in essence I am not gaining here anything compared to calling foobar in the otherwise empty braces:

    try ObjC.catchException {
        foobar()
    }