CS 3210 - Lab 15 - Dynamically Loading Libraries at Run-Time

Way back in Lab 3 we learned how to make shared and static libraries and link with them. Today we will learn how to load shared libraries at run-time and manually call specific functions in them. This technique is used by various programs to support plug-ins, or extensible modules that the application can load and use. Examples of UNIX programs that support plug-ins are GIMP, Apache, and the PAM (Pluggable Authentication Module) security framework.

The chapter correponding to this lab is Chapter 25.

Brief Review: Function Pointers

Before we plunge into talking about calling functions in libraries loaded at run-time, we first need to talk about function pointers. As you know, the C language supports pointers, which is to say, a variable that stores the address of another variable.

Example Program: intptr.c - A simple program that illustrates how variable pointers work.

C allows you to make pointers to functions as well as variables. The syntax is just slightly more involved.

Example Program: funcptr.c - A simple program that shows how function pointers work.

(As you've probably already guessed, this is how signal handler functions are called when you register them with signal or sigaction.)

Note that the return type and argument types are significant; the function pointer you declare must match the function it will point to both for the argument number, types and order (called the function signature), and for the return type as well.

Example Program: typefuncptrs.c - A program that illustrates the need for a differently declared function pointer based on signature.

Dynamic Loading

First things first: all programs that use the dl* functions should do the following:

Calling a Function from an Arbitrary Library

The goal is to call a function from some arbitrary library. These are the steps you need to take to do it:

  1. dlopen - Loads a library into memory. You can pass it the flag RTLD_NOW to make it load the library right now or RTLD_LAZY to make it load the library not right now, but when the program needs to use it. (See p. 460).
  2. dlsym - Retrieves the address of a function in the library. You will need to store that address in a function pointer with a matching signature. (See p. 460)
  3. Call the function with your function pointer. (Which is the point of this whole exercise.)
  4. dlclose - Unloads the library from memory (cleanup). (See p. 461)

You can man any of the above functions to see the documentation for them.

If you're looking for a specific function from a specific library, it's critical that you know the name of the library, the name of the function, and the signature of the function.

Example Program: dlcos.c - This program dynamically loads the math library (libm.so) at run-time, extracts the cosine (cos) function and calls it go get the cosine of 2.0.

Checking For Errors: dlerror

Error checking is always a good idea. It's entirely possible that your program has tried to load a library that doesn't exist or has tried to retrieve a function that doesn't exist. The dlerror function can be used to return a string containing the last error message. (See p. 458, 461)

Example Program: dlcos-check.c - Same program as before, only with error-checking using dlerror. This example comes from the man page for dlopen.

Loading Your Own Libraries

The dl* functions work for pre-installed system libraries, but you can also load symbols from your own libraries.

Example Program: loadhello.c - This example program comes from you book on p. 462. It uses the library libhello.c and the header file libhello.h. The Makefile shows how to build all of these.

Further Study

The man page for dlopen, dlerror, dlsym, and dlclose.

Dynamic Class Loading for C++ on Linux - an article in the online version of Linux Journal.

Your book, Chapter 25.

Lab 15

For this lab, you will create a small library libmathops.so, containing four functions that perform the four basic math operations, add, subtract, multiply, and divide. You will also create a program dlmath.c that takes three command line arguments: the name of one of the functions in the library you created, and two numbers. Your program will load the library libmathops.so, extract the function passed on the command-line, and call the function with the 2 other numbers passed on the command-line.

Some Steps to Help You

In a file called libmathops.c, create four funcitons add, sub, mul, and div each of which take two doubles and returns a double. The whole file can actually be only 4 lines long. (It's fine with me if you make it longer.)

In a file called dlmath.c, do the following:

  1. Check to make sure they passed 3 command-line arguments (argc == 4), print a usage message and exit if they didn't.
  2. Convert the last two command-line args (argv[2] and argv[3]) to doubles by calling strtod like so:
    num1 = strtod(argv[2], NULL);
    
    See the man page for strtod and the strtodex.c example program if you're interested.
  3. Load your libmathops.so library by calling dlopen. Be sure to error-check to make sure it loaded properly.
  4. Extract the symbol for the math operation passed as the first command-line argument (argv[1]) by calling dlsym. You will need a function pointer to store the function. Be sure to error-check again here by calling dlerror.
  5. Call the function to retrieved with the numbers you've converted using strtod. Print the answer to stdout.
  6. At the end of your program, clean up by calling dlclose.

The dlmath.c program should be about 45 lines long.

Be sure to have a Makefile as well with rules to build both the libmathops.so library and the dlmath program that uses it. The Makefile should be about 15 lines long. For a reminder on how to build shared libraries, look at the Makefile included with the lab programs. You can also review the Lab 3 notes.

Put all of these files in a lab15/ directory.

Output

Here are some example tests:

$ ./dlmath 
usage: dlmath mathfunc num1 num2
$ ./dlmath add 3 4
add called with 3.00 and 4.00 = 7.00
$ ./dlmath sub 7 1
sub called with 7.00 and 1.00 = 6.00
$ ./dlmath mul 9 2
mul called with 9.00 and 2.00 = 18.00
$ ./dlmath mul 4.28 9.19
mul called with 4.28 and 9.19 = 39.33
$ ./dlmath div 12 6
div called with 12.00 and 6.00 = 2.00
$ ./dlmath div 12 7
div called with 12.00 and 7.00 = 1.71
$ ./dlmath foo 4 5
./dlmath: undefined symbol: foo.

If you can run the same tests with your program and get the same results, you probably did it right.

Output Formatting Tips

You may get a compiler error that says warning: int format, double arg. This happens if you use the format specifier "%d" in your printf statement and pass it a variable of type double. In this case the "%d" stands for "digit" (i.e. integer), not double. To display a double, use "%f", which stands for "floating point" (i.e. double).

If you want to only display, say, two decimal places, just use "%0.2f" as the format specifier in printf.

Files

Here is a list of the files created in this lab.

Please turn in ALL of the above files with your name, lab # and class # at the top.