Skip to main content

ferroui_base/platform/
native_library.rs

1//! A shared library that is opened at run time and asked for its functions
2//! by name (no upstream file: the reference declares the functions of a
3//! library as platform invokes, which its runtime resolves this way when a
4//! function is first called).
5//!
6//! The one loader of the backends of Unix systems: the X11 crate opens
7//! GLib, GTK, the session management library and the ICE library with it,
8//! the Wayland crate GBM and DRM, and the Linux framebuffer crate libinput,
9//! libdrm and libgbm. Nothing of those libraries is linked: a crate builds
10//! and starts without them, and a library that is missing shows as an
11//! error value where the reference gets an exception
12//! (docs/porting/wayland-platform.md, section 5.1).
13
14use std::ffi::{c_int, c_void, CStr, CString};
15
16/// A library that was opened. It is never closed, so what it exports stays
17/// valid for the life of the process.
18#[derive(Clone, Copy, Debug)]
19pub struct NativeLibrary {
20    /// The handle of the dynamic loader, as a number.
21    handle: usize,
22    name: &'static CStr,
23}
24
25/// A library or a function that cannot be had.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum NativeLibraryError {
28    /// The dynamic loader cannot load the library or one of its
29    /// dependencies.
30    Library {
31        /// The name the library was asked for by.
32        library: String,
33    },
34    /// The library has no function of that name.
35    EntryPoint {
36        /// The name the library was opened by.
37        library: String,
38        /// The name of the function.
39        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    /// Opens the library of that name, as the dynamic loader finds it: its
60    /// functions are bound when first called, and its symbols are available
61    /// to the libraries loaded after it (`RTLD_LAZY | RTLD_GLOBAL`).
62    pub fn open(name: &'static CStr) -> Result<Self, NativeLibraryError> {
63        Self::open_with_flags(name, libc::RTLD_LAZY | libc::RTLD_GLOBAL)
64    }
65
66    /// Opens the library of that name with the flags of `dlopen` given
67    /// (`RTLD_NOW | RTLD_LOCAL`, for one).
68    pub fn open_with_flags(name: &'static CStr, flags: c_int) -> Result<Self, NativeLibraryError> {
69        // SAFETY: the name is a terminated string. The initializers of the library run, as
70        // they do for any library a process loads; the handle is kept for ever.
71        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    /// The name the library was opened by.
79    pub fn name(&self) -> &'static CStr {
80        self.name
81    }
82
83    /// The address of the function of that name.
84    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        // SAFETY: the handle is the one `dlopen` returned and the library is still loaded;
91        // the name is a terminated string that lives for the call.
92        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/// Declares the functions the port calls of one or more libraries as a
101/// table of function pointers, with a constructor that resolves every one
102/// of them or fails with the name of the first that is missing.
103///
104/// Each entry names the library (an argument of `load`) the function is
105/// in, as the `DllImport` attribute of the reference does, and the
106/// exported name when the field is named otherwise (`as "Name"`):
107///
108/// ```ignore
109/// ferroui_base::native_functions! {
110///     /// The functions of libfoo.
111///     pub struct LibFoo(lib) {
112///         lib::foo_create: unsafe extern "C" fn(c_int) -> *mut c_void;
113///     }
114/// }
115///
116/// let table = LibFoo::load(&NativeLibrary::open(c"libfoo.so.1")?)?;
117/// ```
118///
119/// Each field is the function of that name, with the C signature the
120/// declaration gives; it is called as `(table.foo_create)(1)` in an
121/// `unsafe` block whose safety argument is the contract of the C function.
122#[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            /// Resolves every function of the table.
141            #[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                        // SAFETY: the address is that of the exported function of this name,
148                        // whose C declaration is the type of the field; a function pointer
149                        // has the size and the validity of a non-null address.
150                        $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    // Not from the reference.
166    use super::*;
167
168    /// The C library, which is loaded in every process.
169    #[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    /// The C library is loaded in every process: a function it has is
190    /// found, one it does not have is an error that names both.
191    #[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        // SAFETY: `getpid` takes nothing and always succeeds.
214        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    /// A library opened with flags of its own is the same library.
225    #[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}