Yes, this is indeed possible, but this solution is ABI-specific (not guaranteed to work on all platforms), and makes extensive use of the information available at run-time about methods.
What we first must do is get information about the method we are wrapping with the block. This is done via NSMethodSignature
, which contains information such as:
- Number of arguments
- Size (in bytes) of each argument
- Size of return type
This allows us to wrap (almost) any method with no specific code for that method, thus creating a re-usable function.
Secondly, we need a way to safely dispatch method calls at run-time. We do this via NSInvocation
, which grants us the ability to create a dynamic, and safe, method call at run-time.
Thirdly, we need to have a block that can take any number of arguments passed in, and the dispatch that. This is done via C's va_list
APIs, and should work for 99% of methods.
Finally, we need to get the return value, and be able to return that from our block. This is the part of the entire operation that is possible to not work, because of weirdness with returning structs and such with the Objective-C runtime.
However, as long as you keep to primitive types and Objective-C objects, this code should work great for you.
A couple of things to note about this implementation:
It is reliant upon undefined behavior with casting of block & function types, however, because of the calling conventions of iOS and Mac, this should not pose any issues (unless your method has a different return type than what the block expects).
It also relies upon undefined behavior with the result of calling va_arg
with a type that may not be what is passed - however, since the types are of the same size, this should never be an issue.
Without any further ado, here is an example of the code, followed by the implementation:
@interface MyObj : NSObject
-(void) doSomething;
@end
@implementation MyObj
-(void) doSomething
{
NSLog(@"This is me, doing something! %p", self);
}
-(id) doSomethingWithArgs:(long) arg :(short) arg2{
return [NSString stringWithFormat:@"%ld %d", arg, arg2];
}
@end
int main() {
// try out our selector wrapping
MyObj *obj = [MyObj new];
id (^asBlock)(long, short) = createBlock(obj, @selector(doSomethingWithArgs::));
NSLog(@"%@", asBlock(123456789, 456));
}
/* WARNING, ABI SPECIFIC, BLAH BLAH BLAH NOT PORTABLE! */
static inline void getArgFromListOfSize(va_list *args, void *first, size_t size, size_t align, void *dst, BOOL isFirst) {
// create a map of sizes to types
switch (size) {
// varargs are weird, and are aligned to 32 bit boundaries. We still only copy the size needed, though.
// these cases should cover all 32 bit pointers (iOS), boolean values, and floats too.
case sizeof(uint8_t): {
uint8_t tmp = isFirst ? (uint32_t) first : va_arg(*args, uint32_t);
memcpy(dst, &tmp, size);
break;
}
case sizeof(uint16_t): {
uint16_t tmp = isFirst ? (uint32_t) first : va_arg(*args, uint32_t);
memcpy(dst, &tmp, size);
break;
}
case sizeof(uint32_t): {
uint32_t tmp = isFirst ? (uint32_t) first : va_arg(*args, uint32_t);
memcpy(dst, &tmp, size);
break;
}
// this should cover 64 bit pointers (Mac), and longs, and doubles
case sizeof(uint64_t): {
uint64_t tmp = isFirst ? (uint64_t) first : va_arg(*args, uint64_t);
memcpy(dst, &tmp, size);
break;
}
/* This has to be commented out to work on iOS (as CGSizes are 64 bits)
// common 'other' types (covers CGSize, CGPoint)
case sizeof(CGPoint): {
CGPoint tmp = isFirst ? *(CGPoint *) &first : va_arg(*args, CGPoint);
memcpy(dst, &tmp, size);
break;
}
*/
// CGRects are fairly common on iOS, so we'll include those as well
case sizeof(CGRect): {
CGRect tmp = isFirst ? *(CGRect *) &first : va_arg(*args, CGRect);
memcpy(dst, &tmp, size);
break;
}
default: {
fprintf(stderr, "WARNING! Could not bind parameter of size %zu, unkown type! Going to have problems down the road!", size);
break;
}
}
}
id createBlock(id self, SEL _cmd) {
NSMethodSignature *methodSig = [self methodSignatureForSelector:_cmd];
if (methodSig == nil)
return nil;
return ^(void *arg, ...) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setTarget:self];
[invocation setSelector:_cmd];
NSUInteger argc = [methodSig numberOfArguments];
va_list args;
va_start(args, arg);
for (int argi = 2; argi < argc; argi++) {
const char *type = [methodSig getArgumentTypeAtIndex:argi];
NSUInteger size;
NSUInteger align;
// get the size
NSGetSizeAndAlignment(type, &size, &align);
// find the right type
void *argument = alloca(size);
getArgFromListOfSize(&args, arg, size, align, argument, argi == 2);
[invocation setArgument:argument atIndex:argi];
}
va_end(args);
[invocation invoke];
// get the return value
if (methodSig.methodReturnLength != 0) {
void *retVal = alloca(methodSig.methodReturnLength);
[invocation getReturnValue:retVal];
return *((void **) retVal);
}
return nil;
};
}
Let me know if you have any issues with this implementation!