NSBundle: Loading External Code Like A Boss

I’ve always known about NSBundle’s external code loading capabilities, but have never had the immediate need to leverage it. However, one of my more recent projects required me to develop in a “host-plugin” model – I finally had the excuse I needed to dive into the advanced features of NSBundle.

The Host-Plugin Model


The “host-plugin” model is an application model where the host application loads code, resources, and settings from external plugins or bundles. For example, suppose a host application lets a user install custom themes. These themes could be included with the host application, or downloaded from the internet. The host application would load these themes as plugins or bundles and grab code/resources from them as needed.

Creating a Bundle


If you’re developing for OSX then you’re in luck! Xcode comes with a “Bundle” template that serves as a starting point for any OSX-targeted plugin/bundle. In my case, I needed to build plugins for iOS devices. (This isn’t really supported. However, I was developing for jailbroken devices. Thanks to DHowett’s theos makefile system and minor tweaking, I was able to compile & load bundles on iOS as well).

A bundle consists of three components: resources (images, documents, sounds, etc…), an info.plist (hopefully you know what this is by now), and code files. You do not need resources or code for that matter. A bundle containing only resources and no code is completely valid. Likewise, a bundle with no resources and only code is completely fine as well. The title of this post is “Loading External Code” so I’m going to be demonstrating that in this case.

Let’s create a new plugin named “TestPlugin” using Xcode’s bundle template. Notice there are no code files, let’s fix that! Create a new class as a subclass of NSObject and name it “TestPlugin”. Let’s add some very basic code to our plugin:

TestPlugin.h

#import <Foundation/Foundation.h>

@interface TestPlugin : NSObject
- (void)sayHello;
@end

TestPlugin.m

#import "TestPlugin.h"

@implementation TestPlugin

- (void)sayHello {
    NSLog(@"Hello from TestPlugin!");
}

@end

This is incredibly simple, but it will work for our purposes. Next, open the bundle’s Info.plist file (should be located under the ‘Supporting Files’ folder on the left). At the very bottom, you should see the key titled “Principal Class” (“NSPrincipalClass” if you are looking at raw keys/values). For it’s value enter “TestPlugin”.

The “Principal Class” or NSPrincipalClass is the main class that will be used for dynamic instantiation by our plugin host. We have not setup any custom initializers, so our host will simply call -init on our principal class (which is our “TestPlugin” class in this instance).

And that’s it! you can compile the bundle and place it wherever your host application’s plugin directory will be located.

Dynamically Loading Bundles


We’ve created a plugin, now it’s time to call it to action! From anywhere within your host application, you can load your plugin like this:

NSURL *url = <fileURL to plugin goes here>;
NSBundle *bundle = [NSBundle bundleWithURL:url];

if([bundle load]) {
    Class TestPlugin = [bundle principalClass];
    plugin = [[TestPlugin alloc] init];
    [plugin sayHello];
    [bundle unload];
} else { NSLog(@"Failed to load plugin"); }

If all goes well, you should see “Hello from TestPlugin!” in your log! Not too hard right? Notice how we called -init on our TestPlugin? That’s simply because we didn’t specify a custom initializer in our bundle, so we’re calling the default one. We also call our custom method: -sayHello, this is what actually prints our message to the log.

Conclusion


The whole process of creating and dynamically loading bundles is actually quite simple once you do it a couple times. More information on the inner-workings of NSBundle can be found in Apple’s NSBundle Developer Documentation.