Make NSInvocation invoke a specific IMP

1.9k views Asked by At

I'm looking for a way to make an NSInvocation invoke a specific IMP. By default, it invokes the "lowest" IMP it can find (ie, the most-recently-overridden version), but I'm looking for a way to make it invoke an IMP from higher up in the inheritance chain. The IMP I want to invoke is determined dynamically, or else I'd be able to use the super keyword or something like that.

My thought was to use the -forwardInvocation: mechanism to capture a message (easy and already working) and then alter the IMP so it goes to a method that is neither the super implementation nor the furthest descendent's implementation. (hard)

The only thing I've found that comes remotely close is AspectObjectiveC, but that requires libffi, which makes it non-iOS compatible. Ideally I'd like this to be cross platform.

Any ideas?

disclaimer: i'm just experimenting


Trying out @bbum's idea of a trampoline function

So I think I've got things mostly set up; I've got the following trampoline that gets correctly added via class_addMethod(), and it does get entered:

id dd_trampolineFunction(id self, SEL _cmd, ...) {
    IMP imp = [self retrieveTheProperIMP];
    self = [self retrieveTheProperSelfObject];
    asm(
        "jmp %0\n"
        :
        : "r" (imp)
        );
    return nil; //to shut up the compiler
}

I've verified that both the proper self and the proper IMP are the right things prior to the JMP, and the _cmd parameter is also coming in properly. (in other words, I correctly added this method).

However, something is going on. I sometimes find myself jumping to a method (usually not the right one) with a nil self and _cmd. Other times I'll just crash in the middle of nowhere with an EXC_BAD_ACCESS. Ideas? (it's been a long time since I've done anything in assembly...) I'm testing this on x86_64.

7

There are 7 answers

2
bbum On BEST ANSWER

Given that you already have the IMP, you simply need a way to do a very raw forward of the method call to said IMP. And given that you are willing to use an NSInvocation like solution, then you could also build a similar proxy class.

If I were faced with this, I would create a simple proxying class that contained the IMP to be called and the target object (you'll need to set the self parameter). Then, I would write a trampoline function in assembly that takes the first argument, assumes it is an instance of the proxying class, grabs the self, stuffs it into the register holding argument 0, grabs the IMP and *JMPs to it as a tail call.

With trampoline in hand, you would then add that trampoline as an IMP for any selector on the proxying class that you want forwarded to a particular IMP....

To achieve any kind of generic mechanism like this, the key is to avoid anything having to do with rewriting the stack frame. Avoid the C ABI. Avoid moving arguments about.

0
tonklon On

I think your best choice is to use libffi. Have you seen the port to iOS at https://github.com/landonf/libffi-ios? I haven't tried the port, but i have successfully invoked IMP with arbitrary arguments on the Mac.

Have a look at JSCocoa https://github.com/parmanoir/jscocoa it includes code to help you prepare a ffi_cif structure from a Method and it also contains a version of libffi that should compile on iOS. (Haven't tested either)

0
Chuck On

NSInvocation is just an object representation of a message send. As such, it can't invoke a specific IMP any more than a normal message send could. In order to have an invocation call a specific IMP, you'd either need to write a custom NSInvocation class that goes through the IMP-calling routine or you'd have to write a trampoline that implements the behavior and then create an invocation that represents a message to the trampoline (i.e. you basically wouldn't be using NSInvocation for much of anything).

2
BJ Homer On

An untested idea:

Could you use object_setClass() to force the selection of the IMP that you want? That is…

- (void)forwardInvocation:(NSInvocation *)invocation {
    id target = [invocation target];
    Class targetClass = classWithTheImpIWant();
    Class originalClass = objc_setClass(target, targetClass);
    [invocation invoke];
    objc_setClass(target, originalClass);
}
0
user3480335 On

You should probably have a look at how we swizzle the implementation of a certain method on an instance of an object in https://github.com/tuenti/TMInstanceMethodSwizzler

Basically, you swizzle the method for all object of a class so when its called it look up in a dictionary whats is the implementation which has to be called for the target object, falling back to the original implementation if not found.

You can also use the private invokeWithImp: method, but this is discouraged if you intent to submit the app to the store.

0
Moritz On

you could add the IMP to the class using class_addMethod under a new selector and invoke that selector.

the temporary method can't be removed though.

0
rgeorge On

Added long after the fact, for reference:

You can do it with private API. Put this category somewhere convenient:

@interface NSInvocation (naughty)
-(void)invokeUsingIMP:(IMP)imp;
@end

and voila, it does exactly what you'd expect. I dug up this gem from one of Mike Ash's old blog posts.

Private API tricks like this are great for research or in-house code. Just remember to excise it from your appstore-bound builds.