Skip to main content

clack_host/
entry.rs

1//! Loading and handling of CLAP plugin entry files.
2//!
3//! On Windows and Linux (and other non-macOS UNIXes), CLAP plugins are distributed as prebuilt
4//! dynamically-loaded libraries (usually `.dll` or `.so` files) with a `.clap` extension.
5//! They expose a single [`EntryDescriptor`], which, once initialized, acts as the entry
6//! point for the host to read into the library file.
7//!
8//! On macOS, CLAP plugins are distributed as a standard
9//! [bundle](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/AboutBundles/AboutBundles.html),
10//! which contains the aforementioned dynamically-loaded libraries as its executable file.
11//!
12//! CLAP plugin entries expose implementations of various standard [factories](Factory), which are
13//! singletons implementing various functionalities. The most relevant is the [`PluginFactory`],
14//! which allows to list and instantiate plugins. See the [`factory`](crate::factory) module
15//! documentation to learn more about factories.
16//!
17//! Clack handles all of this functionality through the [`PluginEntry`] type, which exposes all
18//! the entry's factory implementation, and allow the entry to be loaded in different ways:
19//!
20//! * From a file, using [`PluginEntry::load`].
21//!
22//!   This is the most common usage, as it allows to load
23//!   third-party CLAP entries present anywhere on the file system, which is most likely the
24//!   functionality "CLAP plugin support" implies for most hosts.
25//!
26//! * From an [`EntryProvider`] type, using [`PluginEntry::load_from`].
27//!
28//!   This is a more advanced usage, and it allows host implementations to not use Clack's
29//!   implementation of entry loading
30//!   (which uses [`libloading`](https://crates.io/crates/libloading) under the hood), and implement
31//!   their own instead.
32//!
33//!   It also allows host implementations to customize Clack's implementation of entry loading by
34//!   leveraging the [`LibraryEntry`] built-in entry provider type.
35//!
36//! * From a `clack-plugin`'s `Entry` type, using [`PluginEntry::load_from_clack`].
37//!
38//!   This allows to load plugins that have been statically built into the host's binary
39//!   (i.e. built-in plugins) without having to distribute them in separate
40//!   files, or to perform any filesystem access or plugin discovery.
41//!
42//!   This is also the only way to load a CLAP plugin using only safe code, since it leverages
43//!   `clap-plugin`'s safety guarantees to enforce that plugins loaded this way cannot cause
44//!   Undefined Behavior.
45//!
46//! See the [`PluginEntry`]'s type documentation for examples.
47//!
48//! # Safety
49//!
50//! All functions that produce [`PluginEntry`]s from a CLAP entry file or pointer are inherently
51//! unsafe.
52//!
53//! Most APIs in this crate operate under the assumption that entries and plugins are compliant
54//! to the CLAP specification, and using a number of those APIs can easily result in
55//! Undefined Behavior if operating on non-compliant plugins.
56//!
57//! Therefore, the safe APIs in this crate are safeguarding the host implementation, not the
58//! plugins it loads. As soon as a plugin is loaded, Undefined Behavior is possible to be triggered,
59//! regardless of the host's implementation.
60//!
61//! Users needing to safeguard against crashes or other kinds of UB from plugins from affecting the
62//! rest of their application should consider using additional process isolation techniques.
63//!
64//! # Plugin entry discovery
65//!
66//! Clack itself does not implement any utilities to aid host implementations with discovering
67//! which CLAP entry files are available to be loaded on the filesystem.
68//!
69//! However, you may use the separate [`clack-finder`](https://github.com/prokopyl/clack/tree/main/finder)
70//! library to perform this task.
71//!
72//! Refer to the
73//! [CLAP specification](https://github.com/free-audio/clap/blob/main/include/clap/entry.h) for more
74//! information about standard search paths and the general discovery process.
75
76use std::error::Error;
77use std::ffi::CStr;
78use std::fmt::{Display, Formatter};
79use std::ptr::NonNull;
80
81mod cache;
82mod entry_provider;
83mod loaded_entry;
84
85#[cfg(feature = "libloading")]
86mod library;
87#[cfg(feature = "libloading")]
88pub use library::LibraryEntry;
89
90#[cfg(feature = "clack-plugin")]
91mod clack_plugin;
92
93#[cfg(test)]
94#[allow(missing_docs)]
95pub mod diva_stub;
96
97use crate::entry::cache::CachedEntry;
98use crate::factory::{Factory, plugin::PluginFactory};
99pub use clack_common::entry::*;
100use clack_common::factory::RawFactoryPointer;
101use clack_common::utils::ClapVersion;
102pub use entry_provider::EntryProvider;
103
104/// A handle to a loaded CLAP plugin entry.
105///
106/// This allows getting all the [factories](Factory) exposed by the entry, mainly the
107/// [`PluginFactory`] which allows to list plugin instances.
108///
109/// This is only a lightweight handle: plugin entries are only loaded once, and the [`Clone`]
110/// operation on this type only clones the handle.
111///
112/// Plugin entries are only unloaded when all handles are dropped and all associated instances
113/// are unloaded.
114///
115/// A [`PluginEntry`] can also be loaded from a static [`EntryDescriptor`] instead of a file.
116/// See [`PluginEntry::load_from_raw`].
117///
118/// See the [module docs](crate::entry) for more information about CLAP entries.
119///
120/// # Example
121///
122/// ```no_run
123/// # pub fn main() -> Result<(), Box<dyn std::error::Error>> {
124/// use clack_host::prelude::PluginEntry;
125///
126/// let entry = unsafe { PluginEntry::load("/home/user/.clap/u-he/libdiva.so")? };
127/// let plugin_factory = entry.get_plugin_factory().unwrap();
128///
129/// println!("Loaded entry CLAP version: {}", entry.version());
130/// # Ok(()) }
131/// ```
132#[derive(Clone)]
133pub struct PluginEntry {
134    inner: PluginEntryInner,
135}
136
137#[derive(Clone)]
138enum PluginEntryInner {
139    Cached(CachedEntry),
140    #[cfg(feature = "clack-plugin")]
141    FromClack(clack_plugin::ClackEntry),
142}
143
144impl PluginEntry {
145    /// Loads and initializes a CLAP entry from a dynamic library file located at the given path.
146    ///
147    /// This function also initializes the loaded entry, using the provided path, and returns a
148    /// handle to it.
149    ///
150    /// If you wish to initialize the entry with a different path than the one of the dynamic
151    /// library file it comes from, you can use [`load_from`](Self::load_from) with a
152    /// [`LibraryEntry`] as an argument.
153    ///
154    /// # Platform compatibility notes
155    ///
156    /// On macOS, plugins are not packaged as their dynamic library file directly, but as a standard
157    /// bundle which contains the dynamic library file instead.
158    ///
159    /// For convenience, on macOS this function can accept both a path to the plugin bundle, or a
160    /// path to the dynamic library file itself.
161    ///
162    /// If a path to a bundle is given to this function, it will use its `Info.plist` file to locate
163    /// the bundle's executable and load that. It will then use the given path to the bundle to
164    /// initialize the entry.
165    ///
166    /// If a path to a file is given to this function, it will load that as the dynamic library file,
167    /// and attempt to find or reconstruct the bundle's path from the executable path. It will then
168    /// use that path to initialize the entry.
169    ///
170    /// If you have access to both paths and want to eliminate this guesswork, you should use
171    /// [`load_from`](Self::load_from) with a [`LibraryEntry`] as an argument instead.
172    ///
173    /// # Safety
174    ///
175    /// This function loads an external library object file, which is inherently unsafe, as even
176    /// just loading it can trigger any behavior in your application, including Undefined Behavior.
177    ///
178    /// Additionally, loading a non-compliant CLAP entry may invalidate safety assumptions other
179    /// APIs in this library rely on. See the [module docs](self)'s Safety section for more
180    /// information.
181    ///
182    /// # Errors
183    ///
184    /// This method returns an error if loading the entry fails.
185    /// See [`PluginEntryError`] for all the possible errors that may occur.
186    ///
187    /// # Example
188    ///
189    /// ```no_run
190    /// # pub fn main() -> Result<(), Box<dyn std::error::Error>> {
191    /// use clack_host::prelude::PluginEntry;
192    ///
193    /// let entry = unsafe { PluginEntry::load("/home/user/.clap/u-he/libdiva.so")? };
194    ///
195    /// println!("Loaded entry CLAP version: {}", entry.version());
196    /// # Ok(()) }
197    /// ```
198    #[cfg(feature = "libloading")]
199    pub unsafe fn load<P: AsRef<std::ffi::OsStr>>(path: P) -> Result<Self, PluginEntryError> {
200        use crate::entry::library::LibraryEntry;
201        use std::path::Path;
202
203        let path = Path::new(&path);
204
205        let (library_path, bundle_path) = LibraryEntry::resolve_path(path)?;
206
207        let library = LibraryEntry::load_from_path(library_path.as_ref())?;
208        Self::load_from(library, &bundle_path)
209    }
210
211    /// Initializes a CLAP entry exposed by the given [`EntryProvider`].
212    ///
213    /// This takes ownership of the given `provider`, and will keep it alive as long as there are
214    /// [`PluginEntry`] references to it, or as long as [`PluginInstance`s](crate::plugin::PluginInstance)
215    /// (or other similar objects) created by this entry also exist.
216    ///
217    /// # Safety
218    ///
219    /// Loading a non-compliant CLAP entry may invalidate safety assumptions other
220    /// APIs in this library rely on. See the [module docs](self)'s Safety section for more
221    /// information.
222    ///
223    /// # Errors
224    ///
225    /// This method returns an error if initializing the entry fails.
226    /// See [`PluginEntryError`] for all the possible errors that may occur.
227    pub unsafe fn load_from(
228        provider: impl EntryProvider,
229        path: &CStr,
230    ) -> Result<Self, PluginEntryError> {
231        let inner = cache::get_or_init(provider, path)?;
232
233        Ok(Self {
234            inner: PluginEntryInner::Cached(inner),
235        })
236    }
237
238    /// Loads a CLAP entry from an [`Entry`](::clack_plugin::entry::Entry) created by [`clack_plugin`](::clack_plugin).
239    ///
240    /// Note that CLAP plugins loaded this way still need a valid path to initialize, as they may
241    /// perform various filesystem operations relative to their entry files.
242    ///
243    /// Note that unlike other methods to load [`PluginEntry`]s, this method is completely safe, as
244    /// it can rely on the safety guarantees provided by [`clack_plugin`](::clack_plugin).
245    ///
246    /// # Errors
247    ///
248    /// This method returns an error if initializing the entry fails.
249    /// See [`PluginEntryError`] for all the possible errors that may occur.
250    ///
251    #[cfg(feature = "clack-plugin")]
252    pub fn load_from_clack<E: ::clack_plugin::entry::Entry>(
253        path: &CStr,
254    ) -> Result<Self, PluginEntryError> {
255        let entry = E::new(Some(path)).map_err(|_| PluginEntryError::EntryInitFailed)?;
256        let inner = PluginEntryInner::FromClack(clack_plugin::ClackEntry::new(entry));
257
258        Ok(Self { inner })
259    }
260
261    /// Loads a CLAP entry from a `'static` [`EntryDescriptor`].
262    ///
263    /// Note that CLAP plugins loaded this way still need a valid path, as they may perform various
264    /// filesystem operations relative to their entry files.
265    ///
266    /// # Safety
267    ///
268    /// Loading a non-compliant CLAP entry may invalidate safety assumptions other
269    /// APIs in this library rely on. See the [module docs](self)'s Safety section for more
270    /// information.
271    ///
272    /// # Errors
273    ///
274    /// This method returns an error if initializing the entry fails.
275    /// See [`PluginEntryError`] for all the possible errors that may occur.
276    ///
277    /// # Example
278    ///
279    /// ```no_run
280    /// use clack_host::entry::EntryDescriptor;
281    /// use clack_host::prelude::PluginEntry;
282    /// # pub fn foo(descriptor: &'static EntryDescriptor) -> Result<(), Box<dyn std::error::Error>> {
283    ///
284    /// let descriptor: &'static EntryDescriptor = /* ... */
285    /// # descriptor;
286    ///
287    /// let path = c"/home/user/.clap/u-he/libdiva.so";
288    /// let entry = unsafe { PluginEntry::load_from_raw(descriptor, path)? };
289    ///
290    /// println!("Loaded entry CLAP version: {}", entry.version());
291    /// # Ok(()) }
292    #[inline]
293    pub unsafe fn load_from_raw(
294        inner: &'static EntryDescriptor,
295        plugin_path: &CStr,
296    ) -> Result<Self, PluginEntryError> {
297        Self::load_from(inner, plugin_path)
298    }
299
300    /// Gets the raw, C-FFI plugin entry descriptor exposed by this entry.
301    #[inline]
302    pub fn raw_entry(&self) -> &EntryDescriptor {
303        match &self.inner {
304            PluginEntryInner::Cached(entry) => entry.as_ref(),
305            #[cfg(feature = "clack-plugin")]
306            PluginEntryInner::FromClack(_) => &clack_plugin::ClackEntry::DUMMY_DESCRIPTOR,
307        }
308    }
309
310    /// Returns the [`Factory`] of type `F` exposed by this entry, if it exists.
311    ///
312    /// If this entry does not expose a factory of the requested type, [`None`] is returned.
313    ///
314    /// If you are looking to fetch the entry's [`PluginFactory`], you can also use the
315    /// [`get_plugin_factory`](PluginEntry::get_plugin_factory) method, which is just a convenience
316    /// wrapper around this method.
317    ///
318    /// # Example
319    ///
320    /// ```no_run
321    /// # pub fn main() -> Result<(), Box<dyn std::error::Error>> {
322    /// use clack_host::factory::plugin::PluginFactory;
323    /// use clack_host::prelude::PluginEntry;
324    ///
325    /// let entry = unsafe { PluginEntry::load("/home/user/.clap/u-he/libdiva.so")? };
326    /// let plugin_factory = entry.get_factory::<PluginFactory>().unwrap();
327    ///
328    /// println!("Found {} plugins.", plugin_factory.plugin_count());
329    /// # Ok(()) }
330    /// ```
331    pub fn get_factory<'a, F: Factory<'a>>(&'a self) -> Option<F> {
332        let identifier = const { *F::IDENTIFIERS.first().unwrap() };
333        match &self.inner {
334            PluginEntryInner::Cached(entry) => {
335                // SAFETY: this type ensures the function pointer is valid.
336                let ptr = unsafe { entry.get().get_factory?(identifier.as_ptr()) };
337                let ptr = NonNull::new(ptr.cast_mut())?;
338                // SAFETY: Per the CLAP spec, if this pointer is non-null it has to be valid for reads
339                let ptr = unsafe { RawFactoryPointer::from_raw(ptr.cast()) };
340
341                // SAFETY: pointer was created using F's own identifier.
342                Some(unsafe { F::from_raw(ptr) })
343            }
344            #[cfg(feature = "clack-plugin")]
345            PluginEntryInner::FromClack(clack) => clack.get_factory(),
346        }
347    }
348
349    /// Returns the [`PluginFactory`] exposed by this entry, if it exists.
350    ///
351    /// If this entry does not expose a [`PluginFactory`], [`None`] is returned.
352    ///
353    /// This is a convenience method, and is equivalent to calling
354    /// [`get_factory`](PluginEntry::get_factory) with a [`PluginFactory`] type parameter.
355    ///
356    /// # Example
357    ///
358    /// ```no_run
359    /// # pub fn main() -> Result<(), Box<dyn std::error::Error>> {
360    /// use clack_host::prelude::PluginEntry;
361    ///
362    /// let entry = unsafe { PluginEntry::load("/home/user/.clap/u-he/libdiva.so")? };
363    /// let plugin_factory = entry.get_plugin_factory().unwrap();
364    ///
365    /// println!("Found {} plugins.", plugin_factory.plugin_count());
366    /// # Ok(()) }
367    /// ```
368    #[inline]
369    pub fn get_plugin_factory(&self) -> Option<PluginFactory<'_>> {
370        self.get_factory()
371    }
372
373    /// Returns the CLAP version used by this entry.
374    #[inline]
375    pub fn version(&self) -> ClapVersion {
376        ClapVersion::from_raw(self.raw_entry().clap_version)
377    }
378}
379
380/// Errors that can occur while loading a [`PluginEntry`].
381///
382/// See [`PluginEntry::load`] and [`PluginEntry::load_from_raw`].
383#[derive(Debug)]
384#[non_exhaustive]
385pub enum PluginEntryError {
386    /// The dynamic library file could not be loaded.
387    ///
388    /// This contains the error type from the underlying
389    /// [`libloading`](https://crates.io/crates/libloading) library.
390    #[cfg(feature = "libloading")]
391    LibraryLoadingError(libloading::Error),
392    #[cfg(feature = "libloading")]
393    /// The given path is not a valid C string.
394    InvalidNulPath(std::ffi::NulError),
395    /// An I/O error occurred.
396    Io(std::io::Error),
397    /// Failed to resolve a CLAP entry path.
398    ///
399    /// This error can only occur on macOS.
400    ResolveFailed,
401    /// The entry pointer exposed by the dynamic library file is `null`.
402    NullEntryPointer,
403    /// The exposed entry used an incompatible CLAP version.
404    IncompatibleClapVersion {
405        /// The CLAP version that the entry uses.
406        ///
407        /// See [`ClapVersion::CURRENT`] to get the current clap version.
408        plugin_version: ClapVersion,
409    },
410    /// The entry's `init` method failed.
411    EntryInitFailed,
412}
413
414impl Error for PluginEntryError {
415    fn source(&self) -> Option<&(dyn Error + 'static)> {
416        match self {
417            #[cfg(feature = "libloading")]
418            PluginEntryError::InvalidNulPath(e) => Some(e),
419            #[cfg(feature = "libloading")]
420            PluginEntryError::LibraryLoadingError(e) => Some(e),
421            PluginEntryError::Io(e) => Some(e),
422            _ => None,
423        }
424    }
425}
426
427impl Display for PluginEntryError {
428    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
429        match self {
430            PluginEntryError::EntryInitFailed => f.write_str("Plugin entry initialization failed"),
431            #[cfg(feature = "libloading")]
432            PluginEntryError::InvalidNulPath(e) => {
433                write!(f, "Invalid plugin descriptor path: {e}")
434            }
435            #[cfg(feature = "libloading")]
436            PluginEntryError::LibraryLoadingError(e) => {
437                write!(f, "Failed to load plugin descriptor library: {e}")
438            }
439            PluginEntryError::Io(e) => Display::fmt(e, f),
440            PluginEntryError::NullEntryPointer => f.write_str("Plugin entry pointer is null"),
441            PluginEntryError::ResolveFailed => f.write_str("Failed to resolve plugin entry path"),
442            PluginEntryError::IncompatibleClapVersion { plugin_version } => write!(
443                f,
444                "Incompatible CLAP version: plugin is v{}, host is v{}",
445                plugin_version,
446                ClapVersion::CURRENT
447            ),
448        }
449    }
450}
451
452#[cfg(feature = "libloading")]
453impl From<std::ffi::NulError> for PluginEntryError {
454    #[inline]
455    fn from(value: std::ffi::NulError) -> Self {
456        Self::InvalidNulPath(value)
457    }
458}
459
460impl From<std::io::Error> for PluginEntryError {
461    #[inline]
462    fn from(value: std::io::Error) -> Self {
463        Self::Io(value)
464    }
465}