I understand that Swift exceptions errors (edited for clarity) are completely different from objc exceptions, and how Swift exceptions map to NSError **
params in bridged Objc, etc.
However, what happens when some Swift sits between two levels of throwing/catching objc like this:
pseudo Stack Frame:
3. Baz.m#objCMethodThatThrows ---------. Expecting to unwind back to Foo,
2. Bar.swift#swiftMethodThatCallsObjC | but sometimes terminating in Baz.
1. Foo.m#objCMethodThatCatches <-------'
(example implementations below if you need more detail)
Background: We're using more and more Swift, but have a lot of legacy objc code to deal with. Some of that legacy code uses exceptions for control flow. (Whether we should be using objc exceptions for control flow like this is outside of the scope of my question ).
We now have a situation where some high level code catches
in objc, some low level code throws
in objc, but some recently introduced middle layer code is written in Swift.
My question is, is there something about unwinding the stack across a swift frame that causes the app to crash or is this supposed to be supported? In practice it seems to generally work, but we're seeing a crash in production.
I can't find any documentation about this particular case, but it appears to work in DEBUG while developing. And even a synthetic example like the code given below works in debug and release builds. I haven't yet determined the simplest failure case (e.g. does the swift have to cross a non-@ objc frame, or a specialized
frame to crash?)
However, in our actual application, which is substantially more complicated, we're seeing something similar crash in production (test flight) builds at the point of the exception being thrown in the objc code. That is - an objc
try block, calls a class implemented in swift, which in turn, calls some other objc code which @throws, and the crash report indicates termination at the point of the @throw, as if it were not inside a try/catch block.
[example implementations]
// Foo.m
@implentation Foo
- (void)objcMethodThatCatches
{
@try {
[[Bar new] swiftMethodThatCallsObjC]
} @catch (NSException *exception) {
// application terminated before it gets here.
[SomeOtherClass handleException:exception];
}
}
@end
// Bar.swift
@objc
class Bar: NSObject {
@objc
func swiftMethodThatCallsObjC() {
Baz().objcMethodThatThrows()
}
}
// Baz.m
@implentation Baz
- (void)objcMethodThatThrows
{
// application terminated here
@throw [NSException exceptionWithName:@"blah" readon:@"blah" userInfo:nil];
}
@end
example work around
In the meanwhile, I can work around the crash by executing the swift code in a more proximate try/catch like this, but as we'd need to do this in several places, it's not ideal.
// WrapExceptions.m
@implementation WrapExceptions
- (BOOL)doBlock:(void ^(void))block error:(NSError **)outError
{
@try {
block();
return YES;
} @catch (NSException *exception) {
// pass exception in userInfo
*outError = [NSError makeErrorWithDomain:"blah" code:123 userInfo:@{@"WrappedException": exception}];
return NO;
}
}
@end
// Foo.m
@implentation Foo
- (void)objcMethodThatCatches
{
@try {
NSError *error;
[[Bar new] swiftMethodThatCallsObjCReturningError:&error];
if (error) {
NSException *exception = error.userInfo[@"WrappedException"]
@throw exception
}
} @catch (NSException *exception) {
[SomeOtherClass handleException:exception];
}
}
@end
// Bar.swift
@objc
class Bar: NSObject {
@objc
func swiftMethodThatCallsObjC() throws {
// turn objc exception into an error
try WrapExceptions.do {
Baz().objcMethodThatThrows()
}
}
}
// Baz.m
@implentation Baz
- (void)objcMethodThatThrows
{
@throw [NSException exceptionWithName:@"blah" readon:@"blah" userInfo:nil];
}
@end