From 8b8aeda09ca49214d2d6a8dcc3946532a9bd7b96 Mon Sep 17 00:00:00 2001 From: Geoffrey Mureithi <95377562+geofmureithi@users.noreply.github.com> Date: Sun, 3 Dec 2023 14:02:43 +0300 Subject: [PATCH] Fix/context export (#37) * fix: improves context api * fix: update context api * docs: improve example * fix: generalize plugin type * improve: types and examples * clippy: minor clippy fixes * docs: ensure running * fix: macro types --- Cargo.lock | 104 +++++++++-- README.md | 8 +- crates/plugy-core/src/lib.rs | 2 +- crates/plugy-macros/Cargo.toml | 3 +- crates/plugy-macros/src/lib.rs | 91 +++++----- crates/plugy-runtime/src/lib.rs | 308 +++++++++++++++++++------------- examples/foo-plugin/src/lib.rs | 7 +- examples/runner/Cargo.toml | 2 + examples/runner/src/lib.rs | 47 ++++- examples/runner/src/main.rs | 33 +++- 10 files changed, 405 insertions(+), 200 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 353849b..2f7e805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" dependencies = [ - "event-listener", + "event-listener 4.0.0", "event-listener-strategy", "pin-project-lite", ] @@ -68,7 +68,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -146,6 +146,15 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "catty" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf0adb3cc1c06945672f8dcc827e42497ac6d0aff49f459ec918132b82a5cbc" +dependencies = [ + "spin", +] + [[package]] name = "cc" version = "1.0.82" @@ -171,6 +180,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -397,7 +415,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.28", ] [[package]] @@ -408,7 +426,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -506,6 +524,12 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "event-listener" version = "4.0.0" @@ -523,7 +547,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener", + "event-listener 4.0.0", "pin-project-lite", ] @@ -1099,7 +1123,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -1201,10 +1225,11 @@ dependencies = [ name = "plugy-macros" version = "0.2.1" dependencies = [ + "convert_case", "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -1391,11 +1416,13 @@ name = "runner" version = "0.2.1" dependencies = [ "anyhow", + "async-trait", "bincode", "plugy", "reqwest", "serde", "tokio", + "xtra", ] [[package]] @@ -1504,7 +1531,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -1581,6 +1608,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "sptr" version = "0.3.2" @@ -1599,6 +1632,17 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.28" @@ -1646,7 +1690,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -1692,7 +1736,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -1787,6 +1831,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.10" @@ -1864,7 +1914,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -1898,7 +1948,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2012,7 +2062,7 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.28", "wasmtime-component-util", "wasmtime-wit-bindgen", "wit-parser", @@ -2200,7 +2250,7 @@ checksum = "f50f51f8d79bfd2aa8e9d9a0ae7c2d02b45fe412e62ff1b87c0c81b07c738231" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -2366,6 +2416,32 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "xtra" +version = "0.6.0" +source = "git+https://github.com/Restioson/xtra#537cfad5115865ff0e510188df8413095eabe0e4" +dependencies = [ + "async-trait", + "catty", + "event-listener 2.5.3", + "futures-core", + "futures-util", + "pin-project-lite", + "spin", + "tokio", + "xtra-macros", +] + +[[package]] +name = "xtra-macros" +version = "0.1.0" +source = "git+https://github.com/Restioson/xtra#537cfad5115865ff0e510188df8413095eabe0e4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/README.md b/README.md index 433ae5f..fa1a191 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ To use plugy in your Rust project, follow these steps: 1. Write your plugin trait: -```rust +```rust,ignore #[plugy::plugin] trait Greeter { fn greet(&self) -> String; @@ -29,7 +29,7 @@ trait Greeter { 2. Write your first plugin implementation -```rust +```rust,ignore #[derive(Debug, Deserialize)] struct FooPlugin; @@ -43,13 +43,13 @@ impl Greeter for FooPlugin { Compile it! -``` +```bash,ignore cargo build --target wasm32-unknown-unknown ``` 3. Import and run -```rust +```rust,ignore #[plugin_import(file = "target/wasm32-unknown-unknown/debug/foo_plugin.wasm")] struct FooPlugin; diff --git a/crates/plugy-core/src/lib.rs b/crates/plugy-core/src/lib.rs index 43d1ff3..0d639a2 100644 --- a/crates/plugy-core/src/lib.rs +++ b/crates/plugy-core/src/lib.rs @@ -32,7 +32,7 @@ pub trait PluginLoader { /// /// Returns a `Pin, anyhow::Error>>>>` /// representing the asynchronous loading process. - fn load(&self) -> Pin, anyhow::Error>>>>; + fn bytes(&self) -> Pin, anyhow::Error>>>>; /// A plugins name should be known before loading. /// It might just be `std::any::type_name::()` diff --git a/crates/plugy-macros/Cargo.toml b/crates/plugy-macros/Cargo.toml index a00fb6b..fec477c 100644 --- a/crates/plugy-macros/Cargo.toml +++ b/crates/plugy-macros/Cargo.toml @@ -16,4 +16,5 @@ proc-macro = true quote = "1.0" syn = { features = ["full", "parsing"], version = "2.0" } proc-macro2 = "1" -darling = "0.20.3" \ No newline at end of file +darling = "0.20.3" +convert_case = "0.6.0" \ No newline at end of file diff --git a/crates/plugy-macros/src/lib.rs b/crates/plugy-macros/src/lib.rs index 6bfb0dd..9b3397a 100644 --- a/crates/plugy-macros/src/lib.rs +++ b/crates/plugy-macros/src/lib.rs @@ -4,6 +4,7 @@ //! bindings and interfaces for plugy's dynamic plugin system. These macros enhance the ergonomics of //! working with plugins written in WebAssembly (Wasm) within your Rust applications. //! +use convert_case::{Case, Casing}; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; use quote::{quote, ToTokens}; @@ -61,20 +62,10 @@ fn generate_async_trait(trait_item: &ItemTrait) -> proc_macro2::TokenStream { FnArg::Receiver(_) => input.to_token_stream(), FnArg::Typed(typed) => match *typed.ty.clone() { syn::Type::Path(path) => { - if path - .path - .segments - .iter() - .find(|seg| { - seg.ident.to_string() == "Self" - || seg - .arguments - .to_token_stream() - .to_string() - .contains("Self") - }) - .is_some() - { + if path.path.segments.iter().any(|seg| { + seg.ident == "Self" + || seg.arguments.to_token_stream().to_string().contains("Self") + }) { let arg_name = &typed.pat; quote! { #arg_name: &Vec @@ -126,17 +117,18 @@ fn generate_async_trait(trait_item: &ItemTrait) -> proc_macro2::TokenStream { #[cfg(not(target_arch = "wasm32"))] #[derive(Debug, Clone)] pub struct #callable_trait_ident { - pub handle: plugy::runtime::PluginHandle, + pub handle: plugy::runtime::PluginHandle>, + inner: std::marker::PhantomData

} #[cfg(not(target_arch = "wasm32"))] - impl #callable_trait_ident { + impl #callable_trait_ident { #(#async_methods)* } #[cfg(not(target_arch = "wasm32"))] impl plugy::runtime::IntoCallable for Box> { type Output = #callable_trait_ident; - fn into_callable(handle: plugy::runtime::PluginHandle) -> Self::Output { - #callable_trait_ident { handle } + fn into_callable(handle: plugy::runtime::PluginHandle>) -> Self::Output { + #callable_trait_ident { handle, inner: std::marker::PhantomData } } } } @@ -241,7 +233,7 @@ pub fn plugin_import(args: TokenStream, input: TokenStream) -> TokenStream { #input impl PluginLoader for #struct_name { - fn load(&self) -> std::pin::Pin, anyhow::Error>>>> { + fn bytes(&self) -> std::pin::Pin, anyhow::Error>>>> { std::boxed::Box::pin(async { let res = std::fs::read(#file_path)?; Ok(res) @@ -255,14 +247,23 @@ pub fn plugin_import(args: TokenStream, input: TokenStream) -> TokenStream { } #[proc_macro_attribute] -pub fn context(_: TokenStream, input: TokenStream) -> TokenStream { +pub fn context(args: TokenStream, input: TokenStream) -> TokenStream { // Parse the input as an ItemImpl let input = parse_macro_input!(input as ItemImpl); + let data_ident = &args + .into_iter() + .nth(2) + .map(|d| Ident::new(&d.to_string(), d.span().into())) + .unwrap_or(Ident::new("_", Span::call_site())); + // Get the name of the struct being implemented let struct_name = &input.self_ty.to_token_stream(); - let struct_name_sync = Ident::new(&format!("{struct_name}Sync"), Span::call_site()); + let mod_name = Ident::new( + &struct_name.to_string().to_case(Case::Snake), + Span::call_site(), + ); let mut externs = Vec::new(); @@ -274,12 +275,13 @@ pub fn context(_: TokenStream, input: TokenStream) -> TokenStream { .iter() .filter_map(|item| { if let syn::ImplItem::Fn(method) = item { + let generics = &method.sig.generics; let method_name = &method.sig.ident; let method_args: Vec<_> = method .sig .inputs .iter() - .skip(2) // Skip &self, &caller + .skip(1) // Skip &caller .map(|arg| { if let FnArg::Typed(pat_type) = arg { pat_type.to_token_stream() @@ -292,7 +294,7 @@ pub fn context(_: TokenStream, input: TokenStream) -> TokenStream { .sig .inputs .iter() - .skip(2) // Skip &self, &caller + .skip(1) // Skip &caller .map(|arg| { if let FnArg::Typed(pat_type) = arg { pat_type.pat.to_token_stream() @@ -320,7 +322,7 @@ pub fn context(_: TokenStream, input: TokenStream) -> TokenStream { .func_wrap1_async( "env", #extern_method_name_str, - move |mut caller: plugy::runtime::Caller<#struct_name>, + move |mut caller: plugy::runtime::Caller<_>, ptr: u64| -> Box + Send> { use plugy::core::bitwise::{from_bitwise, into_bitwise}; @@ -330,7 +332,7 @@ pub fn context(_: TokenStream, input: TokenStream) -> TokenStream { memory, alloc_fn, dealloc_fn, - data: ctx, + plugin } = store; let (ptr, len) = from_bitwise(ptr); @@ -340,9 +342,9 @@ pub fn context(_: TokenStream, input: TokenStream) -> TokenStream { .call_async(&mut caller, into_bitwise(ptr, len)) .await .unwrap(); - let message: (String,) = bincode::deserialize(&buffer).unwrap(); + let (#(#method_pats),*) = bincode::deserialize(&buffer).unwrap(); let buffer = - bincode::serialize(&ctx.fetch(&caller, message.0).await) + bincode::serialize(&#struct_name::#method_name(&mut caller, #(#method_pats),*).await) .unwrap(); let ptr = alloc_fn .call_async(&mut caller, buffer.len() as _) @@ -358,7 +360,7 @@ pub fn context(_: TokenStream, input: TokenStream) -> TokenStream { Some(quote! { #[allow(unused_variables)] - pub fn #method_name(&self, #(#method_args),*) #return_type { + pub fn #method_name #generics (#(#method_args),*) #return_type { #[cfg(target_arch = "wasm32")] { let args = (#(#method_pats),*); @@ -374,32 +376,33 @@ pub fn context(_: TokenStream, input: TokenStream) -> TokenStream { } }) .collect::>(); - // Generate the code for the context methods let generated = quote::quote! { #[cfg(not(target_arch = "wasm32"))] #input - - impl #struct_name { - pub fn current() -> #struct_name_sync { - #struct_name_sync - } - } #[cfg(not(target_arch = "wasm32"))] - impl plugy::runtime::Context for #struct_name { - fn link(&self, linker: &mut plugy::runtime::Linker) { + impl plugy::runtime::Context<#data_ident> for #struct_name { + fn link(&self, linker: &mut plugy::runtime::Linker>) { #(#links)* } } - pub struct #struct_name_sync; - - impl #struct_name_sync { - #(#generated_methods)* + impl #struct_name { + pub fn get(&self) -> #mod_name::sync::#struct_name { + #mod_name::sync::#struct_name + } } - - #[cfg(target_arch = "wasm32")] - #(#externs)* + pub mod #mod_name { + pub mod sync { + #[cfg(target_arch = "wasm32")] + #(#externs)* + + pub struct #struct_name; + impl #struct_name { + #(#generated_methods)* + } + } + } }; // Return the generated code as a TokenStream diff --git a/crates/plugy-runtime/src/lib.rs b/crates/plugy-runtime/src/lib.rs index 4609def..79ee99c 100644 --- a/crates/plugy-runtime/src/lib.rs +++ b/crates/plugy-runtime/src/lib.rs @@ -3,8 +3,10 @@ //! The `plugy-runtime` crate serves as the heart of Plugy's dynamic plugin system, enabling the runtime management //! and execution of plugins written in WebAssembly (Wasm). It provides functionalities for loading, running, //! and interacting with plugins seamlessly within your Rust applications. + use anyhow::Context as ErrorContext; use async_lock::RwLock; +use bincode::Error; use dashmap::DashMap; use plugy_core::bitwise::{from_bitwise, into_bitwise}; use plugy_core::PluginLoader; @@ -13,11 +15,11 @@ use std::fmt; use std::{marker::PhantomData, sync::Arc}; use wasmtime::{Engine, Instance, Module, Store}; -pub type CallerStore = Arc>>>>; +pub type CallerStore = Arc>>>>; -pub type Caller<'a, D> = wasmtime::Caller<'a, Option>>; +pub type Caller<'a, D = Plugin> = wasmtime::Caller<'a, Option>>; -pub type Linker = wasmtime::Linker>>; +pub type Linker = wasmtime::Linker>>; /// A runtime environment for managing plugins and instances. /// @@ -33,50 +35,82 @@ pub type Linker = wasmtime::Linker>>; /// /// ```rust /// use plugy::runtime::Runtime; +/// use plugy_runtime::Plugin; /// -/// trait Plugin { +/// trait Greeter { /// fn greet(&self); /// } -/// let runtime = Runtime::>::new(); +/// let runtime = Runtime::>::new(); /// // Load and manage plugins... /// ``` -pub struct Runtime { +pub struct Runtime { engine: Engine, - linker: Linker, - plugin_interface: PhantomData

