Skip to main content

sim_lib_plugin_wasm/
router.rs

1//! Format-dispatching plugin loader.
2
3use std::path::Path;
4#[cfg(any(feature = "clap-host", all(feature = "lv2-host", target_os = "linux")))]
5use std::sync::Arc;
6
7use sim_kernel::{Error, Result};
8use sim_lib_plugin_core::{CapabilitySet, PluginInstance};
9
10use crate::WasmResourceLimits;
11
12#[cfg(feature = "clap-host")]
13type ClapRoute = dyn Fn(&Path, &CapabilitySet) -> Result<RoutedPlugin> + Send + Sync + 'static;
14#[cfg(all(feature = "lv2-host", target_os = "linux"))]
15type Lv2Route = dyn Fn(&Path, &CapabilitySet) -> Result<RoutedPlugin> + Send + Sync + 'static;
16
17/// Format-agnostic plugin instance returned by [`PluginRouter`].
18pub type RoutedPlugin = Box<dyn PluginInstance>;
19
20/// Dispatches plugin load requests to the wasm, CLAP, or LV2 loader.
21#[derive(Clone)]
22pub struct PluginRouter {
23    limits: WasmResourceLimits,
24    #[cfg(feature = "clap-host")]
25    clap_route: Option<Arc<ClapRoute>>,
26    #[cfg(all(feature = "lv2-host", target_os = "linux"))]
27    lv2_route: Option<Arc<Lv2Route>>,
28}
29
30impl PluginRouter {
31    /// Builds a router with default wasm loading and no native provider
32    /// overrides.
33    pub fn new(limits: WasmResourceLimits) -> Self {
34        PluginRouterBuilder::new(limits).build()
35    }
36
37    /// Starts configuring a plugin router.
38    pub fn builder(limits: WasmResourceLimits) -> PluginRouterBuilder {
39        PluginRouterBuilder::new(limits)
40    }
41
42    /// Loads a plugin by inspecting `path`'s extension.
43    ///
44    /// # Errors
45    ///
46    /// Returns an eval error when the extension is unrecognised, when the
47    /// selected feature is disabled, when no native provider is configured for
48    /// a native route, or when the selected loader rejects the plugin.
49    pub fn load(&self, path: &Path, caps: &CapabilitySet) -> Result<RoutedPlugin> {
50        match path
51            .extension()
52            .and_then(|extension| extension.to_str())
53            .map(str::to_ascii_lowercase)
54            .as_deref()
55        {
56            Some("wasm") => self.load_wasm(path, caps),
57            Some("clap") => self.load_clap(path, caps),
58            Some("lv2") => self.load_lv2(path, caps),
59            other => Err(Error::Eval(format!(
60                "unrecognised plugin extension {other:?}; expected .wasm, .clap, or .lv2"
61            ))),
62        }
63    }
64
65    #[cfg(feature = "wasm-plugin")]
66    fn load_wasm(&self, path: &Path, caps: &CapabilitySet) -> Result<RoutedPlugin> {
67        let bytes = std::fs::read(path)
68            .map_err(|err| Error::Eval(format!("cannot read {}: {err}", path.display())))?;
69        let plugin = crate::load_wasm_plugin(caps, &bytes, self.limits)?;
70        Ok(Box::new(plugin))
71    }
72
73    #[cfg(not(feature = "wasm-plugin"))]
74    fn load_wasm(&self, path: &Path, caps: &CapabilitySet) -> Result<RoutedPlugin> {
75        let _ = (path, caps, self.limits);
76        Err(Error::Eval("wasm-plugin feature not enabled".to_owned()))
77    }
78
79    #[cfg(feature = "clap-host")]
80    fn load_clap(&self, path: &Path, caps: &CapabilitySet) -> Result<RoutedPlugin> {
81        let Some(route) = &self.clap_route else {
82            return Err(Error::Eval("clap-host provider not configured".to_owned()));
83        };
84        route(path, caps)
85    }
86
87    #[cfg(not(feature = "clap-host"))]
88    fn load_clap(&self, path: &Path, caps: &CapabilitySet) -> Result<RoutedPlugin> {
89        let _ = (path, caps);
90        Err(Error::Eval("clap-host feature not enabled".to_owned()))
91    }
92
93    #[cfg(all(feature = "lv2-host", target_os = "linux"))]
94    fn load_lv2(&self, path: &Path, caps: &CapabilitySet) -> Result<RoutedPlugin> {
95        let Some(route) = &self.lv2_route else {
96            return Err(Error::Eval("lv2-host provider not configured".to_owned()));
97        };
98        route(path, caps)
99    }
100
101    #[cfg(not(all(feature = "lv2-host", target_os = "linux")))]
102    fn load_lv2(&self, path: &Path, caps: &CapabilitySet) -> Result<RoutedPlugin> {
103        let _ = (path, caps);
104        Err(Error::Eval(
105            "lv2-host feature not enabled or not Linux".to_owned(),
106        ))
107    }
108}
109
110/// Configures native provider overrides for [`PluginRouter`].
111pub struct PluginRouterBuilder {
112    limits: WasmResourceLimits,
113    #[cfg(feature = "clap-host")]
114    clap_route: Option<Arc<ClapRoute>>,
115    #[cfg(all(feature = "lv2-host", target_os = "linux"))]
116    lv2_route: Option<Arc<Lv2Route>>,
117}
118
119impl PluginRouterBuilder {
120    /// Builds a router configuration with default wasm resource limits.
121    pub fn new(limits: WasmResourceLimits) -> Self {
122        Self {
123            limits,
124            #[cfg(feature = "clap-host")]
125            clap_route: None,
126            #[cfg(all(feature = "lv2-host", target_os = "linux"))]
127            lv2_route: None,
128        }
129    }
130
131    /// Installs a CLAP provider override used for `.clap` paths.
132    #[cfg(feature = "clap-host")]
133    pub fn with_clap_provider<P>(mut self, provider: P) -> Self
134    where
135        P: sim_lib_plugin_clap::native::ClapHostProvider + Send + Sync + 'static,
136        P::Plugin: 'static,
137    {
138        self.clap_route = Some(Arc::new(move |path, caps| {
139            let spec = sim_lib_plugin_core::PluginLoadSpec::new(
140                sim_lib_plugin_core::PluginFormat::Clap,
141                path.to_string_lossy().into_owned(),
142            )?;
143            let plugin = crate::native_fallback::load_clap_plugin(caps, &spec, &provider)?;
144            Ok(Box::new(plugin))
145        }));
146        self
147    }
148
149    /// Installs an LV2 provider override used for `.lv2` bundle paths.
150    #[cfg(all(feature = "lv2-host", target_os = "linux"))]
151    pub fn with_lv2_provider<P>(mut self, provider: P) -> Self
152    where
153        P: sim_lib_plugin_lv2::native::Lv2HostProvider + Send + Sync + 'static,
154        P::Plugin: 'static,
155    {
156        self.lv2_route = Some(Arc::new(move |path, caps| {
157            let plugin = crate::native_fallback::load_lv2_plugin(
158                caps,
159                path.to_string_lossy().as_ref(),
160                &provider,
161            )?;
162            Ok(Box::new(plugin))
163        }));
164        self
165    }
166
167    /// Finishes router construction.
168    pub fn build(self) -> PluginRouter {
169        PluginRouter {
170            limits: self.limits,
171            #[cfg(feature = "clap-host")]
172            clap_route: self.clap_route,
173            #[cfg(all(feature = "lv2-host", target_os = "linux"))]
174            lv2_route: self.lv2_route,
175        }
176    }
177}