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

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()
    }

That’s one reason it’s premature to evaluate this pitch without the context of an interop proposal.

That would require also wrapping every Objective-C function/method call in a trampoline, since ObjC exception handling is ABI compatible with C++ exceptions, and ObjC can call C++ functions (either directly or through function pointers).

Oops. Forum go "splat."

In the short term, exceptions can be caught on the C++ side and converted into CaughtException instances. Heck, you can do this today! Write this helper function in your Swift code:

@_cdecl("createErrorFromCurrentException")
func createErrorFromCurrentException() -> UnsafeRawPointer? {
  return CaughtException.current.map {
    return Unmanaged.passRetained($0).toOpaque()
  }
}

And then in C++:

extern "C" void *createErrorFromCurrentException();

__attribute__((swiftcall))
void maybeThrowMaybeNot_SwiftSafe(
  __attribute__((swift_context)) void *context,
  __attribute__((swift_error_result)) void **outError
) {
  try {
    maybeThrowMaybeNot();
  } catch (...) {
    *outError = createCaughtException();
  }
}

That function, when called from Swift, will throw a caught error as an exception. My original draft pitch included these recipes, but I was asked by a teammate to remove them so we could focus on the design of the CaughtException type. That focus has not materialized. :upside_down_face:


Please note that although this all falls under the C++ interop banner, I'm intentionally trying to leave Cxx out of the type name since exceptions are not specific to C++.

It would be tempting to say we should expose a protocol Exception or CxxError or whatever that refines Error and have types like NSException conform to it. We could even create a private generic _EsotericException type for things caught with catch (...). But this won't work—if NSException conforms to Error, that means it's bridged to NSError, and a lot of wailing and gnashing of Liskovs would ensue.

That's not even taking into consideration object slicing on std::exception.

Basically, we must box exceptions somehow if we want to catch them as Swift Errors, and it's just a matter of figuring out how to spell the box's name.

Having class swift::boxed_exception: public std::exception (or whatever) that boxes a Swift error in an exception is certainly possible. I think it's orthogonal to this proposal, though each type should be aware of the other so we can avoid recursively nesting errors and exceptions in each other.

That just means we should get this type right the first time around. :wink: Unfortunately, a perfectly generic and portable solution is not statically possible but we might be able to get away with CaughtException being generic over T for some set of interesting types (NSException, etc.) that is known at stdlib's compile time. Standard C++ doesn't give us enough flexibility when catching exceptions to be generic over any arbitrary type (though the Itanium and MSVC++ ABIs give us a std::type_info and a mangled type name, respectively.)

I respectfully disagree—I interpret Zoe's comments here to mean we should design this interface well, not that we can't or shouldn't try to design it yet.

Expecting help from the C++/Obj-C side is likely a non-starter. As you point out they could already do it. If they really cared they'd have a Swift shim layer that converted the exception to something that Swift can handle. Since Foundation throws exceptions that kill my apps already I doubt Foundation will be fixed.

Also, I use a couple Obj-C libraries that are delivered as pre-compiled binaries. Both are commercial and I guess they don't want me reading their code. The authors don't know if the libraries will be used by Swift or by Obj-C. I think the solution for this problem has to be done on the Swift side.

Yes, but why bother? Without anything exceptiony in Swift, you could use any non-copyable box for this. (You might even want an Any-compatible representation, which would make casting immediately work. But the “non-copyable” part is important.)

As I see it, whether or not you can throw exceptions through Swift stack frames massively affects the design of this type. If you want to say “assume we will have this feature at some point” for the purposes of this thread, okay; what I don’t want to do, though, is spend a lot of time designing this type, add it to the stdlib, and have it be this awkward opaque box that gets ported around Swift just to release back into C++ elsewhere, cause that doesn’t need a dedicated type.

It is part of the API contract for Foundation that it is not exception-safe:

The Cocoa frameworks are generally not exception-safe. The general pattern is that exceptions are reserved for programmer error only, and the program catching such an exception should quit soon afterwards.

