Search code examples
objective-cmacoscore-foundationi386

Objective-C and CoreFoundation: "unrecognized selector sent" crash on i386


I'm trying to send notifications to Notification Center on Mac OS X 10.11.6 (El Capitan). Now I'm no Objective-C expert, so I'm using this sample as a base.

The complete listing:

//
//  main.m
//  badonkas
//
//  Created by Yoshiki Vázquez Baeza on 21/07/13.
//  Copyright (c) 2013 Yoshiki Vázquez Baeza. All rights reserved.
//
//  Based on the source code as written by Norio Nomura for the project
//  usernotification https://github.com/norio-nomura/usernotification
//  gcc -framework Foundation main.m
//

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

NSString *fakeBundleIdentifier = nil;

@implementation NSBundle(swizle)

// Overriding bundleIdentifier works, but overriding NSUserNotificationAlertStyle does not work.

- (NSString *)__bundleIdentifier{
    if (self == [NSBundle mainBundle]) {
        return fakeBundleIdentifier ? fakeBundleIdentifier : @"com.apple.terminal";
    } else {
        return [self __bundleIdentifier];
    }
}

@end

BOOL installNSBundleHook()
{
    Class class = objc_getClass("NSBundle");
    if (class) {
        method_exchangeImplementations(class_getInstanceMethod(class, @selector(bundleIdentifier)),
                                       class_getInstanceMethod(class, @selector(__bundleIdentifier)));
        return YES;
    }
    return NO;
}


#pragma mark - NotificationCenterDelegate

@interface NotificationCenterDelegate : NSObject<NSUserNotificationCenterDelegate>

@property (nonatomic, assign) BOOL keepRunning;

@end

@implementation NotificationCenterDelegate

- (void)userNotificationCenter:(NSUserNotificationCenter *)center didDeliverNotification:(NSUserNotification *)notification
{
    self.keepRunning = NO;
}

@end

int main(int argc, const char * argv[])
{

    @autoreleasepool {
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        NSString *message = [defaults stringForKey:@"-message"] ?: @"";
        NSString *title = [defaults stringForKey:@"-title"] ?: @"Notification";
        NSString *subtitle = [defaults stringForKey:@"-subtitle"] ?: @"";

        if (installNSBundleHook()) {

            fakeBundleIdentifier = [defaults stringForKey:@"identifier"];

            NSUserNotificationCenter *nc = [NSUserNotificationCenter defaultUserNotificationCenter];
            NotificationCenterDelegate *ncDelegate = [[NotificationCenterDelegate alloc]init];
            ncDelegate.keepRunning = YES;
            nc.delegate = ncDelegate;

            NSUserNotification *note = [[NSUserNotification alloc] init];
            note.title = title;
            note.subtitle = subtitle;
            note.informativeText = message;

            [nc deliverNotification:note];

            while (ncDelegate.keepRunning) {
                [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
            }
        }

    }
    return 0;
}

Now this works fine when compiling a 64-bit binary. However, if I compile it as a 32-bit binary with the -m32 or -arch i386 flags, it crashes:

2017-05-15 22:16:50.886 a.out[68034:2781117] -[NotificationCenterDelegate setKeepRunning:]: unrecognized selector sent to instance 0x7a019ef0
2017-05-15 22:16:50.886 a.out[68034:2781117] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NotificationCenterDelegate setKeepRunning:]: unrecognized selector sent to instance 0x7a019ef0'
*** Call stack at first throw:
(
    0   CoreFoundation                      0x9bc9d9b9 __raiseError + 201
    1   libobjc.A.dylib                     0x9e277fd1 objc_exception_throw + 276
    2   CoreFoundation                      0x9bca1463 -[NSObject(NSObject) doesNotRecognizeSelector:] + 275
    3   CoreFoundation                      0x9bb91dec ___forwarding___ + 1020
    4   CoreFoundation                      0x9bb919ce _CF_forwarding_prep_0 + 14
    5   a.out                               0x000608d5 main + 533
    6   libdyld.dylib                       0x90c4c6ad start + 1
)
68034: trace trap

Why is this, and is there a workaround?

(Because someone will undoubtedly ask [for good reason]: I need it to be 32-bit in order to link with a closed-source 32-bit program. And I can't get a 64-bit version of the program. I already tried linking a 32-bit dylib and ran across this issue.)


Solution

  • Did you examine the compiler warnings when building 32-bit? I'm pretty sure they would have explained.

    In 32-bit, Objective-C does not provide auto-synthesis of properties. You need to do a few things "by hand". If a property should have an instance variable for backing storage, you need to declare that instance variable. Furthermore, you have to do that in the @interface; declaring instance variables in the @implementation isn't supported, either.

    Next, you need to explicitly @synthesize the property or provide implementations of the accessor methods. If you use @synthesize and you named your instance variable anything other than the same name as the property (with no leading underscore), then you need to specify which instance variable to use. For example, @synthesize keepRunning = _keepRunning;. That is, the default instance variable name is different than the common convention and the default with auto-synthesis.

    So:

    @interface NotificationCenterDelegate : NSObject<NSUserNotificationCenterDelegate>
    {
        BOOL _keepRunning;
    }
    
    @property (nonatomic, assign) BOOL keepRunning;
    
    @end
    
    @implementation NotificationCenterDelegate
    
    @synthesize keepRunning = _keepRunning;
    
    - (void)userNotificationCenter:(NSUserNotificationCenter *)center didDeliverNotification:(NSUserNotification *)notification
    {
        self.keepRunning = NO;
    }
    
    @end