dcsimg

Living the Dynamic Life of Plg-Ins

Many C and C++ applications use a plug-in or modular architecture to add features dynamically. Unlike monolithic applications, where all features are compiled into a single executable, modular applications typically have a central engine and a set of complementary feature libraries. Each library -- usually called a plug-in or a module -- implements a unique feature. When that specific feature is needed, the engine simply loads the module on demand, and calls the module to do the work.

Many C and C++ applications use a plug-in or modular architecture to add features dynamically. Unlike monolithic applications, where all features are compiled into a single executable, modular applications typically have a central engine and a set of complementary feature libraries. Each library — usually called a plug-in or a module — implements a unique feature. When that specific feature is needed, the engine simply loads the module on demand, and calls the module to do the work.

In a “pluggable” application, each module typically adheres to a well-defined, prescribed interface, and additional modules can be added at any time, providing ad hoc extensibility.

In Netscape, for example, each plug-in (like the Flash Player or SVG Viewer) is a separate piece of code that adheres to a well-documented interface. When Netscape launches, it loads all of the plug-ins found in its search path, calling the same function (say, initialize()) in each one to discover and register the MIME type that the plug-in handles. In this way, the browser can display an infinite variety of media assets, not just HTML and JPEG, provided that the media type’s corresponding plug-in is available. Other popular modular applications include Adobe Photoshop, the Linux Pluggable Authentication Module (PAM) system, and the GIMP.

Like Netscape or the GIMP, your own applications can use dynamically loaded modules to both simplify the design of your software and provide extensibility.

This month, let’s take a look at how to build a “pluggable” application. The process is not difficult — if you know how to dynamically load libraries and use function pointers. In fact, function pointers are the only way to make direct use of functions in dynamically loaded objects. So, let’s start with a review of just what a function pointer is.

The Gory Details

In both editions of “The C Programming Language,” Kernighan and Ritchie describe pointers to functions as follows:

“In C, a function itself is not a variable, but it is possible to define pointers to functions, which can be assigned, placed in arrays, passed to functions, returned by functions, and so on.”

In other words, like a pointer to a variable, a pointer to a function is a reference to the function. And like other references in C, you can assign, accumulate (say, in arrays), and return pointers to functions in the same way you can process references like &i, where i is any type of variable.

To declare a function pointer in ANSI C, you list a return type, a variable name, and a formal argument list. Here’s an example:


char *(*myref)(char *, int);

This declaration creates a pointer to a function named myref (the syntax (*myref) declares myref as a pointer to a function). The return type of the function that myref references is char *, and the function expects two arguments, a char * and an int.

Assigning a function to a function pointer is easy. Assuming you’ve declared a function char *callme(char *, int), the following code does the trick:


myref = callme;

When assigning a function to a pointer, do not specify a parameter list with the function. Otherwise, you’ll call the function (which of course, you can do, if the function returns a pointer to a function.)

Invoking a function through a function pointer is also easy and is much like dereferencing any other pointer. Previously, Kernighan & Ritchie C required that you wrap the function pointer in parenthesis. However, ANSI C relaxed that restriction and calling a function using a function pointer now looks exactly like calling any other function.


char *name;
char *codestring;
char *(*myref)(char *, int);
.
.
.
myref = callme;
name = myref(codestring, len(codestring));

Used well, function pointers can enhance clarity and even improve performance (but be careful of praying to the false gods of efficiency and performance).

Presenting… A Bit of Code

Listing One shows a snippet of code that every C programmer has probably written at one time or another. After parsing some string data to determine its format, a flag, timefmt, is set, and a switch statement is used to call the corresponding function for the format.




Listing One: Process incoming data according to its format

switch (timefmt) {
case 0:
stdtime =
timeformat0(timestr, len(timestr));
break;
case 1:
stdtime =
timeformat1(timestr, len(timestr));
break;
.
.
.
default:
/*
* of course, this case should be caught at option parsing time, but…
*/
fprintf(stderr, “%s: unknown time format: %d\n”, pgm, timefmt);
exit 1;
}

While the code is adequate, it’s hard to read and maintain, as there are many replicated lines with minimal differences. What if we could remove (or move) the evaluation of the switch statement? What if we could replace the switch statement with just one line of code? That certainly would make the code much clearer, wouldn’t it? Listing Two shows a snippet of new code, similar to Listing One, that uses function pointers instead.




Listing Two: Processing data using function pointers

char *timeformat0(), *timeformat1();
char *standardtime;
char *(*timeconv)(char *, int);
.
.
.
switch (timefmt) {
case 0:
timeconv = timeformat0;
break;
case 1:
timeconv = timeformat1;
break;
.
.
.
default:
/*
* since we’re doing option processing at this point, it’s reasonable to bail now..
*/
fprintf(stderr,
“%s: unknown time format: %d\n”, pgm, timefmt);
exit 1;
}
.
.
.
standardtime = (*timeconv)(timestr, len(timestr));

Choosing which function to call now happens when the data is parsed. The processing code has been reduced to a single line, and as a side effect, we’ve probably improved performance as well.

Listing Three is an additional, complete, albeit simplistic, example of using function pointers. hello(), goodbye(), and whatami() are three void functions, and speak is a function pointer. The program calls one of the three functions depending on what you type as an argument.




