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:
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
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With