2

I want to design a class (TrackingClass) that would be in charge of tracking the calls to some methods of an other class (TrackedClass), i.e. of setting up the method swizzling from what I understood.

So let's say I load up an array with @selectors of the instance methods of TrackedClass i'm interested in. Here is the pseudo-code I would like to run :

@implementation BCTrackedClass

-(void)doA
{
}
@end

and

@implementation BCTrackingClass

#import "BCTrackingClass.h"
#import "BCTrackedClass.h"
#include <objc/runtime.h>
#include <objc/objc-runtime.h>

@implementation BCTrackingClass

void myMethodIMP(id self, SEL _cmd);

void myMethodIMP(id self, SEL _cmd) 
{
    //NSLog(@"_cmd : %@",NSStringFromSelector(_cmd));
    [BCTrackingClass logCallForMethod:NSStringFromSelector(_cmd)];
    objc_msgSend(self,
                 NSSelectorFromString([NSString stringWithFormat:@"tracked%@",NSStringFromSelector(_cmd)]));
}

+(void)setUpTrackingForClass:(Class)aClass andMethodArray:(NSArray*)anArray //Array of selectorsStrings of methods to track
{
    for (NSString* selectorString in anArray)
    {
        SEL selector = NSSelectorFromString(selectorString);
        SEL trackedSelector = NSSelectorFromString([NSString stringWithFormat:@"tracked%@",selectorString]);

        class_addMethod(aClass,
                        trackedSelector,
                        (IMP) myMethodIMP, "v@:"); 

        //Swizzle the original method with the tracked one
        Method original = class_getInstanceMethod(aClass,
                        selector);
        Method swizzled = class_getInstanceMethod(aClass,
                        trackedSelector);
        method_exchangeImplementations(original, swizzled);
    }
}

+(void)logCallForMethod:(NSString*)aSelectorString
{
    NSLog(@"%@",aSelectorString);
}
@end

Theoretically, I'm just missing the bit of code where I could effectively create this new instance method trackedSelector. Can I achieve that ?

Edit

I updated the code with some new piece of information, am I getting closer ?

Edit 2

I set up a Github repository with a Demo application if people want to dynamically try out their ideas. Source : BCTrackingClass on Github

Edit 3

I finally come up with a working version of the code (cf Github repo, or just above). My next problem is : I want my class to be instance based (currently, all my methods are class methods), so that I can assign a property @property NSMutableDictionnary* to instances of the class for call logging. I'm not sure how to achieve that. Any ides ?

Bertrand Caron
  • 2,525
  • 2
  • 22
  • 49
  • The encoding string is `"v@::"` -- `void` return, object, `SEL`, `SEL`. I'm not understanding what you're missing. Could you try to explain more? – jscs Nov 07 '13 at 20:15
  • My bad, that's because I finally got my code to work and didn't update it yet here. If you have an idea how to accommodate my third issue though, I would love it. Feel free to fork ! – Bertrand Caron Nov 07 '13 at 20:49
  • 2
    Your code on github looks like it only works for methods with no arguments. You're probably better off using some type of proxy and utilizing message forwarding than swizzling implementations. – Carl Veazey Nov 07 '13 at 20:51
  • @CarlVeazey Sorry, I forgot to mention is was an obvious limitation right now (although swizzling the arguments too could be implemented properly, right ?). – Bertrand Caron Nov 07 '13 at 20:57
  • You might be able to do variation function arguments actually - I was working through this some last night and hit a road block but I think I was approaching this differently. – Carl Veazey Nov 07 '13 at 21:00
  • @CarlVeazey What do you mean by "variation function arguments" ? Thinking about the arguments problem, I all come down to this `void myMethodIMP` and my inability to generate dynamically designed IMP. Can I do something about it ? – Bertrand Caron Nov 07 '13 at 21:06
  • Sorry I got autocorrected from variadic. I think you'd need an IMP corresponding to every variation of objc_msgSend, and decide which one based on type encodings of method. – Carl Veazey Nov 07 '13 at 21:09
  • @CarlVeazey You are right, this bruteforce-ish dispatching somehow works (I've updated on github to deal with up to 3 (id) objects) but I can't imagine setting it up for every combination of encoding type (combinatorial complexity frightens me). Nothing more for dynamic IMP generation ? – Bertrand Caron Nov 07 '13 at 21:48
  • Nevermind, the approach I was thinking of won't work, as `objc_msgSendv` and the like are deprecated. – Carl Veazey Nov 07 '13 at 22:11

1 Answers1

0

Do you want to do it for all instances of all objects of that class? for some selectors or all of them? ...

If what you want is to track specific instances, then the simplest route is to use isa swizzling, doing that, more or less (the code is absolutely untested)

@interface ClassTracker
+ (void)trackObject:(id)object;
@end

static const char key;
@implementation ClassTracker
+ (void)trackObject:(id)object
{
    objc_setAssociatedObject(object, &key, [object class], OBJC_ASSOCIATION_ASSIGN);
    object_setClass(object, [ClassTracker class]);
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    Class aClass = objc_getAssociatedObject(self, &key);
    return [aClass instanceMethodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    Class aClass = objc_getAssociatedObject(self, &key);

    // do your tracing here

    object_setClass(self, aClass);
    [invocation invoke];
    object_setClass(self, [ClassTracker class]);
}

// dealloc is magical in the sense that you really want to undo your hooking
// and not resume it ever!
- (void)dealloc
{
    Class aClass = objc_getAssociatedObject(self, &key);

    object_setClass(self, aClass);
    [self dealloc];
}
@end

If it's used for reverse engineering or debug purposes, that should (with minor adaptations) do the trick.

If you intend that to be fast, then you have to do instance method swizzling, knowing their type and so forth.

My "solution" has the drawback that it will only trace entering calls, IOW if a selector calls other ones, since the isa swizzling is paused to recurse the call, then you don't see the new ones until you restore the isa swizzling.

There may be a way to forward the invocation to the original class, without undoing isa swizzling, but I reckon I was too lazy to search for it.

JTAS
  • 431
  • 4
  • 6