How Does Super Work?

As Objective-C programmers, we encounter and use the super keyword on a daily basis. We know how it works and what to expect from it. Some metaprogramming approaches may, however, require a deeper understanding of its implementation.

What is super?

Before we start analyzing how it works, let’s start with a short recap of what super is. As we all know, super is an Objective-C keyword just like self. Unlike self, it cannot be used as a function or method argument, only as a message recipient. When used in this manner, it invokes the superclass’s implementation of the selected method. If none of the classes up the class-chain respond to the selector, it will crash during runtime.

Here, for example, we have a very common pattern of using the superclass’s -init method at the beginning of a custom -init designed for our class:

- (instancetype)initWithValue1:(id)value1 value2:(id)value2
{
    self = [super initWithValue1:value1];

    if (self) {
        _value2 = value2;
    }

    return self;
}

How does messaging self work?

Messaging super is a more complicated alternative to messaging self. To understand super, we have to first take a look at self. Every message to self gets converted by the compiler into a call to the objc_msgSend function or a variant of it. The function receives two additional arguments — the receiver and the selector, which are available within the implementation as self and _cmd. All the explicit method arguments are placed after these two.

- (void)methodWithArgument:(id)arg
{
    [self otherMethodWithArgument:arg];
}

Gets converted into:

- (void)methodWithArgument:(id)arg
{
    ((void (*)(id, SEL, id))objc_msgSend)(self, @selector(otherMethodWithArgument:), arg);
}

objc_msgSend finds the method implementation for the recipient’s class and invokes it with all its arguments. The lookup involves multiple locks and caches to make the process thread-safe and efficient. It’s important to notice that the object’s class isn’t used directly, but has to be retrieved from self. If we were to copy -methodWithArgument: to a different class, that class would have to provide its own implementation of -otherMethodWithArgument:. We’ll see later, that this doesn’t apply to super.

For some more details, see Mike Ash’s article on Objective-C Messaging.

How does this change when messaging super?

In order to handle deep class hierarchies, where the super method gets called on multiple layers, super calls have to somehow contain the information about the class they’re called from. This is solved by using a struct objc_super in place of the receiver. This structure contains the receiver and the superclass of the class it’s implemented in (think statically resolved [self superclass]).

struct objc_super {
    __unsafe_unretained id receiver;
    __unsafe_unretained Class super_class;
};

For every variant of objc_msgSend there exists an equivalent objc_msgSendSuper function which, instead of self, takes a pointer to this structure as its first argument.

- (void)methodWithArgument:(id)arg
{
    [super otherMethodWithArgument:arg];
}

Gets converted into:

- (void)methodWithArgument:(id)arg
{
    struct objc_super super = {.receiver = self, .super_class = 0xC0FFEE};

    objc_msgSendSuper(&super, @selector(otherMethodWithArgument:), arg);
}

objc_msgSendSuper behaves very analogously to objc_msgSend, but when looking for the implementation to use, it starts at the specified superclass. It’s important to notice that the super_class element is resolved at compile time. This means that if you copy this method to a different class, the super calls will still be resolved based on the original class’s hierarchy.

The objc_msgSendSuper Experiment

I have limited trust in programming-related articles that don’t provide runnable code to test whether what’s said is actually true. In general it’s usually a good idea to prove your thesis with experiments whenever possible. For these reasons, I decided to write a few short and simple examples of how this knowledge could be used. To run them, you can either copy-paste them to CodeRunner, or download the Xcode project with the examples. CodeRunner’s output is shown below the code.

This first experiment uses objc_msgSendSuper to invoke method implementations from different classes. You can see that calling -abc with the super_class set to B, the A‘s implementation is used, since B doesn’t override it.

enter image description here
#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface A : NSObject @end
@interface B : A @end
@interface C : B @end
@interface X : NSObject @end

@implementation A 
- (void)abc
{
    NSLog(@"-[A abc] called from class %@", NSStringFromClass([self class]));
}
@end

@implementation B
@end

