Bugs Plugins

In the last post, I intimated that moving our models out into their own plugins was going to be really, really easy. It turns out that that's absolutely true! How nice.

We already had several models, loosely coupled with the program logic by the ALifeController protocol, with their own models, views, and controllers. All we need to do to complete the picture is to move them from the main app bundle into their own bundles, which can then be dynamically loaded by the CocoaBugs app.

In XCode, we can create a Cocoa bundle straightforwardly. Choose New Project, and pick Bundle > Cocoa Bundle.

cocoa bundle.png

This makes a new bundle project: all the code in the bundle will be compiled and linked as usual, but instead of making a double-clickable executable, the executable is used merely as an entry point to access the classes created in the bundle.

So, we can copy, for example, code to implement the Game of Life (following the ALifeController protocol) into this new project to make a CocoaBugs plugin.

game of life bundle.png

We also make a few changes to the Info.plist of the file:

game of life plist.png

The most important change is the value for "Principal class". This is the entry point to our bundle; once it's loaded, the main app can query the bundle for its principal class, and interacts with the Game of Life code from there. Here we've set it to the ControllerOfLife class.

(Note that there is another plist, "GameOfLife.plist", which contains the CocoaBugs-specific information about the GameOfLife model.)

We can also change the file extension of the compiled bundle, by getting info on the bundle target. Here we'll use .plugin.

wrapper extension.png

Now, the tricky bit is getting these to actually load from our main app. After building and copying the product into ~/Application Support/CocoaBugs/PlugIns/, we can do the following in our app controller (prepare for Code Overload):

+ (NSMutableArray *)allPlugIns;
{
    NSBundle *currBundle;
    Class currPrincipalClass;

    NSMutableArray *plugIns = [NSMutableArray array];
    for (NSString *plugInPath in [self allPlugInPaths]) {
        currBundle = [NSBundle bundleWithPath:plugInPath];
        NSLog(@"Checking %@", [currBundle bundleIdentifier]);
        if (currBundle) {
            currPrincipalClass = [currBundle principalClass];
            if(currPrincipalClass && [self plugInClassIsValid:currPrincipalClass])  // Validation
            {
                [plugIns addObject:currPrincipalClass];
            }
        }
    }

    NSSortDescriptor *nameSort = [[[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES] autorelease];
    [plugIns sortUsingDescriptors:[NSArray arrayWithObjects:nameSort, nil]];

    NSLog(@"%d plugins loaded", [plugIns count]);
    return plugIns;
}

+ (BOOL)plugInClassIsValid:(Class)plugInClass;
{
    return [plugInClass conformsToProtocol:@protocol(ALifeController)];
}

+ (NSMutableArray *)allPlugInPaths;
{
    NSString *ext = @"plugin";
    NSString *appSupportSubpath = @"Application Support/CocoaBugs/PlugIns";

    NSArray *librarySearchPaths;
    NSMutableArray *bundleSearchPaths = [NSMutableArray array];
    NSMutableArray *allBundles = [NSMutableArray array];

    // find the libraries
    librarySearchPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask - NSSystemDomainMask, YES);

    // we'll look in the App Support/CocoaBugs/PlugIns directory in each library
    for (NSString *currPath in librarySearchPaths) {
        [bundleSearchPaths addObject:[currPath stringByAppendingPathComponent:appSupportSubpath]];
    }
    [bundleSearchPaths addObject:[[NSBundle mainBundle] builtInPlugInsPath]];

    // check for .plugin files
    for (NSString *currPath in bundleSearchPaths) {
        for (NSString *currBundlePath in [[NSFileManager defaultManager] directoryContentsAtPath:currPath]) {
            if ([[currBundlePath pathExtension] isEqualToString:ext]) {
                [allBundles addObject:[currPath stringByAppendingPathComponent:currBundlePath]];
            }
        }
    }

    return allBundles;  
}

It seems like a lot, but it's pretty straightforward. (Mostly, it's just boilerplate from Apple's documentation on plugin loading design patterns.) From bottom to top:

  1. allPlugInPaths returns all the directories we want to search for plugins. In the current code, we look in the app's own PlugIns directory; the /Library/Application Support/ directory, and ~/Library/Application Support/.
  2. plugInClassIsValid validates a purported plugin: here, we just check to see if the principal class conforms to our ALifeController protocol.
  3. allPlugIns uses those two functions to load and check bundles, building an array sorted by name of each plugin and returning them.

We can then use the returned array of plugins to build a configuration window for each plugin. That'll be next time.

Leave a comment