Transparent On-Demand Loading of Bundles
Ralph Zazula
rzazula@next.com
Summer, 1994

Abstract

Problems associated with loading NEXTSTEP bundles that depend on classes not present in the bundle, and of needing to load the code of an NXBundle to obtain basic information are discussed and a solution is presented.

Introduction

Dynamically loadable code, in the form of bundles, adds great extensibility to your applications and the way you develop them (ref. Winter NXApp).  However, there exist two areas of difficulty when dealing with the NXBundle class to perform dynamic loading: determining pre-load requirements and obtaining information about a bundle without loading its code.
		
Problem #1:		Obtaining Information About a Bundle

In some instances, bundles are used to represent individual functions that must be presented to the user in a graphical fashion.  For example, a button, or an entry in a menu for each function.  The information needed to properly configure the user-interface could be as simple as a text string or the name of an image (TIFF) file to use for an iconic representation.  This information should be obtainable without loading the classes contained in a bundle, especially in cases where few of the available functions are used during the same invocation of the application.

Solution #1:		Adding an "Information Resource" to the Bundle

The solution presented here is to include an additional resource file inside each bundle that contains programmer-defined keyword-value pairs.

This "information resource" file would be stored in ASCII form (the format of the NXStringTable data files, for example) and would appear either as a localizable resource (in the Language.lproj folders) or directly inside of the bundle folder.

The NXBundle class would be extended to include method(s) to return the keyword-value pairs, for example:

+ (List *)bundlesWithValue:(const char *)value forString:(const char *)key;
/*
 * Returns a List of RZBundle instances whose 'bundle.table' file contain
 * the key/value pair: "key" = "value".  Returns 'nil' if no bundles contain
 * the key/value pair.  The List should be freed by the caller.
 */

- (NXStringTable *)infoTable;
/*
 * Returns the NXStringTable instance representing the 'bundle.table' file
 * for the reciever.  This table has the user-defined key/value pairs.
 */

- (const char *)valueForString:(const char *)key;
/*
 * Returns the string value for keyword 's' found in the resource
 * 'bundle.table'.  Returns NULL if the keyword is not found.
 */

Using the above method, we could obtain the menu titles for a list of tool bundles, without loading the bundle code, in the following manner:

	List *toolBundleList = [RZBundle bundlesWithValue:"Tool" forString:"Bundle Type"];
	RZBundle *b;
	int i;
		
	for(i=0; i<[toolBundleList count]; i++) {
		b = [toolBundleList objectAt:i];
			[[self toolMenu] addItem:[b valueForString:"Menu Title"]
				action:@selector(loadBundle:) 
				keyEquivalent:NULL];
		}
	}

Problem #2:		Bundle Pre-Load Requirements

A fundamental problem exists due to two competing requirements in the implementation of NXBundles:

	- when loading a bundle, the superclass for each class defined in the bundle 
		must be present in the run-time environment for the application
		
	- the definition for a class can not be loaded by an application more than once
	
A conflict arises if more than one bundle contains a subclass of an object that is not linked into the application.  Possible solutions are:

1 - link the superclass into the application

2 - include the superclass definition in only bundle containing a subclass and be sure to load that bundle first

3 - place the superclass in a bundle by itself and load it before loading any bundles containing a subclass

Item "1" above does solve the problem of where to place the superclass but limits the ability to use bundles as a way to create modular programming projects.  Item "2" above puts a restriction on the loading of any bundle containing a subclass and may cause the subclass code for the bundle containing the superclass to be loaded in a situation where it is not needed.  Item "3" above is clearly a cleaner solution but there exists the problem of finding the bundle that contains the superclass.

Solution #2:		Class Content and Dependency Resources

The idea of an information resource can be extended to include information about the class content and class dependancies of each bundle.  By including the names of classes contained within the bundle, and the external classes required by it, an automated procedure can be employed to determine the pre-load requirements for each bundle without loading the code for the bundle.

The proposed solution is outlined below:

	- methods must be provided to access the additional resources
	- bundle creation (+alloc) must be modified to track all bundles (add them to a list)
	- bundle destruction (-free) must be modified to remove bundles from tracking
	- methods must be added to resolve pre-load requirements
	- methods that cause the bundle to be loaded should be modified to load dependancies first, 
		providing automation of the prerequiste loading process
	- utility methods to find bundles containing particular classes should be provided

Methods to access class resources

