26 June 2008

Frameworks Are Great (Part 2)

Advanced

Ok, the system of loading frameworks I talked about in my previous post doesn't always work. In fact, it can be kind of terrible. Here's and example that I came up against recently that proved very difficult to solve, at least in a clean hi-I'm-a-mac sort of way.

What I want to do was create a screen saver plug-in. So, what would the @executable_path be in this case? After a little poking around, it appears to be the application that runs and dynamically loads the plug-ins is the ScreenSaverEngine.app, located here: /System/Library/Frameworks/ScreenSaver.framework/Versions/Current/Resources/ScreenSaverEngine.app.

That's not very helpful for loading a plugin bundle's frameworks. Any sort of @executable_path relative directory would not be a good place to put any of my bundle's resources since they wouldn't be encapsulated and wouldn't allow a drag-installer. Well, then, let's turn our attention to the @loader_path. This is promising, the loader path would be the location that my plug-in executable is loaded from. Perfect.

Not so fast

But wait, I'm including a framework that itself requires a framework. In the stand-alone-application version of this (i.e. not a plugin) I can put both of these frameworks in the application's Contents/Frameworks/ directory and we'll be all set. Then the executable in the framework can load the second framework relative to the @executable_path/../Frameworks but this breaks down for a plug-in. In this case, the @loader_path of the second framework is the directory containing the main executable of the first framework, not the plugin. However, the plugin needs to link to this framework too (it is a utility framework). The @loader_framework now resolves to two separate places, the plug-in executable and the first framework executable. The @executable_path is equally useless, as described earlier. What to do...?

Three (bad) solutions

Well, I came up with three solutions, all with nasty side effects.

  • First, we could take the executable code out of the two framework bundles and then put it in the plug-in's Contents/MacOS directory. Then the @loader_path would resolve to the same location for all three pieces. This essentially throws away all of the benefits of frameworks. However, in our case, we don't ship headers or resources in these frameworks and we could live without the versioning capabilities. This might work.
  • Second, we could put the frameworks in a known, system-wide location. For example, /System/Library/Application Support/Sidelight/... or something. This would require shipping a package installer to install a screen saver plug-in. Bad. It would also prevent a drag-uninstaller from ever working. Also bad. This solution won't work.
  • Third, we could include the framework twice. Once in the plug-in's Frameworks directory, and another in the Framework's Frameworks directory. The framework we are using is about 10MB. Even with zippy broadband access this seems like a profligate use of disk space and RAM (since both frameworks would have to be loaded). This also might cause problems with conflicting symbol names. I didn't go down this path far enough to try it out.

There you have it. Three pretty terrible solutions. The first one seems the best, but not satisfying.

The real solution

So a not-so-bad solution to this problem would be to statically link the libraries we are using. There would be an initial hit at load time, but since this is a non-interactive application, it would be tolerable. Or, perhaps the real solution is to re-architect the code to remove the second framework. Most of its functionality is already included in the Cocoa frameworks. I just need a way to abstract the utility interface so I can maintain my cross-platform code base and then use Cocoa for the utilities rather than a cross-platform utility framework.

They're still sorta great

Frameworks are still pretty good, especially if you are Apple. When you are Apple you get your own reserved Frameworks directory, to which everyone can link. If you are not Apple, frameworks are great as long as you don't have two levels of dependent frameworks in a loadable bundle. Is this a fringe case? Yes, I think so. But is seems pretty important to me.

Frameworks Are Great (Part 1)

Frameworks

One the of reasons developing on Mac OS is so fun is the functionality and ubiquity of the system frameworks (Cocoa, Core Image, QuickTime, WebKit). Frameworks are bundles (i.e. directories) of related executable code, header files, and (possibly) localized resources. These frameworks encapsulate functionality and prevent the code-base from turning into a giant ball of spaghetti. It also improves dynamic load time by reducing the footprint of the loaded library to just the sort of code that the user needs (don't need QuickTime, it doens't get loaded).

