Autoconf, Automake, and Libtool: A Simple GNU/Linux Module Loader

| ## 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.

This document was generated by Gary V. Vaughan on February, 8 2006 using texi2html