use std::collections::HashMap;
use std::path::Path;
use audio_core_bsd::AudioNode;
use libloading::Library;
use crate::abi::{is_abi_compatible, AUDIO_PLUGIN_ABI_MAGIC, AUDIO_PLUGIN_ABI_VERSION};
use crate::error::{PluginError, Result};
use crate::metadata::PluginMetadata;
use crate::plugin::{HostPlugin, Plugin};
use crate::symbols::{
raw_to_metadata, AbiMagicFn, AbiVersionFn, CreateFn, DestroyFn, MetadataFn,
AUDIO_PLUGIN_ABI_MAGIC_SYMBOL, AUDIO_PLUGIN_ABI_VERSION_SYMBOL, AUDIO_PLUGIN_CREATE_SYMBOL,
AUDIO_PLUGIN_DESTROY_SYMBOL, AUDIO_PLUGIN_METADATA_SYMBOL,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PluginId(u32);
pub struct LoadedPlugin {
#[allow(dead_code)]
library: Library,
metadata: PluginMetadata,
plugin: Box<dyn Plugin>,
id: PluginId,
}
impl LoadedPlugin {
#[must_use]
pub fn metadata(&self) -> &PluginMetadata {
&self.metadata
}
#[must_use]
pub fn id(&self) -> PluginId {
self.id
}
}
pub struct PluginLoader {
plugins: HashMap<PluginId, LoadedPlugin>,
next_id: u32,
}
impl PluginLoader {
#[must_use]
pub fn new() -> Self {
Self {
plugins: HashMap::new(),
next_id: 0,
}
}
pub fn load(&mut self, path: &Path) -> Result<PluginId> {
if !path.is_file() {
return Err(PluginError::InvalidPath(format!(
"{}: not an existing file",
path.display()
)));
}
if path.to_str().is_none() {
return Err(PluginError::InvalidPath(format!(
"{}: path is not valid UTF-8",
path.display()
)));
}
let library = unsafe { Library::new(path) }
.map_err(|e| PluginError::LibraryLoad(format!("{}: {e}", path.display())))?;
let plugin_magic = unsafe {
let sym: libloading::Symbol<AbiMagicFn> = library
.get(AUDIO_PLUGIN_ABI_MAGIC_SYMBOL.as_bytes())
.map_err(|e| {
PluginError::SymbolMissing(format!("{AUDIO_PLUGIN_ABI_MAGIC_SYMBOL}: {e}"))
})?;
sym()
};
if plugin_magic != AUDIO_PLUGIN_ABI_MAGIC {
return Err(PluginError::InvalidMetadata(format!(
"ABI magic mismatch: expected {AUDIO_PLUGIN_ABI_MAGIC:#010x}, got {plugin_magic:#010x}"
)));
}
let plugin_version = unsafe {
let sym: libloading::Symbol<AbiVersionFn> = library
.get(AUDIO_PLUGIN_ABI_VERSION_SYMBOL.as_bytes())
.map_err(|e| {
PluginError::SymbolMissing(format!("{AUDIO_PLUGIN_ABI_VERSION_SYMBOL}: {e}"))
})?;
sym()
};
if !is_abi_compatible(AUDIO_PLUGIN_ABI_VERSION, plugin_version) {
return Err(PluginError::AbiVersionMismatch {
host: AUDIO_PLUGIN_ABI_VERSION,
plugin: plugin_version,
});
}
let metadata = unsafe {
let sym: libloading::Symbol<MetadataFn> = library
.get(AUDIO_PLUGIN_METADATA_SYMBOL.as_bytes())
.map_err(|e| {
PluginError::SymbolMissing(format!("{AUDIO_PLUGIN_METADATA_SYMBOL}: {e}"))
})?;
let raw_ptr = sym();
raw_to_metadata(raw_ptr)?
};
let create_fn: CreateFn = unsafe {
let sym: libloading::Symbol<CreateFn> = library
.get(AUDIO_PLUGIN_CREATE_SYMBOL.as_bytes())
.map_err(|e| {
PluginError::SymbolMissing(format!("{AUDIO_PLUGIN_CREATE_SYMBOL}: {e}"))
})?;
*sym
};
let destroy_fn: Option<DestroyFn> = unsafe {
library
.get::<DestroyFn>(AUDIO_PLUGIN_DESTROY_SYMBOL.as_bytes())
.ok()
.map(|sym| *sym)
};
let plugin: Box<dyn Plugin> =
Box::new(HostPlugin::new(metadata.clone(), create_fn, destroy_fn));
let id = self.fresh_id();
self.plugins.insert(
id,
LoadedPlugin {
library,
metadata,
plugin,
id,
},
);
Ok(id)
}
pub fn unload(&mut self, id: PluginId) -> Result<()> {
if self.plugins.remove(&id).is_some() {
Ok(())
} else {
Err(PluginError::NotLoaded)
}
}
pub fn get(&self, id: PluginId) -> Result<&LoadedPlugin> {
self.plugins.get(&id).ok_or(PluginError::NotLoaded)
}
pub fn get_mut(&mut self, id: PluginId) -> Result<&mut LoadedPlugin> {
self.plugins.get_mut(&id).ok_or(PluginError::NotLoaded)
}
pub fn metadata(&self, id: PluginId) -> Result<&PluginMetadata> {
Ok(self.get(id)?.metadata())
}
pub fn instantiate(&mut self, id: PluginId) -> Result<Box<dyn AudioNode>> {
let loaded = self.get_mut(id)?;
Ok(loaded.plugin.instantiate())
}
#[must_use]
pub fn is_loaded(&self, id: PluginId) -> bool {
self.plugins.contains_key(&id)
}
#[must_use]
pub fn len(&self) -> usize {
self.plugins.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.plugins.is_empty()
}
#[must_use]
pub fn ids(&self) -> Vec<PluginId> {
self.plugins.keys().copied().collect()
}
fn fresh_id(&mut self) -> PluginId {
let id = PluginId(self.next_id);
self.next_id = self.next_id.wrapping_add(1);
id
}
}
impl Default for PluginLoader {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plugin_id_is_copy_eq_hash_debug() {
let a = PluginId(7);
let b = a; assert_eq!(a, b);
assert_eq!(format!("{a:?}"), "PluginId(7)");
let mut set = std::collections::HashSet::new();
set.insert(a);
assert!(set.contains(&b));
assert!(!set.contains(&PluginId(8)));
}
#[test]
fn new_loader_is_empty() {
let loader = PluginLoader::new();
assert!(loader.is_empty());
assert_eq!(loader.len(), 0);
assert!(loader.ids().is_empty());
}
#[test]
fn default_equals_new() {
let d = PluginLoader::default();
assert!(d.is_empty());
assert_eq!(d.len(), 0);
}
#[test]
fn unload_unknown_id_is_not_loaded_err() {
let mut loader = PluginLoader::new();
let err = loader.unload(PluginId(0)).unwrap_err();
assert_eq!(err, PluginError::NotLoaded);
}
#[test]
fn get_unknown_id_is_not_loaded_err() {
let loader = PluginLoader::new();
let err = loader.get(PluginId(42)).map(|_| ()).unwrap_err();
assert_eq!(err, PluginError::NotLoaded);
}
#[test]
fn get_mut_unknown_id_is_not_loaded_err() {
let mut loader = PluginLoader::new();
let err = loader.get_mut(PluginId(99)).map(|_| ()).unwrap_err();
assert_eq!(err, PluginError::NotLoaded);
}
#[test]
fn metadata_unknown_id_is_not_loaded_err() {
let loader = PluginLoader::new();
let err = loader.metadata(PluginId(5)).unwrap_err();
assert_eq!(err, PluginError::NotLoaded);
}
#[test]
fn instantiate_unknown_id_is_not_loaded_err() {
let mut loader = PluginLoader::new();
let err = loader.instantiate(PluginId(3)).map(|_| ()).unwrap_err();
assert_eq!(err, PluginError::NotLoaded);
}
#[test]
fn is_loaded_unknown_id_is_false() {
let loader = PluginLoader::new();
assert!(!loader.is_loaded(PluginId(0)));
assert!(!loader.is_loaded(PluginId(u32::MAX)));
}
#[test]
fn load_missing_file_is_invalid_path_err() {
let mut loader = PluginLoader::new();
let err = loader
.load(Path::new("/nonexistent/path/does-not-exist.so"))
.unwrap_err();
assert!(matches!(err, PluginError::InvalidPath(_)));
assert!(err.to_string().contains("not an existing file"));
}
#[test]
fn load_directory_is_invalid_path_err() {
let mut loader = PluginLoader::new();
let dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let err = loader.load(dir).unwrap_err();
assert!(matches!(err, PluginError::InvalidPath(_)));
assert!(err.to_string().contains("not an existing file"));
}
#[test]
fn fresh_id_is_monotonic_and_does_not_panic_at_max() {
let mut loader = PluginLoader::new();
loader.next_id = u32::MAX;
let first = loader.fresh_id();
assert_eq!(first, PluginId(u32::MAX));
let second = loader.fresh_id();
assert_eq!(second, PluginId(0));
}
}