To see which frameworks are being used by an application, we just need to poke around in the application's main executable file. Let's look at a simple application, Calculator.app, for an example (edited for brevity).

[mycomputer:~]$ otool -L /Applications/Calculator.app/Contents/MacOS/Calculator 
/Applications/Calculator.app/Contents/MacOS/Calculator:
    /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa
    /System/Library/PrivateFrameworks/SpeechDictionary.framework/Versions/A/SpeechDictionar
    /System/Library/PrivateFrameworks/SpeechObjects.framework/Versions/A/SpeechObjects
    /System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration
     ...
    /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

Ah, this is interesting. It looks like Calculator links in Cocoa (duh) but also SystemConfiguration.framework. Hmmm, I wonder why. Anyway, with otool -L we can see exactly which libraries the executable will link in at runtime. Very nice that the Calculator.app didn't have to include all of these libraries in its application bundle and even nicer that when Apple improvies their frameworks, Calculator.app can benefit from the upgrades (as long as no new bugs were introduced).

We can use them too!

Individual developers or companies can use this same strategy of encapsulation in their own applications. Many largish applications break their applications in to a main executable which links not only system frameworks, but frameworks that have been built in house. For example, a networking library could be in a Networking.framework and a library of geometric primitives could be in a Geometry.framework.

So, what do developers do to distribute their third party frameworks with their applications? They put them in the MyApplication.app/Contents/Frameworks directory and viola, the executable can find them. In the above example, all of the libraries have a constant installed location /System/Library/Frameworks. However for third party framework developers, the linker can find the framework relative to the executable. Here is a snippet from a great application, Colloquy.app:

[mycomputer:~]$ otool -L /Volumes/Work/Applications/Colloquy.app/Contents/MacOS/Colloquy 
/Volumes/Work/Applications/Colloquy.app/Contents/MacOS/Colloquy:
    /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa
    /System/Library/Frameworks/Security.framework/Versions/A/Security
    /System/Library/Frameworks/WebKit.framework/Versions/A/WebKit
    /System/Library/Frameworks/AddressBook.framework/Versions/A/AddressBoo
    @executable_path/../Frameworks/AGRegex.framework/Versions/A/AGRegex
    @executable_path/../Frameworks/ChatCore.framework/Versions/A/ChatCore
     ...
    /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

Notice the frameworks with the @executable_path at the beginning. These frameworks are loaded relative to the path of the current executable. When we start up Colloquy.app the loader don't have to know if the application bundle is located in /Applications or ~/Applications or even some other non-standard location like ~/Desktop. This makes things very nice for drag-installers. The application will find the framework no matter where the application bundle has landed.

Even application plugin bundles can locate this framework in the application Framework directory. For example, if I had an application MyApplication.app that loads a plugin bundle MyPlugin.plugin at runtime, and MyPlugin.plugin requires PluginFramework.framework to run, I would structure my application bundle like so:

and my plugin bundle like so:

The plugin executable would load the framework at the path @executable_path/../Frameworks/PluginFramework.framework/.... This framework could be shared by all of the application frameworks. This solution is good for sharing code among plugins.

Plugins are a bit harder

Now, things start to get a little tricky for plugin bundles loaded by system applications, such as the screen saver application. I couldn't really find any examples of plugin bundles that loaded frameworks in any of the system application plugins (didn't do an exhaustive search though). Don't fret! It is possible! In Mac OS 10.4 (Tiger) and later the loader can find frameworks relative to the @loader_path. This means you can have a plug-in bundle with its own Frameworks directory that includes the required frameworks. For example, if MyApplication.app were a system application that loads a plugin bundle MyPlugin.plugin at runtime, and MyPlugin.plugin requires PluginFramework.framework to run, I would structure my plugin bundle like so:

In this case, the plugin executable would load the framework at the path @loader_path/../Frameworks/PluginFramework.framework/Versions/A/PluginFramework. Very nice!

Conclusion

With the dynamic loading of system-level and thrid-party frameworks, the Mac OS goes a long way to making bundles self-contained. It makes applications much more encapsulated and drag-installable.