unix: expose with_dlerror to the end users

Useful for those who are using platform-specific openers like
`dlmopen`.
This commit is contained in:
Simonas Kazlauskas 2024-03-01 19:00:06 +02:00 committed by Simonas Kazlauskas
parent 775a3fd95c
commit d97c71c1c8
3 changed files with 121 additions and 80 deletions

View File

@ -10,10 +10,12 @@
/// ///
/// ## Non-breaking changes /// ## Non-breaking changes
/// ///
/// The crate switches the dependency on `windows-sys` to a `windows-target` one for Windows /// * The crate switches the dependency on `windows-sys` to a `windows-target` one for Windows
/// bindings. In order to enable this `libloading` defines any bindings necessary for its operation /// bindings. In order to enable this `libloading` defines any bindings necessary for its operation
/// internally, just like has been done for `unix` targets. This should result in leaner /// internally, just like has been done for `unix` targets. This should result in leaner dependency
/// dependency trees. /// trees.
/// * `os::unix::with_dlerror` has been exposed for the users who need to invoke `dl*` family of
/// functions manually.
pub mod r0_8_2 {} pub mod r0_8_2 {}
/// Release 0.8.1 (2023-09-30) /// Release 0.8.1 (2023-09-30)

View File

@ -1,4 +1,4 @@
use std::ffi::CString; use std::ffi::{CString, CStr};
/// A `dlerror` error. /// A `dlerror` error.
pub struct DlDescription(pub(crate) CString); pub struct DlDescription(pub(crate) CString);
@ -9,6 +9,12 @@ impl std::fmt::Debug for DlDescription {
} }
} }
impl From<&CStr> for DlDescription {
fn from(value: &CStr) -> Self {
Self(value.into())
}
}
/// A Windows API error. /// A Windows API error.
pub struct WindowsError(pub(crate) std::io::Error); pub struct WindowsError(pub(crate) std::io::Error);

View File

