Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion jre_emul/Classes/JavaThrowable.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#import "jni.h"

#import <execinfo.h>
#import <objc/objc-exception.h>

#ifndef MAX_STACK_FRAMES
// This defines the upper limit of the stack frames for any exception.
Expand Down Expand Up @@ -116,11 +117,112 @@ jarray Java_java_lang_Throwable_nativeGetStackTrace(
return [IOSObjectArray arrayWithNSArray:frames type:JavaLangStackTraceElement_class_()];
}

// ObjC exceptions are meant to be [... raise]'ed or @throw'n near the allocation site.
// As part of that exception raise, CoreFoundation's __exceptionPreprocess() is passed the
// exception. __exceptionPreprocess() has the rough psuedocode:
//
// if (![[[raisedException userInfo] objectForKey:@"NSExceptionOmitCallstacks"] boolValue]) {
// if (raisedException->privateIvar == nil) {
// raisedException->privateIvar = CFDictionaryCreateMutable(NULL, 0, NULL);
// [raisedException->privateIvar setObject:[[NSThread currentThread] callStackReturnAddresses]
// forKey:@"callStackReturnAddresses"];
// [raisedException->privateIvar setObject:[[NSThread currentThread] callStackSymbols]
// forKey:@"callStackSymbols"];
// }
// }
//
// This code is not designed to be thread-safe because exceptions in ObjC are actually exceptional,
// and do not typically migrate across threads. It also assumes the allocation and throw occur in
// the appropriate frame for the stack to be accurate.
//
// In Java, exceptions are often just "error" objects that may not be thrown immediately. This
// makes capturing stacks at throw time incorrect. More dangerously, these Java "error" exceptions
// may be thrown for the first time later from other threads or even concurrently.
//
// There is no direct public API to allow access to CoreFoundation's preprocessor directly, but
// we can install our own preprocessor and use that to get access to the whole preprocessor
// chain.
//
// If that fails we will use NSExceptionOmitCallstacks as a fallback. Note that omitting stacks,
// causes our exceptions to violate some nonnull declared NSException properties. However, this
// appears to be true even of the base class, and so must be something exception handlers and
// other preprocessors must already deal with in practice.
//
// NSLog(@"%@", [[[NSException alloc] initWithName:@"foo" reason:nil userInfo:nil]
// callStackReturnAddresses]);
//
#ifdef J2OBJC_EXCEPTION_PREPROCESSING
static dispatch_once_t gExceptionPreprocessorOnce = 0;
static objc_exception_preprocessor gNextExceptionPreprocessor = nullptr;

// Static but named to make it clearer in stacktraces.
static id J2ObjCThrowableExceptionPreprocessor(id exception) {
// Pure passthrough. gNextExceptionPreprocessor safe because it is guarded by the dispatch_once
// below.
if (gNextExceptionPreprocessor) {
return gNextExceptionPreprocessor(exception);
} else {
return exception;
}
}

static BOOL InstallPreprocessor(void) {
dispatch_once(&gExceptionPreprocessorOnce, ^{
// "Tickle" CoreFoundation in order to try to be sure that it has already installed
// its preprocessor. The conditions for this are not documented, but it seems reasonable
// to assume it happens on framework initialization. There is no documented relationship
// between CFError and CoreFoundation's internal exception preprocessor, but its as good
// as any other CFType for our purpose.
CFErrorRef tickleError = CFErrorCreate(kCFAllocatorDefault, CFSTR("ignored"), 0, nullptr);
if (tickleError) {
CFRelease(tickleError);
}
gNextExceptionPreprocessor =
objc_setExceptionPreprocessor(&J2ObjCThrowableExceptionPreprocessor);
});
if (gNextExceptionPreprocessor) {
return YES; // Assume CoreFoundation was somewhere in the preprocessor chain.
} else {
return NO;
}
}

static void PrePreprocessException(id exception) {
if (InstallPreprocessor() && gNextExceptionPreprocessor) {
gNextExceptionPreprocessor(exception);
}
}

#endif // J2OBJC_EXCEPTION_PREPROCESSING

void NSException_initWithNSString_(NSException *self, NSString *message) {
// The NSException reason string is based on java.lang.Throwable.toString():
// . if there is a message, then the reason is "class-name: message",
// . otherwise, it's "class-name".
NSString *clsName = [[self java_getClass] getName];
NSString *reason = message ? [NSString stringWithFormat:@"%@: %@", clsName, message] : clsName;
[self initWithName:[[self class] description] reason:reason userInfo:nil];
NSDictionary *userInfo = nil;
BOOL preprocessingAvailable = NO;

#ifdef J2OBJC_EXCEPTION_PREPROCESSING
preprocessingAvailable = InstallPreprocessor();
if (!preprocessingAvailable) {
// If no preprocessors ran then the safest thing we can do is omit callstacks.
userInfo = @{
@"NSExceptionOmitCallstacks" : @YES,
};
}
#endif // J2OBJC_EXCEPTION_PREPROCESSING

// Under ObjC initialization rules we should not be ignoring the return value
// here because [... init...] can return a different instance than |self| in
// class clusters and other scenarios. In practice, however, it seems that
// NSException doesn't do that.
[self initWithName:[[self class] description] reason:reason userInfo:userInfo];

#ifdef J2OBJC_EXCEPTION_PREPROCESSING
if (preprocessingAvailable) {
PrePreprocessException(self);
}
#endif // J2OBJC_EXCEPTION_PREPROCESSING
}