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}