Skip to content

Commit ac4495a

Browse files
committed
Auto merge of #146019 - joboet:better-dlsym, r=tgross35
std: optimize `dlsym!` macro and add a test for it The `dlsym!` macro always ensures that the name string is nul-terminated, so there is no need to perform the check at runtime. Also, acquire loads are generally faster than a load and a barrier, so use them. This is only false in the case where the symbol is missing, but that shouldn't matter too much.
2 parents 8e2ed71 + 4c99219 commit ac4495a

File tree

3 files changed

+112
-51
lines changed

3 files changed

+112
-51
lines changed

library/std/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@
350350
#![feature(float_gamma)]
351351
#![feature(float_minimum_maximum)]
352352
#![feature(fmt_internals)]
353+
#![feature(fn_ptr_trait)]
353354
#![feature(generic_atomic)]
354355
#![feature(hasher_prefixfree_extras)]
355356
#![feature(hashmap_internals)]

library/std/src/sys/pal/unix/weak.rs

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,24 @@
2222
#![allow(dead_code, unused_macros)]
2323
#![forbid(unsafe_op_in_unsafe_fn)]
2424

25-
use crate::ffi::CStr;
26-
use crate::marker::PhantomData;
27-
use crate::sync::atomic::{self, Atomic, AtomicPtr, Ordering};
25+
use crate::ffi::{CStr, c_char, c_void};
26+
use crate::marker::{FnPtr, PhantomData};
27+
use crate::sync::atomic::{Atomic, AtomicPtr, Ordering};
2828
use crate::{mem, ptr};
2929

