Dynamically Loaded Libraries

 

42.1 — Dynamically Loaded Libraries
Introduction to the dlopen API | Chapter 42 · TLPI
EmbeddedPathashala.com — Free Embedded & Linux Tutorials

What Is Dynamic Loading?

When you build a normal Linux program that uses a shared library, the dynamic linker automatically loads that library when the program starts. The list of required libraries is baked into the executable’s ELF metadata.

But sometimes you do not want to load a library at startup. You want to load it only when needed — for example:

  • A text editor that loads a spell-check plugin only when the user requests it.
  • A graphics program that loads a file-format decoder only for the selected file type.
  • A server that loads protocol handlers as shared libraries dynamically.

This on-demand loading is called dynamic loading, and Linux provides a dedicated API for it: the dlopen API.

Key Terms

dlopen() dlsym() dlclose() dlerror() -ldl libdl Plug-in Architecture SUSv3 Dynamic Dependency List

Static Dependency vs Dynamic Loading

Here is how the two loading models compare:

Aspect Automatic (Static Dependency) Dynamic Loading (dlopen)
When loaded? At program startup by the dynamic linker At runtime, when your code calls dlopen()
Library in ELF? Yes — listed in dynamic dependency section No — the name is a runtime string
Use case Core libraries always needed (libc, libm) Optional plugins, codecs, drivers
Failure mode Program refuses to start dlopen() returns NULL — handle gracefully
Link flag needed -lname (e.g. -lmath) -ldl (link against libdl)

The Four Core dlopen API Functions

All four functions are declared in <dlfcn.h> and specified in SUSv3 (except dladdr which is Linux/glibc-specific):

Function What It Does Returns
dlopen(path, flags) Loads a shared library into process memory. Returns a handle for subsequent calls. void* handle, or NULL on error
dlsym(handle, name) Looks up a function or variable by name in the loaded library. void* address, or NULL
dlclose(handle) Decrements reference count; unloads library when count reaches 0. 0 on success, -1 on error
dlerror() Returns a human-readable error string after a failed dlopen/dlsym/dlclose. char* string or NULL if no error
Header & Link: Always include #include <dlfcn.h> and compile with -ldl:
gcc myprog.c -ldl -o myprog

Plug-in Architecture Flow

Here is how a typical plug-in based program uses the dlopen API step by step:

Program starts normally
User requests a plugin feature
dlopen(“plugin.so”, RTLD_LAZY) → handle
dlsym(handle, “run_plugin”) → function pointer
Call the function through the pointer
dlclose(handle) when done

Code Example 1: Minimal Dynamic Library Usage

This is the simplest possible use of the dlopen API. We load a math library and call a function from it at runtime:

Step 1: Create the plugin shared library (mylib.c)

/* mylib.c — the shared library we will load dynamically */
#include <stdio.h>

void greet(void) {
    printf("Hello from the dynamically loaded library!\n");
}

int add(int a, int b) {
    return a + b;
}

Step 2: Build the shared library

/* Compile mylib.c into a position-independent shared library */
gcc -fPIC -shared -o mylib.so mylib.c

Step 3: Write the main program that loads it dynamically (main.c)

/* main.c — dynamically loads mylib.so and calls its functions */
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>       /* Required for dlopen, dlsym, dlclose, dlerror */

int main(void) {
    void *handle;          /* Opaque handle returned by dlopen */
    void (*greet_fn)(void); /* Function pointer for greet() */
    int  (*add_fn)(int, int); /* Function pointer for add() */
    const char *err;

    /* --- Step 1: Open the shared library --- */
    handle = dlopen("./mylib.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        exit(EXIT_FAILURE);
    }
    printf("Library loaded successfully.\n");

    /* --- Step 2: Get address of greet() --- */
    dlerror(); /* Clear any existing error */
    *(void **)(&greet_fn) = dlsym(handle, "greet");
    err = dlerror();
    if (err) {
        fprintf(stderr, "dlsym(greet) failed: %s\n", err);
        dlclose(handle);
        exit(EXIT_FAILURE);
    }

    /* --- Step 3: Call the function --- */
    (*greet_fn)();

    /* --- Step 4: Get address of add() and call it --- */
    dlerror();
    *(void **)(&add_fn) = dlsym(handle, "add");
    err = dlerror();
    if (!err) {
        int result = (*add_fn)(10, 20);
        printf("add(10, 20) = %d\n", result);
    }

    /* --- Step 5: Close the library --- */
    dlclose(handle);
    printf("Library closed.\n");

    return 0;
}

Compile and run:

gcc -o main main.c -ldl      # -ldl links against libdl
./main
# Output:
# Library loaded successfully.
# Hello from the dynamically loaded library!
# add(10, 20) = 30
# Library closed.

Code Example 2: Simple Plugin System

This example shows a host program that can load any plugin that implements a standard interface:

/* plugin_api.h — defines the interface every plugin must implement */
#ifndef PLUGIN_API_H
#define PLUGIN_API_H

typedef struct {
    const char *name;            /* Plugin name string */
    void (*init)(void);          /* Called when plugin is activated */
    void (*run)(const char *arg);/* Main action */
    void (*cleanup)(void);       /* Called before unloading */
} PluginAPI;

/* Every plugin .so must export this function */
PluginAPI *get_plugin(void);

#endif
/* hello_plugin.c — a simple plugin that implements PluginAPI */
#include <stdio.h>
#include "plugin_api.h"

static void my_init(void) {
    printf("[HelloPlugin] init called\n");
}

static void my_run(const char *arg) {
    printf("[HelloPlugin] running with arg: %s\n", arg ? arg : "(none)");
}

static void my_cleanup(void) {
    printf("[HelloPlugin] cleanup called\n");
}

