ferroui_base/platform/
native_library.rs1use std::ffi::{c_int, c_void, CStr, CString};
15
16#[derive(Clone, Copy, Debug)]
19pub struct NativeLibrary {
20 handle: usize,
22 name: &'static CStr,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum NativeLibraryError {
28 Library {
31 library: String,
33 },
34 EntryPoint {
36 library: String,
38 symbol: String,
40 },
41}
42
43impl std::fmt::Display for NativeLibraryError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::Library { library } => {
47 write!(f, "Unable to load shared library '{library}' or one of its dependencies.")
48 }
49 Self::EntryPoint { library, symbol } => {
50 write!(f, "Unable to find an entry point named '{symbol}' in shared library '{library}'.")
51 }
52 }
53 }
54}
55
56impl std::error::Error for NativeLibraryError {}
57
58impl NativeLibrary {
59 pub fn open(name: &'static CStr) -> Result<Self, NativeLibraryError> {
63 Self::open_with_flags(name, libc::RTLD_LAZY | libc::RTLD_GLOBAL)
64 }
65
66 pub fn open_with_flags(name: &'static CStr, flags: c_int) -> Result<Self, NativeLibraryError> {
69 let handle = unsafe { libc::dlopen(name.as_ptr(), flags) };
72 if handle.is_null() {
73 return Err(NativeLibraryError::Library { library: name.to_string_lossy().into_owned() });
74 }
75 Ok(Self { handle: handle as usize, name })
76 }
77
78 pub fn name(&self) -> &'static CStr {
80 self.name
81 }
82
83 pub fn symbol(&self, symbol: &str) -> Result<*const c_void, NativeLibraryError> {
85 let missing = || NativeLibraryError::EntryPoint {
86 library: self.name.to_string_lossy().into_owned(),
87 symbol: symbol.to_owned(),
88 };
89 let c_symbol = CString::new(symbol).map_err(|_| missing())?;
90 let address = unsafe { libc::dlsym(self.handle as *mut c_void, c_symbol.as_ptr()) };
93 if address.is_null() {
94 return Err(missing());
95 }
96 Ok(address.cast_const())
97 }
98}
99
100#[macro_export]
123#[doc(hidden)]
124macro_rules! native_functions {
125 (@symbol $f:ident) => { stringify!($f) };
126 (@symbol $f:ident $symbol:literal) => { $symbol };
127 (
128 $(#[$meta:meta])*
129 $vis:vis struct $name:ident ($($lib:ident),+ $(,)?) {
130 $( $(#[$fmeta:meta])* $flib:ident :: $f:ident $(as $symbol:literal)? : $ty:ty ; )*
131 }
132 ) => {
133 $(#[$meta])*
134 #[allow(non_snake_case, dead_code)]
135 $vis struct $name {
136 $( $(#[$fmeta])* pub(crate) $f: $ty, )*
137 }
138
139 impl $name {
140 #[allow(dead_code, non_snake_case)]
142 $vis fn load(
143 $($lib: &$crate::platform::native_library::NativeLibrary),+
144 ) -> Result<Self, $crate::platform::native_library::NativeLibraryError> {
145 Ok(Self {
146 $(
147 $f: unsafe {
151 std::mem::transmute::<*const std::ffi::c_void, $ty>(
152 $flib.symbol($crate::native_functions!(@symbol $f $($symbol)?))?,
153 )
154 },
155 )*
156 })
157 }
158 }
159 };
160}
161pub use native_functions;
162
163#[cfg(test)]
164mod tests {
165 use super::*;
167
168 #[cfg(target_os = "linux")]
170 const LIBC: &CStr = c"libc.so.6";
171 #[cfg(target_os = "macos")]
172 const LIBC: &CStr = c"libSystem.B.dylib";
173 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
174 const LIBC: &CStr = c"libc.so";
175
176 #[test]
177 fn a_library_that_does_not_exist_is_an_error_with_its_name() {
178 let error = NativeLibrary::open(c"libferroui-there-is-no-such-library.so.0").unwrap_err();
179 assert_eq!(
180 error.to_string(),
181 "Unable to load shared library 'libferroui-there-is-no-such-library.so.0' or one of its dependencies."
182 );
183 assert_eq!(
184 error,
185 NativeLibraryError::Library { library: "libferroui-there-is-no-such-library.so.0".to_string() }
186 );
187 }
188
189 #[test]
192 fn a_function_is_found_by_name_and_a_missing_one_is_an_error() {
193 let Ok(library) = NativeLibrary::open(LIBC) else {
194 return;
195 };
196 assert!(library.symbol("getpid").is_ok());
197 let error = library.symbol("ferroui_no_such_function").unwrap_err();
198 assert!(error.to_string().starts_with("Unable to find an entry point named 'ferroui_no_such_function'"));
199 assert_eq!(
200 error,
201 NativeLibraryError::EntryPoint {
202 library: LIBC.to_string_lossy().into_owned(),
203 symbol: "ferroui_no_such_function".to_string(),
204 }
205 );
206
207 native_functions! {
208 struct Table(lib) {
209 lib::getpid: unsafe extern "C" fn() -> libc::pid_t;
210 }
211 }
212 let table = Table::load(&library).unwrap();
213 assert_eq!(unsafe { (table.getpid)() } as u32, std::process::id());
215
216 native_functions! {
217 struct Missing(lib) {
218 lib::ferroui_no_such_function: unsafe extern "C" fn();
219 }
220 }
221 assert!(Missing::load(&library).is_err());
222 }
223
224 #[test]
226 fn a_library_is_opened_with_the_flags_given() {
227 let Ok(library) = NativeLibrary::open_with_flags(LIBC, libc::RTLD_NOW | libc::RTLD_LOCAL) else {
228 return;
229 };
230 assert_eq!(library.name(), LIBC);
231 assert!(library.symbol("getpid").is_ok());
232 assert!(NativeLibrary::open_with_flags(c"libferroui-there-is-no-such-library.so.0", libc::RTLD_NOW).is_err());
233 }
234}