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}