Loading shared objects at run time can be a useful way to structure your applications. Done right, it can make your applications extensible, and it also forces you to partition your code into logically separate modules, which is a useful coding discipline.
Many Unix applications, particularly large ones, are mostly implemented by separate blocks of code, often called plugins or modules. In some cases, they are implemented as completely different programs, which communicate with the application's core code via pipes or some other form of interprocess communication (IPC). In other cases, they are implemented as shared objects.
Shared objects are normally built like standard shared libraries (see Chapter 8), but they are used in a completely different way. The linker is never told about the shared objects, and they do not even need to exist when the application is linked. They do not need to be installed on the system in the same way most shared libraries do.
Just like normal shared libraries, a shared object should be linked explicitly against each library it calls. This ensures that the dynamic loader resolves all external references correctly when the shared object is loaded. If this is not done, then the external references are resolved only in the context of the application loading the shared object in that case. Theoretically, shared objects can be standard object files, but this is not recommended because it does not reliably resolve external shared library dependencies, just like a shared library that is not explicitly linked against all the libraries on which it depends.
The symbol names used in shared objects do not need to be unique among different shared objects loaded into the same program; in fact, they usually are not unique. Different shared objects written for the same interface usually use entry points with the same names. With normal shared libraries, this would be a disaster; with shared objects dynamically loaded at run time, it is the obvious thing to do.
Perhaps the most common use of shared objects loaded at run time is to create an interface to some generic type of capability that might have many different implementations. For instance, consider saving a graphics file. An application might have one internal format for managing its graphics, but there are a lot of file formats in which it might want to save graphics, and more are created on an irregular basis [Murray, 1996]. A generic interface for saving a graphics file that is exported to shared objects loaded at run time allows programmers to add new graphics file formats to the application without recompiling the application. If the interface is well documented, it is even possible for third parties who do not have the application's source code to add new graphics file formats.
A similar use is framework code that provides only an interface, not an implementation. For example, the PAM (Pluggable Authentication Modules) framework provides a general interface to challenge-response authentication methods, such as usernames and passwords. All the authentication is done by modules, and the choice of which authentication modules to use with which application is done at run time, not compile time, by consulting configuration files. The interface is well defined and stable, and new modules can be dropped into place and used at any time without recompiling either the framework or the application. The framework is loaded as a shared library, and code in that shared library loads and unloads the modules that provide the authentication methods.