, - modules: DashMap<&'static str, RuntimeModule>, + linker: Linker

, + modules: DashMap<&'static str, RuntimeModule

>, + structure: PhantomData, +} + +pub trait IntoCallable { + type Output; + fn into_callable(handle: PluginHandle>) -> Self::Output; +} + +/// A concrete type that represents a wasm plugin and its state +#[derive(Debug, Clone)] +pub struct Plugin> { + pub name: String, + pub plugin_type: String, + pub data: D, +} + +impl Plugin { + pub fn name(&self) -> &str { + self.name.as_ref() + } + + pub fn plugin_type(&self) -> &str { + self.plugin_type.as_ref() + } + + pub fn data(&self) -> Result { + bincode::deserialize(&self.data) + } + + pub fn update(&mut self, value: &T) { + self.data = bincode::serialize(value).unwrap() + } } /// Single runnable module #[allow(dead_code)] -pub struct RuntimeModule { +pub struct RuntimeModule

{ inner: Module, - store: CallerStore, + store: CallerStore

, instance: Instance, } /// The caller of a function #[allow(dead_code)] #[derive(Clone)] -pub struct RuntimeCaller { +pub struct RuntimeCaller

{ pub memory: wasmtime::Memory, pub alloc_fn: wasmtime::TypedFunc, pub dealloc_fn: wasmtime::TypedFunc, - pub data: D, + pub plugin: P, } -impl fmt::Debug for RuntimeCaller { +impl fmt::Debug for RuntimeCaller

{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RuntimeCaller") .field("memory", &self.memory) .field("alloc_fn", &"TypedFunc") .field("dealloc_fn", &"TypedFunc") - .field("data", &self.data) + .field("plugin", &self.plugin) .finish() } } -impl Runtime { +impl Runtime> { /// Loads a plugin using the provided loader and returns the plugin instance. /// /// This asynchronous function loads a plugin by calling the `load` method on @@ -98,33 +132,48 @@ impl Runtime { /// /// ```rust /// use plugy_runtime::Runtime; + /// use plugy::runtime::Plugin; /// use plugy_core::PluginLoader; /// use plugy_macros::*; /// use std::future::Future; /// use std::pin::Pin; /// #[plugy_macros::plugin] - /// trait Plugin { - /// fn do_stuff(&self); + /// trait Greeter { + /// fn do_stuff(&self, input: &str); /// } /// /// // impl Plugin for MyPlugin goes to the wasm file /// #[plugin_import(file = "target/wasm32-unknown-unknown/debug/my_plugin.wasm")] /// struct MyPlugin; /// - /// async fn example(runtime: &Runtime>) -> anyhow::Result<()> { - /// let plugin = runtime.load(MyPlugin).await?; - /// Ok(()) + /// impl From for Plugin { + /// fn from(val: MyPlugin) -> Self { + /// Plugin { + /// name: "MyPlugin".to_string(), + /// data: Default::default(), + /// plugin_type: "MyPlugin".to_string(), + /// } + /// } + /// } + /// + /// + /// async fn example(runtime: &Runtime>) { + /// let plugin = runtime.load(MyPlugin).await.unwrap(); + /// // ... /// } /// ``` - pub async fn load(&self, loader: T) -> anyhow::Result + pub async fn load_with>>( + &self, + plugin: P, + ) -> anyhow::Result where - P: IntoCallable, + T: IntoCallable, { - let bytes = loader.load().await?; - let name = loader.name(); + let bytes = plugin.bytes().await?; + let name = plugin.name(); let module = Module::new(&self.engine, bytes)?; let instance_pre = self.linker.instantiate_pre(&module)?; - let mut store: Store>> = Store::new(&self.engine, None); + let mut store: Store>>> = Store::new(&self.engine, None); let instance = instance_pre.instantiate_async(&mut store).await?; let memory = instance .get_memory(&mut store, "memory") @@ -135,7 +184,7 @@ impl Runtime { memory, alloc_fn, dealloc_fn, - data: D::default(), + plugin: plugin.into(), }); self.modules.insert( name, @@ -145,40 +194,14 @@ impl Runtime { instance, }, ); - let plugin = self.get_plugin_by_name(&name)?; + let plugin = self.get_plugin_by_name::

(name)?; Ok(plugin) } -} -impl Runtime { - /// Creates a new instance of the `Runtime` with default configuration. - /// - /// This function initializes a `Runtime` instance using the default configuration - /// settings for the underlying `wasmtime::Config`. It sets up the engine and linker, - /// preparing it to load and manage plugin modules. - /// - /// # Returns - /// - /// Returns a `Result` containing the initialized `Runtime` instance on success, - /// or an `anyhow::Error` if the creation process encounters any issues. - pub fn new() -> anyhow::Result { - let mut config = wasmtime::Config::new(); - config.async_support(true); - let engine = Engine::new(&config)?; - let linker = Linker::new(&engine); - let modules = DashMap::new(); - Ok(Self { - engine, - linker, - modules, - plugin_interface: PhantomData, - }) - } - - /// Retrieves the callable plugin instance for the specified type. + /// Retrieves the callable plugin instance with the specified name. /// - /// This function returns a callable instance of the loaded plugin for the - /// specified type `T`. The plugin must have been previously loaded using + /// This function returns a callable instance of the loaded plugin with the + /// specified name. The plugin must have been previously loaded using /// the `load` method or similar means. /// /// # Returns @@ -186,26 +209,27 @@ impl Runtime { /// Returns a `Result` containing the callable plugin instance on success, /// or an `anyhow::Error` if the instance retrieval encounters any issues. /// - pub fn get_plugin(&self) -> anyhow::Result + pub fn get_plugin_by_name( + &self, + name: &str, + ) -> anyhow::Result where - P: IntoCallable, + T: IntoCallable, { - let name = std::any::type_name::(); let module = self .modules .get(name) .context("missing plugin requested, did you forget .load")?; - Ok(P::into_callable(PluginHandle { + Ok(T::into_callable(PluginHandle { store: module.store.clone(), instance: module.instance, - inner: PhantomData::, })) } - /// Retrieves the callable plugin instance with the specified name. + /// Retrieves the callable plugin instance for the specified type. /// - /// This function returns a callable instance of the loaded plugin with the - /// specified name. The plugin must have been previously loaded using + /// This function returns a callable instance of the loaded plugin for the + /// specified type `T`. The plugin must have been previously loaded using /// the `load` method or similar means. /// /// # Returns @@ -213,22 +237,24 @@ impl Runtime { /// Returns a `Result` containing the callable plugin instance on success, /// or an `anyhow::Error` if the instance retrieval encounters any issues. /// - pub fn get_plugin_by_name(&self, name: &str) -> anyhow::Result + pub fn get_plugin(&self) -> anyhow::Result where - P: IntoCallable, + T: IntoCallable, { + let name = std::any::type_name::

(); let module = self .modules .get(name) .context("missing plugin requested, did you forget .load")?; - Ok(P::into_callable(PluginHandle { + Ok(T::into_callable(PluginHandle { store: module.store.clone(), instance: module.instance, - inner: PhantomData::

, })) } +} - /// Loads a plugin using the provided loader, but can customize the data stored and returns the plugin instance. +impl Runtime { + /// Loads a plugin using the provided loader and returns the plugin instance. /// /// This asynchronous function loads a plugin by calling the `load` method on /// the provided `PluginLoader` instance. It then prepares the plugin for execution, @@ -240,8 +266,6 @@ impl Runtime { /// - `loader`: An instance of a type that implements the `PluginLoader` trait, /// responsible for loading the plugin's Wasm module data. /// - /// - `data_fn`: A function that takes in the module and returns data to be used in that modules functions - /// /// # Returns /// /// Returns a `Result` containing the loaded plugin instance on success, @@ -250,65 +274,57 @@ impl Runtime { /// # Examples /// /// ```rust + /// use plugy_runtime::Plugin as KasukuPlugin; /// use plugy_runtime::Runtime; /// use plugy_core::PluginLoader; - /// use plugy_runtime::Context; - /// use plugy_runtime::Linker; /// use plugy_macros::*; /// use std::future::Future; /// use std::pin::Pin; /// #[plugy_macros::plugin] /// trait Plugin { - /// fn do_stuff(&self); + /// fn do_stuff(&self, input: &str); /// } /// /// // impl Plugin for MyPlugin goes to the wasm file /// #[plugin_import(file = "target/wasm32-unknown-unknown/debug/my_plugin.wasm")] /// struct MyPlugin; - /// - /// struct Addr { - /// //eg actix or xtra - /// } - /// - /// impl Context for Addr { - /// fn link(&self, linker: &mut Linker) { - /// //expose methods here + /// impl From for KasukuPlugin { + /// fn from(val: MyPlugin) -> Self { + /// KasukuPlugin { + /// name: "MyPlugin".to_string(), + /// data: Default::default(), + /// plugin_type: "MyPlugin".to_string(), + /// } /// } /// } - /// - /// async fn example(runtime: &mut Runtime, Addr>) -> anyhow::Result<()> { - /// let plugin = runtime.load_with(MyPlugin, |_plugin| Addr {}).await?; + /// async fn example(runtime: &Runtime>) -> anyhow::Result<()> { + /// let plugin = runtime.load(MyPlugin).await?; /// Ok(()) /// } /// ``` - pub async fn load_with( - &mut self, - loader: T, - data_fn: impl Fn(&T) -> D, - ) -> anyhow::Result + pub async fn load>( + &self, + plugin: P, + ) -> anyhow::Result where - P: IntoCallable, - D: Context, + T: IntoCallable>, { - let data = data_fn(&loader); - data.link(&mut self.linker); - let bytes = loader.load().await?; - let name = loader.name(); + let bytes = plugin.bytes().await?; + let name = plugin.name(); let module = Module::new(&self.engine, bytes)?; let instance_pre = self.linker.instantiate_pre(&module)?; - let mut store: Store>> = Store::new(&self.engine, None); + let mut store: Store>> = Store::new(&self.engine, None); let instance = instance_pre.instantiate_async(&mut store).await?; let memory = instance .get_memory(&mut store, "memory") .context("missing memory")?; let alloc_fn = instance.get_typed_func(&mut store, "alloc")?; let dealloc_fn = instance.get_typed_func(&mut store, "dealloc")?; - *store.data_mut() = Some(RuntimeCaller { memory, alloc_fn, dealloc_fn, - data, + plugin: plugin.into(), }); self.modules.insert( name, @@ -318,12 +334,66 @@ impl Runtime { instance, }, ); - - let plugin = self.get_plugin_by_name(&name)?; + let plugin = self.get_plugin_by_name::

(name)?; Ok(plugin) } } +impl Runtime { + /// Creates a new instance of the `Runtime` with default configuration. + /// + /// This function initializes a `Runtime` instance using the default configuration + /// settings for the underlying `wasmtime::Config`. It sets up the engine and linker, + /// preparing it to load and manage plugin modules. + /// + /// # Returns + /// + /// Returns a `Result` containing the initialized `Runtime` instance on success, + /// or an `anyhow::Error` if the creation process encounters any issues. + pub fn new() -> anyhow::Result { + let mut config = wasmtime::Config::new(); + config.async_support(true); + let engine = Engine::new(&config)?; + let linker = Linker::new(&engine); + let modules = DashMap::new(); + Ok(Self { + engine, + linker, + modules, + structure: PhantomData, + }) + } +} + +impl Runtime> { + /// Allows exposing methods that will run on the runtime side + /// ```rust + /// use plugy_runtime::Runtime; + /// + /// trait Greeter { + /// fn greet(&self, text: &str); + /// } + /// #[derive(Debug)] + /// pub struct Logger; + /// # pub type Data = Vec; + /// + /// #[plugy::macros::context(data = Data)] + /// impl Logger { + /// pub async fn log(_: &mut plugy::runtime::Caller<'_>, text: &str) { + /// dbg!(text); + /// } + /// } + /// let mut runtime = Runtime::>::new().unwrap(); + /// let runtime = runtime + /// .context(Logger); + /// ```` + + pub fn context>(&mut self, ctx: C) -> &mut Self { + ctx.link(&mut self.linker); + self + } +} + /// A handle to a loaded plugin instance. /// /// This struct represents a handle to a loaded plugin instance. It holds a reference @@ -335,13 +405,12 @@ impl Runtime { /// - `P`: The plugin type that corresponds to this handle. /// #[derive(Debug, Clone)] -pub struct PluginHandle { +pub struct PluginHandle

{ instance: Instance, - store: CallerStore, - inner: PhantomData

, + store: CallerStore

, } -impl PluginHandle { +impl PluginHandle> { /// Retrieves a typed function interface from the loaded plugin instance. /// /// This method enables retrieving a typed function interface for a specific @@ -366,7 +435,7 @@ impl PluginHandle { pub async fn get_func( &self, name: &str, - ) -> anyhow::Result> { + ) -> anyhow::Result, I, R>> { let store = self.store.clone(); let inner_wasm_fn = self.instance.get_typed_func::( &mut *store.write().await, @@ -381,19 +450,14 @@ impl PluginHandle { } } -pub trait IntoCallable { - type Output; - fn into_callable(handle: PluginHandle) -> Self::Output; -} - -pub struct Func { +pub struct Func { inner_wasm_fn: wasmtime::TypedFunc, - store: CallerStore, - input: PhantomData

, + store: CallerStore

, + input: PhantomData, output: PhantomData, } -impl Func { +impl Func { /// Invokes the plugin function with the provided input, returning the result. /// /// This asynchronous method calls the plugin function using the provided input data @@ -407,7 +471,7 @@ impl Func { /// # Returns /// /// Returns the result of the plugin function call. - pub async fn call_unchecked(&self, value: &P) -> R { + pub async fn call_unchecked(&self, value: &I) -> R { self.call_checked(value).await.unwrap() } /// Invokes the plugin function with the provided input, returning a checked result. @@ -425,15 +489,15 @@ impl Func { /// Returns a `Result` containing the result of the plugin function call on success, /// or an `anyhow::Error` if the function call or deserialization encounters issues. - pub async fn call_checked(&self, value: &P) -> anyhow::Result { + pub async fn call_checked(&self, value: &I) -> anyhow::Result { let mut store = self.store.write().await; + let data = store.data_mut().clone().unwrap(); let RuntimeCaller { memory, alloc_fn, .. - } = store.data().clone().context("missing data in store")?; + } = data; let buffer = bincode::serialize(value)?; let len = buffer.len() as _; - let ptr = alloc_fn.call_async(&mut *store, len).await?; memory.write(&mut *store, ptr as _, &buffer)?; let ptr = self @@ -447,6 +511,6 @@ impl Func { } } -pub trait Context: Sized { - fn link(&self, linker: &mut Linker); +pub trait Context>: Sized { + fn link(&self, linker: &mut Linker>); } diff --git a/examples/foo-plugin/src/lib.rs b/examples/foo-plugin/src/lib.rs index 934da1a..5c8bd42 100644 --- a/examples/foo-plugin/src/lib.rs +++ b/examples/foo-plugin/src/lib.rs @@ -1,6 +1,6 @@ use plugy::macros::plugin_impl; use serde::Deserialize; -use shared::{Greeter, Fetcher}; +use shared::{Greeter, logger::sync::Logger, fetcher::sync::Fetcher}; #[derive(Debug, Deserialize)] struct FooPlugin; @@ -8,9 +8,10 @@ struct FooPlugin; #[plugin_impl] impl Greeter for FooPlugin { fn greet(&self, name: String, last_name: Option) -> String { - let ctx = Fetcher::current(); - let _res = ctx.fetch("http://example.com".to_owned()); + let res = Fetcher::fetch("http://example.com".to_owned()); + Logger::log(&res); let last_name = last_name.unwrap_or_default(); + format!("Hello From Foo Plugin to {name} {last_name}") } } diff --git a/examples/runner/Cargo.toml b/examples/runner/Cargo.toml index 3bfabfe..e1095d9 100644 --- a/examples/runner/Cargo.toml +++ b/examples/runner/Cargo.toml @@ -19,3 +19,5 @@ tokio = { version = "1", features = ["full"] } plugy = { path = "../../", default-features = false, features = ["runtime"] } bincode = "1" reqwest = "0.11.18" +xtra = { git = "https://github.com/Restioson/xtra", features = ["tokio", "macros"] } +async-trait = "0.1" diff --git a/examples/runner/src/lib.rs b/examples/runner/src/lib.rs index 5d4b5aa..912c06a 100644 --- a/examples/runner/src/lib.rs +++ b/examples/runner/src/lib.rs @@ -1,14 +1,51 @@ +use async_trait::async_trait; +use plugy::runtime::Plugin; +use serde::{Deserialize, Serialize}; +use xtra::{Address, Handler}; + #[plugy::macros::plugin] pub trait Greeter { fn greet(&self, name: String, last_name: Option) -> String; } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct Fetcher; -#[plugy::macros::context] +#[plugy::macros::context(data = Addr)] impl Fetcher { - pub async fn fetch(&self, _: &plugy::runtime::Caller<'_, Self>, url: String) -> String { - let body = reqwest::get(url).await.unwrap().text().await.unwrap(); - body + pub async fn fetch(_: &mut plugy::runtime::Caller<'_, Plugin>, url: String) -> String { + reqwest::get(url).await.unwrap().text().await.unwrap() + } +} + +pub type Addr = Address; + +#[derive(Default, xtra::Actor)] +pub struct Printer { + pub times: usize, +} + +struct Print(String); + +#[async_trait] +impl Handler for Printer { + type Return = (); + + async fn handle(&mut self, print: Print, _ctx: &mut xtra::Context) { + self.times += 1; + println!("Printing {}. Printed {} times so far.", print.0, self.times); + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Logger; + +#[plugy::macros::context(data = Addr)] +impl Logger { + pub async fn log(caller: &mut plugy::runtime::Caller<'_, Plugin>, text: &str) { + let plugin = &caller.data_mut().as_mut().unwrap().plugin; + let addr = &plugin.data; + addr.send(Print(text.to_string())) + .await + .expect("Printer should not be dropped"); } } diff --git a/examples/runner/src/main.rs b/examples/runner/src/main.rs index fab9918..bcb68c4 100644 --- a/examples/runner/src/main.rs +++ b/examples/runner/src/main.rs @@ -1,22 +1,43 @@ use plugy::{ + core::PluginLoader, macros::plugin_import, - runtime::{PluginLoader, Runtime}, + runtime::{Plugin, Runtime}, }; -use shared::{Greeter, Fetcher}; +use shared::{Addr, Fetcher, Greeter, Logger, Printer}; +use xtra::Mailbox; #[plugin_import(file = "target/wasm32-unknown-unknown/debug/foo_plugin.wasm")] -struct FooPlugin; +#[derive(Debug)] +struct FooPlugin { + addr: Addr, +} +impl From for Plugin { + fn from(val: FooPlugin) -> Self { + Plugin { + name: "FooPlugin".to_string(), + data: val.addr, + plugin_type: "FooPlugin".to_string(), + } + } +} #[tokio::main] async fn main() { - let mut runtime = Runtime::, Fetcher>::new().unwrap(); + let mut runtime = Runtime::, Plugin>::new().unwrap(); + let runtime = runtime + // Include the fetcher context + .context(Fetcher) + // Include the logger context + .context(Logger); let handle = runtime - .load_with(FooPlugin, |_| Fetcher) + .load_with(FooPlugin { + addr: xtra::spawn_tokio(Printer::default(), Mailbox::unbounded()), + }) .await .unwrap(); let res = handle .greet("Geoff".to_owned(), Some("Mureithi".to_owned())) .await; - + println!("{res}"); assert_eq!(res, "Hello From Foo Plugin to Geoff Mureithi") }