Pergunta

I'm implementing a "Code Injector Class", that through method swizzling can give you the possibility to do something like this:

FLCodeInjector *injector = [FLCodeInjector injectorForClass:[self class]];
[injector injectCodeBeforeSelector:@selector(aSelector:) code:^{
    NSLog(@"This code should be injected");
}];

aSelector can be a method with variable number of arguments, and variable return type. Arguments / and return type can be objects or primitive type.

First, I attach the code of injectCodeBeforeSelector: to let you understand what I'm doing (I removed not interesting parts of the code):

- (void)injectCodeBeforeSelector:(SEL)method code:(void (^)())completionBlock
{

    NSString *selector = NSStringFromSelector(method);

    [self.dictionaryOfBlocks setObject:completionBlock forKey:selector];

    NSString *swizzleSelector = [NSString stringWithFormat:@"SWZ%@", selector];

    // add a new method to the swizzled class
    Method origMethod = class_getInstanceMethod(self.mainClass, NSSelectorFromString(selector));
    const char *encoding = method_getTypeEncoding(origMethod);

    [self addSelector:NSSelectorFromString(swizzleSelector) toClass:self.mainClass methodTypeEncoding:encoding];
    SwizzleMe(self.mainClass, NSSelectorFromString(selector), NSSelectorFromString(swizzleSelector));

}

-(void)addSelector:(SEL)selector toClass:(Class)aClass methodTypeEncoding:(const char *)encoding
{
    class_addMethod(aClass,
                    selector,
                    (IMP)genericFunction, encoding);
}

Basically, I use class_addMethod to add the fake/swizzle method to the destination class, then do the swizzle. The implementation of the method is set to a function like this:

id genericFunction(id self, SEL cmd, ...) {
    // call the block to inject
    ...
    // now forward the message to the right method, since method are swizzled
    // I need to forward to the "fake" selector SWZxxx

    NSString *actualSelector = NSStringFromSelector(cmd);
    NSString *newSelector = [NSString stringWithFormat:@"SWZ%@", actualSelector];
    SEL finalSelector = NSSelectorFromString(newSelector);

    // forward the argument list
    va_list arguments;
    va_start ( arguments, cmd );

    return objc_msgSend(self, finalSelector, arguments);
}

now the problem: I have an EXC_BAD_INSTRUCTION ( objc_msgSend_corrupt_cache_error ()) on the last line. The problem happens if i forward the va_list arguments to the fake selector. If I change the last line to

return objc_msgSend(self, cmd, arguments);

no error, but obviously an infinite recursion starts.

I've tried to:

  • use va_copy
  • remove the swizzle before sending the message

but no results. I think that the problem is related to this fact: va_list is not a simple pointer, it can be something similar to an offset relative to the stack address of the method. So, I can't call objc_msgsend of a function (the swizzled function) with the arg list of another function (the non-swizzled one).

I tried to change approach and copy all arguments in an NSInvocation, but I had other problems managing the return value of the invocation, and even copy arguments one by one (managing all different types) requires a lot of code, so I preferred to return to this approach, that seems cleaner to me (imho)

Do you have any suggestion? Thanks

Foi útil?

Solução

The main problem here is how the variable arguments are passed to the function.

Usually, they are passed on the stack, but as far as I know, it's not the case with the ARM ABI, where registers are used, at least when possible.

So you've got two issues here.
First, the compiler may mess up those registers, while executing the code of your own method.
I'm not sure about this, as I don't know much about the ARM ABI, so you should check in the reference.

Second issue, more important, you are actually passing a single variable argument to obj_msgSend (the va_list). So the target method won't receive what it expects, obviously.

Imagine the following:

void bar( int x, ... )
{}

void foo( void )
{
    bar( 1, 2, 3, 4 );
}

On ARM, this will mean, for the foo function:

movs    r0, #1
movt    r0, #0
movs    r1, #2
movt    r1, #0
movs    r2, #3
movt    r2, #0
movs    r3, #4
movt    r3, #0
bl      _bar

Variables arguments are passed in R1, R2 and R3, and the int argument in R0.

So in your case, as a call to objc_msdSend was used to call your method, R0 should be the target object's pointer, R1 the selector's pointer, and variable arguments should begin on R2.

And when issuing your own call to objc_msdSend, you're at least overwriting the content of R2, with your va_list.

You should try not to care about the variable arguments. With a little luck, if the code previous to the objc_msgSend call (where you get the final selector) doesn't mess up those registers, the correct values should still be there, making them available for the target method.

This would of course only work on the real device, and not in the simulator (simulator is x86, so variadic parameters are here passed on the stack).

Outras dicas

Unfortunately, no. This is why functions which take variable arguments should provide identical implementations which take va_lists. APIs which provided this functionality (e.g. objc_msgSendv) have been marked 'Unavailable" since ObjC 2. Perhaps it would also be good to figure out why this functionality was removed.

Variadic Macros would often solve the issue, but not in your case because you need a function pointer to swizzle.

So I think you will need to look to your system implementation to understand the mechanics of the VA List on your targeted implementations.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top