How do I observe the invocation of a selector

285 views Asked by At

we sometimes want to observe a invocation of selector so that we could perform pre-process or post-process to the call automatically.

this is not difficult if the target is a custom class in the observation. There are serval approaches to achieve this goal. However, it is difficult to do it for ANY class. for example, observe [NSViewController -initWithNibName:bundle:], [NSObject -init]

What I tried to do is to have something like:

void observe(Class clazz, SEL op, isClassMethod, CALLBACK pre, CALLBACK post);

in order to do this, I need to define:

id replacedMethod(id target, SEL op, ...);

void replacedMethod2(id target, SEL op, ...);

...

and then inside of the observe function above, I get the original implementation of the class and put it into a map.

WFFuncWrap *wrap;
NSString *key;
...
wrap = [[WFFuncWrap alloc] init];
wrap->_isClassMethod = isClassMethod;
if (isClassMethod) {
    wrap->_method = class_getClassMethod(clazz, op);
}else{
    wrap->_method = class_getInstanceMethod(clazz, op);
}
wrap->_func = method_getImplementation(wrap->_method);
[_dict setObject:wrap forKey:key];
...

and after that, I use the 'class_replaceMethod' function to replace the original implementation of the class with one of the above 'replacedMethod' functions.

inside of these 'replacedMethod' functions, i invoke the callbacks at the begin and the end. in theory, i only need to look up the original implementation and put it in between the 'pre' and the 'post' in order to achieve the goal. However, that is the difficult part I cannot get it solve. it is difficult because of the signature the original implementation is not fixed.

I did not found a way to call func(id, SEL, ...) in side of another gunc(id, SEL, ...). Actually, I belief assemble code will help to solve the problem, but that is sth that I am not familiar with. I tried, but it is too complex.

id replacedMethod(id obj, SEL op, ...){
    CALLBACK pre, post;
    IMP originalImp;
    id retObj;

    ...
    pre(obj, op);
    //call original implementation, HOW ?
    post(obj, op);
    return retObj;
}

It there any idea to solve this problem? Thanks a lot!!

2

There are 2 answers

0
john fantasy On

Thanks for Nathan Day, who gave me the idea of using NSProxy. It is a clean solution to my problem if impact to performance is no need to consider seriously. I did not take NSProxy as my solution, because performance is considered seriously in my project.

forwarding a call to another function without knowing its signature is difficult in c language. Actually, there is no way to call a function real don't know its the signature, because in that case, we will not be able to know where to read and how to pass the arguments.

I found that, different cpu architectures and compilers have different calling conventions. gcc compiler on 32 bit cpu is easier to be handled since the arguments are stored one by one in order. You could read them and pass them once you know their types, order and basic address.

However, gcc for 64bit cpu stores the arguments in a complex way. integers, flow points and overflowed arguments are stored in 3 different segments in the memory. structs arguments could stored in all three segments in different situations. Only knowing the types, order and basic address will not be enough to find out the address of the arguments, but also need the exact rule of the convention. I found the rules of convention. However, there is no document to prove that what I found is actually what the compiler does. Moreover, that requires process the runtime method description, something like: '@32@0:8@16{?=id}24', which is not easy to be parse.

If there is an approach could tell the exact offsets of the arguments, then my problem could be solved. There is a function 'method_getArgumentInfo', looks like what I was looking for. However, it is not available in OBJC2

Fortunately, I finally found that the debug description of the NSMethodSignature class could help me to find out where are the argument located by providing a string description that marks down the offsets of the arguments:

number of arguments = 3
frame size = 248
is special struct return? NO
return value: -------- -------- -------- --------
    type encoding (@) '@'
    flags {isObject}
    modifiers {}
    frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
    memory {offset = 0, size = 8}
argument 0: -------- -------- -------- --------
    type encoding (@) '@'
    flags {isObject}
    modifiers {}
    frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
    memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
    type encoding (:) ':'
    flags {}
    modifiers {}
    frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
    memory {offset = 0, size = 8}
