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}