Search code examples
objective-csuperclassobjective-c-runtimeswizzlingisa-swizzling

ISA swizzling and calls to `super`


Assume the following class hierarchy. Class A is publicly declared:

@interface A : NSObject

+ (A)createInstance;
- (void)a;

@end

Class _B is a private subclass of A:

@interface _B : A

- (void)a;
- (void)b;

@end

Assume objects of class A should only be created using the factory method createInstance, which creates and returns an instance of _B.

I want to enhance the functionality of an instance of A on a per-instance basis. So I decided to do some ISA swizzling to achieve:

@interface ExtA : A

- (void)a;

@end

@implementation ExtA

- (void)a
{
    NSLog("ExtA_a");
    [super a];
}

@end

And I do the ISA swizzling using the following method on an NSObject category (naive implementation shown here):

- (void)changeToSubclass:(Class)cls prefix:(NSString*)prefix suffix:(NSString*)suffix
{
    NSString* className = [NSString stringWithFormat:@"%@%@%@", prefix ? prefix : @"", NSStringFromClass(object_getClass(self)), suffix ? suffix : @""];

    if([className isEqualToString:NSStringFromClass(object_getClass(self))])
    {
        className = [NSString stringWithFormat:@"%@(%@)", NSStringFromClass(object_getClass(self)), NSStringFromClass(cls)];
    }

    Class newSubclass = objc_getClass(className.UTF8String);

    if(newSubclass == nil)
    {
        newSubclass = objc_allocateClassPair(object_getClass(self), className.UTF8String, 0);
        objc_registerClassPair(newSubclass);

        unsigned int listCount = 0;
        Method *list = class_copyMethodList(cls, &listCount);

        for(int i = 0; i < listCount; i++)
        {
            class_addMethod(newSubclass, method_getName(list[i]), method_getImplementation(list[i]), method_getTypeEncoding(list[i]));
        }
        free(list);

        listCount = 0;
        list = class_copyMethodList(objc_getMetaClass(class_getName(cls)), &listCount);

        for(int i = 0; i < listCount; i++)
        {
            class_addMethod(objc_getMetaClass(class_getName(newSubclass)), method_getName(list[i]), method_getImplementation(list[i]), method_getTypeEncoding(list[i]));
        }
        free(list);
    }

    object_setClass(self, newSubclass);
}

Everything seemingly works, but I noticed that [super a]; does not behave as expected, actually the implementation of -[A a] is called, if if the superclass in runtime is actually _B.

Replacing the call to super with the following code works, but is ugly, and requires knowledge of and work by developers:

struct objc_super superInfo = {
    self,
    [self superclass]
};
objc_msgSendSuper(&superInfo, @selector(a));

What does the compiler emit when calling super and any way to change this emitted code?


Solution

  • The difference is minor, but important. The compiler is issuing a function call, not to objc_msgSendSuper, but to objc_msgSendSuper2.

    What's the difference, you may ask? It's minor, but important.

    From apple's open source:

    /********************************************************************
     *
     * id objc_msgSendSuper(struct objc_super *super, SEL _cmd,...);
     *
     * struct objc_super {
     *      id  receiver;
     *      Class   class;
     * };
     ********************************************************************/
    
        ENTRY   _objc_msgSendSuper
        MESSENGER_START
    
    // search the cache (objc_super in %a1)
        movq    class(%a1), %r11    // class = objc_super->class
        CacheLookup SUPER       // calls IMP on success
    
    /* Snipped code for brevity */
    
    /********************************************************************
     * id objc_msgSendSuper2
     ********************************************************************/
    
        ENTRY _objc_msgSendSuper2
        MESSENGER_START
    
        // objc_super->class is superclass of class to search
    
    // search the cache (objc_super in %a1)
        movq    class(%a1), %r11    // cls = objc_super->class
        movq    8(%r11), %r11       // cls = class->superclass
        CacheLookup SUPER2      // calls IMP on success
    

    For those reading who are unfamiliar with x86_64 assembly, the important line of code is here:

    movq    8(%r11), %r11       // cls = class->superclass
    

    What does this do, you may ask? It's fairly simple - instead of the caller passing the superclass to search, the objc_msgSend implementation does it.

    However, this important distinction causes one crucial problem - when doing a super call, it does not invoke [self class]. Instead, it uses the class of the current implementation, which is, of course, ExtA.

    Therefore, the only way to 'fix' this is to change the superclass of ExtA at run-time, which should cause your method invoking to perform as expected.