Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call instance method with objc_msgSend

I'm trying to use the objc_msgSend method to call some method dynamically. Say I want call some method in Class B from Class A and there are two methods in class B like:

- (void) instanceTestWithStr1:(NSString *)str1 str2:(NSString *)str1;
+ (void) methodTestWithStr1:(NSString *)str1 str2:(NSString *)str1;

And I can call the class method like this in Class A successfully:

objc_msgSend(objc_getClass("ClassB"), sel_registerName("methodTestWithStr1:str2:"), @"111", @"222");

And I can call the instance method like this in Class A successfully as well:

objc_msgSend([[objc_getClass("ClassB") alloc] init], sel_registerName("instanceTestWithStr1:str2:"), @"111", @"222");

But the thing is to get a instance of Class B I have to call "initWithXXXXX:XXXXXX:XXXXXX" instead of "init" so that to pass some necessary parameters to class B to do the init stuff. So I stored a instance of ClassB in class A as variable: self.classBInstance = [[ClassB alloc] initWithXXXXX:XXXXXX:XXXXXX];

And then I call the method like this (successfully):

The problem is, I want to call a method by simply applying the classname and the method sel like "ClassName" and "SEL" and then call it dynamically:

  1. If it's a class method. then call it like: objc_msgSend(objc_getClass("ClassName"), sel_registerName("SEL"));

  2. If it's a instance method, find the existing class instance variable in the calling class then: objc_msgSend([self.classInstance, sel_registerName("SEL"));

So I want to know if there is any way to:

  1. Check if a class has a given method (I found "responseToSelector" will be the one)

  2. Check if a given method in class method or instance method (maybe can use responseToSelector as well)

  3. Check if a class has a instance variable of a given class So I can call a instance method like: objc_msgSend(objc_getClassInstance(self, "ClassB"), sel_registerName("SEL"));
like image 953
supersuraccoon Avatar asked Sep 19 '12 03:09

supersuraccoon


2 Answers

You'll probably want to read this. What you are asking is effectively "I want to make a new dispatcher" and to answer that question, you ought to have a thorough understanding of how the existing dispatcher works.

Please tell met that is what you are doing? Building a bridge between languages? Because if not, you are way deep down a rabbit hole that'll be damned interesting to explore, but likely not a terribly efficient nor elegant solution.

Now:

The problem is, I want to call a method by simply applying the classname and the method sel like "ClassName" and "SEL" and then call it dynamically:

  1. If it's a class method. then call it like: objc_msgSend(objc_getClass("ClassName"), sel_registerName("SEL"));
Class klass = objc_getClass("ClassName"); // NSClassFromString(@"ClassName")
SEL sel = sel_getUID("selector"); // NSSelectorFromString(@"selector");
if ( [klass respondsToSelector:sel] )
    objc_msgSend(klass, sel);

If you have arguments you want to pass, see below. NSInvocation in Richard's answer is a high level approach, but is an indirect use of objc_msgSend() (and NSInvocation has limitations).

"2". If it's a instance method, find the existing class instance variable in the calling class then: objc_msgSend([self.classInstance, sel_registerName("SEL"));

That doesn't make sense. A class doesn't have an instance variable. An instance of a class has an instance variable, but then you likely need a specific instance and not some random instance you create in this one spot. Instances carry state and accrete that state over time.

In any case, you can easily call the classInstance method on the class using the mechanism above (which would be completely pointless -- just write [self classInstance] and be done with it) and, from there:

id classInstance = [self classInstance];
SEL sel = ... get yer SEL here ...;
if ([classInstance respondsToSelector:sel])
   objc_msgSend(classInstance, sel);

Obviously, if you need arguments, see below.

So I want to know if there is any way to:

  1. Check if a class has a given method (I found "responseToSelector" will be the one)

See above. Classes responds to respondsToSeletor:. If you want to check to see if instances of the class respond to a selector, you can invoke instancesRespondToSelector:.

Class klass = ... get yer class on...;
SEL someSelector = ... get that SEL ...;
if ([klass instancesRespondToSelector:someSelector])
    objc_msgSend(instanceOfKlassObtainedFromSomewhere, someSelector);

Again, arguments? See below.

"2". Check if a given method in class method or instance method (maybe can use responseToSelector as well)

See above. Given a class, you check to see if the class or instances respond to any given selector. Note that for many selectors in the NSObject protocol, classes will respond to many of the NSObject instance methods because the meta class -- the class that classes are an instance of -- implements quite a few of said methods.

"3". Check if a class has a instance variable of a given class So I can call a instance method like: objc_msgSend(objc_getClassInstance(self, "ClassB"), sel_registerName("SEL"));

The relationship between a setter/getter method and an instance variable is entirely coincidental. There doesn't need to be an ivar, nor does there need to be a setter and/or getter for any given ivar. Thus, this question doesn't make sense because arbitrarily calling a method based on an ivar name will often fail.

