Skip to main content

darwin_kperf_sys/
load.rs

1//! Minimal dynamic library loading primitives built directly on `dlopen(3)`.
2//!
3//! This module provides [`LibraryHandle`] and [`LibrarySymbol`], thin wrappers around
4//! the POSIX `dl*` family, without pulling in `libc` or `libloading`. This works on
5//! macOS because `libSystem.B.dylib` (which exports `dlopen`, `dlsym`, `dlclose`, and
6//! `dlerror`) is always implicitly linked into every process.
7//!
8//! # Lifecycle
9//!
10//! 1. [`LibraryHandle::open`] loads a dylib and returns a handle.
11//! 2. [`LibraryHandle::symbol`] resolves a named symbol from the loaded library.
12//! 3. The symbol is typically transmuted into a concrete function pointer via
13//!    [`core::mem::transmute`] (see the `load_sym!` macros in [`crate::kperf`] and
14//!    [`crate::kperfdata`]).
15//! 4. [`LibraryHandle::close`] unloads the library, or [`Drop`] does so automatically.
16//!
17//! # Safety invariant
18//!
19//! A [`LibrarySymbol`] (and any function pointer derived from it) borrows the library's
20//! mapped code pages. The caller must ensure that no such pointer is used after the
21//! originating [`LibraryHandle`] is closed or dropped.
22
23use alloc::{borrow::ToOwned as _, ffi::CString};
24use core::{
25    error::Error,
26    ffi::{CStr, c_char, c_int, c_void},
27    fmt,
28    ptr::NonNull,
29};
30
31/// Bind symbols lazily; each symbol is resolved on first use.
32pub const RTLD_LAZY: c_int = 0x1;
33/// Bind symbols eagerly; all symbols are resolved when `dlopen` returns.
34pub const RTLD_NOW: c_int = 0x2;
35
36unsafe extern "C" {
37    pub fn dlopen(path: *const c_char, mode: c_int) -> *mut c_void;
38    pub fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
39    pub fn dlclose(handle: *mut c_void) -> c_int;
40    pub fn dlerror() -> *const c_char;
41}
42
43/// An error produced by `dlopen`, `dlsym`, or `dlclose`.
44///
45/// Owns a copy of the message returned by `dlerror()`. The message is copied immediately
46/// because `dlerror` returns a pointer to a thread-local buffer that is overwritten by the
47/// next `dl*` call.
48#[derive(Debug)]
49pub struct LoadError {
50    message: CString,
51}
52
53impl fmt::Display for LoadError {
54    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
55        fmt::Debug::fmt(&self.message, fmt)
56    }
57}
58
59impl Error for LoadError {}
60
61/// An opaque, non-null pointer to a symbol resolved from a loaded library.
62///
63/// This is a `repr(transparent)` wrapper around `NonNull<c_void>`, which makes it safe to
64/// transmute into a function pointer of the correct signature. The null case is already
65/// handled by [`LibraryHandle::symbol`], which returns `Err` instead.
66///
67/// # Safety
68///
69/// A `LibrarySymbol` borrows the memory-mapped pages of its originating library. It must
70/// not be used (directly or after transmuting to a function pointer) once the
71/// [`LibraryHandle`] that produced it has been closed or dropped.
72#[derive(Debug)]
73#[repr(transparent)]
74pub struct LibrarySymbol(NonNull<c_void>);
75
76/// A RAII wrapper around a `dlopen` handle.
77///
78/// Opens a dynamic library on construction and closes it on drop. After an
79/// explicit [`close`](Self::close), the destructor is disarmed.
80#[derive(Debug)]
81#[repr(transparent)]
82pub struct LibraryHandle(Option<NonNull<c_void>>);
83
84// SAFETY: a `LibraryHandle` is an opaque token for a `dlopen`-ed library.
85// `dlsym` and `dlclose` are thread-safe per POSIX, and `&self` methods only
86// call `dlsym`. No interior mutability.
87unsafe impl Send for LibraryHandle {}
88
89// SAFETY: a `LibraryHandle` is an opaque token for a `dlopen`-ed library.
90// `dlsym` and `dlclose` are thread-safe per POSIX, and `&self` methods only
91// call `dlsym`. No interior mutability.
92unsafe impl Sync for LibraryHandle {}
93
94impl LibraryHandle {
95    /// Loads the dynamic library at `path` with `RTLD_LAZY` binding.
96    ///
97    /// Returns a handle that keeps the library mapped until it is closed or dropped.
98    ///
99    /// # Errors
100    ///
101    /// Returns [`LoadError`] if the library cannot be loaded.
102    pub fn open(path: &CStr) -> Result<Self, LoadError> {
103        // SAFETY: `path` is a valid, null-terminated C string.
104        let ptr = unsafe { dlopen(path.as_ptr(), RTLD_LAZY) };
105        let Some(ptr) = NonNull::new(ptr) else {
106            // SAFETY: `dlerror` is guaranteed to return a valid C string
107            let message = unsafe { CStr::from_ptr(dlerror()).to_owned() };
108            return Err(LoadError { message });
109        };
110
111        Ok(Self(Some(ptr)))
112    }
113
114    /// Resolves a named symbol from the loaded library.
115    ///
116    /// The returned [`LibrarySymbol`] is typically transmuted into a typed function pointer
117    /// via [`core::mem::transmute`].
118    ///
119    /// # Safety
120    ///
121    /// The caller must ensure that:
122    /// - `name` identifies a symbol whose actual signature matches the type it will be transmuted
123    ///   to.
124    /// - The resulting `LibrarySymbol` (or any pointer derived from it) is not used after this
125    ///   handle is closed or dropped.
126    /// # Errors
127    ///
128    /// Returns [`LoadError`] if the symbol cannot be found in the library.
129    pub unsafe fn symbol(&self, name: &CStr) -> Result<LibrarySymbol, LoadError> {
130        // SAFETY: `self.handle()` is a valid `dlopen` handle, and `name` is a valid C string.
131        let ptr = unsafe { dlsym(self.handle().as_ptr(), name.as_ptr()) };
132        let Some(ptr) = NonNull::new(ptr) else {
133            // SAFETY: `dlerror` is guaranteed to return a valid C string
134            let message = unsafe { CStr::from_ptr(dlerror()).to_owned() };
135            return Err(LoadError { message });
136        };
137
138        Ok(LibrarySymbol(ptr))
139    }
140
141    const fn handle(&self) -> NonNull<c_void> {
142        self.0.expect("library should not have been closed")
143    }
144
145    fn close_handle(handle: NonNull<c_void>) -> Result<(), LoadError> {
146        // SAFETY: we're the only ones that have access to the library
147        let res = unsafe { dlclose(handle.as_ptr()) };
148
149        if res == 0 {
150            return Ok(());
151        }
152
153        // SAFETY: `dlerror` is guaranteed to return a valid C string
154        let message = unsafe { CStr::from_ptr(dlerror()).to_owned() };
155        Err(LoadError { message })
156    }
157
158    /// Explicitly closes the library handle and returns any error from `dlclose`.
159    ///
160    /// After this call the destructor is disarmed; dropping the handle is a no-op.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`LoadError`] if `dlclose` fails.
165    ///
166    /// # Safety
167    ///
168    /// The caller must ensure that no [`LibrarySymbol`] (or function pointer derived from
169    /// one) obtained from this handle is used after this call returns.
170    pub unsafe fn close(mut self) -> Result<(), LoadError> {
171        self.0.take().map_or(Ok(()), Self::close_handle)
172    }
173}
174
175impl Drop for LibraryHandle {
176    fn drop(&mut self) {
177        if let Some(handle) = self.0.take() {
178            let _result = Self::close_handle(handle);
179        }
180    }
181}