static PluginAPI plugin = {
    .name    = "HelloPlugin",
    .init    = my_init,
    .run     = my_run,
    .cleanup = my_cleanup
};

PluginAPI *get_plugin(void) {
    return &plugin;
}
/* host.c — loads a plugin dynamically and uses it */
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include "plugin_api.h"

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <plugin.so>\n", argv[0]);
        return 1;
    }

    /* Load the plugin */
    void *handle = dlopen(argv[1], RTLD_LAZY | RTLD_LOCAL);
    if (!handle) {
        fprintf(stderr, "Cannot load plugin: %s\n", dlerror());
        return 1;
    }

    /* Get the plugin descriptor */
    dlerror();
    PluginAPI *(*get_plugin_fn)(void);
    *(void **)(&get_plugin_fn) = dlsym(handle, "get_plugin");
    char *err = dlerror();
    if (err) {
        fprintf(stderr, "No get_plugin() in this .so: %s\n", err);
        dlclose(handle);
        return 1;
    }

    PluginAPI *p = get_plugin_fn();
    printf("Loaded plugin: %s\n", p->name);

    p->init();
    p->run("hello world");
    p->cleanup();

    dlclose(handle);
    return 0;
}
# Build:
gcc -fPIC -shared -o hello_plugin.so hello_plugin.c
gcc -o host host.c -ldl

# Run:
./host ./hello_plugin.so
# Output:
# Loaded plugin: HelloPlugin
# [HelloPlugin] init called
# [HelloPlugin] running with arg: hello world
# [HelloPlugin] cleanup called
Real-world uses: Apache web server uses this model for modules (mod_php.so, mod_ssl.so). Python’s ctypes, GStreamer codec plugins, and LibreOffice extensions all rely on dynamic loading.

Code Example 3: Error-Safe dlopen Wrapper

In production code, always wrap dlopen/dlsym with proper error handling:

/* safe_dl.c — production-grade wrapper for dlopen API */
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

/* Safe dlopen: prints error and exits if load fails */
void *safe_dlopen(const char *path, int flags) {
    void *handle = dlopen(path, flags);
    if (!handle) {
        fprintf(stderr, "ERROR: dlopen('%s') failed: %s\n", path, dlerror());
        exit(EXIT_FAILURE);
    }
    return handle;
}

/* Safe dlsym: prints error and exits if symbol not found */
void *safe_dlsym(void *handle, const char *symbol) {
    dlerror(); /* clear previous error */
    void *addr = dlsym(handle, symbol);
    const char *err = dlerror();
    if (err) {
        fprintf(stderr, "ERROR: dlsym('%s') failed: %s\n", symbol, err);
        exit(EXIT_FAILURE);
    }
    return addr;
}

/* ---- Demo usage ---- */
int main(void) {
    /* Load libm.so.6 (the math library) dynamically */
    void *libm = safe_dlopen("libm.so.6", RTLD_LAZY);

    /* Get the sin() function */
    double (*sin_fn)(double);
    *(void **)(&sin_fn) = safe_dlsym(libm, "sin");

    printf("sin(3.14159 / 2) = %f\n", sin_fn(3.14159 / 2.0));

    dlclose(libm);
    return 0;
}
gcc safe_dl.c -ldl -o safe_dl
./safe_dl
# sin(3.14159 / 2) = 1.000000
Important: The dlopen API originated on Solaris and is now standardized in SUSv3. On Linux it is implemented in glibc. The libdl library (-ldl) is needed only as a linking hint — on modern glibc systems dlopen is actually part of libc itself, but always include -ldl for portability.

Interview Questions & Answers

Q1. What is the difference between a shared library loaded at startup and one loaded with dlopen()?
A library listed in the ELF dynamic dependency section is loaded automatically by the dynamic linker when the program starts. If the library is missing, the program cannot start at all. A library loaded with dlopen() is loaded on demand at runtime. If the library is missing, dlopen() returns NULL and the program can handle the error gracefully and continue running.
Q2. Why do we compile with -ldl when using the dlopen API?
The dlopen, dlsym, dlclose, and dlerror functions are declared in <dlfcn.h> and implemented in libdl. The -ldl linker flag tells the linker to search this library for those symbols. On modern glibc these functions are actually in libc itself, but the -ldl flag is still required for portability across Linux systems and older glibc versions.
Q3. What does dlopen() return when called multiple times on the same library?
The library is only loaded into memory once. All subsequent calls to dlopen() on the same library return the same handle value. However, a reference counter is incremented with each dlopen() call. The library is only unloaded from memory when dlclose() has been called as many times as dlopen() — that is, when the reference count drops to 0.
Q4. What is a plug-in architecture and how does dlopen() enable it?
A plug-in architecture allows an application to load extra functionality (plug-ins) at runtime without recompiling. Each plug-in is a shared library (.so) that implements a known interface. The host program uses dlopen() to load the plugin .so, dlsym() to get function pointers for the interface functions, and then calls them. This allows developers to add new features to a running application simply by placing a new .so file in the plugin directory.
Q5. If dlopen() is given a filename without a slash, how does it find the library?
Without a slash in the filename, dlopen() uses the same search rules as the dynamic linker: it checks LD_LIBRARY_PATH, then /etc/ld.so.cache (built from /etc/ld.so.conf), then the default library directories /lib and /usr/lib. If the filename contains a slash (e.g., “./mylib.so”), dlopen() interprets it as a relative or absolute pathname and does not search the standard directories.
Q6. What happens to a dynamically loaded library when a process terminates?
All libraries loaded via dlopen() are implicitly closed when the process terminates. The dynamic linker performs an implicit dlclose() on all such libraries, which triggers any registered finalization functions (destructor attributes or atexit handlers) before the library is unmapped from memory.

Leave a Reply

Your email address will not be published. Required fields are marked *