Under what conditions might instancesRespondToSelector: return true, but performSelector: throw an exception
Asked Answered
P

5

15

I have code distributed in a library which looks like this:

if ([[NSString class] instancesRespondToSelector: @selector(JSONValue)]) {
  NSString *jsonString = [[[NSString alloc] initWithData: jsonData encoding: NSUTF8StringEncoding] autorelease];
  dict = [jsonString performSelector: @selector(JSONValue)];
}

For some reason a -[__NSCFString JSONValue]: unrecognized selector sent to instance exception is getting thrown when the performSelector: method gets called. This is code that is distributed in a library that I wrote, but I can't reproduce or debug it myself. Instead a third-party is reporting this problem. Under what conditions could instancesRespondToSelector: while actually calling the method using performSelector: throw an exception?

edit There is a case which could explain why this occurs, but it doesn't make sense. If the developers were to do something like this:

@implementation NSString (OurHappyCategory)

+ (BOOL)instancesRespondToSelector:(SEL)aSelector
{
  return YES;
}

@end

It would explain why the code is executing, but it would of course be a very bad thing to do. Is there a way this problem could occur that makes sense?

Plata answered 9/4, 2013 at 7:34 Comment(16)
Are you sure your "JSONValue" method returns dictionary as a result? Maybe that is your problem.Sect
@Sect It doesn't matter what the method returns, the exception is caused by calling the method. The return type for the JSONValue method is id.Plata
Are you sure that the 3rd-party is using this code, with the if clause?Especially
@MarceloFabri This code is in my static library that they're including in their application. I know the exception is getting thrown there because my code catches the exception and writes a particular message.Plata
are you sure this is the only possible call to JSONValue? Maybe the 3rd-party is not linking your lib proper and calling JSONValue by themself?Debauchee
@JonathanCichon My code catches the exception and writes a particular message, so I know this is occurring in my code.Plata
Could the third party have redefined JSONValue: such that it throws the exception when you pass the (type of) data your example uses?Hebrides
@Hebrides there is no parameter passed to JSONValue, so I don't think that is the problem. It is possible they changed the NSString class's behavior, but I don't understand why they would.Plata
I haven't been able to come up with code that would prove this, but any chance it has to do with conflicting category names/arguments? If your code is being distributed in a library you really should prefix it anyway, in case they have their own JSONValue category, which wouldn't be uncommon.Whiffen
@Whiffen but if they had their own implementation of JSONValue that NSString implements, why would it throw an exception?Plata
Eh, forget that theory. This seems promising: developer.apple.com/library/mac/#qa/qa2006/qa1490.html Also this SO question: #10417279 I think this lends strong credence to Andrea's theory. Maybe ask them to double check their compiler flags?Whiffen
I just tried a test, and if you don't use the -ObjC flag then instancesRespondToSelector: will return NO. If you do use the -ObjC flag then instancesRespondToSelector: will return YES and no exception will get thrown. -all_load has the same effect as -ObjC.Plata
Ask the third party to produce a minimal test case that exhibits this problem.Calvinna
@Calvinna I've asked them for a test case, but I have yet to hear from them.Plata
@Plata do you have any more information? for example is the failure only occurring on the ios 4.x or any ABI /runtime differences? simulator vs device?Embolectomy
Apparently, the 3rd-party has found a solution or workaround for this problem, but hasn't provided any details to me yet.Plata
E
7

NSString is basically a class cluster, and brings up all sorts of complications... you actually need to ask the instance if it responds to the selector.

NSString *jsonString = [[[NSString alloc] initWithData: jsonData encoding: NSUTF8StringEncoding] autorelease];
if ([jsonString respondsToSelector: @selector(JSONValue)]) {
  dict = [jsonString performSelector: @selector(JSONValue)];
}

but your problem probably has to do with compiling or linking the extension... if the category is added in a library then you will need to sprinkle in the -ObjC linker flag.

EDIT:
I have been working a little bit on reproducing this issue... which I am unable to... do you have any more information.. for example is the failure only occurring on the simulator, or just on device, iOS 4.x, GNU linker vs LLDB's linker, ABI/Runtime differences?

Embolectomy answered 23/4, 2013 at 22:20 Comment(6)
I agree that it sounds like it could be cluster-related, but I can't think of any actual way this could be a problem. Do you have a concrete example of how you could end up with the result described? I assume the OP is trying to avoid creating the string at all if can't be JSONValue'd. (e.g. he has a fallback which involves parsing the data directly)Hanshaw
well if the category is on NSString and NSCFString is actually the superclass of NSStringEmbolectomy
__NSCFString is a subclass of NSString (NSMutableString, actually), so I still don't see how it could cause the issue.Hanshaw
I'm guessing it's an interaction between static libs and categories and performSelector. When performSelector goes looking for NSString, it seems possible that it could find a version bound into a static lib that doesn't have the category attached. Kind of like the same class loaded under two class loaders in Java. A straight call works because the instance pointer is always linked to the "right" version of the class.Singularity
@HotLicks the ABI/Runtime kind of doesn't work that way... if we have the compiled version we could look at the object file with nm and see if the symbol is there at all, and if it is in the undefined section.Embolectomy
Apparently it does work that way -- sort of. Just no one's identified the specific quirk.Singularity
S
4

