dlsym
Bindings around the platform's dynamic library loading primitives with greatly improved memory safety. The most important safety guarantee of this library is the prevention of dangling symbols that may occur after a library is unloaded.
It comes with dlsym-gen, which turns C headers into Rust bindings (or a
whole crate) that load the library at run time: no link-time dependency, no
-sys crate, and no LLVM needed to build.
Loading symbols
use Library;
// SAFETY: libm's initialisers are trusted.
let libm = unsafe ;
// SAFETY: `cos` has this signature.
let cos = unsafe ;
// SAFETY: `cos` is defined for every `f64`.
assert_eq!;
// drop(libm) here would not compile: `cos` borrows it.
Symbol<'lib, T>borrows itsLibrary, so the borrow checker rejects unloading while a symbol is in use.- Symbols don't
Derefto the raw pointer. Functions are called withcall((args,)), variadic ones withwith(|f| f(..)), and the raw pointer only comes out throughunsafe fn into_inner. ArcSymbol<T>(fromLibrary::get_arc) keeps the library alive through anArc, andLibrary::leakgives'staticsymbols, both withouttransmute.- Only
unsafe externfunction pointers and raw data pointers can be requested:get::<u64>and safeextern "C" fntypes don't compile. - A symbol whose address is null (
NullSymbol) is told apart from a missing one (SymbolNotFound). Library::path()andAddressInfo::of(ptr)/Symbol::info()(which library, and which symbol, an address belongs to).Library::open_first([...]),Library::this(),library_filename("z"), plusos::unix::OpenOptions(RTLD_LAZY/GLOBAL/NODELETE/NOLOAD/DEEPBIND,dlvsym) andos::windows::OpenOptions(LOAD_LIBRARY_SEARCH_*, already-loaded modules; loader error dialogs are suppressed).
Binding a whole API
library! declares a struct that owns the library and every symbol, using
the same syntax as a Rust 2024 unsafe extern block:
use ;
use Bindings;
library!
}
// SAFETY: libc's initialisers are trusted.
let libc = unsafe ;
assert_eq!;
assert_eq!;
unsafe ;
Every non-#[optional] symbol is resolved when the struct is created, so a
call is a constant-index load plus the call itself. The function pointers
live inside the struct next to the library, so they cannot outlive it.
Generating bindings from C headers
Install the generator with cargo install dlsym --features codegen, or
download a prebuilt dlsym-gen (Linux, macOS universal and Windows, for
x86_64 and aarch64) from the
releases page. It needs a
libclang at run time: one from LLVM, Xcode, or your distribution's
libclang package. It is found through --libclang, $LIBCLANG_PATH,
llvm-config, or the usual install locations.
# A crate (Cargo.toml and lib.rs are only written if missing):
# Or a single module:
use *;
let z = unsafe ?; // tries each -l name in turn
let mut strm = default; // zeroed, like C's `= {0}`
let rc = unsafe ; // the C macro
assert_eq!; // #defines become constants
What it handles:
- Functions (variadic, calling conventions), global variables, structs and
unions (packed, aligned,
#pragma pack, bitfields with typed accessors, C11 anonymous members, flexible array members), enums (as newtypes, so an unexpected value from C is never undefined behaviour), typedefs and function pointers. - Object-like
#defines, evaluated by clang: integers, floats, strings and constant expressions. Function-like macros that wrap a single call (zlib'sdeflateInit(strm, level)) become methods too. - Owned handles:
--owned gzFile=gzclose,gzopengenerates aGzFile<'lib>that callsgzclosewhen dropped, and agzopen_ownedconstructor. The handle borrows the bindings, so it cannot leak, be closed twice, or outlive the library. Functions taking the handle first become its methods (f.gzwrite(..), orh.get()fordemo_handle_get(h)), and--safe-handle gzeofmakes a method safe while the raw function, which would accept any pointer, staysunsafe. - Struct layouts are checked at compile time against the C compiler's
(
size_of,align_of,offset_of!).--targetgenerates for another platform; bindings built for the wrong one fail to compile. - It binds what the given headers declare, plus the headers they include
with
#include "..."(--header-diradds more). Types from elsewhere are pulled in as needed: in full when used by value, opaque when only used behind pointers. - Headers often declare functions that only some builds of a library export
(sqlite's Windows-only functions, optional features). With
-l, the generator opens the installed library and marks whatever it lacks#[optional], so the bindings still load; calling such a function returnsNone. --allow/--blockfilter by name,--optionalmarks more symbols that may be missing, and--safedeclares functions callable withoutunsafe. Unsupported items (e.g.long double) are skipped with a warning.- Tested on zlib, sqlite3, curl, libpng, xz, bzip2, zstd, expat, libarchive, libmagic, ogg, opus, libsodium, libuv, libyaml, lz4, pcre2, SDL2, Xlib, OpenGL, EGL, Vulkan, OpenSSL, libxml2, GLib, D-Bus, cairo, FreeType, ALSA, PulseAudio, ncurses, readline, libpcap, libusb, libudev, libevent, libjpeg, libtiff, libwebp, fontconfig, gdbm and libuuid: every generated module compiles.
Typed wrappers turn C strings, (pointer, length) pairs, out-parameters
and error codes into Rust types. Each annotated function keeps its raw
method and gets a _rs one next to it (handle methods use the annotations
directly):
let mut db = s.sqlite3_open_rs?; // Result<Sqlite3Owned, c_int>
unsafe ?;
let v = s.sqlite3_libversion_rs; // Option<&CStr>, safe
// `db` is closed when dropped, and so is the handle sqlite3_open returns
// on failure.
let mut f = unsafe .unwrap;
f.gzwrite?; // &[u8] in, Result out
str? makes strings nullable (Option<&CStr>). out= of an --owned
handle type returns the handle's RAII wrapper. safe makes the wrapper
callable without unsafe: it is the author's assertion that the function
is sound for any values of the remaining parameters. ret=errno reads
errno right after the call.
Long option lists can live in a file, passed as @zlib.args (whitespace-
separated arguments, "quotes", # comments).
C++ headers (--cpp, automatic for .hpp/.hh/.hxx) work for the parts
with a C-like ABI: functions and variables in namespaces and public static
members are bound through the mangled names clang computes for the target
(geo::add(int, int) becomes geo_add_int_int when overloaded), references
become pointers, and functions not declared noexcept use
extern "C-unwind". Non-virtual member functions take this first, and a
class with public constructors and a destructor gets an RAII wrapper: Rust
allocates the object, constructs it in place, never moves it, and destroys
and frees it on drop, with the methods on the wrapper:
let mut h = unsafe ;
let ok = unsafe ;
// `~Hunspell()` runs and the memory is freed here.
Classes that are not trivially copyable keep their size but not their fields, and are never passed by value. Virtual functions are not bound.
From a build.rs, use dlsym::codegen::Builder (see the
codegen docs).
Static, libc-free binaries (fullrust)
On the fullrust target
(x86_64-unknown-linux-fullrust), which builds Rust programs with no libc and
no dynamic loader, dlsym depends on dldota
and loads libraries through the system's own ld.so, booted on first use. The
same code works unchanged; function symbols resolve to trampolines that switch
to the system libc's thread pointer around each call. Host callbacks passed to
foreign code must be wrapped with dlsym::os::fullrust::context()?.callback(..),
and AddressInfo and Library::path are unavailable. See
src/os/fullrust.rs (docs.rs does not build that target).
Compared to libloading
| libloading | dlsym | |
|---|---|---|
| Symbols borrow their library | ✓ | ✓ |
| Raw pointer copyable out of a symbol | via *sym |
only unsafe |
'static symbols without transmute |
ArcSymbol, leak |
|
| Symbol type checked to be a pointer | ✓ | |
Whole-API structs (library!) |
✓ | |
| Header → bindings generator | ✓ |
See ROADMAP.md for what's next.
License
MIT