As Richard suggests, you can use Key Value Coding, but that will imply manual boxing of values passed to the setter and manual unboxing of values retrieved from the getter for non-object types.

Under the covers, KVC implements a heuristic to search the class for a method or ivar with a name that mostly matches the requested name. Mostly because it'll do things like search for _ prefixes, etc. The NSKeyValueCoding.h header is an interesting read.

In any case, no need for a selector. Given a name, just do:

id foo = [myInstance valueForKey:@"iVarName"];

And:

[myInstance setValue:[NSNumber numberWithInt:42] forKey:@"ivarName"];

Obviously, typing is a major issue. If you have non-object types, then you are going to have to deal with getting 'em in/out of the NSValue containers and not all things will fit, which leaves you with reverse engineering the KVC method/ivar search algorithm (not too hard -- just a bunch of string manipulation and lookups) and then passing the arbitrary arguments as below.


Note that both of your calls to objc_msgSend() are technically wrong because neither is typecasting objc_msgSend() to a non-varargs form with the explicit argument types. You would need something like:

// - (void) instanceTestWithStr1:(NSString *)str1 str2:(NSString *)str1;
void (*msgSendVoidStrStr(id, SEL, NSString*, NSString*) = (void*)objc_msgSend;
msgSendVoidStrStr(...obj..., @selector(instanceTestWithStr1:str2:), str1, str2);

This is because the varargs ABI and explicit argument typed ABI is not necessarily compatible on all architectures. ARC, IIRC, enforces this explicitly.


Note also that the notion of arbitrarily calling class or instance methods where calling the instance method instantiates an instance of the class on the fly really doesn't make much sense. But, hey... your code.


Note that you also don't ever want to call sel_registerName() in that fashion; if you are about to call a selector, it better already exist. That function exists explicitly for defining classes at runtime. Best to use NSSelectorFromString() or sel_getUid() (which, unfortunately, effectively ends up calling sel_registerName() because of undisciplined programmers over the years). At least your intentions will be right.


Now, to use objc_msgSend() as you desire requires you to answer one question for which the resulting answers will be radically difference. One answer is an easy route to "oh, just do X", the other is "oh, holy cow, you are heading down a path of pain".

The question: Do you have a fixed set of method signatures or must you pass an arbitrary set of arguments of many types?

Ultimately, how many and how many different kinds of arguments will dictate how complex the code will be. If you only ever have 0,1 or 2 arguments and they are always objects, stick with invokeSelector:, invokeSelector:withObject: and invokeSelector:withObject:withObject:.

If the answer is "fixed set of method signatures" then the answer is above; simply declare a function pointer with all the different possible method signatures you want to use and pick the right one at runtime and call it as a function call as per above.

Now, if the answer is "arbitrary set of selectors with many different combinations of arguments", the answer is much more difficult. You'll need to use libffi (or something like it) to programmatically do what the compiler does when it compiles msgSendVoidStrStr(...obj..., @selector(instanceTestWithStr1:str2:), str1, str2);. libffi provides everything you need to encode calls with nearly arbitrary arguments and return types.

It is not easy to use. In fact, constructing your own stack frames using libffi is hard enough that it may be easier to write a script that dumps all the possible combinations of calls and creates a cover function for each combination, potentially taking the arguments as an NSArray* container and decoding them internally. Something like (auto-genned):

void msgSendVoidStrStr(id obj, SEL _cmd, NSArray*args) {
    objc_msgSend(obj, _cmd, [args objectAtIndex:0], [args objectAtIndex:1]);
}

This proves to be significantly easier to debug than writing a bunch of tricksie runtime code.

like image 145
bbum Avatar answered Sep 21 '22 14:09

bbum


Alright, so here's my rudimentary implementation. It assumes quite a bit:

  • All objects MUST subclass NSObject. Period. If you don't, then you will have issues down the line with -methodSignatureForSelector.
  • All methods must have a valid signature. This means that runtime-hackers, like myself, are sort of screwed when it comes to adding methods dynamically and must do our research first.
  • It assumes that sending a message to a primitive is OK (using the auto-boxing provided by KVC, e.g. double is promoted to NSNumber)
  • It does not support primitive arguments passed to a function (so only objects here, or pointers if you want to be crazy with bridging)
  • It also does not support variable-length functions (a limitation of NSInvocation). If you want to do this, try looking for the versions of the functions that take a va_list and use them instead.
  • It only checks for iVars, not properties, however @synthesize'd properties should already be in the list.

Here are some compilation notes:

  • ARC must be enabled. I don't code for non-ARC anymore, so if someone wants to try and back-port this, they can be my guest.
  • C99 VLAs are also required. Any compiler that is compiling ARC Objective-C should already have this (in fact, I think clang is the only compiler that supports ARC, which indeed supports C99 VLAs), but if it doesn't then you can try and mess with malloc & friends.
  • This is untested when compiled for iOS architectures. I only tested using Mac OS, but the methods I am using here should be available on iOS, if they aren't, let me know and I'll fix it.

Without further ado, here is the code (I added a few NSNumber and NSString categories in there for testing, but they are irrelevant to the purpose of this code):

#import <objc/runtime.h>

@interface NSObject(dynamicSELlookup)

-(void) performSelectorOnClassOrIvar:(Class) cls selector:(SEL) selector arguments:(NSArray *) args;

@end

@implementation NSObject (dynamicSELlookup)

-(void) performSelectorOnClassOrIvar:(Class) cls selector:(SEL) selector arguments:(NSArray *) args
{
    // we must copy to a C-array so we can take adresses. here we use C99's VLAs, so we don't have to free anything
    __unsafe_unretained id argsArray[args.count];
    [args getObjects:argsArray];

    // if its a static method, then our job is simple. create a NSInvocation from our arguments, and send it on it's way
    if ([cls respondsToSelector:selector])
    {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[cls methodSignatureForSelector:selector]];

        for (int i = 0; i < args.count; i++)
        {
            // notice the '+ 2' here. this is because there are two 'hidden' arguments to an objective-c message call - '_cmd' & 'self'.
            [invocation setArgument:&argsArray[i] atIndex:i + 2];
        }

        // set the selector of the invocation, and fire it off!
        [invocation setSelector:selector];
        [invocation invokeWithTarget:cls];
        return;
    }

    // otherwise loop through all the iVars.
    unsigned iVarCount = 0;
    Ivar *iVars = class_copyIvarList([self class], &iVarCount);

    for (int i = 0; i < iVarCount; i++)
    {
        // We are going to use KVC here, so we can auto-box our return values (thus it works for primitives too)
        id value = [self valueForKey:@(ivar_getName(iVars[i]))];

        // make sure the target class is OK, and that we respond to the selector
        if ([value isKindOfClass:cls] && [value respondsToSelector:selector])
        {
            // just like before, we create our invocation
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[value methodSignatureForSelector:selector]];

            for (int i = 0; i < args.count; i++)
            {
                // notice the '+ 2' here. this is because there are two 'hidden' arguments to an objective-c message call - '_cmd' & 'self'. 
                [invocation setArgument:&argsArray[i] atIndex:i + 2];
            }

            // set the selector of the invocation, and fire it off!
            [invocation setSelector:selector];
            [invocation invokeWithTarget:value];
            // uncomment the below line if you only want to execute on the first target found
            // break;
        }
    }

    free(iVars);
}

