« Fail Faster | Main

May 17, 2007

Look, Ma. No framework!

When I first sat down to set up the project files for Sandvox, I knew that we wanted to support plugins. These are little bundles of Cocoa code and related resources that can be loaded into an application at runtime to extend its funcationality. A lot of Cocoa apps work this way. Watson did. So do Mail, iPhoto, iDVD, even Calculator. If an app keeps its plugins in the standard location (Contents/PlugIns) you can even browse them and activate/deactivate, add or remove them via Finder.

If, in Finder, you do Get Info on Sandvox you'll see the list of plugins that are included in the .app.

svx_plugins.png

Though you can activate and deactive them using Finder, I don't recommend doing it, for any app. Apple seems to have implemented half of a good idea here, but with no documentation or expectation as to how it should be used. Activating/deactivating plugins this way may have unfortunate results. So don't do it. But it's still an interesting way to browse and see if an app supports plugins, at least in the standard way.

Nevertheless, what's important is that plugins allow you to modularize and build out an application's functionality over time, including after the bulk of your app has already shipped. Not only can you extend your application with new plugins, but so can third parties. That's a big bonus and well worth supporting in your application if it makes sense.

The typical way that you encourage plugin development for your app is to move the general code required to support a plugin to its own framework that you then bundle inside your .app. Another developer comes along, learns (via SDK documentation, if they're lucky) that your app supports plugins, and that the way to do that is by linking to one or more frameworks within your app's main bundle.

Sandvox works this way. In fact, Sandvox supports six different types of plugins: Pages, Pagelets, Elements, DataSources, Indexes, and Designs. In all shipping versions of Sandvox up to the current, 1.2, we've made available a framework called KTComponents that you need to link your plugin against so it knows how to load itself into Sandvox, talk to Core Data, etc.

Wanting to be as open with our code as is reasonably possible, we kept moving more and more functionality from Sandvox over to KTComponents so that plugins could use this code, too. This included things like HTML parsing, UI classes, value transformers, and a bunch of different categories extending not only Foundation and AppKit, but Core Data, Core Image, and WebKit. Over a hundred classes or categories in all. A lot of code.

And, frankly, it's been a big pain maintaining it. Having all this code in a separate framework slows down the build. Makes the build target more complicated. Makes it harder to figure out where to set breakpoints. Makes it harder to locate resources. Makes maintaining localized strings more work that it should be. And did I mention that it really slows down the build?

Well, no more. When the next major update to Sandvox ships, KTComponents will be no more. All the code is going into the app. And plugins will work just fine. In fact, getting new plugins to build and link will be easier than I ever thought possible.

How can this be?

About a year and a half ago Wil Shipley posted a piece called Frameworks are Teh Suck, Err wherein he makes many fine points about where framworks came from, what they're good for, what their limitations are, and why you shouldn't use them.

At the time I thought "Well, that's nice, Wil. But we have to support plugins and we need a framework for that." Dan had posted in the comments to Wil's piece asking about this, but no one offered up a solution. I didn't think much about it at the time; just resigned myself to the annoying code split and build headaches.

About a year later, Wincent Colaiuta posted some thoughts about Cocoa development and the things he wished he'd done a little differently, had he only known, including where and when to use frameworks. Included in his post is a little piece of linker trivia that I had never heard of before. A flag for ld called bundle_loader.

Wincent didn't include any links to any information about bundle_loader and it only appears once in Apple's documentation, under Dead Code Stripping. I didn't think much about it at the time: just filed it away until I had some time to explore. This week, I explored...

Here's what 'man ld' has to say about bundle_loader:

-bundle_loader executable (32-bit only)
This specifies the executable that will be loading the bundle
output file being linked. Undefined symbols from the bundle are
checked against the specified executable like it was one of the
dynamic libraries the bundle was linked with.

I had no idea. This is basically how you get a plugin to link against an app without the linker going belly up. It works and it works well. (The 32-bit only comment is a slight concern. I don't think it will be an issue for the foreseeable future for Sandvox. I intend to confirm this at WWDC.)

If you've made it this far, rather than bore you with all the things I tried to make the linker happy before I stumbled onto bundle_loader, let me just share with you how we're going to be building plugins from here on out.

Rather that having to know about and locate KTComponents in the Sandvox appwrapper, somehow adding that to your plugin project, getting your framework search paths right, worrying about framework version numbers, if you're so inclined, and all that other junk, you now just add these two settings to your plugin's target's build configurations.

USER_HEADER_SEARCH_PATHS = /path/to/Sandvox.app/Contents/Headers
BUNDLE_LOADER = /path/to/Sandvox.app/Contents/MacOS/Sandvox

That's it! Really, that's it. That's all you have to do. Your plugin doesn't have to know anything about Sandvox except those two settings. Unbelievable. What was I doing messing around with frameworks for all these years?

You can put these in the Build tab of your target's inspector or you can put them into xcconfig files.

Suddenly, now, life just got a whole lot simpler. Plugins are trivial to get up and running. And we now have all the Sandvox code under one roof. (ConnectionKit, iMediaBrowser, and WebKit notwithstanding.) Suddently refactoring Sandvox becomes doable, since all the code is in the same project now. Very happy news. And, oh yeah, did I mention builds are a lot faster?

One last word of advice: to make this work, you do have to pay attention to what symbols you're exporting. Typically when you make an install build, the executable is stripped of many of its symbols: symbols that plugins are going to need to link against. Basically you can either leave your app unstripped (though still strip debugging symbols) or pass flags to strip that are more akin to those used with frameworks than with apps. E.g.,

STRIPFLAGS="-S"

And if you have things marked "extern", like string constants and functions that you want accessible across files, you need to tell gcc not to make those symbols private or plugins won't link for that reason as well. E.g.

GCC_SYMBOLS_PRIVATE_EXTERN=NO

So that's the big secret. No more splitting projects into app+framwork(s). I wish I had known about this years ago.

bundle_loader is the way to go.

Posted by ttalbot at May 17, 2007 3:06 PM

Comments

Thanks for the very nice tips!

One little additional tip is to use this path for the bundle loader setting when using it for plugins for your own app:

$BUILT_PRODUCTS_DIR/MacFusion.app/Contents/MacOS/MacFusion

this way, it links against the latest build (usually what you want).

Additional little piece of info:

I had seen the bundle loader before too and did not connect the dots. It is used by the unit testing system!! it allows the unit test framework to link against the executable to get the symbols right:
http://developer.apple.com/documentation/DeveloperTools/Conceptual/UnitTesting/Articles/CreatingTests.html

Posted by: charles at May 17, 2007 7:58 PM

I feel old. If you guys were around for classic Mac OS, you might've known to look around for this feature. The Code Fragment Manager had this from the start.

And if you have things marked "extern", like string constants and functions that you want accessible across files, you need to tell gcc not to make those symbols private or plugins won't link for that reason as well

Might also want to look into the symbol visibility attributes.

Posted by: Chris at May 17, 2007 8:40 PM