diff --git a/crates/clap-host/src/ffi/bundle.rs b/crates/clap-host/src/ffi/bundle.rs index 5f203e49..b537abb1 100644 --- a/crates/clap-host/src/ffi/bundle.rs +++ b/crates/clap-host/src/ffi/bundle.rs @@ -43,6 +43,16 @@ fn loaded() -> &'static Mutex>> { 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, 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())); diff --git a/crates/clap-host/src/lib.rs b/crates/clap-host/src/lib.rs index f6b93d13..5e99eb04 100644 --- a/crates/clap-host/src/lib.rs +++ b/crates/clap-host/src/lib.rs @@ -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); + } } diff --git a/crates/vst3-host/src/ffi/module.rs b/crates/vst3-host/src/ffi/module.rs index 8ad94235..988a24fd 100644 --- a/crates/vst3-host/src/ffi/module.rs +++ b/crates/vst3-host/src/ffi/module.rs @@ -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::(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 = 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::(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() }; diff --git a/crates/vst3-host/src/lib.rs b/crates/vst3-host/src/lib.rs index a0324f88..a320dedf 100644 --- a/crates/vst3-host/src/lib.rs +++ b/crates/vst3-host/src/lib.rs @@ -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); } }