@implementation C
- (void)abc
{
    NSLog(@"-[C abc] called from class %@", NSStringFromClass([self class]));
}
@end

@implementation X
-(void)A_abc
{
    struct objc_super sup = {self, [A class]};
    objc_msgSendSuper(&sup, @selector(abc));
}
-(void)B_abc
{
    struct objc_super sup = {self, [B class]};
    objc_msgSendSuper(&sup, @selector(abc));
}
-(void)C_abc
{
    struct objc_super sup = {self, [C class]};
    objc_msgSendSuper(&sup, @selector(abc));
}
@end

int main(int argc, char *argv[])
{
    @autoreleasepool {
        [[X new] A_abc];
        [[X new] B_abc];
        [[X new] C_abc];
    }
}

Output:

-[A abc] called from class X
-[A abc] called from class X
-[C abc] called from class X

Message Forwarding Experiment

In this experiment, we forward an invocation using a method from a different class. You can see that the super call bound the super_class statically in -[C abc], so even though we’re calling it from a different class hierarchy, it still uses A‘s implementation.

enter image description here
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface NSInvocation (PrivateAPI)
- (void)invokeUsingIMP:(IMP)imp;
@end

@interface A : NSObject @end
@interface B : A @end
@interface C : B @end
@interface X : NSObject @end

@implementation A 
- (void)abc
{
    NSLog(@"-[A abc]");
}
@end

@implementation B
@end

@implementation C
- (void)abc
{
    [super abc];
    NSLog(@"-[C abc]");
}
@end

@interface X (ForwardedMethods)
- (void)xyz;
@end

@implementation X
- (BOOL)respondsToSelector:(SEL)selector
{
    return selector == @selector(xyz);
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    if (selector == @selector(xyz)) {
        return [C instanceMethodSignatureForSelector:@selector(abc)];
    }

    return nil;
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    if (invocation.selector == @selector(xyz)) {
        Method method = class_getInstanceMethod([C class], @selector(abc));
        [invocation invokeUsingIMP:method_getImplementation(method)];
    }
}
@end

int main(int argc, char *argv[])
{
    @autoreleasepool {
        [[X new] xyz];  
    }
}

Output:

-[A abc]
-[C abc]

Method Copying Experiment

This experiment is very similar to the previous one. The class hierarchies are the same and the methods are the same, but instead of forwarding -[X xyz], we copy the method from C when the class gets loaded. While it doesn’t demonstrate anything insightful about super, it is another example of a situation in which the behavior of super calls makes a big difference.

enter image description here
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface A : NSObject @end
@interface B : A @end
@interface C : B @end
@interface X : NSObject @end

@implementation A 
- (void)abc
{
    NSLog(@"-[A abc]");
}
@end

@implementation B
@end

@implementation C
- (void)abc
{
    [super abc];
    NSLog(@"-[C abc]");
}
@end

@interface X (CopiedMethods)
- (void)xyz;
@end

@implementation X
+ (void)load
{
    Method method = class_getInstanceMethod([C class], @selector(abc));
    IMP imp = method_getImplementation(method);
    const char *typeEncoding = method_getTypeEncoding(method);
    class_addMethod(self, @selector(xyz), imp, typeEncoding);
}
@end

int main(int argc, char *argv[])
{
    @autoreleasepool {
        [[X new] xyz];  
    }
}

Output:

-[A abc]
-[C abc]

Summary

The way super works is fairly simple — it uses objc_msgSendSuper in combination with struct objc_super to statically embed the superclass in the call. Despite the simplicity, it significantly influences many metaprogramming solutions.

There aren’t that many uses for metaprogramming in day-to-day app development, but it proves useful on certain occasions. While most of the time it’s better to achieve the desired effect using other ways and means, in rare cases there is simply no other way to go about it. Even if you don’t use it directly, knowledge about the runtime comes in handy when debugging or analyzing crashes.

Introducing SwiftyStateMachine
How Do We Hire the Best of the Best?