Ian Spence Ian Spence - 1 month ago 15
iOS Question

Method parameters are nil when called using IMP in Release configuration

Our Swift application needed some lower-level C/Objective-C code, so we added a Dynamic Library to make integration with the application easier.

The library has a single, shared instance of a controller, but the style of the callbacks doesn't work well for closures, so we went with a protocol. However since multiple classes need to use this controller it would need to have multiple delegates. So each class registers itself as a delegate and when a protocol method is called it iterates through each delegate, gets the IMP for the selector, and calls it.

On debug builds this worked fine, it was only until we used the Release configuration that we noticed that the parameters to these functions were nil in the implementation of the protocol methods, even if they were not nil when called.

This is how our protocol methods are called:

- (void) delegateCall:(SEL)sel withObject:(id)object {
for (id delegate in self.delegates) {
if ([delegate respondsToSelector:sel]) {
IMP imp = [delegate methodForSelector:sel];
void (*func)(__strong id,SEL,...) = (void (*)(__strong id, SEL, ...))imp;
func(delegate, sel, object);
}
}
}


Let's use the example protocol method:
- (void) blah:(NSNumber * _Null_unspecified)aNumber;


If we call
[self delegateCall:@selector(blah:) withObject:@32];
, the object will be nil in the implementation of
blah
:

func blah(_ aNumber: NSNumber) {
if aNumber == nil {
print("The number is nil somehow?!?!?!") // <-- Release
} else {
print("The number is: \(aNumber.intValue)") // <-- Debug, prints 32
}
}


If we use call the method in code on the delegates (rather than using IMP) the issue does not happen:

for (id delegate in self.delegates) {
[delegate blah:@32];
}

Answer

Having never tried casting an IMP instance to a function with variadic arguments, I can't say for sure how it would/should work (it would probably involve parsing a va_list, for instance), but since you know that you have one and only one parameter, I think you should be able to solve this particular problem by just eliminating your use of variadic arguments when you cast your IMP instance to a function pointer:

- (void) delegateCall:(SEL)sel withObject:(id)object {
    for (id delegate in self.delegates) {
        if ([delegate respondsToSelector:sel]) {
            IMP imp = [delegate methodForSelector:sel];
            void (*func)(__strong id, SEL, id) = (void (*)(_strong id, SEL, id))imp;
            func(delegate, sel, object);
        }
    }
}

Since you know that the argument is already an id, this should be a perfectly safe replacement.

As to why your original implementation works in a debug build but not in a release build, I can only guess; it might be related to the fact that release builds typically strip all symbols during link, and the runtime might be able to take advantage of the symbols, if present, in order to guess the correct argument ordering when invoking? Perhaps the compiler uses the wrong calling convention in a release configuration when generating the call to a function declared with a fixed argument footprint but invoked with variadic arguments? I'd be interested in further information if anyone has a more definitive answer to the debug/release question.

See the discussion on calling conventions here for a possible alternative using reinterpret_cast, if in fact your problem is due to a calling convention mismatch.

Comments