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
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
(theva_list
). So the target method won't receive what it expects, obviously.Imagine the following:
On ARM, this will mean, for the
foo
function:Variables arguments are passed in
R1
,R2
andR3
, and theint
argument inR0
.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 onR2
.And when issuing your own call to
objc_msdSend
, you're at least overwriting the content ofR2
, with yourva_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).
Unfortunately, no. This is why functions which take variable arguments should provide identical implementations which take
va_list
s. 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.