
15 Most Recent [RSS]
More...
|
Plug-ins with Cocoa
There were a few questions and answers going around on Twitter this week regarding how one can get plug-ins to work in Cocoa. The basics are very simple, but there are one or two little pitfalls, so I thought I'd offer a few thoughts of my own:
Creating and loading a simple plug-in
The basics are very simple: The plug-ins are loadable bundles, and there's a project template for those. Every loadable bundle can have a 'main class' specified in its Info.plist. You can write code in your app that uses NSDirectoryEnumerator to walk through each folder (e.g. ~/Library/Application Support/MyApp/Plugins/, /Library/Application Support/MyApp/Plugins/ and MyApp.app/Contents/Plugins/) and uses NSBundle to load the appropriate bundle. That will make the classes in it available to your app and you can ask NSBundle for the main class of your bundle.
You can also use class methods on the class defined in your bundle or an additional plist file (or if you're careful, the existing Info.plist) to store some more information. E.g. if your plugins are for importing/exporting to other file formats, you could provide a name for the file format for the menu, the file extension to use in the save panel etc.
And finally, remember a class is just another object. You can keep an NSArray of classes.
Watch out for class name collisions
This is important if you're loading any code into an existing Cocoa app: Objective C will quietly ignore a class in a bundle that has the same name as a class it already knows. Anyone asking for that second class will get the first one. So if you provide a helper class in source code to your bundle developers, then update the source code and seed it again, one plug-in could include an older or modified version. It gets loaded, and the second plugin, relying on a newer version of that class, will fail in spectacular ways.
This becomes even harder if you're relying on popular third-party code like my UKKQueue of Rainer Brockerhoff's RBSplitView, or Andreas Mayer's AMRolloverButton. You simply can't know whether someone else might not also be using this class, or whether a revised version of the host app will start using it, and then the plugin might break.
In general, it's a good idea to prefix even third-party classes used in a plugin with your own prefix. You can for example use a #define in the precompiled header to turn RBSplitView into UKHulaHoopPluginRBSplitView. Just keep in mind that you'll have to use that name in NIBs as well.
The Fragile Base Class problem
I go into more detail about this problem in My Mac Developer Network Screencast on Frameworks, but in short, you can not add instance variables to a class in a host application that is subclassed by its plug-ins. The compiler hard-codes the size of base class instance variables and just adds its instance variables after that.
So either pad your class out with id-typed instance variables for later use so you can add some more, or put one instance variable in containing a pointer to a struct that contains what would be your instance variables. That way, the base class's size will never change.
If your application is 64-bit only, there is no fragile base class problem, as it uses a different runtime that works around this issue.
Providing access to classes in your application
An application can export its symbols, just like a library. So, with some trickery, you can have the plugins link to your application. You'll have to fiddle with the DyLd library path in your app's settings to make sure plugins linking to it will look for it in the right place (use @executable_path or @loader_path for that), and not in /Library/Frameworks or whatever, though. A tad easier than making your app the library is to have a framework inside your app that contains all the classes accessible by both the host app and the plugins.
Even easier but slightly limited would be to not allow access to the classes, and not to let your plugins subclass classes in the host application. Instead, make your plugins work more like delegates, giving them a protocol that your plug-in's main class has to implement. The advantage of this is no worrying about fragile base class, more explicit control over what symbols are 'published' to plug-ins without the need for keeping a private and public header in sync (the compiler will make sure you implement the protocol fully), and more freedom to change the internal class hierarchy of the app without a plug-in noticing.
You can use -respondsToSelector: and execute some default behaviour to make it easier on your plug-in developers, and/or pass another object that implements a particular 'host app protocol' to each plugin. The trick here is to only deal in protocols and id pointers here, which means all resolution happens at runtime, and nobody needs to link to your app to call back into it.
Clark S. Cox III writes: Note that under the "new" runtime (i.e. iPhone and 64-bit), there is no fragile base-class problem, so the padding is unneeded there. If you do use extra variables for padding, wrap them in:
#ifndef __OBJC2__
...
#endif
to make sure that they are only defined on the legacy runtime.
|
Karsten writes: I just wanted to point to a bundle that i wrote that can load plugins into an application. It automatically searches the user's folder, in the system's folder and in the application's folder.
It's under MIT license and available on codebeach:
http://www.codebeach.org/code/show/5
Karsten
|
Uli Kusterer replies: ★ Clark, thanks, I've amended the article with the general info. The __OBJC2__ constant isn't a good choice, though, as you can have that on the 32-bit runtime as well, I think (for @property and @synthesize etc.).
|
Harvey writes: Clark, 64 bit definitely does not have the fragile base class problem. I believe the iPhone does still have this problem though. Can you provide a link to documentation that says otherwise?
For avoiding class name collisions, this is important but super long class names are hard to read. Using a #define can work around most of this.
#define RBSplitView UKHulaHoopPluginRBSplitView
(You still need the @interface to use the true class name or IB will be confused.)
"not to let your plugins subclass classes in the host application"
This is definitely an all-around better idea. Do this if you can afford it at all. It's much less work to support.
|
Uli Kusterer replies: ★ Yes, even if you don't export the symbol, they still end up in the table that NSStringFromClass and NIB loading use.
|
Clark Cox writes: Uli, the macro __OBJC2__ is never defined on the old runtime. It is the correct macro for detecting whether or not code is being built for the new (i.e. 64-bit/iPhone) runtime.
Harvey, no, the iPhone uses the new runtime, and does not suffer from the fragile base class problem.
|
Uli Kusterer replies: ★ Clark, the iPhone uses the new runtime. Too bad that the iPhome simulator still uses the old one. |
|  |