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.
use dlsym::Library;
// SAFETY: libm's initialisers are trusted.
let libm = unsafe { Library::open("libm.so.6")? };
// SAFETY: `cos` has this signature.
let cos = unsafe { libm.get::<unsafe extern "C" fn(f64) -> f64>(c"cos")? };
// SAFETY: `cos` is defined for every `f64`.
assert_eq!(unsafe { cos.call((0.0,)) }, 1.0);
// 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).
library! declares a struct that owns the library and every symbol, using
the same syntax as a Rust 2024 unsafe extern block:
use std::ffi::{c_char, c_int};
use dlsym::Bindings;
dlsym::library! {
pub struct LibC {
unsafe extern "C" {
pub safe fn abs(x: c_int) -> c_int; // callable without `unsafe`
pub fn strlen(s: *const c_char) -> usize; // an `unsafe` method
pub fn printf(fmt: *const c_char, ...) -> c_int; // variadic
#[optional] // may be missing
pub fn strlcpy(d: *mut c_char, s: *const c_char, n: usize) -> usize;
#[link_name = "strlen"] // renamed
pub fn len(s: *const c_char) -> usize;
pub static mut environ: *const *const c_char; // data
}
}
}
// SAFETY: libc's initialisers are trusted.
let libc = unsafe { LibC::open("libc.so.6")? };
assert_eq!(libc.abs(-4), 4);
assert_eq!(unsafe { libc.strlen(c"abc".as_ptr()) }, 3);
unsafe { libc.printf().with(|f| f(c"%d\n".as_ptr(), 42)) };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.
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):
dlsym-gen /usr/include/zlib.h --crate zlib-dl -l libz.so.1 -l libz.1.dylib -l zlib1.dll
# Or a single module:
dlsym-gen /usr/include/foo/foo.h -o src/foo.rs --struct Foo -- -I/usr/include/foouse zlib_dl::*;
let z = unsafe { Zlib::open_default() }?; // tries each -l name in turn
let mut strm = z_stream::default(); // zeroed, like C's `= {0}`
let rc = unsafe { z.deflateInit(&mut strm, Z_BEST_COMPRESSION) }; // the C macro
assert_eq!(rc, Z_OK); // #defines become constantsWhat 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):
dlsym-gen sqlite3.h -l libsqlite3.so.0 --owned sqlite3=sqlite3_close \
--wrap 'sqlite3_open:str,out=ppDb,ret=nonzero,safe' \
--wrap 'sqlite3_exec:str=sql,ret=nonzero' \
--wrap 'sqlite3_libversion:ret=static-str,safe'
dlsym-gen zlib.h -l libz.so.1 --owned gzFile=gzclose,gzopen \
--wrap gzopen:str --wrap 'gzwrite:slice=buf/len,ret=negative,safe'let mut db = s.sqlite3_open_rs(c":memory:")?; // Result<Sqlite3Owned, c_int>
unsafe { db.exec(c"create table t(x)", None, null_mut(), null_mut()) }?;
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 { z.gzopen_owned(c"out.gz", c"wb") }.unwrap();
f.gzwrite(b"hello")?; // &[u8] in, Result outstr? 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 { HunspellObject::new(&lib, aff.as_ptr(), dic.as_ptr(), null()) };
let ok = unsafe { h.spell_const_char_ptr_int_ptr_char_ptr_ptr(word.as_ptr(), null_mut(), null_mut()) };
// `~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).
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).
| 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.
MIT