Uli's Web Site
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
Created: 2008-12-07 @937 Last change: 2021-09-24 @187 | Home | Admin | Edit|
© Copyright 2003-2021 by M. Uli Kusterer, all rights reserved.