Skip to main content

clack_host/entry/
library.rs

1use crate::entry::PluginEntryError;
2use crate::entry::entry_provider::EntryProvider;
3use clack_common::entry::EntryDescriptor;
4use libloading::Library;
5use std::borrow::Cow;
6use std::ffi::{CStr, CString, OsStr};
7use std::path::Path;
8use std::ptr::NonNull;
9
10/// An [`EntryProvider`] which loads an entry from a dynamic library file.
11///
12/// This uses the [`libloading`] library under the hood. It will not be available if the associated
13/// `libloading` Cargo feature of `clack-host` is not enabled.
14pub struct LibraryEntry {
15    _library: Library,
16    entry_ptr: NonNull<EntryDescriptor>,
17}
18
19const SYMBOL_NAME: &CStr = c"clap_entry";
20impl LibraryEntry {
21    #[cfg(feature = "libloading")]
22    pub(crate) fn resolve_path(path: &Path) -> Result<(Cow<'_, Path>, CString), PluginEntryError> {
23        #[cfg(target_os = "macos")]
24        {
25            macos_resolve::resolve_path(path)
26        }
27        #[cfg(not(target_os = "macos"))]
28        {
29            let bundle_path = CString::new(path.to_string_lossy().into_owned())?;
30
31            Ok((Cow::Borrowed(path), bundle_path))
32        }
33    }
34
35    /// Loads a CLAP entry from a dynamic library file located at the given path.
36    ///
37    /// # Errors
38    ///
39    /// This function will return an error if library file could not be loaded, if the `clap_entry`
40    /// symbol could not be extracted from the given library, or if the symbol's value is NULL.
41    ///
42    /// # Safety
43    ///
44    /// This function loads an external library object file, which is inherently unsafe, as even
45    /// just loading it can trigger any behavior in your application, including Undefined Behavior.
46    ///
47    /// Additionally, users
48    pub unsafe fn load_from_path(path: impl AsRef<OsStr>) -> Result<Self, PluginEntryError> {
49        let library = Library::new(path).map_err(PluginEntryError::LibraryLoadingError)?;
50
51        Self::load_from_library(library)
52    }
53
54    /// Wraps a given [`Library`] object to load a CLAP entry from.
55    ///
56    /// This function will look for the standard `clap_entry` symbol to get the [`EntryDescriptor`]
57    /// pointer from. If you need to load that pointer from another, non-standard symbol, see
58    /// [`load_from_symbol_in_library`](Self::load_from_symbol_in_library).
59    ///
60    /// # Errors
61    ///
62    /// This function will return an error if the `clap_entry` symbol could not be extracted from the given
63    /// library, or if the symbol's value is NULL.
64    ///
65    /// # Safety
66    ///
67    /// Users of this function must ensure the symbol `clap_entry` in the given library is ABI
68    /// compatible with a pointer to an [`EntryDescriptor`].
69    pub unsafe fn load_from_library(library: Library) -> Result<Self, PluginEntryError> {
70        Self::load_from_symbol_in_library(library, SYMBOL_NAME)
71    }
72
73    /// Wraps a given [`Library`] object to load a CLAP entry from a given symbol it contains.
74    ///
75    /// # Errors
76    ///
77    /// This function will return an error if the symbol could not be extracted from the given
78    /// library, or if the symbol's value is NULL.
79    ///
80    /// # Safety
81    ///
82    /// Users of this function must ensure the given symbol `symbol_name` in the given library is ABI
83    /// compatible with a pointer to an [`EntryDescriptor`].
84    pub unsafe fn load_from_symbol_in_library(
85        library: Library,
86        symbol_name: &CStr,
87    ) -> Result<Self, PluginEntryError> {
88        let symbol = library
89            .get::<*mut EntryDescriptor>(symbol_name.to_bytes_with_nul())
90            .map_err(PluginEntryError::LibraryLoadingError)?;
91
92        let entry_ptr = NonNull::new(*symbol).ok_or(PluginEntryError::NullEntryPointer)?;
93
94        Ok(Self {
95            _library: library,
96            entry_ptr,
97        })
98    }
99}
100
101// SAFETY: The pointer's value never changes, and since we hold onto the Library it comes from, it
102// will not be unloaded as long as this is alive.
103unsafe impl EntryProvider for LibraryEntry {
104    #[inline]
105    fn entry_pointer(&self) -> NonNull<EntryDescriptor> {
106        self.entry_ptr
107    }
108}
109
110// SAFETY: Entries and factories are all thread-safe by the CLAP spec
111unsafe impl Send for LibraryEntry {}
112// SAFETY: Entries and factories are all thread-safe by the CLAP spec
113unsafe impl Sync for LibraryEntry {}
114
115#[cfg(target_os = "macos")]
116mod macos_resolve {
117    use super::*;
118    use std::path::{Path, PathBuf};
119
120    pub(super) fn resolve_path(path: &Path) -> Result<(Cow<'_, Path>, CString), PluginEntryError> {
121        if std::fs::metadata(path)?.is_dir() {
122            // The given path is to the bundle.
123            let executable_path =
124                executable_path_from_bundle(path).ok_or(PluginEntryError::ResolveFailed)?;
125
126            let bundle_path = CString::new(path.to_string_lossy().into_owned())?;
127
128            Ok((Cow::Owned(executable_path), bundle_path))
129        } else {
130            // The given path is to the executable.
131            let bundle_path = bundle_path_from_executable(path);
132            let bundle_path = CString::new(bundle_path.to_string_lossy().into_owned())?;
133
134            Ok((Cow::Borrowed(path), bundle_path))
135        }
136    }
137
138    fn executable_path_from_bundle(path: &Path) -> Option<PathBuf> {
139        use objc2_foundation::{NSBundle, NSURL};
140
141        let url = NSURL::from_directory_path(path)?;
142        let bundle = NSBundle::bundleWithURL(&url)?;
143
144        let executable_url = bundle.executableURL()?.absoluteURL()?;
145        let executable_path = executable_url.to_file_path()?;
146
147        Some(executable_path)
148    }
149
150    fn bundle_path_from_executable(path: &Path) -> &Path {
151        if let Some(bundle_path) = bundle_path_from_structure(path) {
152            return bundle_path;
153        };
154
155        if let Some(bundle_path) = bundle_path_from_clap_package_ancestor(path) {
156            return bundle_path;
157        };
158
159        path
160    }
161
162    fn bundle_path_from_structure(path: &Path) -> Option<&Path> {
163        let mut ancestors = path.ancestors();
164        if ancestors.next()? != OsStr::new("MacOS") {
165            return None;
166        }
167
168        if ancestors.next()? != OsStr::new("Contents") {
169            return None;
170        }
171
172        ancestors.next()
173    }
174
175    fn bundle_path_from_clap_package_ancestor(path: &Path) -> Option<&Path> {
176        path.ancestors()
177            .find(|a| a.extension().is_some_and(|e| e == "clap"))
178    }
179}