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.
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 |
#include <dlfcn.h> and compile with -ldl:gcc myprog.c -ldl -o myprogPlug-in Architecture Flow
Here is how a typical plug-in based program uses the dlopen API step by step:
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
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
-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.