8

UPDATE:

With some key suggestions and back and forth with George, I've come up with two different ways to achieve exactly what I want in CodeRunner and posted it on Github's gist site: Objective-C AOP gist

The code is rough because it's a new concept and I just finished at 1:30am. It definitely works though and has some niceties like auto-adding all methods that aren't initializers, getters or setters. [END UPDATE]

Several times (but certainly not very often) I've come across a situation where my code would be a bit DRYer if I could call a context-sensitive piece of code for each method in a class. Use of the Objective-C runtime is totally fine, I'd accept C or C++ solutions as well.

Instead of:

- (void)methodName1
{
   self->selector = _cmd;
   NSLog(@"This method is named: %@",_cmd);
   //more code
}

- (void)methodName2
{
   self->selector = _cmd;
   NSLog(@"This method is named: %@",_cmd);
   //more code
}

Have something like this, with the result being the same:

+ (void)AOPMethod
{
   self->selector = _cmd;
   NSLog(@"This method is named: %@",_cmd);
}

- (void)methodName1
{
   //more code
}

- (void)methodName2
{
   //more code
}

In a real-world application, AOPMethod would contain more code and there'd be more methods in the class.

P.S., I'm fairly obsessed with DRY. Along with clarity of prose and performance it's a key component of how I assess my code's quality over the long term. For each new way I can avoid repeating myself, the benefit is exponential because I break off as much code as possible in reusable classes that are shared across many projects.

james_womack
  • 10,028
  • 6
  • 55
  • 74
  • Are you just looking for enter/exit intercepts? – Georg Fritzsche Feb 13 '12 at 21:03
  • @GeorgFritzsche Thanks for the question. If a method intercept is platform-agnostic and will allow method-level context-sensitive data such as _cmd to be used within each method of a class—and without code duplication—then yes. In other words, if you can share a technique in one of the base languages mentioned (and not frameworks not available on all platforms) that will allow some version of the latter example to have the same result as the former, than that's an answer. Thanks again. – james_womack Feb 13 '12 at 22:52
  • For your specific use-case (all methods with the same signature) [this approach](http://stackoverflow.com/questions/9242571/copy-a-method-imp-for-multiple-method-swizzles) could be extended to patch all suitable methods in the method list. At the moment i can't think of an elegant more general solution, only inefficient ones. – Georg Fritzsche Feb 14 '12 at 13:02
  • @GeorgFritzsche I think some version of the suggested technique at the link you provided may work. I've run some test though and haven't found a way to be able to make one or two calls that sets this up for each method. In the end I think using NSProxy may be cleaner. – james_womack Feb 14 '12 at 22:51
  • What i linked would always require the presence of an interceptor method (i.e. the one that would call the "before" and "after" handlers) with the method signature of the methods you want. The only more general solution i can think of would be to handle it using `NSInvocation`s, but to get `forwardInvocation:` invoked you'd need to remove the original methods (not possible anymore in ObjC 2). I think `NSProxy` standins are the next-best thing without resorting to hacks. – Georg Fritzsche Feb 14 '12 at 23:08
  • @GeorgFritzsche With some of your though-leadership, and my hard work, I've developed working solutions using either NSProxy+enter/exit blocks properties+swizzling or runtime IMP caching+logical operator(to turn enter/exit on or off)+swizzling. I encapsulated both way of achieving AOP in an NSProxy subclass for greatest flexibility. My class intelligently prevents initialization methods, getters and setters for being duplicated with a prefix (prefixing is part of how the IMP caching / method forwarding works). Feel free to move your comments to an answer so I grant you the rep. – james_womack Feb 15 '12 at 09:24

1 Answers1

5

For the specific use-case in the question, one could provide a handler that replaces the original implementation functions and calls before/after handlers as well as the original functions using something like this approach. In general however method implementation patching won't work as one would have to provide a handler/interception method for every intercepted method signature.

What would work more general (i.e. for everything except variable argument functions) would be handling -forwardInvocation:. The problem here though is that we would have to get that method invoked in the first place. As we can't remove methods in ObjC2, that can't be done in place.

What can be done however is using proxies that implement forwardInvocation: and call our before/after handlers.

@interface AspectProxy : NSProxy {
    id target_;
}
- (id)initWithTarget:(id)target;
@end

@implementation AspectProxy
- (id)initWithTarget:(id)target {
    target_ = [target retain];
    return self;
}
- (void)dealloc {
    [target_ release];
    [super dealloc];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [target_ methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)inv {
    SEL sel = [inv selector];
    NSLog(@"forwardInvocation for: %@", NSStringFromSelector(sel));
    if (sel == @selector(aspectBefore:) || sel == @selector(aspectAfter:)) {
        return;
    }
    if ([target_ respondsToSelector:@selector(aspectBefore:)]) {
        [target_ performSelector:@selector(aspectBefore:) withObject:inv];
    }
    [inv invokeWithTarget:target_];
    if ([target_ respondsToSelector:@selector(aspectAfter:)]) {
        [target_ performSelector:@selector(aspectAfter:) withObject:inv];
    }
}
@end

As we don't need to return the actual instance from an init method, this could even be done transparently:

@interface Test : NSObject
- (void)someFunction;
@end

@implementation Test
- (id)init {
    if (self = [super init]) {
        return [[AspectProxy alloc] initWithTarget:[self autorelease]];
    }
    return self;
}
- (void)aspectBefore:(NSInvocation *)inv {
    NSLog(@"before %@", NSStringFromSelector([inv selector]));
}
- (void)aspectAfter:(NSInvocation *)inv {
    NSLog(@"after %@", NSStringFromSelector([inv selector]));
}
- (void)someFunction {
    NSLog(@"some function called");
}
@end

Now the following code:

Test *x = [[[Test alloc] init] autorelease];
[x someFunction];

... will print:

forwardInvocation for: someFunction
before someFunction
some function called
after someFunction

A runnable sample can be found in this gist.

Community
  • 1
  • 1
Georg Fritzsche
  • 97,545
  • 26
  • 194
  • 236
  • 1
    This is a really cool solution. I did something like this once to create a delegate proxy that lets me avoid having to write `if ([delegate respondsToSelector:]) {}` for optional methods. Works great! – Alex Feb 15 '12 at 14:39
  • I did end up using forwardInvocation & NSProxy as one of the options (which I learned of shortly after posting the question), but was more obsessed with using the runtime so I came up with a way to do that as well. Both methods are linked to in the gist in my question. I like how clean your NSProxy version is. – james_womack Feb 16 '12 at 03:03