@ -16,18 +16,33 @@ use util::{cstr_cow_from_bytes, ensure_compatible_types};
mod consts; mod consts;
// dl* family of functions did not have enough thought put into it. /// Run code and handle errors reported by `dlerror`.
// ///
// Whole error handling scheme is done via setting and querying some global state, therefore it is /// This function first executes the `closure` function containing calls to the functions that
// not safe to use dynamic library loading in MT-capable environment at all. Only in POSIX 2008+TC1 /// report their errors via `dlerror`. This closure may return either `None` or `Some(*)` to
// a thread-local state was allowed for `dlerror`, making the dl* family of functions MT-safe. /// further affect operation of this function.
// ///
// In practice (as of 2020-04-01) most of the widely used targets use a thread-local for error /// In case the `closure` returns `None`, `with_dlerror` inspects the `dlerror`. `dlerror` may
// state and have been doing so for a long time. Regardless the comments in this function shall /// decide to not provide any error description, in which case `Err(None)` is returned to the
// remain as a documentation for the future generations. /// caller. Otherwise the `error` callback is invoked to allow inspection and conversion of the
fn with_dlerror<T, F>(wrap: fn(crate::error::DlDescription) -> crate::Error, closure: F) /// error message. The conversion result is returned as `Err(Some(Error))`.
-> Result<T, Option<crate::Error>> ///
where F: FnOnce() -> Option<T> { /// If the operations that report their errors via `dlerror` were all successful, `closure` should
/// return `Some(T)` instead. In this case `dlerror` is not inspected at all.
///
/// # Notes
///
/// The whole `dlerror` handling scheme is done via setting and querying some global state. For
/// that reason it is not safe to use dynamic library loading in MT-capable environment at all.
/// Only in POSIX 2008+TC1 a thread-local state was allowed for `dlerror`, making the dl* family of
/// functions possibly MT-safe, depending on the implementation of `dlerror`.
///
/// In practice (as of 2020-04-01) most of the widely used targets use a thread-local for error
/// state and have been doing so for a long time.
pub fn with_dlerror<T, F, Error>(closure: F, error: fn(&CStr) -> Error) -> Result<T, Option<Error>>
where
F: FnOnce() -> Option<T>,
{
// We used to guard all uses of dl* functions with our own mutex. This made them safe to use in // We used to guard all uses of dl* functions with our own mutex. This made them safe to use in
// MT programs provided the only way a program used dl* was via this library. However, it also // MT programs provided the only way a program used dl* was via this library. However, it also
// had a number of downsides or cases where it failed to handle the problems. For instance, // had a number of downsides or cases where it failed to handle the problems. For instance,
@ -53,8 +68,8 @@ where F: FnOnce() -> Option<T> {
// or a bug in implementation of dl* family of functions. // or a bug in implementation of dl* family of functions.
closure().ok_or_else(|| unsafe { closure().ok_or_else(|| unsafe {
// This code will only get executed if the `closure` returns `None`. // This code will only get executed if the `closure` returns `None`.
let error = dlerror(); let dlerror_str = dlerror();
if error.is_null() { if dlerror_str.is_null() {
// In non-dlsym case this may happen when therere bugs in our bindings or theres // In non-dlsym case this may happen when therere bugs in our bindings or theres
// non-libloading user of libdl; possibly in another thread. // non-libloading user of libdl; possibly in another thread.
None None
@ -64,8 +79,7 @@ where F: FnOnce() -> Option<T> {
// ownership over the message? // ownership over the message?
// TODO: should do locale-aware conversion here. OTOH Rust doesnt seem to work well in // TODO: should do locale-aware conversion here. OTOH Rust doesnt seem to work well in
// any system that uses non-utf8 locale, so I doubt theres a problem here. // any system that uses non-utf8 locale, so I doubt theres a problem here.
let message = CStr::from_ptr(error).into(); Some(error(CStr::from_ptr(dlerror_str)))
Some(wrap(crate::error::DlDescription(message)))
// Since we do a copy of the error string above, maybe we should call dlerror again to // Since we do a copy of the error string above, maybe we should call dlerror again to
// let libdl know it may free its copy of the string now? // let libdl know it may free its copy of the string now?
} }
@ -74,7 +88,7 @@ where F: FnOnce() -> Option<T> {
/// A platform-specific counterpart of the cross-platform [`Library`](crate::Library). /// A platform-specific counterpart of the cross-platform [`Library`](crate::Library).
pub struct Library { pub struct Library {
handle: *mut raw::c_void handle: *mut raw::c_void,
} }
unsafe impl Send for Library {} unsafe impl Send for Library {}
@ -164,30 +178,38 @@ impl Library {
/// termination routines contained within the library is safe as well. These routines may be /// termination routines contained within the library is safe as well. These routines may be
/// executed when the library is unloaded. /// executed when the library is unloaded.
pub unsafe fn open<P>(filename: Option<P>, flags: raw::c_int) -> Result<Library, crate::Error> pub unsafe fn open<P>(filename: Option<P>, flags: raw::c_int) -> Result<Library, crate::Error>
where P: AsRef<OsStr> { where
P: AsRef<OsStr>,
{
let filename = match filename { let filename = match filename {
None => None, None => None,
Some(ref f) => Some(cstr_cow_from_bytes(f.as_ref().as_bytes())?), Some(ref f) => Some(cstr_cow_from_bytes(f.as_ref().as_bytes())?),
}; };
with_dlerror(|desc| crate::Error::DlOpen { desc }, move || { with_dlerror(
let result = dlopen(match filename { move || {
None => ptr::null(), let result = dlopen(
Some(ref f) => f.as_ptr() match filename {
}, flags); None => ptr::null(),
// ensure filename lives until dlopen completes Some(ref f) => f.as_ptr(),
drop(filename); },
if result.is_null() { flags,
None );
} else { // ensure filename lives until dlopen completes
Some(Library { drop(filename);
handle: result if result.is_null() {
}) None
} } else {
}).map_err(|e| e.unwrap_or(crate::Error::DlOpenUnknown)) Some(Library { handle: result })
}
},
|desc| crate::Error::DlOpen { desc: desc.into() },
)
.map_err(|e| e.unwrap_or(crate::Error::DlOpenUnknown))
} }
unsafe fn get_impl<T, F>(&self, symbol: &[u8], on_null: F) -> Result<Symbol<T>, crate::Error> unsafe fn get_impl<T, F>(&self, symbol: &[u8], on_null: F) -> Result<Symbol<T>, crate::Error>
where F: FnOnce() -> Result<Symbol<T>, crate::Error> where
F: FnOnce() -> Result<Symbol<T>, crate::Error>,
{ {
ensure_compatible_types::<T, *mut raw::c_void>()?; ensure_compatible_types::<T, *mut raw::c_void>()?;
let symbol = cstr_cow_from_bytes(symbol)?; let symbol = cstr_cow_from_bytes(symbol)?;
@ -197,24 +219,26 @@ impl Library {
// //
// We try to leave as little space as possible for this to occur, but we cant exactly // We try to leave as little space as possible for this to occur, but we cant exactly
// fully prevent it. // fully prevent it.
let result = with_dlerror(|desc| crate::Error::DlSym { desc }, || { let result = with_dlerror(
dlerror(); || {
let symbol = dlsym(self.handle, symbol.as_ptr()); dlerror();
if symbol.is_null() { let symbol = dlsym(self.handle, symbol.as_ptr());
None if symbol.is_null() {
} else { None
Some(Symbol { } else {
pointer: symbol, Some(Symbol {
pd: marker::PhantomData pointer: symbol,
}) pd: marker::PhantomData,
} })
}); }
},
|desc| crate::Error::DlSym { desc: desc.into() },
);
match result { match result {
Err(None) => on_null(), Err(None) => on_null(),
Err(Some(e)) => Err(e), Err(Some(e)) => Err(e),
Ok(x) => Ok(x) Ok(x) => Ok(x),
} }
} }
/// Get a pointer to a function or static variable by symbol name. /// Get a pointer to a function or static variable by symbol name.
@ -285,10 +309,12 @@ impl Library {
/// variables that work on e.g. Linux may have unintended behaviour on other targets. /// variables that work on e.g. Linux may have unintended behaviour on other targets.
#[inline(always)] #[inline(always)]
pub unsafe fn get_singlethreaded<T>(&self, symbol: &[u8]) -> Result<Symbol<T>, crate::Error> { pub unsafe fn get_singlethreaded<T>(&self, symbol: &[u8]) -> Result<Symbol<T>, crate::Error> {
self.get_impl(symbol, || Ok(Symbol { self.get_impl(symbol, || {
pointer: ptr::null_mut(), Ok(Symbol {
pd: marker::PhantomData pointer: ptr::null_mut(),
})) pd: marker::PhantomData,
})
})
} }
/// Convert the `Library` to a raw handle. /// Convert the `Library` to a raw handle.
@ -309,9 +335,7 @@ impl Library {
/// pointer previously returned by `Library::into_raw` call. It must be valid to call `dlclose` /// pointer previously returned by `Library::into_raw` call. It must be valid to call `dlclose`
/// with this pointer as an argument. /// with this pointer as an argument.
pub unsafe fn from_raw(handle: *mut raw::c_void) -> Library { pub unsafe fn from_raw(handle: *mut raw::c_void) -> Library {
Library { Library { handle }
handle
}
} }
/// Unload the library. /// Unload the library.
@ -325,13 +349,17 @@ impl Library {
/// ///
/// The underlying data structures may still get leaked if an error does occur. /// The underlying data structures may still get leaked if an error does occur.
pub fn close(self) -> Result<(), crate::Error> { pub fn close(self) -> Result<(), crate::Error> {
let result = with_dlerror(|desc| crate::Error::DlClose { desc }, || { let result = with_dlerror(
if unsafe { dlclose(self.handle) } == 0 { || {
Some(()) if unsafe { dlclose(self.handle) } == 0 {
} else { Some(())
None } else {
} None
}).map_err(|e| e.unwrap_or(crate::Error::DlCloseUnknown)); }
},
|desc| crate::Error::DlClose { desc: desc.into() },
)
.map_err(|e| e.unwrap_or(crate::Error::DlCloseUnknown));
// While the library is not free'd yet in case of an error, there is no reason to try // While the library is not free'd yet in case of an error, there is no reason to try
// dropping it again, because all that will do is try calling `dlclose` again. only // dropping it again, because all that will do is try calling `dlclose` again. only
// this time it would ignore the return result, which we already seen failing… // this time it would ignore the return result, which we already seen failing…
@ -360,7 +388,7 @@ impl fmt::Debug for Library {
/// `Symbol` does not outlive the `Library` it comes from. /// `Symbol` does not outlive the `Library` it comes from.
pub struct Symbol<T> { pub struct Symbol<T> {
pointer: *mut raw::c_void, pointer: *mut raw::c_void,
pd: marker::PhantomData<T> pd: marker::PhantomData<T>,
} }
impl<T> Symbol<T> { impl<T> Symbol<T> {
@ -410,13 +438,18 @@ impl<T> fmt::Debug for Symbol<T> {
if dladdr(self.pointer, info.as_mut_ptr()) != 0 { if dladdr(self.pointer, info.as_mut_ptr()) != 0 {
let info = info.assume_init(); let info = info.assume_init();
if info.dli_sname.is_null() { if info.dli_sname.is_null() {
f.write_str(&format!("Symbol@{:p} from {:?}", f.write_str(&format!(
self.pointer, "Symbol@{:p} from {:?}",
CStr::from_ptr(info.dli_fname))) self.pointer,
CStr::from_ptr(info.dli_fname)
))
} else { } else {
f.write_str(&format!("Symbol {:?}@{:p} from {:?}", f.write_str(&format!(
CStr::from_ptr(info.dli_sname), self.pointer, "Symbol {:?}@{:p} from {:?}",
CStr::from_ptr(info.dli_fname))) CStr::from_ptr(info.dli_sname),
self.pointer,
CStr::from_ptr(info.dli_fname)
))
} }
} else { } else {
f.write_str(&format!("Symbol@{:p}", self.pointer)) f.write_str(&format!("Symbol@{:p}", self.pointer))
@ -426,9 +459,9 @@ impl<T> fmt::Debug for Symbol<T> {
} }
// Platform specific things // Platform specific things
#[cfg_attr(any(target_os = "linux", target_os = "android"), link(name="dl"))] #[cfg_attr(any(target_os = "linux", target_os = "android"), link(name = "dl"))]
#[cfg_attr(any(target_os = "freebsd", target_os = "dragonfly"), link(name="c"))] #[cfg_attr(any(target_os = "freebsd", target_os = "dragonfly"), link(name = "c"))]
extern { extern "C" {
fn dlopen(filename: *const raw::c_char, flags: raw::c_int) -> *mut raw::c_void; fn dlopen(filename: *const raw::c_char, flags: raw::c_int) -> *mut raw::c_void;
fn dlclose(handle: *mut raw::c_void) -> raw::c_int; fn dlclose(handle: *mut raw::c_void) -> raw::c_int;
fn dlsym(handle: *mut raw::c_void, symbol: *const raw::c_char) -> *mut raw::c_void; fn dlsym(handle: *mut raw::c_void, symbol: *const raw::c_char) -> *mut raw::c_void;
@ -438,8 +471,8 @@ extern {
#[repr(C)] #[repr(C)]
struct DlInfo { struct DlInfo {
dli_fname: *const raw::c_char, dli_fname: *const raw::c_char,
dli_fbase: *mut raw::c_void, dli_fbase: *mut raw::c_void,
dli_sname: *const raw::c_char, dli_sname: *const raw::c_char,
dli_saddr: *mut raw::c_void dli_saddr: *mut raw::c_void,
} }