iphoneiosmacrosmessage-forwarding

iOS -- use macros to forward a bunch of messages?


ForwardInvocation does exist, but it is slow and has the annoying problem of compiler warnings. So that got me to thinking -- is there a way to use macroes to quickly implement a bunch of getter methods that get the property in question from another object?

For example, if I have a Car object, it might want to implement the following:

Car.h:

@class SparkPlug;
@class Engine;

. . .

-(int) nPistons;
-(float) horsepower;
-(SparkPlug*) sparkPlug;

Car.m:

. . .

-(int) nPistons {
    return self.engine.nPistons;
}

-(float) horsepower {
    return self.engine.horsepower;
}

-(SparkPlug*) sparkPlug {
    return self.engine.sparkPlug;
}

Question -- would it be possible to set up some macroes so that by making one change somewhere, I could add another such method to both the header and implementation files?

e.g. MagicForwardingMacro (nPistons, int, engine);

Ideally, in such a way that the macroes would be reusable if I later wanted to later use a similar strategy to get the firstName, lastName, placeOfBirth, and dateOfBirth properties of a Person from his or her birthCertificate.


Solution

  • The easiest way is probably to add the methods dynamically:

    Elaborating on the second step:

    For each type, add a method like

    -(int)getEngineInt {
      return (int()(id,SEL))(objc_msgSend)(engine, _cmd);
    }
    

    Note that for structs you need objc_msgSend_stret and for floats/doubles you might need objc_msgSend_fpret (I think you only need it on i386; not sure about AMD64). The easy hack to support both the simulator and device is something like (I forget the macro name GCC uses...)

    #if __i386
    #define objc_msgSend_fpret objc_msgSend
    #endif
    

    Now to implement +resolveInstanceMethod:, you need to know the class you're forwarding to ahead of time. Let's say it's Engine.

    +(BOOL)instancesRespondToSelector:(SEL)name
    {
      return [Engine instancesRespondToSelector:name];
    }
    
    +(BOOL)resolveInstanceMethod:(SEL)name
    {
      // Do we want to super-call first or last? Who knows...
      if ([super resolveInstanceMethod:name]) { return YES; }
      // Find the return type, which determines the "template" IMP we call.
      const char * returntype = [Engine instanceMethodSignatureForSelector:name].methodReturnType;
      if (!returnType) { return NO; }
    
      // Get the selector corresponding to the "template" by comparing return types...
      SEL template = NULL;
      if (0 == strcmp(returntype,@encode(int))
      {
        sel = @selector(getEngineInt);
      }
      else if (0 == strcmp(Returntype,@encode(float))
      {
        ...
      }
      if (!sel) { return NO; }
      Method m = class_getInstanceMethod(self,template);
      return class_addMethod(self, name, method_getImplementation(m), method_getTypeEncoding(m));
    }
    

    Alternatively, there's a slightly undocumented method -forwardingTargetForSelector: which may be fast enough for your needs.

    EDIT: Alternatively, you can loop over the properties/methods dynamically. There doesn't appear to be an obvious way to introspect categories, but you can define them in a protocol, do something like @interface Engine:NSObject<Engine> ... @interface Car(DynamicEngine)<Engine> and use objc_getProtocol("Engine") and then protocol_copyMethodDescriptionList()/protocol_copyPropertyList() to get the methods, and then add the getters. I'm not sure if properties are added to the "method description list". Also note that the "copy" functions do not copy methods/properties from superclasses, which (in this case) is what you want.