@end

@interface MyObject : NSObject
{
    @public
    int someIntegerVar;
    double someDoubleVar;

    NSObject *someObjectVar;
}

@end

@implementation MyObject
@end

@implementation NSNumber(print)

+(void) classMethod
{
    NSLog(@"Hey, I'm a class method!");
}

// simple category for showing ivars off
-(void) printValue
{
    NSLog(@"%@", self);
}

-(void) printValueWithArg:(id) argument
{
    NSLog(@"%@ - %@", self, argument);
}

@end

@implementation NSString (print)

-(void) print
{
    NSLog(@"%@", self);
}

-(void) printFormat:(id) arg
{
    NSLog(self, arg);
}

@end

// Sample Usage
int main()
{
    @autoreleasepool
    {
        MyObject *obj = [MyObject new];
        obj->someDoubleVar = M_PI;
        obj->someIntegerVar = 5;
        obj->someObjectVar = @"hello there, %@";

        [obj performSelectorOnClassOrIvar:[NSNumber class] selector:@selector(printValue) arguments:nil];
        [obj performSelectorOnClassOrIvar:[NSNumber class] selector:@selector(classMethod) arguments:nil];
        [obj performSelectorOnClassOrIvar:[NSNumber class] selector:@selector(printValueWithArg:) arguments:@[ @"Hello" ]];
        [obj performSelectorOnClassOrIvar:[NSString class] selector:@selector(print) arguments:nil];
        [obj performSelectorOnClassOrIvar:[NSString class] selector:@selector(printFormat:) arguments:@[ @"Richard J Ross III"]];
    }
}

Output:

2012-09-19 00:05:07.150 TestProj[8592:303] 5
2012-09-19 00:05:07.152 TestProj[8592:303] 3.141592653589793
2012-09-19 00:05:07.152 TestProj[8592:303] Hey, I'm a class method!
2012-09-19 00:05:07.153 TestProj[8592:303] 5 - Hello
2012-09-19 00:05:07.153 TestProj[8592:303] 3.141592653589793 - Hello
2012-09-19 00:05:07.154 TestProj[8592:303] hello there, %@
2012-09-19 00:05:07.154 TestProj[8592:303] hello there, Richard J Ross III
like image 43
Richard J. Ross III Avatar answered Sep 19 '22 14:09

Richard J. Ross III