Features


Easy Dynamic Loading in C++

Sasha Gontmakher

Many programs today configure themselves on the fly. Dynamic loading can really benefit from encapsulation, to hide tedious details and system dependencies.


Introduction

Suppose you're building a web-browsing application that will support several kinds of protocols (ftp, http, etc.). For this kind of application, you will probably want to define a set of classes, one for each protocol, with all of them implementing a common interface, as shown in Figure 1.

As the user clicks the link (or types in the URL), the application checks the address for the protocol identification string and instantiates the appropriate class.

If space is at premium, it would make sense to put only the most frequently used classes into the executable file, and leave the rest in shared libraries that would be loaded on demand. You might also want to use shared libraries if you expected to handle new protocols in the future. You could then define additional protocol classes without needing to recompile the whole application.

COM and CORBA both implement this sort of mechanism, but using them would be overkill. They would introduce unnecessary complexities to the code and the overhead of a middle-tier system. Fortunately, C++ provides a way to obtain this functionality with very little overhead and with just a minimal amount of work from the programmer.

Regime — A Dynamic Loading System

The basic idea presented here is the same as utilized in COM: knowing the layout of an object's virtual table, you can execute its methods, regardless of whether they are defined inside the executable or in the shared library (see Figure 2). C++ calls the correct function automatically, so all you need to do is create an object and obtain a pointer to it through a mechanism commonly known as a factory.

I call the dynamically loadable classes in this system the products. Because products are dynamically loadable, the main program should be aware only of the base class of their hierarchy. To enable users to specify concrete products to be loaded, I provide a system of keys, which can be simple string types. For example, you can use string keys like "ftp" and "http" to request protocol products.

To summarize, I designed this system to meet the following requirements:

I called this library "regime," which stands for REGIstration MEthods. Thus, I have placed all the code in namespace regime.

Registering a Class

The first task in this system is to register the class that will be dynamically loaded. This is where the Factory design pattern comes in. Each dynamically loadable class defined within a hierarchy has an associated creator object. If you know where this object is you can use it to instantiate a product that you need. The creator object's interface looks like this:

class ProtocolCreator {
   Protocol* create(const string&
      url_address) = 0;
};

The creator of http protocol objects looks like this:

class HTTPCreator : ProtocolCreator {
   Protocol* create(const string&
      url_address) {
      return new
         HTTPProtocol(url_address);
   }
};

The following is then a factory that performs the actual creation:

class ProtocolFactory {
   static Protocol*
   create(const string& proto,
   const string& url_address) {
      ProtocolCreator* c =
         ... // function that finds
             // creator for protocol
      if (c == 0) return 0;
      return c->create(address);
   }
   static void register(const string& proto,
                        ProtocolCreator* creator);
   static void unregister(const string& proto);
};

The register method should be called by the constructor of the HTTPCreator. What remains to be done is to put the HTTPCreator object in the file scope of the HTTPProtocol.cpp file.

Automating the Job

The essential problem with the method presented above is that it requires a lot of manual work. Also, you must redo it for any class hierarchy you write that needs dynamic registration. This sounds like a perfect case for using templates — and it is. Templates enable an implementation of this technique that requires only one line of code for each registered class [1].

First I define a factory class, as shown in the snippet below (see Figure 3 for full listing). It must be aware of two things: the base type of the objects it is generating and the type of key used to identify the product. To make different keys available dynamically to any client in the application, factory is designed following the singleton pattern.

template <class KEY, class BASE> class factory {
public:
   static BASE* create(const KEY& key);
   static void register(const KEY& key,
      CreatorBase<KEY, BASE>* creator);
   static void unregister(const KEY& key);
private:
   static factory<KEY, BASE>& instance();
};

All the static methods obtain a reference to the factory implementation through the instance method, which instantiates the factory object if necessary.

Implementing the creator classes is now straightforward:

template <class BASE> class creator_base {
   virtual BASE* create() const = 0;
};
template <class KEY, class BASE, class PRODUCT>
class creator : public creator_base<BASE> {
   virtual BASE* create() const { return new PRODUCT; }
};

Now all the programmer has to do to register a class is create the following object at the file scope within the shared library source file:

static creator<string, Protocol, HTTPProtocol>
   s_creator("HTTP");

The programmer can even go further and define several creator objects to make the class available under several names (i.e. keys).

Requesting creation of an object from any module is also simple:

Protocol *p = factory<string, Protocol>::create("HTTP");

Plugging in a Library

There is still a problem when the factory does not find a class creator. Without any additional information, the factory can only report to the user that the object cannot be created, by returning a null pointer. But if the factory knew the name of the shared library that contained the code for the object, it could load the library dynamically. When the library was loaded the constructors for the creators defined within would be executed (because an instance of each creator object is defined at file scope within the library) [2]. The creator objects would thus register themselves with the factory. An additional search for a creator would then succeed.

Note that this model of dynamic loading is fundamentally different from what is available in C. In this C++ model, once the library is loaded, the user does not have to manually resolve functions or mess with casts. Type safety is guaranteed by design.

To be able to tell the factory where to find the shared library, I define a load_strategy<KEY> interface, which users can implement to their liking. For example, a strategy can just append ".so" to the key string or it can use a database lookup to find the location of the shared library.

template <class KEY> class load_strategy {
   virtual string find(const KEY& key) = 0;
};

A simple way to dynamically configure the set of available keys is just to use a configuration file that provides a mapping between keys and shared library names. An alternative way would be to use the information provided in the shared library itself to get the names of all the keys that are contained in it.