- (NXStringTable *)classTable;
/*
 * Returns the NXStringTable instance representing the 'class.table' file
 * for the reciever.  This table has the names of the classes defined
 * in the receiver.
 */

- (NXStringTable *)dependTable;
/*
 * Returns the NXStringTable instance representing the 'depend.table' file
 * for the reciever.  This table has the names of the external classes 
 * required by the receiver.  NOTE: the class list may contain classes already
 * present in the runtime system (use objc_lookUpClass() to check).
 */

Methods to Resolve Pre-Load Requirements

+ (BOOL)doPreloadsFor:(RZBundle *)bundle;
/*
 * Attempts to load any bundles containing classes required to load "bundle".
 * Returns YES on success (all external classes found) and NO on failure.
 * Upon success, the code for "bundle" and the code of any required bundles 
 * will be loaded.  Upon failure, the code for "bundle" will not be loaded but 
 * some or all of the prerequisite bundles may be loaded.
 */

- (BOOL)doPreloads;
/*
 * Checks for any external class dependancies and tries to resolve them.  Returns
 * YES on success and NO on failure.  Upon success, the recivers code and the code
 * of any required bundles will be loaded.  Upon failure, the receivers code will
 * not be loaded but some or all of the prerequisite bundles may be loaded.
 */

Utility Methods

+ (RZBundle *)bundleContainingClass:(const char *)className;
/*
 * Returns the first bundle found containing the class "className" or 'nil' if
 * no bundles currently initialized contain the class "className".  
 */

+ classNamed:(const char *)className;
/*
 * Searches all initialized bundles for one containing "className".  If a bundle
 * is found, the receiver performs any preloading required and then returns the
 * class for "className".  Returns 'Nil' if the class "className" cannot be found,
 * or if the preloading could not be performed.
 */

- (BOOL)containsClass:(const char *)name;
/*
 * Returns YES if this bundle contains the class named 'name'.  This information
 * is based on the contents of the file 'class.table' in the receivers bundle.
 */

Automating the Process

- classNamed:(const char *)className
/*
 * Overriden to perform dependency checking and bundle pre-loading
 * before loading the code for the receiving bundle.
 */

- principalClass
/*
 * Overriden to perform dependency checking and bundle pre-loading
 * before loading the code for the receiving bundle.
 */

Additional Hacks

By far, the most inconvinient aspect of programming with dynamically loaded code is the inability to use class names in Objective-C message expressions.  For example, code that used to look like this:

	id anObject = [[MyClass alloc] init];
	
looks like this with a NXBundle:

	id anObject = [[[[[NXBundle bundleForClass:"MyClass"] 
							classNamed:"MyClass"] alloc] init];
	
and now looks like this:

	id anObject = [[[RZBundle classNamed:"MyClass"] alloc] init];
	
We can further reclaim our rights to simplified programming by leveraging the Objective-C run-time system.  

On-Demand Loading Using the Run-Time System

First, lets examine the tools available to search for classes in the run-time system.  The functions provided to find classes by name are:

	objc_lookUpClass(const char *name);
	objc_getClass(const char *name);
	
The difference between these two functions is that objc_lookUpClass() returns Nil when it cannot find the requested class and objc_getClass() calls an error handling function and aborts the application.  But wait!  We can install our own error handling function to "trap" these failed object lookup calls using the run-time function:  objc_setClassHandler().  This undocumented function (well, except its declaration in /usr/include/objc/objc-runtime.h) accepts a pointer to a function that has the following form:

	int my_objc_classHandler(const char *className)
	
The return value of this function (ignoring the cast of int) is the id of the class object for className, or Nil if className cannot be loaded.  We can use a custom class handling function to perform loading as follows:

	int my_objc_classHandler(const char *name)
	{
		return (int)[RZBundle classNamed:name];
	}
	
	// some time early in our application life-cycle
	objc_setClassHandler(my_objc_classHandler);

This would allow us to write our code as:

	id anObject = [[objc_getClass("MyClass") alloc] init];
	
or, a skillfully crafted macro yields:

	#define @(x) objc_getClass(#x)
	id anObject = [[@(MyClass) alloc] init];

Here, the @(ClassName) notation is different enough to remind us we're dynamically loading ClassName but not too cumbersome to type.  The header file for an object could also include a predefined macro for loading that class, for example:

	#define _MyClass objc_getClass("MyClass");

	id anObject = [[_MyClass alloc] init];