Cocoa Libraries and Categories
First off I have created a cocoa library which contains a few classes / methods that I use in several projects. This works fine - I add the library to my project's external frameworks group and the linker adds the executable code to my projects during compile.
Following Aaron Hillegass' book, ( 3rd Ed, pg 295 ) I have successfully extended the NSString class by creating a category of methods ( ie a better string padding routine ). This works if the category class definition is in my project. But if I try to create the category in my cocoa library, my project will compile without errors or warnings, but I get an "unrecognized selector sent to instance" when I actually try to execute a call to the extended NSString method.
In short, how can I get a category definition to work successfully in a cocoa library... ?? Thanks for any help.
- Mark
Mark
This is a very interesting question. I believe the way in which categories work is by updating the dispatch table of the class which has been extended. (To be honest, I haven't thought of any other explanation for how it could work). So if you're getting this "unrecogonized selector" I think that's saying "I can't dispatch to "boxerMans:wee:string:extension: (or whatever methods your category has added). This is a run-time error, correct?
When I've used categories, I've always had the extension compiled into my .app and that works. So I think you might need to compile and link your category code with your application.
From what you've said, I think you're trying to add the category from an external Framework and I think you're saying that you can't get it to work. Is it a major problem to compile and link the category into your application?
clanmills -
Thanks! Yes, it is a runtime error and no, not a big deal to include the category class with my project. And, I'm simply making a linkable library, not a full framework ( I think ). The non-category methods in my lib work fine.
But.... it is odd that it won't work the way I'm trying it. If you look at the definition headers within the cocoa framworks ( ie NSString.h ), you see that there are category definitions all over the place... And ultimately these are really just external libraries.... kind of like mine.
It's one of those things - you feel that you're just missing some little piece of code that will get you where you want to be, you know? I think it would be cool to have my own linkable library of core-class extension categories available to all my projects. You know, so I don't have to make changes to all my projects when I need to make alterations to the categories.
Thanks again.
- Mark
Mark
I'm sure it's the category stuff in the library has not updated the run-time correctly. You can interact with the objc runtime system and it's documented here:
There a method:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
I think that if you modify your NSBundle load: function in the Framework, then you'll be able call class_addMethod and all will be well. And of course you'll be in the happy place of being able to use your category in all your apps simply by linking/loading your framework.
If you try this, and get it to work, then I think it may be worth reporting a bug against Apple's bundle loader. If you don't try it (or can't get it to work), I think I'm willing to do some digging around as this is an unusually interesting issue. Let me know your progress and together I think we'll solve this one.
Have not looked at the addMethod yet, but....
There are three ways I know of creating external ( linkable ) libraries in Cocoa:
Dynamic Cocoa Library
Static Cocoa Library
Cocoa Framework
Both the dynamic and static library methods fail - ie they compile just fine but fail during runtime with the "unrecognized selector" error, which in my case does not kill the app.
The Cocoa Framework method works correctly. It just isn't what I want because distribution of my app would require the end user install the framework as well as the application. I've found this to be very troubling to users in the past.
Oh well.... I can actually keep the source files to my Cocoa Categories in a single place and add them to my projects as references. The only hit is that the same code would get recompiled every time I work on a project that uses them. Not a big hit at all. But I'm a big believer in maintaining separate libraries for your "tried and true" utility code and it would be nice if the Category thing worked in that context.
Thanks.
Mark
Hey wait.
Sometimes, I can browse inside an application's package contents, and find Application.app/Contents/Frameworks -- no need to install anything.
For example, these contain Growl or Sparkle.
Besides, you can check for conformance with a method:
if ([object respondsToSelector:@selector(switchToGear:pressClutch:stallInfo:)]) {
NSLog(@"object respondsToSelector \"switchToGear:pressClutch:stallInfo:\"");
}
Is that what you're looking for?
Mark
I've been able to the linking and everything to work using CocoaFramework. I've written a simple Command Line program (CLandFramework) that calls RobinsFramework (which is a sub project of the command line program). I've defined an NSString extension in the Framework and it's working. Take a look:
http://clanmills.com/CLandFramework.zip
The linking isn't trivial. I used a post build script in CLandFramework to change the install_name in the executable to correctly find RobinsFramework. Alternatively, you could change the install_name in RobinsFramework. I've written a tutorial about linking:
http://clanmills.com/articles/cocoatutorials - it's the Tutorial console that discusses linking.
I think you have to choose the wizard's CocoaFramework to be sure that the category is registered when the framework is loaded. From your comments, it seems that your category/extension is not registered by the other types. You could of course include a static C++ object or something in the framework that would be initialized when your Framework is loaded and call class_addMethod appropriately from there.
If you use a variant of the linking strategy I use in CLandFramework, you can "hide" the Framework inside your applications bundle (of course thats impossible for a command-line program as it doesn't have app bundle). However if you look inside many commercial products, you'll find foo.app/Contents/Frameworks and the application is linked to @executable_path/../Frameworks/SomeFramework/bla/bla/bla. This very nicely avoids the problem of having to install your Framework on the user's machine as it's hidden in the app bundle. (good, eh?) - and that's the scenario being described by K.
Of course nothing is perfect. Each of your apps then has a copy of the Frameworks that it uses. That's a plus and a minus. The plus is that your app is shielded from changes to the Framework - once you've shipped it, you don't need to worry about it breaking because of a global Framework change. The minus is of course that you have a copy of your frameworks in every app. You choose what works for you.
A new linking option @rpath arrived with 10.6. I hadn't noticed that until today (man dyld) and enables some kind of path search which may be to more to your liking. As I only discovered this today, I haven't had a chance to think about to take advantage of that. However Apple must have added for good reason.
Thanks. I was not able to download the CLandFramework.zip file ( dead link? ). I did look at your link tutorials - good work!
Yes, I see that I can place a FrameWork inside my .app package - not sure how to get my .app to point to it during runtime tho....
Will take a look at the addMethod you mentioned earlier.
- Mark
Hi Mark
I've attached the file. I'm at work and downloaded it from the link I gave. That's odd that it was broken for you.
Anyway, there are 3 file commands concerning linking that you'll want to know something about:
otool -L object will list the linkage
install_name_tool enables you to change the linkage on a binary
lipo - to examine the architecture (i386 or whatever)
In CLandFramework, I use install_name_tool to modify the linkage from CLandFramework to @executable_path/../../RobinsFramework/bla/bla/bla - and you can do something similar to setup @executable_path/../Frameworks I've a "run script" step to the build on CLandFramework to do this.
The default linkage is taken incidentally from object being linked. So when you build RobinsFramwork it has linkage something like ~/Library/Frameworks/RobinsFramework/bla/bla/bla and that gets copied into you application's excutable (which normall lives in foo.app/Contents/MacOSX). So an alternative is set the install_name in the build of your framework to be @executable_path/../Frameworks/YourFramework/bla/bla
I know this isn't too easy.
To avoid making this thread very long, you can email directly if you wish (robin@clanmills.com) and I'll update the forum with a summary when we're done.
Attachment: CLandFramework.zip (24.0KB)
Well, I finally got the category ( class extension ) feature working with a static ( link at compile time ) cocoa library. I did this using class_addMethod and adding the runtime.h header to my library project.
What is really weird to me is that to do this I add to my static lib project the following:
id myDynamicFunction( id self, SEL _cmd );
- (id) myStringExtension;
then SOMEWHERE in ANY method within my lib source I place the call:
class_addMethod([NSString class], @selector(countChars), (IMP) myDynamicFunction, "i@:");
it works. ie, when I make the method call: [aString myStringExtension]; I get the correct return value. AND THE DYNAMIC FUNCTION IS NOT CALLED.
It works. Don't know what's going on, but it does. So, Apple should be aware ( might already be ) that to create class extensions in static libraries, one must jump through the addMethod hoops.
- Mark
Mark
Goodness, that's spooky and difficult to believe (well maybe not, software is some kind of magic).
It looks as though forcing the linker to link class_addMethod has modified the linking (and startup execution) of your host application. Is the host application an app or a command-line utility?
I have created a Cocoa App and a Static Cocoa Library and linked them. The extension is good without any special magic. http://clanmills.com/files/AppAndLib.zip I've also attached AppAndLib.zip to this thread.
When you run the app, you'll have to press the button "PressMe" in the UI to see running code in the library (and extension). I have not special reason to have used an app instead of a command-line program.
Incidentally, if you want to run the executable that's buried in the app bundle, you can do that from the terminal. printf and NSLog conveniently report to the terminal output.
543 /Users/rmills/Projects/AppAndLib/build/Debug/AppAndLib.app/Contents/MacOS $ ./AppAndLib
2010-09-28 20:05:09.494 AppAndLib[8374:903] pressMe
2010-09-28 20:05:09.496 AppAndLib[8374:903] MyLibObject helloWorld
2010-09-28 20:05:09.497 AppAndLib[8374:903] hello wee string, I am ObiOne Nobody
I knew when you asked about yesterday that this would turn out to be interesting (well more interesting than the stuff I'm working on in the office anyway).
Attachment: AppAndLib.zip (36.0KB)
I think I have at least the nuances of the class extension use in static libraries figured out. From your AppAndLib project I saw that you have the category working without making the class_addMethod call.
I don't fully know the relationships within ObjC between object definitions and their files ( ie .m & .h), but there are some differences when you have code in separate files…
In your AppAndLib you defined your category extension to NSString in the same file as a unique object, MyLibObject.
Now in the AppAndLib.app you create an instance of MyLibObject which makes subsequent calls to your MyLibObjectExtension method(s) work. If you do not create an instance of MyLibObject, it will not work ( kind of makes sense ). You cannot make an instance of MyLibObjectExtension, which I guess is simply a mnemonic.
In my case, I had created a ( .m & .h ) file pair which only contained my version of your MyLibObjectExtension. In my staticLib project I have other objects defined also within their own file pairs. If I create the convoluted class_addMethod and required C-implementation function in any of the other objects - and make the class_addMethod call within a routine that is NEVER called, then the call from my main app to the category extension WORKS. ( lightening, dogs barking in the distance….)
But it's probably not a mystery at all. It is simply that using a static library in Objective-C is really not like doing the same thing in a C program. Objects have to be loaded to be used, period. And category definitions apparently are not objects themselves, but must be contained within the file of a real object - and that object needs to exist for anything to work….
So at the end of the day, I can do what I originally wanted. I can maintain my "tried-and-true" methods and category extensions in a cocoa static library, but to utilize this, I will simply create a global (static lib) object in my main app(s) and load ( ie alloc/init) it so that I can get to all the class extensions that I create.
BTW - I am converting to ObjC an engineering application that I wrote and sold through the 80's and 90's for the Mac called Engineering Assistant. It is a great way to learn ObjC - because I'm not really spending time on design right now, just making the whole thing work in ObjC and XCode3. Of course, when I'm done, I'll have to start over and re-design it.
Thanks so much for your help on this - an interesting journey this coding thing….
Mark
Thanks for the update about this. I'm really pleased to hear you've arrived at your destination with something that works. That's really good.
For sure, it seems that this "sort of" works. In both CLandFramework and AppAndLib, it worked for me without difficulty. You've been experiencing the opposite!
Thanks to your determination (and the dogs and lightening) we now have a 100% reproducible Obj/C bug which reduces to 1 line of code. I'll report it on the Apple Radar. This is it:
- (IBAction) pressMe : (id) sender
{
[[MyLibObject alloc]init]; // <--- comment that off and the category is not added
NSString* foo = @"foo" ;
[foo speakToMeObiOne:nil];
}
My mental model (which is 100% invented in my head, so could be wrong) is that there's a table of method names in Obj/C, each with a unique selector (integer). There's also a table of classes and each class has a table of SEL vs function pointers (addresses of functions). When you add a Category, the method names table are added and the classes SEL vs function pointer table are updated. And that's how it's possible to add new methods to existing pre-compiled classes.
So Obj/C is failing to respect the library's linked category code until the application causes (any) code in the library to execute. (calling a straight "C" method in the library also works). I'll send you the Radar reference number and you can track it.
Best Wishes with the port of Engineering Assistant. I'm always willing to help if you get into difficulty.
Hi Mark. OK, I've reported this on the Apple Radar (bug reporting system). I'm sure they'll fix this (and probably have some fun with this).
You'll need a (free) Apple Developer Account to access the Radar at:
https://bugreport.apple.com/cgi-bin/WebObjects/RadarWeb.woa/wa/signIn
-----------
Robin,
Thank you for submitting this report. Apple values your time and input. While we cannot respond to every bug, please be
assured that your report will be reviewed by the appropriate group.
Your tracking number for this issue is Bug ID# 8501111. You may check status on this report via the 'My Originated Problems'
tab.
Please know that you may append additional information, including files, to your report through the 'My Originated Problem'
section by clicking the Bug ID number you wish to modify.
----
Here's what I said:
30-Sep-2010 07:47 PM Robin Mills:
Title: Obj/C Category is not initialized correctly in libraries
Summary: Obj/C Category is not initialized correctly in libraries
Steps to Reproduce: Please download http://clanmills.com/files/AppAndLib.zip and build the code.
Expected Results: When you press the button "Press Me', you'll NSLog() output in the console. The main program is calling a category in the library. The output is correct:
2010-09-30 19:39:24.469 AppAndLib[11683:a0f] pressMe
2010-09-30 19:39:24.471 AppAndLib[11683:a0f] hello wee string, I am ObiOne Nobody and you are "foo"
Now comment off the the call in AppAndLibAppDeletegate.m and run again. You'll get the error message
2010-09-30 19:38:19.588 AppAndLib[11459:a0f] pressMe
2010-09-30 19:38:19.590 AppAndLib[11459:a0f] -[NSCFString speakToMeObiOne:]: unrecognized selector sent to instance 0x100002078
2010-09-30 19:38:19.592 AppAndLib[11459:a0f] -[NSCFString speakToMeObiOne:]: unrecognized selector sent to instance 0x100002078
- (void) noBodyLovesMe:(id)sender andNobodyEverCalls:(id)hereAnyMore
{
// myLibObjectHelloWorld(); // <--- uncomment this and the Category comes to life
}
Actual Results:
Noted above.
Regression:
A work around is explained above.
Notes:
There has been a considerable discussion of this matter at (and more test projects) at
http://www.cocoaforum.com/2010/09/27/cocoa-libraries-and-categories/#post13
We have reproduced this issue with Frameworks and Static Libraries, with command-line programs and apps. The issue concerns the library category not being initialized correctly. It appears to be a linker issue because nobody executes the method noBodyLovesMe:andNobodyEverCalls: - it seems that simply linking from that code to the library is sufficient to get Obj/C to respect the library's Categories. However without that, the library's category has not been registered and we get the "unrecognized selector" error.
I'm sure you'll have some fun pinning this one down and fixing it for us.
---
I attached AppAndLib.zip and CLandFramework.zip