The factory keeps track of this information as long as it loads the shared library itself. So you can use the factory's load_library method to load the library, and then use the get_keys method to list the keys defined in that library. Note that if the shared library contains keys for factories of several types, you may need to query them all. For this reason, I would recommend putting only keys for one type of factory in a shared library.

Additional Features

Managing Instances

In a typical application, some products need to be singletons, while others in the same hierarchy should be created each time they are requested. To provide this flexibility, I define an additional creator class, SingletonCreator. It remembers the object that it created and returns it next time you request its key.

In this scheme it is transparent to the user which products are singletons and which are not. However, to enable deletion of a created singleton object, the application needs a data structure that registers all such objects. Such a scheme could be implemented in the factory. But this could impose significant overhead for users that do not use it. Thus, if you need to delete the objects created, define a static set in the root class of the hierarchy, with a constructor that registers the object and a destructor that unregisters it. The only interaction you need with the factory is to tell it that the objects were deleted, so the singleton product creators can reinitialize themselves. You can do this using the factory's clean or clean(const KEY& key) method.

Unloading the Library

If you know that your application no longer needs a shared library, you may want to unload it to save memory. This is done through the unload(const KEY& key) method. The factory keeps track of which creators it has loaded; if you have requested to unload all the keys associated with a library, the factory unloads the library. However, if you use some key from a library for creating an object, the library will be loaded again.

Note that a shared library cannot be sure that none of the objects it created still exist. It is thus the user's responsibility to keep track of this. You could use an object-counting scheme to automatically request the unloading of a key when all the products of that key have been destroyed.

A factory keeps track of the keys that the shared library provided for that factory. If the library provided keys for different factories, one factory could decide to unload the library while its keys were still in use by other factories. Then when the user innocently tried to use an object belonging to the second factory, the program would crash. This is one more reason to use one shared library to provide keys of only one type.

Passing Initialization Parameters

In some cases a user may want to customize the way a product is initialized. It is not obvious how to go about this. Note that at compile time, you don't know which class will be created, so all the class constructors must be able to receive the same parameters. This is only logical — all the products should obey the same interface, and the signature of the constructor is part of the interface too.

In most cases, you could happily use default constructors and use some init method to initialize the classes. However, if you really need to pass parameters to the constructor, you can encapsulate them into a class and use the second factory create method that accepts a parameter.

class MyBase {
public:
   class FactoryParams {
   public:
      FactoryParams(int, X&);
      // ...
   };
};

template <class BASE, class KEY> class factory {
   void create(const typename BASE::FactoryParams& params);
};

To use this method you may have to define two variants of factory — one with second create method and one without.

Portability

I have used the code presented here on all Unix platforms I could get my hands on — including Linux, Solaris, and SGI. The model of Unix shared libraries fits nicely. You could have problems with some antique compiler that does not support templates or namespaces very well, or has an old implementation of the Standard C++ library. For example, you may need to replace the #include <map> with #include <map.h> and remove the std::prefix. In any case, you could use the latest GNU C++ compiler, which will compile this code easily.

I have compiled this code on Windows. The MS Visual C++ 6.0 compiler handles the code okay, but its model of symbol export from the dynamic libraries is slightly different — you need to prefix the instance method with a _dllexport directive method. Also VC++ provides several versions of the C++ runtime library (shared/static, multithreaded/single-threaded, debug/release), so be sure to choose the same C++ runtime DLL for all modules in your project.

Technical Issues

The typical linkers provided in Unix compiler implementations have an important limitation: when building a shared library, you cannot supply an executable as an argument to the linker, so as to make the shared library import symbols from the executable. I don't think this is a fundamental limitation, it's just an unavailable feature. The workaround here is to put the kernel of the program in a shared library containing all the code and define a small driver executable. The executable is then linked to the kernel, as are the shared libraries defining the products. This situation is depicted in Figure 4.

You would probably want to partition your application in this style anyway, especially if you want to provide several interfaces (GUI and command shell) to your code.

It can also be tricky getting templates to compile and work correctly. In most cases, it is okay to have multiple instantiations of the same object in a program. Therefore, most compilers by default store template instantiations statically in each object file. However, since the regime system relies heavily on uniqueness of template instances, you will need to ask the compiler to use a template repository. Fortunately, this feature is available in all modern compilers.

And please remember to always define virtual destructors for your products. While in other programs you may know their exact type when deleting them, when using regime you will have only a pointer to the base class.

Note that regime is not thread safe. It would impose too big an overhead to serialize all instances of object creation or destruction. You can protect your library in several ways:

Conclusion

Templates present infinite possibilities for automating routine things that programmers are (and should be) lazy about.

In this article I used templates to implement a simple and flexible scheme for dynamic loading. It provides transparency with respect to how the classes are distributed between executables and shared libraries. The programmer is free to define the distribution in any way he wishes.

The source code for this article is available on the CUJ ftp site (see p. 3 for downloading instructions.) It includes the files that define the library and several examples that demonstrate its use.

Acknowledgment

I would like to thank Michael Plavnik for careful reviewing of the manuscript and the port to Windows.

Notes

[1] Actually, one line per registered key, but in most cases it is the same.

[2] The C++ Standard does not guarantee that when a shared library is loaded, that all static objects will automatically be constructed. However, I have used this scheme successfully with Linux, Solaris, Digital Unix, IRIX, and Windows.

Sasha Gontmakher has a B.A. in Computer Science from the Technion Institute of Technology, Israel, and he is currently working on a Ph.D. He is currently employed as a C++ programmer.