Gendlopen is a small tool intended to help with the creation of code that dynamically loads shared libraries. It takes text files with C prototype declarations as input and creates C or C++ header files as output. It's designed to add dynamic loading support to existing code with minimal effort.
Features:
- can generate code for C and C++
- win32 API
LoadLibraryEx()and POSIXdlopen() - wide and narrow characters on win32 API
- option to automatically load library and symbols
Limitations:
- auto-loading only works on functions
- auto-loading functions with variable arguments require GNU builtins (C only) and inlining
- any type declaration more complicated than a function pointer will not be recognized
Writing macros and formatted prototype lists to be used with dlopen() and
dlsym() is annoying, so I wanted to automate this process as much as possible.
I could have written a script but I wanted the tool to be able to be used in
Windows too without the need to install a script interpreter first.
I also didn't want to just write a header-only library relying on X-macros, that
would still require formatting lists by hand. And X-macros are complicated and ugly.
So in the end I wrote my own small macro tool.
You can use the provided Makefiles or the Meson build system to build gendlopen. No thirdparty build dependencies are required.
Use the Makefile with make -f Makefile.posix.mk or to build with MSVC
run nmake -f Makefile.msvc.mk from a Visual Studio Developer Command Prompt.
To compile with tests and examples enabled:
meson setup -Denable_tests=true -Denable_examples=true build
meson compile -C buildRun the tests with meson test -C build.
You can find configurations for cross-compiling in the directory cross-files:
meson setup --cross-file cross-files/{cross-toolchain}.ini build
meson compile -C buildSmall wrapper scripts are provided to make cross-compiling with clang-cl easier.
gendlopen reads C prototype declarations from text files or STDIN.
Here's how the input text format must be:
- all symbols that should be loaded must be listed as C-style prototypes, ending on semi-colon (;)
- comments, preprocessor lines, structs, unions, enums and typedefs are ignored
- line-breaks are treated as spaces
- you can set some options on a line beginning with
%option
This means you can normally copy the declarations verbatim from header files or other documentation.
If you're unsure which symbols of a library you need to dynamically load you can for example try to link your binary without the library and take a look at the error messages.
Or you can use tools such as nm.
For example to list all symbols prefixed with gtk_ from a binary linked against libgtk
run nm --dynamic myexecutable | grep gtk_.
On Windows you can use dumpbin from Visual Studio Developer Command Prompt:
dumpbin /imports myexecutable.exe | findstr gtk_
Copying these declarations verbatim from i.e. online documentation into a text file:
guint
gtk_get_major_version (
void
);
guint
gtk_get_micro_version (
void
);
guint
gtk_get_minor_version (
void
);
void
gtk_init (
int* argc,
char*** argv
);Since comments, preprocessor lines, structs, etc. are ignored this would be fine too:
#include <xyz.h> // ignored!
/* ignored */
typedef struct _mystruct {
struct nested_struct {
int a;
int b;
};
long l;
int i;
} mystruct_t;
guint gtk_get_major_version (void);
guint gtk_get_micro_version (void);
guint gtk_get_minor_version (void);
// this conditional has no effect
#ifdef USE_GTK_INIT
void gtk_init (int* argc, char*** argv);
#endifHowever I recommend to keep it simple.
I also prefer to use the file ending .txt for the input list to separate it
from regular source code, but it's not a requirement.
You can set some options on lines beginning with %option instead of passing
them through command line.
Line splitting and multiple %option lines are supported:
%option opt1
%option opt2
%option opt3 \
opt4 \
opt5
Available options:
format=<string>
prefix=<string>
library=[<mode>:]<lib>
include=[nq:]<file>
define=<string>
param=[skip|create|read]
no-date
no-pragma-once
line
For an explanation look for the corresponding command line options in gendlopen -full-help.
Since macros are not supported it's recommended to remove DLL_EXPORT macros
from the declarations.
Sometimes declarations come as macros, for example:
PNG_EXPORT(1, png_uint_32, png_access_version_number, (void));
PNG_EXPORT(2, void, png_set_sig_bytes, (png_structrp png_ptr, int num_bytes));You can use the C preprocessor to format that list.
Save the list above in a temporary file temp.h and add a simple PNG_EXPORT
macro definition at the top:
#define PNG_EXPORT(ignored, type, symbol, param) type symbol param
PNG_EXPORT(1, png_uint_32, png_access_version_number, (void));
PNG_EXPORT(2, void, png_set_sig_bytes, (png_structrp png_ptr, int num_bytes));Use the preprocessor: gcc -E temp.h > proto.txt or cl.exe -E temp.h > proto.txt
Possible output:
# 0 "temp.h"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "temp.h"
png_uint_32 png_access_version_number (void);
void png_set_sig_bytes (png_structrp png_ptr, int num_bytes);All lines beginning with # are ignored, so this can be used as input.
Another way to format the input and get a clean list of prototypes is
to use gcc -aux-info.
Process png.h and save the list as aux.txt:
echo '#include <png.h>' | gcc -xc -c - -o /dev/null -aux-info aux.txtThe output will contain lines like these:
/* /usr/include/png.h:2545:NC */ extern png_uint_32 png_get_uint_31 (png_const_structrp restrict , png_const_bytep);
/* /usr/include/png.h:2551:NC */ extern void png_save_uint_32 (png_bytep, png_uint_32);
/* /usr/include/png.h:2554:NC */ extern void png_save_int_32 (png_bytep, png_int_32);Be careful: all kind of unwanted symbols from C standard headers are likely to be included too! And there may be multiple definitions too. Delete all lines you don't need.
Alternatively the input can be an Abstract Syntax Tree generated by Clang:
clang -Xclang -ast-dump /usr/include/png.h > ast.txt
Since all kind of unwanted symbols from C standard headers are likely to be
included too, it's recommened to later use gendlopen's command line options
-P (symbol prefix) or -S (symbol name) to filter the input.
But you can also force to read all symbols with -ast-all-symbols.
Assuming the following input file input.txt:
guint gtk_get_major_version (void);
guint gtk_get_micro_version (void);
guint gtk_get_minor_version (void);
void gtk_init (int *argc,
char ***argv);Calling gendlopen with this file as input will generate a header file and print
it to STDOUT: gendlopen input.txt
Reading from STDIN works too if the input filename is a dash:
cat input.txt | gendlopen - or gendlopen - < input.txt
We can also use the option -print-symbols to check which symbols were found:
gendlopen -print-symbols input.txt should print a list of all prototypes.
Use -param=skip or -param=create if parameter names are missing from the prototypes.
You can also filter the input with -S or -P.
-S will pick a specific symbol and -P will pick all symbols that start with a specified prefix.
-Pgtk_get_ for example will pick only symbols prefixed with gtk_get_ and
-Sgtk_get_major_version -Sgtk_get_minor_version will pick only those two symbols.
You can also combine both:
-Pgtk_get_mi -Sgtk_init will in this case pick the following three symbols:
gtk_get_micro_version
gtk_get_minor_version
gtk_init
Calling gendlopen with an input file will generate a header file and print
it to STDOUT: gendlopen input.txt
To save it into a file use -out: gendlopen input.txt -out=output.h
A couple of different output "formats" are supported right now:
-format=C: C header file with many features (this is the default)-format=C++: C++ header file with many features (no exception handling)-format=plugin: C header intended to help writing a plugin loader-format=minimal: small C header-format=minimal-C++: small C++ header with exception handling
Furthermore you can save the output into separate body and header files
using the option -separate.
The filename extensions will be set to .c/.h or .cpp/.hpp accordingly.
This is ignored however for "minimal-C" and "minimal-C++".
You can force to overwrite an existing output file with -force.
To avoid conflicts all functions/macros/etc. are prefixed with gdo_ or GDO_.
If you want to use more than one generated header you can change the prefix
in the output with -prefix=<string>.
The output files contain information about how to use the API. In most cases however you will only need a function to load a library, load all symbols and to free the library.
You don't need to manually assign any function pointers, that's all done by these helper functions. The functions you're loading are called/used the same way in your source code as before.
Here's a list of the most important functions and methods for each output format:
//-format=C
bool gdo_load_lib_name(const gdo_char_t *filename);
bool gdo_load_all_symbols();
bool gdo_free_lib();
const gdo_char_t *gdo_last_error();//-format=C++
gdo::dl(); // empty c'tor
bool gdo::load(const std::string &filename, int flags=default_flags, bool new_namespace=false);
bool gdo::load_all_symbols();
bool gdo::free(bool force=false); // called by d'tor
std::string gdo::error();
#ifdef GDO_WINAPI
bool gdo::load(const std::wstring &filename, int flags=default_flags, bool new_namespace=false);
std::wstring gdo::error_w();
#endif//-format=minimal-C
const char *gdo_load_library_and_symbols(const char *filename);
void gdo_free_library();//-format=minimal-C++
void gdo::load_library_and_symbols(const char *filename); // throws exceptions
void gdo::free_library();//-format=plugin
gdo_plugin_t *gdo_load_plugins(const gdo_char_t **files, size_t num);
void gdo_release_plugins(gdo_plugin_t *plug);Theses are short examples quickly illustrating how to use the API.
For more examples you can take a look at the tests and examples files.
//-format=C
if (gdo_load_lib_name("mylib.so") && gdo_load_all_symbols()) {
// run optional code
} else {
fprintf(stderr, "%s\n", gdo_last_error());
}
gdo_free_lib(); // always safe to call//-format=C++
gdo::dl mydl();
if (mydl.load("mylib.so") && mydl.load_all_symbols()) {
// run optional code
} else {
std::cerr << mydl.error() << std::endl;
}//-format=minimal-C
const char *error_message = gdo_load_library_and_symbols("mylib.so");
if (error_message) {
fprintf(stderr, "%s\n", error_message);
} else {
// run optional code
}
gdo_free_library(); // always safe to call//-format=minimal-C++
try {
gdo::load_library_and_symbols("mylib.so");
// run optional code
}
catch (const gdo::LibraryError &e) {
std::cerr << "error: failed to load library: " << e.what() << std::endl;
}
catch (const gdo::SymbolError &e) {
std::cerr << "error: failed to load symbol: " << e.what() << std::endl;
}
catch (...) {
std::cerr << "an unknown error has occurred" << std::endl;
}
gdo::free_library(); // always safe to callMacros are available to help with using filenames for different targets:
GDO_LIBNAME(NAME, API)will create a default library name with API number;GDO_LIBNAME(xyz,0)for example will becomelibxyz.so.0on Linux,libxyz.0.dylibon macOS orxyz-0.dllon Windows (MSVC)GDO_LIBEXTis the library file extension including dot (i.e..dllor.so)- for compatibility with Windows there are always specific wide characters (
wchar_t) and narrow characters (char) versions available for these macros:GDO_LIBNAMEA GDO_LIBNAMEWandGDO_LIBEXTA GDO_LIBEXTW
A step-by-step example using the cross-platform library SDL.
Let's start with this simple C code:
#include <SDL.h>
int main()
{
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Test", "Hello", NULL);
return 0;
}This will show up a window with a message. Save it as test.c and compile:
cc $(pkg-config --cflags sdl2) -o sdl_test test.c $(pkg-config --libs sdl2)Inspecting the compiled binary file should reveal that it's linked against SDL2:
$ LANG=C readelf -d sdl_test | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libSDL2-2.0.so.0]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
Let's now try to dynamically load it instead and use a fallback solution if loading it wasn't successful. We want something like this:
if (LOAD_LIBRARY_AND_SYMBOLS()) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Test", "Hello", NULL);
} else {
puts("Hello"); //fallback solution
}First step is to create a list of prototype declarations of all symbols we want to load.
In this case it's only SDL_ShowSimpleMessageBox:
extern int SDL_ShowSimpleMessageBox(Uint32 flags, const char *title, const char *message, SDL_Window *window);Save it as sdl.txt and generate a header file sdl_dynload.h: gendlopen sdl.txt -out=sdl_dynload.h
Let's include the header into our source. The generated header file needs to be included after the SDL header because it uses typedefs from there:
#include <SDL.h>
#include "sdl_dynload.h"Several functions are provided to load the library and symbols.
The functions usually return a boolean value (<stdbool.h>).
Let's use gdo_load_lib_name() to load libSDL and gdo_load_all_symbols() to load the function pointer(s):
if (gdo_load_lib_name("libSDL2-2.0.so.0") && gdo_load_all_symbols()) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Test", "Hello", NULL);
} else {
puts("Hello"); //fallback solution
}We can also print a useful error message using gdo_last_error():
if (gdo_load_lib_name("libSDL2-2.0.so.0") && gdo_load_all_symbols()) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Test", "Hello", NULL);
} else {
fprintf(stderr, "%s\n", gdo_last_error());
puts("Hello"); //fallback solution
}Don't forget to free the library at the end with gdo_free_lib().
We can also use the macro LIBNAME() to provide the correct filename for each platform.
Here's our updated source code:
#include <SDL.h>
#include "sdl_dynload.h"
int main()
{
if (gdo_load_lib_name(LIBNAME(SDL2-2.0,0)) && gdo_load_all_symbols()) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Test", "Hello", NULL);
} else {
fprintf(stderr, "%s\n", gdo_last_error());
puts("Hello"); //fallback solution
}
gdo_free_lib(); //can always be called safely
return 0;
}Compile again but this time don't link against libSDL. You may however need to explicitly link against libdl on some targets.
cc $(pkg-config --cflags sdl2) -o sdl_test test.c -ldlThe program should work the same as before but now it's no longer linked against libSDL:
$ LANG=C readelf -d sdl_test | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
To test the fallback solution you can change the library name with something invalid:
$ ./sdl_test
invalid.so: cannot open shared object file: No such file or directory
Hello
As expected our program prints out a diagnostic error message and falls back to printing the message to stdout.