From e4f0599a2a08ccdb81c821a5b33a8b7c51d3c7d4 Mon Sep 17 00:00:00 2001 From: JonRC Date: Tue, 3 Nov 2026 01:19:13 -0300 Subject: [PATCH 1/2] Plugin hosts: keep the system's reason when a plugin binary fails to load libloading 0.9 prints only "dlopen failed" ("dlsym failed", "LoadLibraryExW failed") and keeps the dlerror text or the Windows error as the error's source. The CLAP and VST3 loaders formatted the error alone, so a plugin built for a newer glibc was reported as "dlopen failed" with no reason. Append the source. --- crates/clap-host/src/ffi/bundle.rs | 14 ++++++++++++-- crates/clap-host/src/lib.rs | 13 +++++++++++++ crates/vst3-host/src/ffi/module.rs | 16 +++++++++++++--- crates/vst3-host/src/lib.rs | 16 ++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/crates/clap-host/src/ffi/bundle.rs b/crates/clap-host/src/ffi/bundle.rs index 503b6391..d16d6abf 100644 --- a/crates/clap-host/src/ffi/bundle.rs +++ b/crates/clap-host/src/ffi/bundle.rs @@ -42,6 +42,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> { @@ -59,12 +69,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: {}", 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 61e415b4..fefe681c 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: {}", 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); } } From 636454e6393138cb451b6905a38562ac8df16b2c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 3 Nov 2026 09:56:35 -0500 Subject: [PATCH 2/2] Name the missing entry point in the not-a-plugin errors on every platform Co-Authored-By: Claude Opus 5.5 --- crates/clap-host/src/ffi/bundle.rs | 2 +- crates/vst3-host/src/ffi/module.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/clap-host/src/ffi/bundle.rs b/crates/clap-host/src/ffi/bundle.rs index d16d6abf..6f5d2d28 100644 --- a/crates/clap-host/src/ffi/bundle.rs +++ b/crates/clap-host/src/ffi/bundle.rs @@ -74,7 +74,7 @@ impl Bundle { // `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: {}", with_reason(&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/vst3-host/src/ffi/module.rs b/crates/vst3-host/src/ffi/module.rs index fefe681c..5c2cafe0 100644 --- a/crates/vst3-host/src/ffi/module.rs +++ b/crates/vst3-host/src/ffi/module.rs @@ -345,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: {}", with_reason(&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() };