# dlsym
[](https://github.com/KarpelesLab/dlsym/actions/workflows/ci.yml)
[](https://crates.io/crates/dlsym)
[](https://docs.rs/dlsym)
[](Cargo.toml)
[](LICENSE)
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
```rust
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 its `Library`, so the borrow checker rejects
unloading while a symbol is in use.
- Symbols don't `Deref` to the raw pointer. Functions are called with
`call((args,))`, variadic ones with `with(|f| f(..))`, and the raw pointer
only comes out through `unsafe fn into_inner`.
- `ArcSymbol<T>` (from `Library::get_arc`) keeps the library alive through an
`Arc`, and `Library::leak` gives `'static` symbols, both without
`transmute`.
- Only `unsafe extern` function pointers and raw data pointers can be
requested: `get::<u64>` and safe `extern "C" fn` types don't compile.
- A symbol whose address is null (`NullSymbol`) is told apart from a missing
one (`SymbolNotFound`).
- `Library::path()` and `AddressInfo::of(ptr)` / `Symbol::info()` (which
library, and which symbol, an address belongs to).
- `Library::open_first([...])`, `Library::this()`, `library_filename("z")`,
plus `os::unix::OpenOptions` (`RTLD_LAZY`/`GLOBAL`/`NODELETE`/`NOLOAD`/
`DEEPBIND`, `dlvsym`) and `os::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:
```rust
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);
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](https://github.com/KarpelesLab/dlsym/releases). 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.
```sh
# 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/foo
```
```rust
use 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 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 `#define`s, evaluated by clang: integers, floats, strings and
constant expressions. Function-like macros that wrap a single call (zlib's
`deflateInit(strm, level)`) become methods too.
- Owned handles: `--owned gzFile=gzclose,gzopen` generates a `GzFile<'lib>`
that calls `gzclose` when dropped, and a `gzopen_owned` constructor. 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(..)`, or `h.get()` for `demo_handle_get(h)`), and
`--safe-handle gzeof` makes a method safe while the raw function, which
would accept any pointer, stays `unsafe`.
- Struct layouts are checked at compile time against the C compiler's
(`size_of`, `align_of`, `offset_of!`). `--target` generates 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-dir` adds 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 returns
`None`.
- `--allow`/`--block` filter by name, `--optional` marks more symbols that
may be missing, and `--safe` declares functions callable without
`unsafe`. 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):
```sh
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'
```
```rust
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 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:
```rust
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](https://docs.rs/dlsym/latest/dlsym/codegen/)).
## Static, libc-free binaries (fullrust)
On the [fullrust](https://github.com/KarpelesLab/fullrust) target
(`x86_64-unknown-linux-fullrust`), which builds Rust programs with no libc and
no dynamic loader, dlsym depends on [dldota](https://crates.io/crates/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`](src/os/fullrust.rs) (docs.rs does not build that target).
## Compared to libloading
| 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](ROADMAP.md) for what's next.
## License
MIT