An object-oriented programming environment is not a set of rules. Programs do not need to be constructed according to the rules supplied by the environment. An object-oriented environment includes tools for constructing new rules, and programs can use these to good effect. Let’s build multiple method inheritance for Objective-C, to see a new set of rules.
The goal
Given three classes, A
, B
, and C
:
@interface A : NSObject
-(NSInteger)a;
-(NSInteger)double:(NSInteger)n;
@end
@implementation A
-(NSInteger)a { return [self double:6]; }
@end
@interface B : NSObject
-(NSInteger)b;
@end
@implementation B
-(NSInteger)b { return 30; }
@end
@interface C : NSObject
-c;
@end
@implementation C
-(NSInteger)double:(NSInteger)a
{
return a*2;
}
-c { return @([self a] + [self b]); }
@end
We want to find the value of a C
instance’s c
property. That depends on its values of a
and b
, but that class doesn’t have those methods. We should add them.
int main(int argc, const char * argv[]) {
@autoreleasepool {
C *c = [C new];
[c addSuperclass:[A class]];
[c addSuperclass:[B class]];
NSLog(@"The answer is %@", c.c);
}
}
We want the following output:
2015-02-17 20:23:36.810 Mixins[59019:418112] The answer is 42
Find a method from any superclass
Clearly there’s some chicanery going on here. I’ve changed the rules: methods are no longer simply being looked up in a single class. My instance of C
has three superclasses: A
, B
and NSObject
.
@interface NSObject (Mixable)
- (void)addSuperclass:(Class)aSuperclass;
@end
@implementation NSObject (Mixable)
-superclasses
{
return objc_getAssociatedObject(self, "superclasses");
}
- (void)addSuperclass:(Class)aSuperclass
{
id superclasses = [self superclasses]?:@[];
id newSupers = [superclasses arrayByAddingObject:aSuperclass];
objc_setAssociatedObject(self, "superclasses", newSupers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (Class)superclassForSelector:(SEL)aSelector
{
__block Class potentialSuperclass = Nil;
[[self superclasses] enumerateObjectsUsingBlock:^(Class aClass, NSUInteger idx, BOOL *stop) {
if ([aClass instancesRespondToSelector:aSelector])
{
potentialSuperclass = aClass;
*stop = YES;
}
}];
return potentialSuperclass;
}
- (NSMethodSignature *)original_methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *signature = [self original_methodSignatureForSelector:aSelector];
if (signature)
{
return signature;
}
Class potentialSuperclass = [self superclassForSelector:aSelector];
return [potentialSuperclass instanceMethodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL aSelector = [anInvocation selector];
Class potentialSuperclass = [self superclassForSelector:aSelector];
[anInvocation invokeSuperImplementation:potentialSuperclass];
}
+ (void)load
{
if (self == [NSObject class])
{
method_exchangeImplementations(class_getInstanceMethod(self, @selector(original_methodSignatureForSelector:)),
class_getInstanceMethod(self, @selector(methodSignatureForSelector:)));
}
}
@end
Now invoke that method.
When you write [super foo]
, the Objective-C runtime needs to send a message to your object but tell the resolution machinery to look at the superclass for the method implementation, not at the current class. It uses a function objc_msgSendSuper
to do this. In this case, I don’t have the superclass: I have a superclass, one of potentially many. So what I need to do is more general than what messaging super
does.
Luckily for me, objc_msgSendSuper
is already sufficiently general. It receives a pointer to self
, just like the usual objc_msgSend
, but in addition it receives a pointer to the class to be used as the superclass. By controlling that class pointer, I can tell the system which superclass to use.
A category on NSInvocation
calls objc_msgSendSuper
with the appropriate arguments to get the correct method from the correct class. But how can it call the function correctly? Objective-C messages could receive any number of arguments of any type, and return a value of any type. Constructing a function call when the parameters are discovered at runtime is the job of libffi
, which is used here (not shown: a simple, if boring, map of Objective-C value encodings to libffi type descriptions).
@interface NSInvocation (SuperInvoke)
-(void)invokeSuperImplementation:(Class)superclass;
@end
@implementation NSInvocation (SuperInvoke)
- (BOOL)isVoidReturn
{
return (strcmp([[self methodSignature] methodReturnType], "v") == 0);
}
-(void)invokeSuperImplementation:(Class)superclass
{
NSMethodSignature *signature = [self methodSignature];
if (superclass)
{
struct objc_super super_class = { .receiver = [self target],
.super_class = superclass };
struct objc_super *superPointer = &super_class;
ffi_cif callInterface;
NSUInteger argsCount = [signature numberOfArguments];
ffi_type **args = malloc(sizeof(ffi_type *) * argsCount);
for (int i = 0; i < argsCount; i++) {
args[i] = [self ffiTypeForObjCType:[signature getArgumentTypeAtIndex:i]];
}
ffi_type *returnType;
if ([self isVoidReturn]) {
returnType = &ffi_type_void;
}
else {
returnType = [self ffiTypeForObjCType:[signature methodReturnType]];
}
ffi_status status = ffi_prep_cif(&callInterface,
FFI_DEFAULT_ABI,
(unsigned int)[signature numberOfArguments],
returnType,
args);
if (status != FFI_OK) {
NSLog(@"I can't make an FFI frame");
free(args);
return;
}
void *argsBuffer = malloc([signature frameLength]);
int cursor = 0;
cursor += args[0]->size;
void **values = malloc([signature numberOfArguments] * sizeof(void *));
values[0] = &superPointer;
for (int i = 1; i < [signature numberOfArguments]; i++) {
values[i] = (argsBuffer + cursor);
[self getArgument:values[i] atIndex:i];
cursor += args[i]->size;
}
if ([self isVoidReturn]) {
ffi_call(&callInterface, objc_msgSendSuper, NULL, values);
} else {
void *result = malloc(returnType->size);
ffi_call(&callInterface, objc_msgSendSuper, result, values);
[self setReturnValue:result];
free(result);
}
free(args);
free(values);
free(argsBuffer);
}
}
@end
Conclusion
You’ve seen this conclusion before: blah blah awesome power of the runtime. It doesn’t just let you do expressive things in the Objective-C game, it lets you define a better game.