Listing Three: A simple, complete example of using function pointers

#include <stdio.h>

void hello(void) {
puts(“hello world!”);
}

void goodbye(void) {
puts(“goodbye (cruel) world”);
}

void whatami(void) {
puts(“what am i, a postage stamp?”);
}

int main(int argc, char **argv) {
void (*speak)(void);

if (argc < 2) {
printf(“usage: %s [h|g]\n”, argv[0]);
exit(1);
}

switch (argv[1][0]) {
case ‘h’:
speak = hello;
break;
case ‘g’:
speak = goodbye;
break;
default:
speak = whatami;
break;
}
speak();
exit(0);
}

Hopefully, function pointers aren’t so mysterious any more. Function pointers certainly provide a useful mechanism for certain programming tasks. But be careful that their use doesn’t obscure the flow of the program, or make it hard for another programmer (or for yourself, two years hence) to maintain the software.

Now, let’s discuss dynamic loading.

Getting Plugged In

The most common dynamic loading interface is dlopen(), originally defined in Sun’s Solaris operating system, but now also supported on HP-UX, Linux, NetBSD, FreeBSD, and others. There are four important functions:


  • dlopen(const char *filename, int flag) opens the dynamic library found in the named file. flag controls when unresolved symbols in the library are resolved. If dlopen() succeeds, the call returns an opaque “handle” that’s used in dlsym() and dlclose().

  • dlsym(void *handle, char *symbol) is used to find the address of a function in the dynamically loaded library. The arguments to dlsym() are a “handle” (returned by dlopen()) and a null-terminated symbol name.

  • dlclose(void *handle) unloads the dynamic library. (To be more specific, dlclose() only unloads the library if the reference count is zero. Dynamic libraries can be loaded multiple times.)

  • dlerror(void) should be used to determine the cause of errors in any of the dynamic loading routines (dlopen(), dlsym(), or dlclose()). dlerror() returns NULL if no errors have occurred since initialization or since it was last called (calling dlerror() twice consecutively, always results in the second call returning NULL.)

Some operating systems have added other functions to the dlopen() suite, so check your local man pages for specifics.

Listings Four through Six show some sample code that uses dlopen(). Listing Four and Listing Five are two dynamic libraries used by the application shown in Listing Six. The main program loads the shared objects, locates symbols within the shared objects, and then calls the corresponding functions.




Listing Four: dh0.c, a plug-in to print greetings

#include <stdio.h>

void greeting(void)
{
puts(“hello world!”);
}




Listing Five: dh1.c, another plug-in

#include <stdio.h>

void goodbye(void)
{
puts(“goodbye (cruel) world”);
}




Listing Six: The “pluggable” application


1 #include <stdio.h>
2 #include <dlfcn.h>
3
4 main(int argc, char **argv)
5 {
6 int (*speak)(void);
7 void *dh0, *dh1;
8 void (*dh0f)(void), (*dh1f)(void);
9
10 dh0 = dlopen(“./dh0.so”, RTLD_LAZY);
11 if (dh0 == NULL) {
12 fprintf(stderr, “%s: open/load of dh0.so failed: %s\n”,
13 argv[0], dlerror());
14 exit(1);
15 }
16
17 dh1 = dlopen(“./dh1.so”, RTLD_LAZY);
18 if (dh1 == NULL) {
19 fprintf(stderr, “%s: open/load of dh1.so failed: %s\n”,
20 argv[0], dlerror());
21 exit(1);
22 }
23
24 dh0f = (void(*)(void))dlsym(dh0, “greeting”);
25 if (dh0f == NULL) {
26 fprintf(stderr, “%s: symbol lookup in dh0.so failed: %s\n”,
27 argv[0], dlerror());
28 exit(2);
29 }
30
31 dh1f = (void(*)(void))dlsym(dh1, “goodbye”);
32 if (dh1f == NULL) {
33 fprintf(stderr, “%s: symbol lookup in dh1.so failed: %s\n”,
34 argv[0], dlerror());
35 exit(2);
36 }
37
38 dh0f();
39 dh1f();
40
41 dlclose(dh0);
42 dlclose(dh1);
43
44 exit(0);
45 }

The following commands build the code:


% rm *.so ld
% gcc -o dh0.so -shared dh0.c
% gcc -o dh1.so -shared dh1.c
% gcc -ldl -o ld dl.c

In Listing Six, lines 10 and 17 load the two dynamic libraries. Line 24 finds the address of the routine greeting() in the first library; similarly, line 31 finds the routine goodbye() in the second library. Once function pointers dh0f and dh1f are set, lines 38 and 39 call the dynamically loaded functions. Lines 41 and 42 clean up.

A Handy Pointer

Function pointers are very useful and versatile. Combined with dynamically loaded modules, function pointers let you design and implement an extensible software architecture with no muss, no fuss, and little pain.



Eric Schnoebelen is a software developer with 20 years experience in both operating system and applications development. Eric can be reached at compiletime@cirr.com. The code shown in this column can be downloaded from http://www.linux-mag.com/downloads/2003-02/compile.

Comments are closed.