Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ObjC: +[NSObject isSubclassOfClass:] gives incorrect failure

  • I have an iOS static library which defines an NSOperation base class that clients should subclass to add their own logic to: @interface BaseClass : NSOperation

  • Clients register their subclasses with a manager: -[OperationManagerClass registerClass:forType:]

  • In the manager, I want to enforce that you must register a subclass of BaseClass not just NSOperation

Okay, so seems like asserting +isSubclassOfClass: should get the job done. But... it doesn't.

@implementation OperationManagerClass

- (void)registerClass:(Class)aClass forType:(NSString *)type
{
    NSAssert([aClass isSubclassOfClass:[BaseClass class]);

    self.registeredClasses[type] = aClass;
}

@end

The assert is always NO, even when passed BaseClass.

What about traveling higher up the class hierarchy? NSOperation and NSObject both respond YES!

(lldb) p (BOOL)[aClass isSubclassOfClass:[BaseClass class]]
(BOOL) $0 = NO
(lldb) po aClass
BaseClass
(lldb) p (BOOL)[aClass isSubclassOfClass:[NSOperation class]]
(BOOL) $2 = YES
(lldb) p (BOOL)[aClass isSubclassOfClass:[NSObject class]]
(BOOL) $3 = YES

Mind you, the consumers of the base operation class are subclassing in the iOS application project, and OperationManagerClass and BaseClass are in an included static library. Why do I think this might have something to do with isSubclassOfClass: being wrong? Because of the following...

Still in libSharedStuff.a

@implementation OperationManagerClass

- (void)registerClass:(Class)aClass forType:(NSString *)type
{
    // Obviously OperationManagerClass.m cannot #import "OutsideClass.h"
    NSAssert([aClass isSubclassOfClass:NSClassFromString(@"OutsideClass");

    self.registeredClasses[type] = aClass;
}

@end

In application target

@interface OutsideClass : NSOperation
@end

@interface OutsideClassSubA : OutsideClass
@end

... yields the following results when OutsideClassSubA is passed in:

(lldb) p (BOOL)[aClass isSubclassOfClass:[OutsideClass class]]
(BOOL) $0 = YES
(lldb) po aClass
OutsideClassSubA
(lldb) p (BOOL)[aClass isSubclassOfClass:[NSOperation class]]
(BOOL) $2 = YES
(lldb) p (BOOL)[aClass isSubclassOfClass:[NSObject class]]
(BOOL) $3 = YES

What's going on here? Why does +isSubclassOfClass: give the wrong answer? How can I enforce that the aClass parameter must be a subclass of my BaseClass?


Edit:
I realized the example I posted works fine after pulling the pieces into a separate Xcode workspace. I've posted the toy source code from above with a twist I left out of my original description.

I actually have two static libraries (libSharedStuff.a and libHelperSharedStuff.a). The application target links with libSharedStuff.a, and the unit test target depends on the application target as well as links libHelperSharedStuff.a. When BaseClass.m is a member of both static library targets, then +isSubclassOfClass: will fail in unit test assertions. Specifically, it fails when I pass MockBaseClass, which is a subclass of BaseClass that is part of the unit test target.

All of this is illustrated in the project linked above.

like image 501
edelaney05 Avatar asked Jan 11 '23 22:01

edelaney05


2 Answers

As I explain in the edit of my question, I'm including BaseClass.m in both static libraries libSharedStuff.a and libHelperSharedStuff.a. The main application target links against libSharedStuff.a and the unit test target links with libHelperSharedStuff.a. This means that when the .xctest bundle is injected into my application at runtime, the BaseClass symbol is defined twice

(Maybe? Is that possible? What actually happens in the runtime?).

I seems that in my unit test target MockSubclassOfBaseClass inherits from the BaseClass symbol in libHelperSharedStuff.a, whereas in my application target SubclassOfBaseClass inherits from the BaseClass symbol in libSharedStuff.a. If that's the case, then it makes sense why +isSublcassOfClass: responds NO when MockSubclassOfBaseClass is passed in!

Any answers that clarifies, confirms, or gives better insight that I've got the right idea here are greatly appreciated.

like image 107
edelaney05 Avatar answered Jan 14 '23 12:01

edelaney05


Make sure that the static library is actually causing the symbols to be linked in correctly. My immediate guess would be that the static lib–provided classes passed as aClass are actually Nil at runtime, and therefore returning 0 to any messages sent it.

One way to ensure this is to make a constructor function which uses them:

__attribute__((constructor)) static void EnsureClassesAreLoaded(void) {
    [BaseClass self];
}

Weak-linked classes can be a bit tricksy.

like image 27
Rob Rix Avatar answered Jan 14 '23 12:01

Rob Rix