Skip to main content

mimium_lang/plugin/
system_plugin.rs

1//! Defines the plugin interface used by mimium's runtime.
2//!
3//! A system plugin can hook into the VM execution by providing callback
4//! functions.  Each plugin exposes its callbacks through [`SysPluginSignature`]
5//! values that are registered as external closures.
6
7use super::ExtClsInfo;
8use crate::{
9    compiler::EvalStage,
10    interner::{ToSymbol, TypeNodeId},
11    interpreter::Value,
12    plugin::MacroInfo,
13    runtime::{
14        Time,
15        vm::{Machine, ReturnCode},
16    },
17};
18use std::{any::Any, cell::RefCell, rc::Rc};
19pub type SystemPluginFnType<T> = fn(&mut T, &mut Machine) -> ReturnCode;
20pub type SystemPluginMacroType<T> = fn(&mut T, &[(Value, TypeNodeId)]) -> Value;
21
22/// Metadata for a callback provided by a [`SystemPlugin`].
23///
24/// Each signature stores the callback name, erased function pointer and the
25/// type of the closure expected by the VM.
26pub struct SysPluginSignature {
27    name: &'static str,
28    /// The function internally implements `Fn(&mut T:SystemPlugin,&mut Machine)->ReturnCode`
29    /// but the type is erased for dynamic dispatching. later the function is downcasted into their own type.
30    fun: Rc<dyn Any>,
31    ty: TypeNodeId,
32    /// The stage at which the function is available.
33    /// This is used to determine whether the function can be called in a macro or
34    /// in the VM. Note that any persistent functions are not allowed to be used in `SystemPlugin`.
35    stage: EvalStage,
36}
37impl SysPluginSignature {
38    pub fn new<F, T>(name: &'static str, fun: F, ty: TypeNodeId) -> Self
39    where
40        F: Fn(&mut T, &mut Machine) -> ReturnCode + 'static,
41        T: SystemPlugin,
42    {
43        Self {
44            name,
45            fun: Rc::new(fun),
46            ty,
47            stage: EvalStage::Stage(1),
48        }
49    }
50    pub fn new_macro<F, T>(name: &'static str, fun: F, ty: TypeNodeId) -> Self
51    where
52        F: Fn(&mut T, &[(Value, TypeNodeId)]) -> Value + 'static,
53        T: SystemPlugin,
54    {
55        Self {
56            name,
57            fun: Rc::new(fun),
58            ty,
59            stage: EvalStage::Stage(0),
60        }
61    }
62
63    /// Get the public name of this signature.
64    pub fn get_name(&self) -> &'static str {
65        self.name
66    }
67
68    /// Get the type of this signature.
69    pub fn get_type(&self) -> TypeNodeId {
70        self.ty
71    }
72
73    /// Get the stage at which this function/macro is available.
74    pub fn get_stage(&self) -> EvalStage {
75        self.stage
76    }
77}
78
79/// Trait implemented by runtime plugins.
80///
81/// The default implementations of the callback methods do nothing. Plugins can
82/// override these to perform setup in [`on_init`], teardown in [`after_main`],
83/// or per-sample processing in [`on_sample`].
84pub trait SystemPlugin {
85    /// Downcast helper for safe access through `RefCell<dyn SystemPlugin>`.
86    fn as_any_mut(&mut self) -> &mut dyn Any;
87
88    fn generate_audioworker(&mut self) -> Option<Box<dyn SystemPluginAudioWorker>> {
89        None
90    }
91    fn on_init(&mut self, _machine: &mut Machine) -> ReturnCode {
92        0
93    }
94    fn after_main(&mut self, _machine: &mut Machine) -> ReturnCode {
95        0
96    }
97
98    /// WASM-side lifecycle hook called before `main()`.
99    ///
100    /// This is the WASM analogue of [`on_init`].  Receives a mutable
101    /// reference to the [`WasmEngine`] so the plugin can interact with
102    /// the WASM module before execution.
103    #[cfg(not(target_arch = "wasm32"))]
104    fn on_init_wasm(
105        &mut self,
106        _engine: &mut crate::runtime::wasm::engine::WasmEngine,
107    ) -> ReturnCode {
108        0
109    }
110
111    /// WASM-side lifecycle hook called after `main()` completes.
112    ///
113    /// This is the WASM analogue of [`after_main`].  Receives a mutable
114    /// reference to the [`WasmEngine`] so the plugin can inspect the
115    /// module state or connect to external devices.
116    #[cfg(not(target_arch = "wasm32"))]
117    fn after_main_wasm(
118        &mut self,
119        _engine: &mut crate::runtime::wasm::engine::WasmEngine,
120    ) -> ReturnCode {
121        0
122    }
123
124    fn gen_interfaces(&self) -> Vec<SysPluginSignature>;
125    fn try_get_main_loop(&mut self) -> Option<Box<dyn FnOnce()>> {
126        None
127    }
128    /// Produce a lock-free audio handle after setup completes.
129    ///
130    /// Called after macro expansion is finished and before the runtime is
131    /// moved to the audio thread.  The returned `Box<dyn Any + Send>` is
132    /// passed to the audio backend and can be downcast to the concrete
133    /// handle type inside trampoline closures.
134    fn freeze_audio_handle(&mut self) -> Option<Box<dyn Any + Send>> {
135        None
136    }
137
138    /// Produce a per-function closure map for the WASM backend.
139    ///
140    /// Called after macro expansion and before WASM module instantiation.
141    /// Each entry maps a plugin function name (e.g. `"__get_slider"`) to a
142    /// closure that implements the function. The closures must be `Send +
143    /// Sync` since they are captured by wasmtime host trampolines.
144    ///
145    /// Plugins that do not need custom WASM handlers can leave this as
146    /// `None` — the runtime will provide a default pass-through / zero
147    /// trampoline.
148    #[cfg(not(target_arch = "wasm32"))]
149    fn freeze_for_wasm(&mut self) -> Option<crate::runtime::wasm::WasmPluginFnMap> {
150        None
151    }
152
153    /// Produce a per-sample audio worker for the WASM backend.
154    ///
155    /// This is the WASM analogue of [`generate_audioworker`].  The returned
156    /// worker is stored by [`WasmDspRuntime`](crate::runtime::wasm::engine::WasmDspRuntime)
157    /// and its [`on_sample`](crate::runtime::wasm::WasmSystemPluginAudioWorker::on_sample)
158    /// method is called once per sample, before `dsp()`.
159    #[cfg(not(target_arch = "wasm32"))]
160    fn generate_wasm_audioworker(
161        &mut self,
162    ) -> Option<Box<dyn crate::runtime::wasm::WasmSystemPluginAudioWorker>> {
163        None
164    }
165}
166
167pub trait SystemPluginAudioWorker {
168    fn on_sample(&mut self, _time: Time, _machine: &mut Machine) -> ReturnCode {
169        0
170    }
171    fn gen_interfaces(&self) -> Vec<SysPluginSignature>;
172}
173
174/// A dynamically dispatched plugin wrapped in reference-counted storage.
175pub struct DynSystemPlugin {
176    inner: Rc<RefCell<dyn SystemPlugin>>,
177    audioworker: Option<Box<dyn SystemPluginAudioWorker>>,
178    pub clsinfos: Vec<ExtClsInfo>,
179    pub macroinfos: Vec<MacroInfo>,
180}
181
182impl DynSystemPlugin {
183    pub fn take_audioworker(&mut self) -> Option<Box<dyn SystemPluginAudioWorker>> {
184        self.audioworker.take()
185    }
186    /// Delegate to the inner plugin's `freeze_audio_handle()`.
187    ///
188    /// Must only be called from the main thread before the audio thread starts.
189    pub fn freeze_audio_handle(&mut self) -> Option<Box<dyn Any + Send>> {
190        self.inner.borrow_mut().freeze_audio_handle()
191    }
192
193    /// Delegate to the inner plugin's `freeze_for_wasm()`.
194    #[cfg(not(target_arch = "wasm32"))]
195    pub fn freeze_for_wasm(&mut self) -> Option<crate::runtime::wasm::WasmPluginFnMap> {
196        self.inner.borrow_mut().freeze_for_wasm()
197    }
198
199    /// Delegate to the inner plugin's `generate_wasm_audioworker()`.
200    #[cfg(not(target_arch = "wasm32"))]
201    pub fn generate_wasm_audioworker(
202        &mut self,
203    ) -> Option<Box<dyn crate::runtime::wasm::WasmSystemPluginAudioWorker>> {
204        self.inner.borrow_mut().generate_wasm_audioworker()
205    }
206
207    /// Delegate to the inner plugin's `on_init_wasm()`.
208    #[cfg(not(target_arch = "wasm32"))]
209    pub fn on_init_wasm(
210        &self,
211        engine: &mut crate::runtime::wasm::engine::WasmEngine,
212    ) -> crate::runtime::vm::ReturnCode {
213        self.inner.borrow_mut().on_init_wasm(engine)
214    }
215
216    /// Delegate to the inner plugin's `after_main_wasm()`.
217    #[cfg(not(target_arch = "wasm32"))]
218    pub fn after_main_wasm(
219        &self,
220        engine: &mut crate::runtime::wasm::engine::WasmEngine,
221    ) -> crate::runtime::vm::ReturnCode {
222        self.inner.borrow_mut().after_main_wasm(engine)
223    }
224
225    /// Get a mutable reference to the inner plugin.
226    ///
227    /// Panics at runtime if the plugin is already borrowed (e.g. by a
228    /// closure captured from `gen_interfaces()`).
229    pub fn borrow_inner_mut(&self) -> std::cell::RefMut<'_, dyn SystemPlugin> {
230        self.inner.borrow_mut()
231    }
232}
233/// Convert a plugin into the VM-facing representation.
234///
235/// The returned [`DynSystemPlugin`] is stored by the runtime, while the
236/// accompanying `Vec<ExtClsInfo>` contains closures that expose the plugin's
237/// callback methods to mimium code.
238impl<T> From<T> for DynSystemPlugin
239where
240    T: SystemPlugin + Sized + 'static,
241{
242    fn from(mut plugin: T) -> Self {
243        let mut audioworker = plugin.generate_audioworker();
244
245        let ifs = plugin.gen_interfaces();
246        let inner: Rc<RefCell<dyn SystemPlugin>> = Rc::new(RefCell::new(plugin));
247        let macroinfos = ifs
248            .iter()
249            .filter(|&SysPluginSignature { stage, .. }| matches!(stage, EvalStage::Stage(0)))
250            .map(|SysPluginSignature { name, fun, ty, .. }| {
251                let inner = inner.clone();
252                let fun = fun
253                    .clone()
254                    .downcast::<SystemPluginMacroType<T>>()
255                    .expect("invalid conversion applied in the system plugin resolution.");
256                MacroInfo::new(
257                    name.to_symbol(),
258                    *ty,
259                    Rc::new(RefCell::new(move |args: &[(Value, TypeNodeId)]| -> Value {
260                        // SAFETY: downcast is valid because T was the concrete type
261                        // used when constructing this DynSystemPlugin.
262                        let mut plugin_ref = inner.borrow_mut();
263                        let p: &mut T = plugin_ref
264                            .as_any_mut()
265                            .downcast_mut::<T>()
266                            .expect("plugin type mismatch");
267                        fun(p, args)
268                    })),
269                )
270            })
271            .collect();
272        let clsinfos = ifs
273            .into_iter()
274            .chain(
275                audioworker
276                    .as_mut()
277                    .map(|worker| worker.gen_interfaces())
278                    .into_iter()
279                    .flatten(),
280            )
281            .filter(|SysPluginSignature { stage, .. }| matches!(stage, EvalStage::Stage(1)))
282            .map(|SysPluginSignature { name, fun, ty, .. }| {
283                let inner = inner.clone();
284                let fun = fun
285                    .clone()
286                    .downcast::<SystemPluginFnType<T>>()
287                    .expect("invalid conversion applied in the system plugin resolution.");
288                let fun = Rc::new(RefCell::new(move |machine: &mut Machine| -> ReturnCode {
289                    let mut plugin_ref = inner.borrow_mut();
290                    let p: &mut T = plugin_ref
291                        .as_any_mut()
292                        .downcast_mut::<T>()
293                        .expect("plugin type mismatch");
294                    fun(p, machine)
295                }));
296                ExtClsInfo::new(name.to_symbol(), ty, fun)
297            })
298            .collect();
299        DynSystemPlugin {
300            inner,
301            audioworker,
302            clsinfos,
303            macroinfos,
304        }
305    }
306}