30+
// We currently only test `dlsym!`, but that doesn't work on all platforms, so
31+
// we gate the tests to only the platforms where it is actually used.
32+
//
33+
// FIXME(joboet): add more tests, reorganise the whole module and get rid of
34+
// `#[allow(dead_code, unused_macros)]`.
35+
#[cfg(any(
36+
target_vendor = "apple",
37+
all(target_os = "linux", target_env = "gnu"),
38+
target_os = "freebsd",
39+
))]
40+
#[cfg(test)]
41+
mod tests;
42+
3043
// We can use true weak linkage on ELF targets.
3144
#[cfg(all(unix, not(target_vendor = "apple")))]
3245
pub(crate) macro weak {
@@ -64,7 +77,7 @@ impl<F: Copy> ExternWeak<F> {
6477

6578
pub(crate) macro dlsym {
6679
(fn $name:ident($($param:ident : $t:ty),* $(,)?) -> $ret:ty;) => (
67-
dlsym!(
80+
dlsym!(
6881
#[link_name = stringify!($name)]
6982
fn $name($($param : $t),*) -> $ret;
7083
);
@@ -73,84 +86,99 @@ pub(crate) macro dlsym {
7386
#[link_name = $sym:expr]
7487
fn $name:ident($($param:ident : $t:ty),* $(,)?) -> $ret:ty;
7588
) => (
76-
static DLSYM: DlsymWeak<unsafe extern "C" fn($($t),*) -> $ret> =
77-
DlsymWeak::new(concat!($sym, '\0'));
89+
static DLSYM: DlsymWeak<unsafe extern "C" fn($($t),*) -> $ret> = {
90+
let Ok(name) = CStr::from_bytes_with_nul(concat!($sym, '\0').as_bytes()) else {
91+
panic!("symbol name may not contain NUL")
92+
};
93+
94+
// SAFETY: Whoever calls the function pointer returned by `get()`
95+
// is responsible for ensuring that the signature is correct. Just
96+
// like with extern blocks, this is syntactically enforced by making
97+
// the function pointer be unsafe.
98+
unsafe { DlsymWeak::new(name) }
99+
};
100+
78101
let $name = &DLSYM;
79102
)
80103
}
104+
81105
pub(crate) struct DlsymWeak<F> {
82-
name: &'static str,
106+
/// A pointer to the nul-terminated name of the symbol.
107+
// Use a pointer instead of `&'static CStr` to save space.
108+
name: *const c_char,
83109
func: Atomic<*mut libc::c_void>,
84110
_marker: PhantomData<F>,
85111
}
86112

87-
impl<F> DlsymWeak<F> {
88-
pub(crate) const fn new(name: &'static str) -> Self {
113+
impl<F: FnPtr> DlsymWeak<F> {
114+
/// # Safety
115+
///
116+
/// If the signature of `F` does not match the signature of the symbol (if
117+
/// it exists), calling the function pointer returned by `get()` is
118+
/// undefined behaviour.
119+
pub(crate) const unsafe fn new(name: &'static CStr) -> Self {
89120
DlsymWeak {
90-
name,
121+
name: name.as_ptr(),
91122
func: AtomicPtr::new(ptr::without_provenance_mut(1)),
92123
_marker: PhantomData,
93124
}
94125
}
95126

96127
#[inline]
97128
pub(crate) fn get(&self) -> Option<F> {
98-
unsafe {
99-
// Relaxed is fine here because we fence before reading through the
100-
// pointer (see the comment below).
101-
match self.func.load(Ordering::Relaxed) {
102-
func if func.addr() == 1 => self.initialize(),
103-
func if func.is_null() => None,
104-
func => {
105-
let func = mem::transmute_copy::<*mut libc::c_void, F>(&func);
106-
// The caller is presumably going to read through this value
107-
// (by calling the function we've dlsymed). This means we'd
108-
// need to have loaded it with at least C11's consume
109-
// ordering in order to be guaranteed that the data we read
110-
// from the pointer isn't from before the pointer was
111-
// stored. Rust has no equivalent to memory_order_consume,
112-
// so we use an acquire fence (sorry, ARM).
113-
//
114-
// Now, in practice this likely isn't needed even on CPUs
115-
// where relaxed and consume mean different things. The
116-
// symbols we're loading are probably present (or not) at
117-
// init, and even if they aren't the runtime dynamic loader
118-
// is extremely likely have sufficient barriers internally
119-
// (possibly implicitly, for example the ones provided by
120-
// invoking `mprotect`).
121-
//
122-
// That said, none of that's *guaranteed*, and so we fence.
123-
atomic::fence(Ordering::Acquire);
124-
Some(func)
125-
}
126-
}
129+
// The caller is presumably going to read through this value
130+
// (by calling the function we've dlsymed). This means we'd
131+
// need to have loaded it with at least C11's consume
132+
// ordering in order to be guaranteed that the data we read
133+
// from the pointer isn't from before the pointer was
134+
// stored. Rust has no equivalent to memory_order_consume,
135+
// so we use an acquire load (sorry, ARM).
136+
//
137+
// Now, in practice this likely isn't needed even on CPUs
138+
// where relaxed and consume mean different things. The
139+
// symbols we're loading are probably present (or not) at
140+
// init, and even if they aren't the runtime dynamic loader
141+
// is extremely likely have sufficient barriers internally
142+
// (possibly implicitly, for example the ones provided by
143+
// invoking `mprotect`).
144+
//
145+
// That said, none of that's *guaranteed*, so we use acquire.
146+
match self.func.load(Ordering::Acquire) {
147+
func if func.addr() == 1 => self.initialize(),
148+
func if func.is_null() => None,
149+
// SAFETY:
150+
// `func` is not null and `F` implements `FnPtr`, thus this
151+
// transmutation is well-defined. It is the responsibility of the
152+
// creator of this `DlsymWeak` to ensure that calling the resulting
153+
// function pointer does not result in undefined behaviour (though
154+
// the `dlsym!` macro delegates this responsibility to the caller
155+
// of the function by using `unsafe` function pointers).
156+
// FIXME: use `transmute` once it stops complaining about generics.
157+
func => Some(unsafe { mem::transmute_copy::<*mut c_void, F>(&func) }),
127158
}
128159
}
129160

130161
// Cold because it should only happen during first-time initialization.
131162
#[cold]
132-
unsafe fn initialize(&self) -> Option<F> {
133-
assert_eq!(size_of::<F>(), size_of::<*mut libc::c_void>());
134-
135-
let val = unsafe { fetch(self.name) };
136-
// This synchronizes with the acquire fence in `get`.
163+
fn initialize(&self) -> Option<F> {
164+
// SAFETY: `self.name` was created from a `&'static CStr` and is
165+
// therefore a valid C string pointer.
166+
let val = unsafe { libc::dlsym(libc::RTLD_DEFAULT, self.name) };
167+
// This synchronizes with the acquire load in `get`.
137168
self.func.store(val, Ordering::Release);
138169

139170
if val.is_null() {
140171
None
141172
} else {
173+
// SAFETY: see the comment in `get`.
174+
// FIXME: use `transmute` once it stops complaining about generics.
142175
Some(unsafe { mem::transmute_copy::<*mut libc::c_void, F>(&val) })
143176
}
144177
}
145178
}
146179

147-
unsafe fn fetch(name: &str) -> *mut libc::c_void {
148-
let name = match CStr::from_bytes_with_nul(name.as_bytes()) {
149-
Ok(cstr) => cstr,
150-
Err(..) => return ptr::null_mut(),
151-
};
152-
unsafe { libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr()) }
153-
}
180+
unsafe impl<F> Send for DlsymWeak<F> {}
181+
unsafe impl<F> Sync for DlsymWeak<F> {}
154182

155183
#[cfg(not(any(target_os = "linux", target_os = "android")))]
156184
pub(crate) macro syscall {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use super::*;
2+
3+
#[test]
4+
fn dlsym_existing() {
5+
const TEST_STRING: &'static CStr = c"Ferris!";
6+
7+
// Try to find a symbol that definitely exists.
8+
dlsym! {
9+
fn strlen(cs: *const c_char) -> usize;
10+
}
11+
12+
dlsym! {
13+
#[link_name = "strlen"]
14+
fn custom_name(cs: *const c_char) -> usize;
15+
}
16+
17+
let strlen = strlen.get().unwrap();
18+
assert_eq!(unsafe { strlen(TEST_STRING.as_ptr()) }, TEST_STRING.count_bytes());
19+
20+
let custom_name = custom_name.get().unwrap();
21+
assert_eq!(unsafe { custom_name(TEST_STRING.as_ptr()) }, TEST_STRING.count_bytes());
22+
}
23+
24+
#[test]
25+
fn dlsym_missing() {
26+
// Try to find a symbol that definitely does not exist.
27+
dlsym! {
28+
fn test_symbol_that_does_not_exist() -> i32;
29+
}
30+
31+
assert!(test_symbol_that_does_not_exist.get().is_none());
32+
}

0 commit comments

Comments
 (0)