When you use dlopen() to load a shared library at runtime, the functions inside that library can normally only call functions from other shared libraries already loaded. But sometimes, you want the library to call back into a function that lives in your main program — this is the classic callback pattern.
By default, the dynamic linker does NOT make the main program’s symbols available to dynamically loaded libraries. To enable this, you must compile your main program with a special linker flag: --export-dynamic.
to dynamic libs
❌ FAILS — symbol not found
to dynamic libs
✅ SUCCESS
When a shared library is loaded with dlopen(), and a function inside that library tries to call another function, the dynamic linker looks for that function in:
- The library itself
- Other shared libraries already loaded (like libc.so, libpthread.so, etc.)
The main program’s own functions are NOT in this search list by default. This is why calling a main-program function from inside a plugin will fail with an “undefined symbol” error.
--export-dynamic does.This option tells the linker to add all global symbols from the main program into the dynamic symbol table of the resulting ELF executable. Normally, executables only have a small symbol table (just enough for linking). With this option, the full global symbol table is exported.
| Option | Meaning |
|---|---|
-Wl,--export-dynamic |
Standard form — passes –export-dynamic to the linker via gcc |
-export-dynamic |
Equivalent shorthand |
-rdynamic |
Another synonym (commonly used in practice) |
-Wl,-E |
Yet another synonym (less common) |
-rdynamic as it’s the shortest form.This plugin defines a function run_plugin(). Inside it, it tries to call host_callback() — a function that lives in the main program.
/* plugin.c — compiled into plugin.so */
#include <stdio.h>
/* Declaration: host_callback lives in main program, not here */
extern void host_callback(const char *msg);
void run_plugin(void) {
printf("[plugin] run_plugin() called\n");
/* Call back into the main program */
host_callback("Hello from plugin!");
}
Compile the plugin as a shared library:
gcc -g -fPIC -shared -o plugin.so plugin.c
The main program defines host_callback() and dynamically loads the plugin.
/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
/* This function is in main — we want the plugin to be able to call it */
void host_callback(const char *msg) {
printf("[main] host_callback received: %s\n", msg);
}
int main(void) {
void *handle;
void (*run_plugin)(void);
char *error;
/* Load the plugin */
handle = dlopen("./plugin.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
exit(EXIT_FAILURE);
}
/* Get address of run_plugin() */
run_plugin = dlsym(handle, "run_plugin");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "dlsym failed: %s\n", error);
dlclose(handle);
exit(EXIT_FAILURE);
}
/* Call the plugin function — it will call back host_callback() */
run_plugin();
dlclose(handle);
return 0;
}
Compile the main program — notice -rdynamic:
# WITHOUT -rdynamic: plugin cannot find host_callback → crash
gcc -g -o main_no_export main.c -ldl
# WITH -rdynamic: plugin can find host_callback → works
gcc -g -rdynamic -o main_with_export main.c -ldl
This pattern is common in event-driven systems, GUI frameworks, and plugin architectures.
/* event_plugin.c — compiled as event_plugin.so */
#include <stdio.h>
/* These are defined in the main program */
extern void on_start(void);
extern void on_stop(void);
void trigger_events(void) {
printf("[plugin] triggering start event\n");
on_start(); /* calls back into main */
printf("[plugin] triggering stop event\n");
on_stop(); /* calls back into main */
}
/* event_main.c */
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
/* Event handler functions — defined in main */
void on_start(void) { printf("[main] START event handled!\n"); }
void on_stop(void) { printf("[main] STOP event handled!\n"); }
int main(void) {
void *lib = dlopen("./event_plugin.so", RTLD_LAZY);
if (!lib) { fprintf(stderr, "%s\n", dlerror()); return 1; }
void (*trigger)(void) = dlsym(lib, "trigger_events");
if (dlerror()) { fprintf(stderr, "symbol not found\n"); return 1; }
trigger(); /* Plugin calls on_start() and on_stop() in main */
dlclose(lib);
return 0;
}
gcc -fPIC -shared -o event_plugin.so event_plugin.c
gcc -rdynamic -o event_main event_main.c -ldl
./event_main
Expected output:
[plugin] triggering start event
[main] START event handled!
[plugin] triggering stop event
[main] STOP event handled!
/* symbols_demo.c */
#include <stdio.h>
#include <stdlib.h>
/* These are global functions — should appear in dynamic symbol table */
void public_func_a(void) { printf("A\n"); }
void public_func_b(void) { printf("B\n"); }
/* static = private, should NOT appear */
static void private_func(void) { printf("private\n"); }
int main(void) {
public_func_a();
public_func_b();
return 0;
}
# Compile WITHOUT -rdynamic
gcc -o demo_no_export symbols_demo.c
readelf --dyn-syms demo_no_export | grep func
# Result: public_func_a and public_func_b may NOT appear
# Compile WITH -rdynamic
gcc -rdynamic -o demo_with_export symbols_demo.c
readelf --dyn-syms demo_with_export | grep func
# Result: public_func_a and public_func_b WILL appear
# private_func NEVER appears (it's static)
An ELF executable has two symbol tables:
| Table | Used For | Present Without -rdynamic? | Present With -rdynamic? |
|---|---|---|---|
.symtab |
Debugging, static analysis | Yes (if not stripped) | Yes |
.dynsym |
Runtime dynamic linking | Minimal (only needed imports) | Full global symbol set |
The --export-dynamic flag makes the linker populate .dynsym with all global-scope symbols from the main program. The dynamic linker (ld.so) uses .dynsym when resolving symbols for loaded libraries.
-rdynamic (or -Wl,--export-dynamic) flag. This adds all global symbols from the main program’s ELF into the dynamic symbol table (.dynsym), making them visible to dlopen()-loaded libraries.-rdynamic is a gcc driver shorthand. -Wl,--export-dynamic explicitly passes the --export-dynamic flag to the underlying linker (ld). Both cause the same ELF output. The other synonyms are -export-dynamic and -Wl,-E.static keyword limits a symbol’s scope to its compilation unit. -rdynamic only exports symbols that already have global scope (i.e., are not declared static). Static symbols never appear in the dynamic symbol table regardless of this flag.RTLD_GLOBAL is a flag passed to dlopen() — it makes the symbols of the loaded shared library available to subsequently loaded libraries. --export-dynamic is a linker flag used at compile time — it makes the symbols of the main program available to all dynamically loaded libraries. They work in opposite directions: RTLD_GLOBAL is library→future libraries; –export-dynamic is main→all loaded libraries.