From a902f057a99d04acfff06c5b12b8ccd4bc406b1f Mon Sep 17 00:00:00 2001 From: J2ObjC Team Date: Fri, 10 Jul 2026 17:24:34 -0700 Subject: [PATCH] Add a build flag to J2ObjC that runs the exception preprocessor at exception allocation time instead of throw time and explain what this fixes. PiperOrigin-RevId: 945967793 --- jre_emul/Classes/JavaThrowable.m | 104 ++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/jre_emul/Classes/JavaThrowable.m b/jre_emul/Classes/JavaThrowable.m index b2ea767c72..331c48f27e 100644 --- a/jre_emul/Classes/JavaThrowable.m +++ b/jre_emul/Classes/JavaThrowable.m @@ -27,6 +27,7 @@ #import "jni.h" #import +#import #ifndef MAX_STACK_FRAMES // This defines the upper limit of the stack frames for any exception. @@ -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 }