Skip to content

Instantly share code, notes, and snippets.

@depth42
Last active August 9, 2016 14:14
Show Gist options
  • Save depth42/8363309 to your computer and use it in GitHub Desktop.
Save depth42/8363309 to your computer and use it in GitHub Desktop.
Breaking on exceptions in Objective-C is cumbersome, because a breakpoint on objc_exception_throw cannot differentiate between an expected (caught) exception and an unexpected exception (like one from a failed assertion). To work around this, we intercept objc_exception_throw using mach_override and apply filter rules to decide whether to break …
@implementation NSManagedObjectContext (PWExtensions)
#ifndef NDEBUG
// Core Data uses exceptions to notify itself about optimistic locking failures. These exceptions are intercepted by
// Core Data and never reach the client code. Such an exception should not drop in the debugger because it is not raised
// due to a programming error.
// Since the exception is thrown inside -[NSManagedObjectContext save:], this category is a logical place to install
// the filter.
+ (void) load
{
registerExpectedExceptionFilter (^(NSException* exception) {
// Core Data throws an exception of the private class _NSCoreDataOptimisticLockingException with name
// NSInternalInconsistencyException. Until otherwise proven it seems therefore simplest to test against the
// rather specific reason string.
return [exception.reason isEqualToString:@"optimistic locking failure"];
});
}
#endif
@end
#ifdef __cplusplus
extern "C" {
#endif
// Return value of YES means that 'exception' is expected in normal operation and the exception breakpoint should not
// be triggered.
typedef BOOL (^PWExpectedExceptionFilter) (NSException* exception);
// Patches objc_exception_throw(), unless NDEBUG is defined.
void intercept_objc_exception_throw();
void registerExpectedExceptionFilter (PWExpectedExceptionFilter filter);
// Unregistering will work only if the block has been copied before passing it to registerExpectedExceptionFilter()
// and that same pointer is passed to unregisterExpectedExceptionFilter().
// Returns whether 'filter' was found and unregistered.
BOOL unregisterExpectedExceptionFilter (PWExpectedExceptionFilter filter);
#ifndef NDEBUG
BOOL isExceptionExpected (NSException* exception);
#endif
#ifdef __cplusplus
}
#endif
#import "Intercept_objc_exception_throw.h"
#import "mach_override.h"
#import "PWLog.h"
#import "PWDispatch.h"
#ifndef NDEBUG
void objc_exception_throw (NSException* exception);
// The wrapper for objc_exception_throw is implemented in a separate file (Intercepted_objc_exception_throw.m) which
// is compiled without ARC. Somehow ARC messed up the retain count of the exception, resulting in the exception being
// overretained and leaked.
void intercepted_objc_exception_throw (NSException* exception);
typedef void (*objc_exception_throw_ptr) (NSException* exception);
objc_exception_throw_ptr g_objc_exception_throw;
static NSCountedSet* gExpectedExceptionFilters;
// Two accessor functions used by Intercepted_objc_exception_throw.m
objc_exception_throw_ptr original_objc_exception_throw()
{
return g_objc_exception_throw;
}
#endif
PWDispatchQueue* expectedExceptionFiltersDispatchQueue()
{
static PWDispatchQueue* queue;
PWDispatchOnce(^{
queue = [[PWDispatchQueue alloc] initWithLabel:@"expectedExceptionFiltersDispatchQueue"];
});
return queue;
}
void registerExpectedExceptionFilter (PWExpectedExceptionFilter filter)
{
#ifndef NDEBUG
NSCParameterAssert (filter);
NSCParameterAssert( [filter copy] == (id)filter); // To make unregistering work, the block needs to be already copied so that we remain pointer identity
[expectedExceptionFiltersDispatchQueue() asynchronouslyDispatchBlock:^{
if (!gExpectedExceptionFilters)
gExpectedExceptionFilters = [[NSCountedSet alloc] init];
[gExpectedExceptionFilters addObject:filter];
}];
#endif
}
BOOL unregisterExpectedExceptionFilter (PWExpectedExceptionFilter filter)
{
__block BOOL success = YES;
#ifndef NDEBUG
NSCParameterAssert (filter);
[expectedExceptionFiltersDispatchQueue() synchronouslyDispatchBlock:^{
success = [gExpectedExceptionFilters containsObject:filter];
[gExpectedExceptionFilters removeObject:filter];
}];
#endif
return success;
}
#ifndef NDEBUG
BOOL isExceptionExpected (NSException* exception)
{
NSCParameterAssert (exception);
__block BOOL result = NO;
[expectedExceptionFiltersDispatchQueue() synchronouslyDispatchBlock:^{
for (PWExpectedExceptionFilter iFilter in gExpectedExceptionFilters)
{
if(iFilter(exception))
{
result = YES;
break;
}
}
}];
return result;
}
#endif
void intercept_objc_exception_throw()
{
#ifndef NDEBUG
// Patch once only.
if (!g_objc_exception_throw) {
mach_error_t err = mach_override_ptr (objc_exception_throw,
intercepted_objc_exception_throw,
(void**)&g_objc_exception_throw);
if (err) {
PWLog (@"Could not patch objc_exception_throw(). ");
if (err == err_cannot_override)
PWLog (@"Make sure you do not have an active Objective-C Exception Breakpoint.\n");
else
PWLog (@"Mach error = %i.\n", err);
}
}
#endif
}
#import "Intercept_objc_exception_throw.h"
#import "mach_override.h"
#import "PWLog.h"
// IMPORTANT: this file must be compiled with ARC disabled.
// Somehow ARC messes up the retain count of the exception, resulting in the exception being overretained and leaked.
#ifndef NDEBUG
void intercepted_objc_exception_throw (NSException* exception);
typedef void (*objc_exception_throw_ptr) (NSException* exception);
// Accessor function for global in Intercept_objc_exception_throw.m
objc_exception_throw_ptr original_objc_exception_throw();
void intercepted_objc_exception_throw (NSException* exception)
{
if (!isExceptionExpected(exception))
PWLog (@"Throwing exception %@\n", exception); // set exeption catch breakpoint on this line
original_objc_exception_throw() (exception);
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment