plugy_runtime/
lib.rs

1//! # plugy-runtime
2//!
3//! The `plugy-runtime` crate serves as the heart of Plugy's dynamic plugin system, enabling the runtime management
4//! and execution of plugins written in WebAssembly (Wasm). It provides functionalities for loading, running,
5//! and interacting with plugins seamlessly within your Rust applications.
6
7use anyhow::Context as ErrorContext;
8use async_lock::RwLock;
9use bincode::Error;
10use dashmap::DashMap;
11use plugy_core::bitwise::{from_bitwise, into_bitwise};
12use plugy_core::PluginLoader;
13use serde::{de::DeserializeOwned, Serialize};
14use std::fmt;
15use std::{marker::PhantomData, sync::Arc};
16use wasmtime::{Engine, Instance, Module, Store};
17
18pub type CallerStore<D = Plugin> = Arc<RwLock<Store<Option<RuntimeCaller<D>>>>>;
19
20pub type Caller<'a, D = Plugin> = wasmtime::Caller<'a, Option<RuntimeCaller<D>>>;
21
22pub type Linker<D = Plugin> = wasmtime::Linker<Option<RuntimeCaller<D>>>;
23
24/// A runtime environment for managing plugins and instances.
25///
26/// The `Runtime` struct provides a runtime environment for managing plugins
27/// and their instances. It allows you to load, manage, and interact with plugins
28/// written in WebAssembly (Wasm). The runtime maintains a collection of loaded modules,
29/// instances, and associated data for efficient plugin management.
30///
31/// The generic parameter `P` represents the trait that your plugins must implement.
32/// This trait defines the methods that can be called on the plugins using their instances.
33///
34/// # Example
35///
36/// ```rust
37/// use plugy::runtime::Runtime;
38/// use plugy_runtime::Plugin;
39///
40/// trait Greeter {
41///     fn greet(&self);
42/// }
43/// let runtime = Runtime::<Box<dyn Greeter>>::new();
44/// // Load and manage plugins...
45/// ```
46pub struct Runtime<T, P = Plugin> {
47    engine: Engine,
48    linker: Linker<P>,
49    modules: DashMap<&'static str, RuntimeModule<P>>,
50    structure: PhantomData<T>,
51}
52
53pub trait IntoCallable<P, D> {
54    type Output;
55    fn into_callable(handle: PluginHandle<Plugin<D>>) -> Self::Output;
56}
57
58/// A concrete type that represents a wasm plugin and its state
59#[derive(Debug, Clone)]
60pub struct Plugin<D = Vec<u8>> {
61    pub name: String,
62    pub plugin_type: String,
63    pub data: D,
64}
65
66impl Plugin {
67    pub fn name(&self) -> &str {
68        self.name.as_ref()
69    }
70
71    pub fn plugin_type(&self) -> &str {
72        self.plugin_type.as_ref()
73    }
74
75    pub fn data<T: DeserializeOwned>(&self) -> Result<T, Error> {
76        bincode::deserialize(&self.data)
77    }
78
79    pub fn update<T: Serialize>(&mut self, value: &T) {
80        self.data = bincode::serialize(value).unwrap()
81    }
82}
83
84/// Single runnable module
85#[allow(dead_code)]
86pub struct RuntimeModule<P> {
87    inner: Module,
88    store: CallerStore<P>,
89    instance: Instance,
90}
91
92/// The caller of a function
93#[allow(dead_code)]
94#[derive(Clone)]
95pub struct RuntimeCaller<P> {
96    pub memory: wasmtime::Memory,
97    pub alloc_fn: wasmtime::TypedFunc<u32, u32>,
98    pub dealloc_fn: wasmtime::TypedFunc<u64, ()>,
99    pub plugin: P,
100}
101
102impl<P: std::fmt::Debug> fmt::Debug for RuntimeCaller<P> {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.debug_struct("RuntimeCaller")
105            .field("memory", &self.memory)
106            .field("alloc_fn", &"TypedFunc<u32, u32>")
107            .field("dealloc_fn", &"TypedFunc<u64, ()>")
108            .field("plugin", &self.plugin)
109            .finish()
110    }
111}
112
113impl<T, D: Send> Runtime<T, Plugin<D>> {
114    /// Loads a plugin using the provided loader and returns the plugin instance.
115    ///
116    /// This asynchronous function loads a plugin by calling the `load` method on
117    /// the provided `PluginLoader` instance. It then prepares the plugin for execution,
118    /// instantiates it, and returns the plugin instance wrapped in the appropriate
119    /// callable type.
120    ///
121    /// # Parameters
122    ///
123    /// - `loader`: An instance of a type that implements the `PluginLoader` trait,
124    ///   responsible for loading the plugin's Wasm module data.
125    ///
126    /// # Returns
127    ///
128    /// Returns a `Result` containing the loaded plugin instance on success,
129    /// or an `anyhow::Error` if the loading and instantiation process encounters any issues.
130    ///
131    /// # Examples
132    ///
133    /// ```rust
134    /// use plugy_runtime::Runtime;
135    /// use plugy::runtime::Plugin;
136    /// use plugy_core::PluginLoader;
137    /// use plugy_macros::*;
138    /// use std::future::Future;
139    /// use std::pin::Pin;
140    /// #[plugy_macros::plugin]
141    /// trait Greeter {
142    ///     fn do_stuff(&self, input: &str);
143    /// }
144    ///
145    /// // impl Plugin for MyPlugin goes to the wasm file
146    /// #[plugin_import(file = "target/wasm32-unknown-unknown/debug/my_plugin.wasm")]
147    /// struct MyPlugin;
148    ///
149    /// impl From<MyPlugin> for Plugin {
150    ///     fn from(val: MyPlugin) -> Self {
151    ///         Plugin {
152    ///             name: "MyPlugin".to_string(),
153    ///             data: Default::default(),
154    ///             plugin_type: "MyPlugin".to_string(),
155    ///         }
156    ///     }
157    /// }
158    ///
159    ///
160    /// async fn example(runtime: &Runtime<Box<dyn Greeter>>) {
161    ///     let plugin = runtime.load(MyPlugin).await.unwrap();
162    ///     // ...
163    /// }
164    /// ```
165    pub async fn load_with<P: Send + PluginLoader + Into<Plugin<D>>>(
166        &self,
167        plugin: P,
168    ) -> anyhow::Result<T::Output>
169    where
170        T: IntoCallable<P, D>,
171    {
172        let bytes = plugin.bytes().await?;
173        let name = plugin.name();
174        let module = Module::new(&self.engine, bytes)?;
175        let instance_pre = self.linker.instantiate_pre(&module)?;
176        let mut store: Store<Option<RuntimeCaller<Plugin<D>>>> = Store::new(&self.engine, None);
177        let instance = instance_pre.instantiate_async(&mut store).await?;
178        let memory = instance
179            .get_memory(&mut store, "memory")
180            .context("missing memory")?;
181        let alloc_fn = instance.get_typed_func(&mut store, "alloc")?;
182        let dealloc_fn = instance.get_typed_func(&mut store, "dealloc")?;
183        *store.data_mut() = Some(RuntimeCaller {
184            memory,
185            alloc_fn,
186            dealloc_fn,
187            plugin: plugin.into(),
188        });
189        self.modules.insert(
190            name,
191            RuntimeModule {
192                inner: module.clone(),
193                store: Arc::new(RwLock::new(store)),
194                instance,
195            },
196        );
197        let plugin = self.get_plugin_by_name::<P>(name)?;
198        Ok(plugin)
199    }
200
201    /// Retrieves the callable plugin instance with the specified name.
202    ///
203    /// This function returns a callable instance of the loaded plugin with the
204    /// specified name. The plugin must have been previously loaded using
205    /// the `load` method or similar means.
206    ///
207    /// # Returns
208    ///
209    /// Returns a `Result` containing the callable plugin instance on success,
210    /// or an `anyhow::Error` if the instance retrieval encounters any issues.
211    ///
212    pub fn get_plugin_by_name<P: Send + PluginLoader>(
213        &self,
214        name: &str,
215    ) -> anyhow::Result<T::Output>
216    where
217        T: IntoCallable<P, D>,
218    {
219        let module = self
220            .modules
221            .get(name)
222            .context("missing plugin requested, did you forget .load")?;
223        Ok(T::into_callable(PluginHandle {
224            store: module.store.clone(),
225            instance: module.instance,
226        }))
227    }
228
229    /// Retrieves the callable plugin instance for the specified type.
230    ///
231    /// This function returns a callable instance of the loaded plugin for the
232    /// specified type `T`. The plugin must have been previously loaded using
233    /// the `load` method or similar means.
234    ///
235    /// # Returns
236    ///
237    /// Returns a `Result` containing the callable plugin instance on success,
238    /// or an `anyhow::Error` if the instance retrieval encounters any issues.
239    ///
240    pub fn get_plugin<P: Send + PluginLoader>(&self) -> anyhow::Result<T::Output>
241    where
242        T: IntoCallable<P, D>,
243    {
244        let name = std::any::type_name::<P>();
245        let module = self
246            .modules
247            .get(name)
248            .context("missing plugin requested, did you forget .load")?;
249        Ok(T::into_callable(PluginHandle {
250            store: module.store.clone(),
251            instance: module.instance,
252        }))
253    }
254}
255
256impl<T> Runtime<T> {
257    /// Loads a plugin using the provided loader and returns the plugin instance.
258    ///
259    /// This asynchronous function loads a plugin by calling the `load` method on
260    /// the provided `PluginLoader` instance. It then prepares the plugin for execution,
261    /// instantiates it, and returns the plugin instance wrapped in the appropriate
262    /// callable type.
263    ///
264    /// # Parameters
265    ///
266    /// - `loader`: An instance of a type that implements the `PluginLoader` trait,
267    ///   responsible for loading the plugin's Wasm module data.
268    ///
269    /// # Returns
270    ///
271    /// Returns a `Result` containing the loaded plugin instance on success,
272    /// or an `anyhow::Error` if the loading and instantiation process encounters any issues.
273    ///
274    /// # Examples
275    ///
276    /// ```rust
277    /// use plugy_runtime::Plugin as KasukuPlugin;
278    /// use plugy_runtime::Runtime;
279    /// use plugy_core::PluginLoader;
280    /// use plugy_macros::*;
281    /// use std::future::Future;
282    /// use std::pin::Pin;
283    /// #[plugy_macros::plugin]
284    /// trait Plugin {
285    ///     fn do_stuff(&self, input: &str);
286    /// }
287    ///
288    /// // impl Plugin for MyPlugin goes to the wasm file
289    /// #[plugin_import(file = "target/wasm32-unknown-unknown/debug/my_plugin.wasm")]
290    /// struct MyPlugin;
291    /// impl From<MyPlugin> for KasukuPlugin {
292    ///     fn from(val: MyPlugin) -> Self {
293    ///         KasukuPlugin {
294    ///             name: "MyPlugin".to_string(),
295    ///             data: Default::default(),
296    ///             plugin_type: "MyPlugin".to_string(),
297    ///         }
298    ///     }
299    /// }
300    /// async fn example(runtime: &Runtime<Box<dyn Plugin>>) -> anyhow::Result<()> {
301    ///     let plugin = runtime.load(MyPlugin).await?;
302    ///     Ok(())
303    /// }
304    /// ```
305    pub async fn load<P: Send + PluginLoader + Into<Plugin>>(
306        &self,
307        plugin: P,
308    ) -> anyhow::Result<T::Output>
309    where
310        T: IntoCallable<P, Vec<u8>>,
311    {
312        let bytes = plugin.bytes().await?;
313        let name = plugin.name();
314        let module = Module::new(&self.engine, bytes)?;
315        let instance_pre = self.linker.instantiate_pre(&module)?;
316        let mut store: Store<Option<RuntimeCaller<Plugin>>> = Store::new(&self.engine, None);
317        let instance = instance_pre.instantiate_async(&mut store).await?;
318        let memory = instance
319            .get_memory(&mut store, "memory")
320            .context("missing memory")?;
321        let alloc_fn = instance.get_typed_func(&mut store, "alloc")?;
322        let dealloc_fn = instance.get_typed_func(&mut store, "dealloc")?;
323        *store.data_mut() = Some(RuntimeCaller {
324            memory,
325            alloc_fn,
326            dealloc_fn,
327            plugin: plugin.into(),
328        });
329        self.modules.insert(
330            name,
331            RuntimeModule {
332                inner: module.clone(),
333                store: Arc::new(RwLock::new(store)),
334                instance,
335            },
336        );
337        let plugin = self.get_plugin_by_name::<P>(name)?;
338        Ok(plugin)
339    }
340}
341
342impl<T, P> Runtime<T, P> {
343    /// Creates a new instance of the `Runtime` with default configuration.
344    ///
345    /// This function initializes a `Runtime` instance using the default configuration
346    /// settings for the underlying `wasmtime::Config`. It sets up the engine and linker,
347    /// preparing it to load and manage plugin modules.
348    ///
349    /// # Returns
350    ///
351    /// Returns a `Result` containing the initialized `Runtime` instance on success,
352    /// or an `anyhow::Error` if the creation process encounters any issues.
353    pub fn new() -> anyhow::Result<Self> {
354        let mut config = wasmtime::Config::new();
355        config.async_support(true);
356        let engine = Engine::new(&config)?;
357        let linker = Linker::new(&engine);
358        let modules = DashMap::new();
359        Ok(Self {
360            engine,
361            linker,
362            modules,
363            structure: PhantomData,
364        })
365    }
366}
367
368impl<T, D> Runtime<T, Plugin<D>> {
369    /// Allows exposing methods that will run on the runtime side
370    /// ```rust
371    /// use plugy_runtime::Runtime;
372    ///
373    /// trait Greeter {
374    ///     fn greet(&self, text: &str);
375    /// }
376    /// #[derive(Debug)]
377    /// pub struct Logger;
378    /// # pub type Data = Vec<u8>;
379    ///
380    /// #[plugy::macros::context(data = Data)]
381    /// impl Logger {
382    ///     pub async fn log(_: &mut plugy::runtime::Caller<'_>, text: &str) {
383    ///         dbg!(text);
384    ///     }
385    /// }
386    /// let mut runtime = Runtime::<Box<dyn Greeter>>::new().unwrap();
387    /// let runtime = runtime
388    ///     .context(Logger);
389    /// ````
390
391    pub fn context<C: Context<D>>(mut self, ctx: C) -> Self {
392        ctx.link(&mut self.linker);
393        self
394    }
395}
396
397/// A handle to a loaded plugin instance.
398///
399/// This struct represents a handle to a loaded plugin instance. It holds a reference
400/// to the underlying instance, along with a reference to the associated store and
401/// any additional data (`PhantomData<P>`) specific to the plugin type `P`.
402///
403/// # Type Parameters
404///
405/// - `P`: The plugin type that corresponds to this handle.
406///
407#[derive(Debug, Clone)]
408pub struct PluginHandle<P = Plugin> {
409    instance: Instance,
410    store: CallerStore<P>,
411}
412
413impl<D> PluginHandle<Plugin<D>> {
414    /// Retrieves a typed function interface from the loaded plugin instance.
415    ///
416    /// This method enables retrieving a typed function interface for a specific
417    /// function name defined in the loaded plugin instance. The typed function
418    /// interface provides a convenient way to invoke plugin functions and
419    /// deserialize their input and output data.
420    ///
421    /// # Parameters
422    ///
423    /// - `name`: The name of the function in the plugin instance.
424    ///
425    /// # Type Parameters
426    ///
427    /// - `I`: The input data type expected by the function.
428    /// - `R`: The output data type returned by the function.
429    ///
430    /// # Returns
431    ///
432    /// Returns a `Result` containing the typed function interface on success,
433    /// or an `anyhow::Error` if the function retrieval encounters any issues.
434
435    pub async fn get_func<I: Serialize, R: DeserializeOwned>(
436        &self,
437        name: &str,
438    ) -> anyhow::Result<Func<Plugin<D>, I, R>> {
439        let store = self.store.clone();
440        let inner_wasm_fn = self.instance.get_typed_func::<u64, u64>(
441            &mut *store.write().await,
442            &format!("_plugy_guest_{name}"),
443        )?;
444        Ok(Func {
445            inner_wasm_fn,
446            store,
447            input: std::marker::PhantomData::<I>,
448            output: std::marker::PhantomData::<R>,
449        })
450    }
451}
452
453pub struct Func<P, I: Serialize, R: DeserializeOwned> {
454    inner_wasm_fn: wasmtime::TypedFunc<u64, u64>,
455    store: CallerStore<P>,
456    input: PhantomData<I>,
457    output: PhantomData<R>,
458}
459
460impl<P: Send + Clone, R: DeserializeOwned, I: Serialize> Func<P, I, R> {
461    /// Invokes the plugin function with the provided input, returning the result.
462    ///
463    /// This asynchronous method calls the plugin function using the provided input data
464    /// without performing any error handling or result checking. If the function call
465    /// fails, it will panic.
466    ///
467    /// # Parameters
468    ///
469    /// - `value`: The input data to be passed to the plugin function.
470    ///
471    /// # Returns
472    ///
473    /// Returns the result of the plugin function call.
474    pub async fn call_unchecked(&self, value: &I) -> R {
475        self.call_checked(value).await.unwrap()
476    }
477    /// Invokes the plugin function with the provided input, returning a checked result.
478    ///
479    /// This asynchronous method calls the plugin function using the provided input data
480    /// and performs error handling to return a `Result` containing the result or any
481    /// encountered errors.
482    ///
483    /// # Parameters
484    ///
485    /// - `value`: The input data to be passed to the plugin function.
486    ///
487    /// # Returns
488    ///
489    /// Returns a `Result` containing the result of the plugin function call on success,
490    /// or an `anyhow::Error` if the function call or deserialization encounters issues.
491
492    pub async fn call_checked(&self, value: &I) -> anyhow::Result<R> {
493        let mut store = self.store.write().await;
494        let data = store.data_mut().clone().unwrap();
495        let RuntimeCaller {
496            memory, alloc_fn, ..
497        } = data;
498
499        let buffer = bincode::serialize(value)?;
500        let len = buffer.len() as _;
501        let ptr = alloc_fn.call_async(&mut *store, len).await?;
502        memory.write(&mut *store, ptr as _, &buffer)?;
503        let ptr = self
504            .inner_wasm_fn
505            .call_async(&mut *store, into_bitwise(ptr, len))
506            .await?;
507        let (ptr, len) = from_bitwise(ptr);
508        let mut buffer = vec![0u8; len as _];
509        memory.read(&mut *store, ptr as _, &mut buffer)?;
510        Ok(bincode::deserialize(&buffer)?)
511    }
512}
513
514pub trait Context<D = Vec<u8>>: Sized {
515    fn link(&self, linker: &mut Linker<Plugin<D>>);
516}