argument 2: -------- -------- -------- --------
    type encoding ({) '{?=[3q]}'
    flags {isStruct}
    modifiers {}
    frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
    memory {offset = 0, size = 24}
        type encoding ([) '[3q]'
        flags {isArray}
        modifiers {}
        frame {offset = 224, offset adjust = 0, size = 24, size adjust = 0}
        memory {offset = 0, size = 24}
            type encoding (q) 'q'
            flags {isSigned}
            modifiers {}
            frame {offset = 224, offset adjust = 0, size = 8, size adjust = 0}
            memory {offset = 0, size = 8}
            type encoding (q) 'q'
            flags {isSigned}
            modifiers {}
            frame {offset = 232, offset adjust = 0, size = 8, size adjust = 0}
            memory {offset = 8, size = 8}
            type encoding (q) 'q'
            flags {isSigned}
            modifiers {}
            frame {offset = 240, offset adjust = 0, size = 8, size adjust = 0}
            memory {offset = 16, size = 8}

And by the assist of NSInvocation and class_replaceMethod I can finally find the solution. The weakness is that there is no guarantee that the description of NSMethodSignature will always describe the accurate informations.

the following code segment is the detail implementation of the solution. NOTE 1. this is not a complete implementation, but just show the idea of doing this. 2. this code only tested with x64 cpu 3. in order to handle any selector, it need some more agent functions to handle different return types

#import "WFObserveMessageException.h"
#import <objc/runtime.h>

@interface WFImplementationInfo : NSObject{
    @package
    Class _class;
    IMP _func;
    Method _method;
    BOOL _isClassMethod;
    NSInvocation *_invoke;
    int _countArg;
    int *_argOffsets;
    _wf_message_observer_t _observer;
}
@end


@implementation WFImplementationInfo
-(void)dealloc{
    if (_argOffsets) {
        free(_argOffsets);
        _argOffsets = NULL;
    }
}
@end



NSMutableDictionary *getFunctionDict(){
    static NSMutableDictionary *dict;
    if(!dict){
        dict = [NSMutableDictionary dictionary];
    }
    return dict;
}

NSString *keyForClassAndOp(NSString *className, NSString *opName, BOOL isClassMethod){
    return [NSString stringWithFormat:@"%c[%@ %@]", (isClassMethod ? '+' : '-'), className, opName];
}

WFImplementationInfo *prepareInvocation(id obj, SEL op, va_list ap){
    Class clazz;
    WFImplementationInfo *func;
    BOOL isMetaClass;
    NSString *key;
    NSDictionary *dict;

    clazz = [obj class];
    dict = getFunctionDict();
    //search and see if the this class or its parent classes is observed, there must at lest one of them is observed
    while (clazz) {
        isMetaClass = class_isMetaClass(clazz);
        key = keyForClassAndOp(NSStringFromClass(clazz), NSStringFromSelector(op), isMetaClass);
        func = [dict objectForKey:key];
        if (func) {
            break;
        }
        clazz = class_getSuperclass(clazz);
    }

    func->_invoke.target = obj;
    for (int i = 2; i < func->_countArg; i++) {
        //set up the arguments of the invocation.
        [func->_invoke setArgument:ap->reg_save_area + func->_argOffsets[i] atIndex:i];
    }
    return func;
}


id agent(id obj, SEL op, ...){
    va_list ap;
    WFImplementationInfo *func;
    id retObj;

    //the va_list could tell where is the base of the arguments
    va_start(ap, op);
    func = prepareInvocation(obj, op, ap);
    va_end(ap);
    class_replaceMethod(func->_class, op, func->_func, nil);
    func->_observer(func->_invoke, &retObj);
    class_replaceMethod(func->_class, op, (IMP)agent, nil);
    return retObj;
}

void wf_observe_message(NSString *className, NSString *opName, _wf_message_observer_t observer){
    WFImplementationInfo *func;
    NSMethodSignature *signature;
    NSScanner *scanner;
    NSString *desc, *key;
    SEL op;
    int sign, count;

    func = [[WFImplementationInfo alloc] init];
    func->_observer = observer;
    func->_class = NSClassFromString(className);

    sign = [opName characterAtIndex:0];
    opName = [opName substringFromIndex:1];
    op = NSSelectorFromString(opName);
    switch (sign) {
        case '-':
            func->_isClassMethod = NO;
            func->_method = class_getInstanceMethod(func->_class, op);
            signature = [func->_class instanceMethodSignatureForSelector:op];
            break;
        case '+':
            func->_isClassMethod = YES;
            func->_method = class_getClassMethod(func->_class, op);
            signature = [func->_class methodSignatureForSelector:op];
            break;
        default:
            WFThrow WFObserveMessageExceptionA(@"Selector name MUST start with '-' or '+' for indicating whether it is a instance method");
            break;
    }

    key = keyForClassAndOp(className, opName, func->_isClassMethod);

    func->_func = method_getImplementation(func->_method);
    func->_countArg = method_getNumberOfArguments(func->_method);
    func->_argOffsets = malloc((func->_countArg) * sizeof(int));
    func->_invoke = [NSInvocation invocationWithMethodSignature:signature];
    func->_invoke.selector = op;
    func->_argOffsets[0] = 0; //offset of id
    func->_argOffsets[1] = 8; //offset of SEL
    count = 2;

    desc = [signature debugDescription];
    scanner = [NSScanner scannerWithString:desc];
    [scanner scanUpToString:@"argument 2" intoString:nil];

    //scan the offsets of the arguments
    while (!scanner.isAtEnd) {
        [scanner scanUpToString:@"offset = " intoString:nil];
        scanner.scanLocation = scanner.scanLocation + 9;
        [scanner scanInt:&func->_argOffsets[count]];
        if(func->_argOffsets[count] == 0){  //if the offset is 0, that means the argument is a struct, the offset of the struct is the offset of the its first member
            [scanner scanUpToString:@"offset = " intoString:nil];
            scanner.scanLocation = scanner.scanLocation + 9;
            [scanner scanInt:&func->_argOffsets[count]];
        }
        [scanner scanUpToString:@"argument" intoString:nil];
        count++;
    }

    [getFunctionDict() setObject:func forKey:key];

    //check if the method is valid
    if (!func->_method) {
        WFThrow WFObserveMessageExceptionA(@"Class has no selector '%@' for class '%@'", opName, className);
    }
    class_replaceMethod(func->_class, op, (IMP)agent, nil);
}
2
Nathan Day On

A completely different way to do what you are trying to do, so I don't know if this will help, is to create a proxy class that can forward all methods invoked onto it to a target class (look at NSProxy), you can then use -[NSObject forwardInvocation:] which gives you all of the arguments packaged in a NSInvocation object. At runtime your proxy object can pass itself of as pretty much any other class, their may be some edge cases, because your proxy class can override methods like -[NSObject isKindOfClass:], -[NSObject respondsToSelector:] etc.

You can then do anything you want before and after the method invocation.