I guess that you didn't import a third party library in a correct way. Usually this methods are added as category to NSString, it happened to me that I could see the .h file but the .m wasn't compiled. You can check it inside xcode target-->build phases-->compile sources. Or check if you aheve this flag inside Project-->Build Settings-->Other linker flag = -all_load

Stortz answered 11/4, 2013 at 6:9 Comment(5)
-all_load is not needed anymore, -ObjC is enough. Also this would not explain why instancesRespondToSelector returns true.Debauchee
In this case we're talking about a third party app which is using my library, so I don't have access to the build settings.Plata
Ask them if they can check or give you the whole project, I've got almost the same problem integrating the MKNetworkKit in a new project. I did a lot of times, but yesterday it simply didn't work, I could send messages but no implementation on the categories of that library. Adding the -all-load flag everything went fine. For sake of completeness I didn't send respondToSelector messages, but the autocompletion and the linker was building fine.Stortz
I was wondering if maybe the .m for the category wasn't included, but then I can't see how instancesRespondToSelector would return YES. And presumably the instances.. call and the JSONValue call are in the same compilation unit and would be bound to the same definitions.Singularity
So wouldn't -all_load not being needed anymore depend on what version of Xcode they were running? I have heard of some people on crazy old versions of Xcode.Whiffen
C
4

The exception does not come directly from the Objective-C runtime in response to sending a message that the instance doesn't recognize. It comes from -[NSObject doesNotRecognizeSelector:], which is invoked at the end of various method lookup and forwarding mechanisms.

However, anything can invoke -doesNotRecognizeSelector: when it wants. A class can "disavow" an inherited method by overriding it and making the override just invoke -doesNotRecognizeSelector:. As documented, this will result in the exception you're seeing in spite of the fact that -respondsToSelector: (and +instancesRespondToSelector:) returning YES.

I couldn't tell you why __NSCFString is doing that in your end user's case. Is the app which is using your library using categories or method swizzling to modify the methods on that class?

Also, does your logging show the actual stack trace capture in the exception? That might be informative.

Cameron answered 24/4, 2013 at 3:36 Comment(0)
S
0

There is an odd possibility: It could be that performSelector itself is somehow executing in a different link-loaded environment than the rest of the code. Not exactly (or even approximately) sure how this could happen, though.

Singularity answered 18/4, 2013 at 2:34 Comment(10)
Could you clarify what you mean by "link-loaded environment"?Plata
@Plata - The results of binding, executable module building, whatever you want to call it. The terminology is different with every platform and I can't keep up with them. In particular, if some parts are in a separate library module it's possible that they can't "see" all the stuff in another module or vice-versa.Singularity
In any event, if this is the problem it would probably work to cast jsonString to a dummy interface type that defines JSONValue and execute the call directly (or as directly as Objective-C does any call).Singularity
I don't see how casting the jsonString could change the list of methods it responds to.Hanshaw
@JesseRusak - I'm guessing you don't see a lot of things. Casting wouldn't change what the string responds to, but would allow the compiler to "swallow" it, avoiding the need to use performSelector (which is a likely suspect in this mystery).Singularity
@HotLicks There's no need to be rude; I just didn't see what you were getting at. Sorry!Hanshaw
So, performSelector:'s disassembly is basically: - (id)performSelector:(SEL)selector { if (selector) { return [self selector]; } else { return [self doesNotRecognizeSelector:NULL]; } (If you'll excuse the abuse of syntax.) So, I don't think this is the source of the problem, since either: the selector is non-zero, and it's just like a message send, or the selector is zero, in which case the error would not say the name of the selector.Hanshaw
@JesseRusak - Whatever you say. You got a better theory?Singularity
@JesseRusak - (Keep in mind that we're talking about a CATEGORY on NSString. And multiple static libs.)Singularity
I completely agree that performSelector: is fishy here (I would just cast to id and call directly, as you suggested) but I can't see how it could cause the symptoms above. (Take a look at the disassembly!) Honestly, I think the most likely answer is the one proposed in the OP's edit! I'll tell you what: I'll upvote your answer and you can stop being angry at me for trying to help.Hanshaw
U
0

Make sure nobody is swizzling a function

Uwton answered 23/4, 2013 at 22:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.