Indeed, the full solution is primarily a Swift solution, hence why I'm pitching a Swift type to box exceptions. :slight_smile: The recipe I posted above would be one you'd use in your code where it calls into those libraries (if you expect that they're going to throw exceptions.)

The purpose of this type is not just to say "hey, here's a type. Look at it. It's neat." It's to provide a standard representation of an exception in the Swift type system.

@zoecarver has more information about where we want to take the language with regards to exceptions and C++ interop. But yes, we can sum it up as "assume we will have this feature at some point" if that helps.

I do! Still ruminating on the rest of the proposal and enjoying learning from folks who are more qualified than me to talk about C++ exception handling. In the meantime, though, your choice of wording in a few spots already hints at a pretty good name:

I’d like to suggest, therefore, that the type be named BridgedException.

7 Likes

Since people are working C++ interop, I think it's reasonable to assume that we will have a way to invoke a function and catch exceptions at that call site, even if we don't generally allow exceptions to pass through Swift frames generally. We can bridge some subset of well-behaved exception types to Swift Error-conforming types, but since C++ (and ObjC) let you throw anything, and it doesn't make sense to make every type conform to Error in Swift, it seems reasonable to discuss what a fallback container for exceptions looks like that we can't bridge more transparently.

C++ isn’t the only language with exceptions that Swift does or may interoperate with. For example, some people are already calling into Python, which has exceptions. .Net has its own exception model. And the pitch even mentions SEH, which is a language-agnostic concept implemented in Win32.

All of these exception handling constructs might have slightly different capabilities and semantics. Since this proposal is being driven solely by C++ interop, it seems very out of line to propose such a general “BridgedException” type. Instead, this pitch should focus solely on C++ exceptions, including which semantics and operations will be supported when handling these exceptions on the Swift side.

1 Like

That's fair. On some platforms we support, some of those other languages throw exceptions in a way that interoperates to some degree or another with C++ exceptions, but that doesn't mean we have to use the same container type to bridge all of them.

1 Like

Joe, please confirm that in this code foobar is wrapped inside some invisible swift closure:

objcCall(foobar) // not foobar()

and it is equivalent to:

objcCall {
    foobar()
}

at least in regards to the topic being discussed.

Yes, there could be a Swift frame in either formulation.

Would it help to phrase this concept as "exceptions compatible with the platform's C++ exception-handling ABI can be represented by CaughtException"? Objective-C and SEH exceptions are both ABI-compatible with C++ exceptions.

To my knowledge, .NET exceptions are technically ABI-compatible as well (on Windows and if thrown through unmanaged code.) I'm not an expert on their implementation though and so I don't know how well they interoperate in practice.

In a sense, all the supported exception types I mentioned in the pitch are C++ exceptions. But more broadly it would make sense for us to design something that's generically applicable to "foreign thrown errors termed 'exception'" If we then end up with a two-level design (protocol Exception and protocol CxxCompatibleException: Exception) then I can live with that.

I don’t think I agree with this. Swift’s equivalent to other languages‘ exceptions is struct Error. The value of an intermediary protocol ForeignException is not self-evident to me.

I see your point. Perhaps we'd want to say that protocol Exception is reserved for things that need C++-style stack unwinding to work correctly. I do want to try to future-proof the name—I'm thinking of calling code that looks something like:

do {
  try f()
} catch let ex as Exception {
  ...
}

But where f() is a function imported from some other language that terms its errors "exceptions" but which doesn't use a C++-compatible ABI. On the other hand, I don't know of such a language that the Swift team would reasonably care to interoperate with.

Regarding non-C++ exception handling in general… there's some relevant musings about "foreign" exception handling on the Itanium ABI website (the Itanium ABI is what's used on Apple platforms and GNU's libstdc++.) Might be worth a read.

Hey Jonathan. Sorry, I was unclear, but I really don't think we can assume we have "this feature." I want to make sure neither of us promises something that might not be well defined, planned work.

I don't think it's really even clear what "this feature" is. I stated what I think might be a good way to handle C++ exceptions, but that's not a promise of what will be implemented. Before we rely on this functionality, we need to have a formal proposal about how to handle C++ exceptions and discuss alternative implementations and issues we might face (there are many).

I wonder, if someone needs the functionality you're proposing in the short term, could they implement this themself in their App/Library/whatever? I know this isn't a great short-term solution, but I think we should get this right on the first try, not get it out quickly.

We should discuss this a bit more off of the forums. Sounds like we've got some crossed wires.

Sure, there's nothing to prevent somebody from writing C++ code today that catches exceptions and propagates them out to Swift in a safe way. But we could say the same of most of the standard library: there's nothing to prevent me from writing my own String type and disavowing Swift.String.

The hard (impossible) part is ensuring that if an exception is thrown through a Swift frame, it is handled safely (for some definition of "safely.") That still needs to be done within the compiler and runtime. It was my understanding that we did intend to build a solution here and that a Swift representation of a caught exception was part and parcel, but it seems I may have been misinformed. :smiling_face_with_tear:

AFAIK, Apple platforms are the only ones on which Objective-C is supported, and with Apple ABIs that are still supported, exceptions are thrown following the Itanium ABI using the same personality function for both ObjC and C++. This probably means that there doesn't need to be a separate case for ObjC and C++. One way we could benefit from this is that the as<T> method can just always be available and have no generic constraints. as<NSObject> can work just as well as as<Int32>. Any type from a language using a different personality function can just fail all conversions.

If it starts being possible to make C++ types conform to Swift's ErrorProtocol, then we can just accept both the type itself and the ForeignException/BridgedException wrapper, and just match the first one that Swift sees.

do {
    try cppfunc()
} catch let error as? std.exception { // strawman exception syntax
    // ...
} catch let error as? ForeignException {
    // also matches std.exception, but is never executed for this case because
    // std.exception is caught above
    // ...
}

For the Windows SEH case, since the try/catch syntax is different even in C++, I'd propose that EXCEPTION_RECORD just gets an ErrorProtocol conformance (although you'd need someone much more familiar than me with it to confirm this is okay).

One thing I haven't seen discussed is passing through exceptions thrown from C++ back to more C++ code. If you have have a Swift sandwich with C++ bread (C++1 -> Swift -> C++2), and C++2 throws, it's important that the exception can make it back to C++1 in the same shape it started with. IMO, this means Swift doesn't need any stack unwinding support, but there has to be a way to re-throw the exact same value at language boundaries. Answering ksluder, to me this makes it self-evident that we need a wrapper type for exceptions coming from other languages, or else Swift could tamper with the value and throw back something slightly different.

Indeed, they're the same code path in my POC branch.

Unfortunately the relatively dynamic nature of the Swift type system is at odds with the static nature of the C++ type system in ways we haven't figured out yet, and that makes it impossible to generically cast a CaughtException to any Swift type (at least, not without deep C++ ABI knowledge.)

It would be impossible to make NSException conform to Error, but std.exception could theoretically do so (keeping in mind that object slicing might ruin our lunch here.)

SEH exceptions use the same ABI as standard C++ exceptions. The __try/__except syntax is effectively optional. Microsoft documents how to throw and catch SEH exceptions using the standard C++ syntax.

It's reasonable to assume that throwing an exception through a Swift stack frame is still going to be a very constrained (if not UB) operation. The details of how that would all work are beyond the scope of this pitch (which, it turns out, was itself a bit premature due to miscommunication between me and other stakeholders. Oops.)