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?

ThomasW
  • 16,981
  • 4
  • 79
  • 106
  • Are you sure your "JSONValue" method returns dictionary as a result? Maybe that is your problem. – Maggie Apr 09 '13 at 08:06
  • @Maggie 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`. – ThomasW Apr 09 '13 at 08:21
  • Are you sure that the 3rd-party is using this code, with the `if` clause? – Marcelo Apr 09 '13 at 08:27
  • @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. – ThomasW Apr 09 '13 at 08:29
  • 3
    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? – Jonathan Cichon Apr 09 '13 at 09:04
  • @JonathanCichon My code catches the exception and writes a particular message, so I know this is occurring in my code. – ThomasW Apr 09 '13 at 09:09
  • Could the third party have redefined JSONValue: such that it throws the exception when you pass the (type of) data your example uses? – Fred Apr 09 '13 at 10:19
  • @Fred 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. – ThomasW Apr 09 '13 at 14:14
  • 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. – MaxGabriel Apr 18 '13 at 04:33
  • @MaxGabriel but if they had their own implementation of `JSONValue` that `NSString` implements, why would it throw an exception? – ThomasW Apr 18 '13 at 04:37
  • Eh, forget that theory. This seems promising: https://developer.apple.com/library/mac/#qa/qa2006/qa1490.html Also this SO question: http://stackoverflow.com/questions/10416779/unrecognized-selector-sent-to-class-when-calling-category-method-from-a-librar?rq=1 I think this lends strong credence to Andrea's theory. Maybe ask them to double check their compiler flags? – MaxGabriel Apr 18 '13 at 04:52
  • 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`. – ThomasW Apr 18 '13 at 06:00
  • Ask the third party to produce a minimal test case that exhibits this problem. – tc. Apr 23 '13 at 23:40
  • @tc. I've asked them for a test case, but I have yet to hear from them. – ThomasW Apr 23 '13 at 23:53
  • @ThomasW 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? – Grady Player Apr 24 '13 at 17:29
  • Apparently, the 3rd-party has found a solution or workaround for this problem, but hasn't provided any details to me yet. – ThomasW Apr 25 '13 at 13:57

5 Answers5

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?

Grady Player
  • 14,399
  • 2
  • 48
  • 76
  • 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) – Jesse Rusak Apr 23 '13 at 22:26
  • well if the category is on NSString and NSCFString is actually the superclass of NSString – Grady Player Apr 23 '13 at 22:27
  • 1
    __NSCFString is a subclass of NSString (NSMutableString, actually), so I still don't see how it could cause the issue. – Jesse Rusak Apr 23 '13 at 22:55
  • 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. – Hot Licks Apr 24 '13 at 17:42
  • 1
    @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. – Grady Player Apr 24 '13 at 17:55
  • Apparently it does work that way -- sort of. Just no one's identified the specific quirk. – Hot Licks Apr 26 '13 at 00:40
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

Andrea
  • 26,120
  • 10
  • 85
  • 131
  • 1
    `-all_load` is not needed anymore, `-ObjC` is enough. Also this would not explain why `instancesRespondToSelector` returns true. – Jonathan Cichon Apr 11 '13 at 06:24
  • 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. – ThomasW Apr 11 '13 at 08:06
  • 1
    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. – Andrea Apr 11 '13 at 08:16
  • 1
    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. – Hot Licks Apr 18 '13 at 02:30
  • 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. – MaxGabriel Apr 18 '13 at 05:04
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.

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
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.

Hot Licks
  • 47,103
  • 17
  • 93
  • 151
  • Could you clarify what you mean by "link-loaded environment"? – ThomasW Apr 18 '13 at 02:39
  • @ThomasW - 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. – Hot Licks Apr 18 '13 at 11:33
  • 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). – Hot Licks Apr 18 '13 at 11:36
  • I don't see how casting the jsonString could change the list of methods it responds to. – Jesse Rusak Apr 23 '13 at 23:05
  • @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). – Hot Licks Apr 23 '13 at 23:56
  • @HotLicks There's no need to be rude; I just didn't see what you were getting at. Sorry! – Jesse Rusak Apr 24 '13 at 00:05
  • 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. – Jesse Rusak Apr 24 '13 at 00:21
  • @JesseRusak - Whatever you say. You got a better theory? – Hot Licks Apr 24 '13 at 00:23
  • @JesseRusak - (Keep in mind that we're talking about a CATEGORY on NSString. And multiple static libs.) – Hot Licks Apr 24 '13 at 00:25
  • 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. – Jesse Rusak Apr 24 '13 at 00:28
0

Make sure nobody is swizzling a function

Stephen J
  • 2,367
  • 2
  • 25
  • 31