mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-09 03:42:16 +00:00
Merge pull request #1316 from JonRC/fix/plugin-load-error-reason
Plugin hosts: keep the system's reason when a plugin binary fails to load
This commit is contained in:
4 files changed
+54
-5
No files matched your search
@@ -43,6 +43,16 @@ fn loaded() -> &'static Mutex<HashMap<PathBuf, Arc<Bundle>>> {
|
||||
L.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// A libloading error with the system's reason. libloading's own text is only "dlopen failed"
|
||||
/// (or "dlsym failed", "LoadLibraryExW failed"); the reason (`dlerror`, the Windows error) is its
|
||||
/// source.
|
||||
fn with_reason(e: &libloading::Error) -> String {
|
||||
match std::error::Error::source(e) {
|
||||
Some(why) => format!("{e}: {why}"),
|
||||
None => e.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
/// Loads (once per path, then cached for the life of the process) and initialises a `.clap`.
|
||||
pub fn load(path: &Path) -> Result<Arc<Bundle>, ClapError> {
|
||||
@@ -60,12 +70,12 @@ impl Bundle {
|
||||
let exe = scan::bundle_executable(path).ok_or_else(|| err("no loadable binary".into()))?;
|
||||
// SAFETY: loading a shared library runs its static initialisers. That is inherent to
|
||||
// hosting plugins; we only load files found in CLAP plugin folders or named explicitly.
|
||||
let lib = unsafe { libloading::Library::new(&exe) }.map_err(|e| err(e.to_string()))?;
|
||||
let lib = unsafe { libloading::Library::new(&exe) }.map_err(|e| err(with_reason(&e)))?;
|
||||
// SAFETY: `clap_entry` is the CLAP-mandated exported data symbol of type
|
||||
// `const clap_plugin_entry`; the symbol's address is therefore a `*const clap_plugin_entry`.
|
||||
let entry: *const clap_plugin_entry = match unsafe { lib.get::<*const clap_plugin_entry>(b"clap_entry\0") } {
|
||||
Ok(sym) => *sym,
|
||||
Err(e) => return Err(err(format!("not a CLAP plugin: {e}"))),
|
||||
Err(e) => return Err(err(format!("not a CLAP plugin (no clap_entry): {}", with_reason(&e)))),
|
||||
};
|
||||
if entry.is_null() {
|
||||
return Err(err("null clap_entry".into()));
|
||||
|
||||
@@ -262,4 +262,17 @@ mod tests {
|
||||
assert!(instantiate("clap:does.not.exist").is_err());
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
/// A binary the system refuses to load reports the system's reason (missing library, wrong
|
||||
/// glibc, not an ELF file), not just "dlopen failed".
|
||||
#[test]
|
||||
fn a_binary_that_does_not_load_says_why() {
|
||||
let d = fresh_private_dir("soundcraft-clap-reason");
|
||||
let f = d.join("garbage.clap");
|
||||
std::fs::write(&f, b"\x7fELF this is not a plugin \x00\x01\x02").unwrap();
|
||||
let msg = load_bundle(&f).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
let reason = msg.split_once("failed: ").map_or("", |(_, r)| r.trim());
|
||||
assert!(!reason.is_empty(), "no reason in: {msg}");
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,16 @@ mod cf {
|
||||
}
|
||||
}
|
||||
|
||||
/// A libloading error with the system's reason. libloading's own text is only "dlopen failed"
|
||||
/// (or "dlsym failed", "LoadLibraryExW failed"); the reason (`dlerror`, the Windows error) is its
|
||||
/// source.
|
||||
fn with_reason(e: &libloading::Error) -> String {
|
||||
match std::error::Error::source(e) {
|
||||
Some(why) => format!("{e}: {why}"),
|
||||
None => e.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the shared library and runs the platform module entry (`bundleEntry` with a
|
||||
/// `CFBundleRef` on macOS, `InitDll` on Windows, `ModuleEntry` with the `dlopen` handle on Linux).
|
||||
/// Also returns the matching module exit, when the entry ran and the module exports one.
|
||||
@@ -146,7 +156,7 @@ fn open_library(bundle: &Path, exe: &Path) -> Result<(libloading::Library, Optio
|
||||
use libloading::os::unix::{Library as UnixLib, RTLD_LOCAL, RTLD_NOW};
|
||||
// SAFETY: loading a shared library runs its static initialisers. That is inherent to
|
||||
// hosting plugins; we only load files found in VST3 plugin folders or named explicitly.
|
||||
let lib = unsafe { UnixLib::open(Some(exe), RTLD_NOW | RTLD_LOCAL) }.map_err(|e| e.to_string())?;
|
||||
let lib = unsafe { UnixLib::open(Some(exe), RTLD_NOW | RTLD_LOCAL) }.map_err(|e| with_reason(&e))?;
|
||||
type ModuleEntry = unsafe extern "C" fn(*mut c_void) -> bool;
|
||||
// SAFETY: `ModuleEntry` is the VST3-mandated Linux entry with this signature.
|
||||
let entry = unsafe { lib.get::<ModuleEntry>(b"ModuleEntry\0") }.ok().map(|s| *s);
|
||||
@@ -170,7 +180,7 @@ fn open_library(bundle: &Path, exe: &Path) -> Result<(libloading::Library, Optio
|
||||
{
|
||||
// SAFETY: loading a shared library runs its static initialisers. That is inherent to
|
||||
// hosting plugins; we only load files found in VST3 plugin folders or named explicitly.
|
||||
let lib = unsafe { libloading::Library::new(exe) }.map_err(|e| e.to_string())?;
|
||||
let lib = unsafe { libloading::Library::new(exe) }.map_err(|e| with_reason(&e))?;
|
||||
let mut exit: Option<ModuleExit> = None;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
@@ -335,7 +345,7 @@ impl Bundle {
|
||||
// SAFETY: `GetPluginFactory` is the VST3-mandated export with this signature.
|
||||
let get = match unsafe { lib.get::<GetFactory>(b"GetPluginFactory\0") } {
|
||||
Ok(sym) => *sym,
|
||||
Err(e) => return Err(fail(format!("not a VST3 plugin: {e}"))),
|
||||
Err(e) => return Err(fail(format!("not a VST3 plugin (no GetPluginFactory): {}", with_reason(&e)))),
|
||||
};
|
||||
// SAFETY: the module is loaded and its entry function (if any) has run.
|
||||
let raw = unsafe { get() };
|
||||
|
||||
@@ -307,6 +307,21 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
/// A binary the system refuses to load reports the system's reason (missing library, wrong
|
||||
/// glibc, not an ELF file), not just "dlopen failed".
|
||||
#[test]
|
||||
fn a_binary_that_does_not_load_says_why() {
|
||||
let d = std::env::temp_dir().join(format!("soundcraft-vst3-reason-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
let f = d.join("garbage.vst3");
|
||||
std::fs::write(&f, b"\x7fELF this is not a plugin \x00\x01\x02").unwrap();
|
||||
let msg = load_bundle(&f).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
let reason = msg.split_once("failed: ").map_or("", |(_, r)| r.trim());
|
||||
assert!(!reason.is_empty(), "no reason in: {msg}");
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
// Unix only: the test points a ".vst3" at a system library through a symlink.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
@@ -326,6 +341,7 @@ mod tests {
|
||||
std::os::unix::fs::symlink(&lib, d.join("z.vst3")).unwrap();
|
||||
let e = load_bundle(&d.join("z.vst3")).unwrap_err();
|
||||
assert!(e.to_string().contains("not a VST3 plugin"), "{e}");
|
||||
assert!(e.to_string().contains("GetPluginFactory"), "the system's reason names the missing export: {e}");
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user