8

I have a class method returning a CGSize and I'd like to call it via the Objective-C runtime functions because I'm given the class and method names as string values.

I'm compiling with ARC flags in XCode 4.2.

Method signature:

+(CGSize)contentSize:(NSString *)text;

The first thing I tried was to invoke it with objc_msgSend like this:

Class clazz = NSClassFromString(@"someClassName);
SEL method = NSSelectorFromString(@"contentSize:");

id result = objc_msgSend(clazz, method, text);

This crashed with "EXC_BAD_ACCESS" and without a stack trace. I used this first because the documentation for objc_msgSend says,

When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. [...] Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

Next, I used objc_msgSend_stret like this:

Class clazz = NSClassFromString(@"someClassName);
SEL method = NSSelectorFromString(@"contentSize:");

CGSize size = CGSizeMake(0, 0);
objc_msgSend_stret(&size, clazz, method, text);

Using the above signature gives me the following two compiler errors and two warnings:

error: Automatic Reference Counting Issue: Implicit conversion of a non-Objective-C pointer type 'CGSize *' (aka 'struct CGSize *') to 'id' is disallowed with ARC

warning: Semantic Issue: Incompatible pointer types passing 'CGSize *' (aka 'struct CGSize *') to parameter of type 'id'

error: Automatic Reference Counting Issue: Implicit conversion of an Objective-C pointer to 'SEL' is disallowed with ARC

warning: Semantic Issue: Incompatible pointer types passing '__unsafe_unretained Class' to parameter of type 'SEL'

If I look at the declaration of the method, it is:

OBJC_EXPORT void objc_msgSend_stret(id self, SEL op, ...)
    __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

which is identical to objc_msgSend:

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
    __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

This explains the compiler errors to me but what do I use at the run-time level to invoke a class and its static method at run-time and return a struct value?

jscs
  • 63,694
  • 13
  • 151
  • 195
Jonas Gardner
  • 2,458
  • 5
  • 22
  • 28

2 Answers2

16

You need to cast objc_msgSend_stret to the correct function pointer type. It's defined as void objc_msgSend_stret(id, SEL, ...), which is an inappropriate type to actually call. You'll want to use something like

CGSize size = ((CGSize(*)(id, SEL, NSString*))objc_msgSend_stret)(clazz, @selector(contentSize:), text);

Here we're just casting objc_msgSend_stret to a function of type (CGSize (*)(id, SEL, NSString*)), which is the actual type of the IMP that implements +contentSize:.

Note, we're also using @selector(contentSize:) because there's no reason to use NSSelectorFromString() for selectors known at compile-time.

Also note that casting the function pointer is required even for regular invocations of objc_msgSend(). Even if calling objc_msgSend() directly works in your particular case, it's relying on the assumption that the varargs method invocation ABI is the same as the ABI for calling a non-varargs method, which may not be true on all platforms.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • 1
    It turns out that you don't even want to use 'objc_msgSend_stret' but instead, objc_msgSend with the correct casting you pointed out. Thanks! – Jonas Gardner Dec 06 '11 at 21:03
  • 4
    @JonasGardner: I believe this is actually dependent upon the size of the struct and the architecture in question. It's very possible that `CGSize` on the architectures you care about are a simple `objc_msgSend()`. You'd need to consult the architecture ABI documentation to find out which struct sizes/types will be returned in registers versus returned on the stack. – Lily Ballard Dec 06 '11 at 21:38
7

If you wish to retrieve a struct from your class method, you can use an NSInvocation as follows:

Class clazz = NSClassFromString(@"MyClass");
SEL aSelector = NSSelectorFromString(@"testMethod");

CGSize returnStruct; // Or whatever type you're retrieving

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[clazz methodSignatureForSelector:aSelector]];

[invocation setTarget:clazz];
[invocation setSelector:aSelector];

[invocation invoke];
[invocation getReturnValue:&returnStruct];

At the end of this, returnStruct should contain your struct value. I just tested this in an ARC-enabled application and this works fine.

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • 1
    `NSInvocation` is rather expensive. It's useful in many cases, but there are often faster ways to accomplish what you want. – Lily Ballard Dec 06 '11 at 20:34
  • @KevinBallard - Yup, just thought I'd throw it out there in case someone wanted a more abstracted solution than `objc_msgSend()`. – Brad Larson Dec 06 '11 at 22:49
  • Fair enough. `NSInvocation` would also remove the confusion about which architectures use `objc_msgSend()` vs `objc_msgSend_stret()` for a given method. – Lily Ballard Dec 06 '11 at 22:53