|
17.4 A Simple GNU/Linux Module Loader
Something to be aware of, is that when your users write dynamic modules
for your application, they are subject to the interface you design. It
is very important to design a dynamic module interface that is clean and
functional before other people start to write modules for your code. If
you ever need to change the interface, your users will need to rewrite
their modules. Of course you can carefully change the interface to
retain backwards compatibility to save your users the trouble of
rewriting their modules, but that is no substitute for designing a
good interface from the outset. If you do get it wrong, and
subsequently discover that the design you implemented is misconceived
(this is the voice of experience speaking!), you will be left with a
difficult choice: try to tweak the broken API so that it does work
while retaining backwards compatibility, and the maintenance and
performance penalty that brings? Or start again with a fresh design born
of the experience gained last time, and rewrite all of the modules you
have so far?
If there are other applications which have similar module requirements
to you, it is worth writing a loader that uses the same interface and
semantics. That way, you will (hopefully) be building from a known good
API design, and you will have access to all the modules for that
other application too, and vice versa.
For the sake of clarity, I have sidestepped any issues of API
design for the following example, by choosing this minimal interface:
- Function: int run (const char *argument)
- When the module is successfully loaded a function with the following
prototype is called with the argument given on the command line. If
this entry point is found and called, but returns `-1', an error
message is displayed by the calling program.
Here's a simplistic but complete dynamic module loading application you
can build for this interface with the GNU/Linux dynamic loading
API:
|
#include <stdio.h>
#include <stdlib.h>
#ifndef EXIT_FAILURE
# define EXIT_FAILURE 1
# define EXIT_SUCCESS 0
#endif
#include <limits.h>
#ifndef PATH_MAX
# define PATH_MAX 255
#endif
#include <dlfcn.h>
/* This is missing from very old Linux libc. */
#ifndef RTLD_NOW
# define RTLD_NOW 2
#endif
typedef int entrypoint (const char *argument);
/* Save and return a copy of the dlerror() error message,
since the next API call may overwrite the original. */
static char *dlerrordup (char *errormsg);
int
main (int argc, const char *argv[])
{
const char modulepath[1+ PATH_MAX];
const char *errormsg = NULL;
void *module = NULL;
entrypoint *run = NULL;
int errors = 0;
if (argc != 3)
{
fprintf (stderr, "USAGE: main MODULENAME ARGUMENT\n");
exit (EXIT_FAILURE);
}
/* Set the module search path. */
getcwd (modulepath, PATH_MAX);
strcat (modulepath, "/");
strcat (modulepath, argv[1]);
/* Load the module. */
module = dlopen (modulepath, RTLD_NOW);
if (!module)
{
strcat (modulepath, ".so");
module = dlopen (modulepath, RTLD_NOW);
}
if (!module)
errors = 1;
/* Find the entry point. */
if (!errors)
{
run = dlsym (module, "run");
/* In principle, run might legitimately be NULL, so
I don't use run == NULL as an error indicator. */
errormsg = dlerrordup (errormsg);
if (errormsg != NULL)
errors = dlclose (module);
}
/* Call the entry point function. */
if (!errors)
{
int result = (*run) (argv[2]);
if (result < 0)
errormsg = strdup ("module entry point execution failed");
else
printf ("\t=> %d\n", result);
}
/* Unload the module, now that we are done with it. */
if (!errors)
errors = dlclose (module);
if (errors)
{
/* Diagnose the encountered error. */
errormsg = dlerrordup (errormsg);
if (!errormsg)
{
fprintf (stderr, "%s: dlerror() failed.\n", argv[0]);
return EXIT_FAILURE;
}
}
if (errormsg)
{
fprintf (stderr, "%s: %s.\n", argv[0], errormsg);
free (errormsg);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/* Be careful to save a copy of the error message,
since the next API call may overwrite the original. */
static char *
dlerrordup (char *errormsg)
{
char *error = (char *) dlerror ();
if (error && !errormsg)
errormsg = strdup (error);
return errormsg;
}
|
You would compile this on a GNU/Linux machine like so:
|
$ gcc -o simple-loader simple-loader.c -ldl
|
However, despite making reasonable effort with this loader, and ignoring
features which could easily be added, it still has some seemingly
insoluble problems:
-
It will fail if the user's platform doesn't have the
dlopen
API. This also includes platforms which have no shared libraries.
-
It relies on the implementation to provide a working self-opening
mechanism. `dlopen (NULL, RTLD_NOW)' is very often unimplemented,
or buggy, and without that, it is impossible to access the symbols of
the main program through the `dlsym' mechanism.
-
It is quite difficult to figure out at compile time whether the target
host needs `libdl.so' to be linked.
I will use GNU Autotools to tackle these problems in the next chapter.
|