wasm_link/plugin.rs
1//! Plugin metadata types.
2//!
3//! A plugin is a WASM component that implements one [`Binding`]( crate::Binding )
4//! (its **plug**) and may depend on zero or more other [`Binding`]( crate::Binding )s
5//! (its **sockets**). The plug declares what the plugin exports; sockets declare what
6//! the plugin expects to import from other plugins.
7
8use std::collections::HashMap ;
9use wasmtime::{ Engine, Store };
10use wasmtime::component::{ Component, ResourceTable, Linker, Val };
11
12use crate::BindingAny ;
13use crate::plugin_instance::PluginInstance ;
14use crate::Function ;
15use crate::Remap ;
16
17/// Trait for accessing a [`ResourceTable`] from the store's data type.
18///
19/// Resources that flow between plugins need to be wrapped to track ownership.
20/// This trait provides access to the table where those wrapped resources are stored.
21/// [`ResourceTable`] is part of the wasmtime component model; see the
22/// [wasmtime docs](https://docs.rs/wasmtime/latest/wasmtime/component/) for details.
23///
24/// # Example
25///
26/// ```
27/// use wasmtime::component::ResourceTable ;
28/// use wasm_link::PluginContext ;
29///
30/// struct MyPluginData {
31/// resource_table: ResourceTable,
32/// // ... other fields
33/// }
34///
35/// impl PluginContext for MyPluginData {
36/// fn resource_table( &mut self ) -> &mut ResourceTable {
37/// &mut self.resource_table
38/// }
39/// }
40/// ```
41pub trait PluginContext: Send {
42 /// Returns a mutable reference to a resource table.
43 fn resource_table( &mut self ) -> &mut ResourceTable ;
44}
45
46/// A WASM component bundled with its runtime context, ready for instantiation.
47///
48/// The component's exports (its **plug**) and imports (its **sockets**) are defined through
49/// the [`crate::Binding`], not by this struct.
50///
51/// The `context` is consumed during linking to become the wasmtime [`Store`]( wasmtime::Store )'s data.
52///
53/// # Type Parameters
54/// - `Ctx`: User context type that will be stored in the wasmtime [`Store`]( wasmtime::Store )
55///
56/// # Example
57///
58/// ```
59/// # use wasm_link::{ Plugin, PluginContext, ResourceTable, Component, Engine, Linker };
60/// # struct Ctx { resource_table: ResourceTable }
61/// # impl PluginContext for Ctx {
62/// # fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
63/// # }
64/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
65/// let engine = Engine::default();
66/// let linker = Linker::new( &engine );
67///
68/// let plugin = Plugin::new(
69/// Component::new( &engine, "(component)" )?,
70/// Ctx { resource_table: ResourceTable::new() },
71/// ).instantiate( &engine, &linker )?;
72/// # let _ = plugin;
73/// # Ok(())
74/// # }
75/// ```
76#[must_use = "call .instantiate() or .link() to create a PluginInstance"]
77pub struct Plugin<Ctx: 'static> {
78 /// Compiled WASM component
79 component: Component,
80 /// User context consumed at load time to become `Store<Ctx>`
81 context: Ctx,
82 /// Per-interface export name remaps for this plugin
83 interface_remaps: HashMap<String, Remap>,
84 /// Closure that determines fuel for each function call
85 #[allow( clippy::type_complexity )]
86 fuel_limiter: Option<Box<dyn FnMut( &mut Store<Ctx>, &str, &str, &Function ) -> u64 + Send>>,
87 /// Closure that determines epoch deadline for each function call
88 #[allow( clippy::type_complexity )]
89 epoch_limiter: Option<Box<dyn FnMut( &mut Store<Ctx>, &str, &str, &Function ) -> u64 + Send>>,
90 /// Closure that returns a mutable reference to the `ResourceLimiter` in the context
91 #[allow( clippy::type_complexity )]
92 memory_limiter: Option<Box<dyn (FnMut( &mut Ctx ) -> &mut dyn wasmtime::ResourceLimiter) + Send + Sync>>,
93}
94
95impl<Ctx> Plugin<Ctx>
96where
97 Ctx: PluginContext + 'static,
98{
99
100 /// Creates a new plugin declaration.
101 ///
102 /// Note that the plugin ID is not specified here - it's provided when constructing
103 /// the cardinality wrapper that holds this plugin. This is done to prevent duplicate ids.
104 pub fn new(
105 component: Component,
106 context: Ctx,
107 ) -> Self {
108 Self {
109 component,
110 context,
111 interface_remaps: HashMap::new(),
112 fuel_limiter: None,
113 epoch_limiter: None,
114 memory_limiter: None,
115 }
116 }
117
118 /// Sets a closure that determines the fuel limit for each function call.
119 ///
120 /// The closure receives the store, the interface path (e.g., `"my:package/api"`),
121 /// the function name, and the [`Function`] metadata. It returns the fuel to set.
122 ///
123 /// **Warning:** Fuel consumption must be enabled in the [`Engine`]( wasmtime::Engine )
124 /// via [`Config::consume_fuel`]( wasmtime::Config::consume_fuel ). If not enabled,
125 /// dispatch will fail with a [`RuntimeException`]( crate::DispatchError::RuntimeException )
126 /// at call time.
127 ///
128 /// ```
129 /// # use wasm_link::{ Plugin, PluginContext, ResourceTable, Component, Engine };
130 /// # struct Ctx { resource_table: ResourceTable }
131 /// # impl PluginContext for Ctx {
132 /// # fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
133 /// # }
134 /// # fn example( component: Component ) {
135 /// let plugin = Plugin::new( component, Ctx { resource_table: ResourceTable::new() })
136 /// .with_fuel_limiter(| _store, _interface, _function, _metadata | 100_000 );
137 /// # }
138 /// ```
139 pub fn with_fuel_limiter( mut self, limiter: impl FnMut( &mut Store<Ctx>, &str, &str, &Function ) -> u64 + Send + 'static ) -> Self {
140 self.fuel_limiter = Some( Box::new( limiter ));
141 self
142 }
143
144 /// Sets a closure that determines the epoch deadline for each function call.
145 ///
146 /// The closure receives the store, the interface path (e.g., `"my:package/api"`),
147 /// the function name, and the [`Function`] metadata. It returns the epoch deadline
148 /// in ticks.
149 ///
150 /// **Warning:** Epoch interruption must be enabled in the [`Engine`]( wasmtime::Engine )
151 /// via [`Config::epoch_interruption`]( wasmtime::Config::epoch_interruption ). If not
152 /// enabled, the deadline is silently ignored.
153 ///
154 /// ```
155 /// # use wasm_link::{ Plugin, PluginContext, ResourceTable, Component, Engine };
156 /// # struct Ctx { resource_table: ResourceTable }
157 /// # impl PluginContext for Ctx {
158 /// # fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
159 /// # }
160 /// # fn example( component: Component ) {
161 /// let plugin = Plugin::new( component, Ctx { resource_table: ResourceTable::new() })
162 /// .with_epoch_limiter(| _store, _interface, _function, _metadata | 5 );
163 /// # }
164 /// ```
165 pub fn with_epoch_limiter( mut self, limiter: impl FnMut( &mut Store<Ctx>, &str, &str, &Function ) -> u64 + Send + 'static ) -> Self {
166 self.epoch_limiter = Some( Box::new( limiter ));
167 self
168 }
169
170 /// Sets a closure that returns a mutable reference to a [`ResourceLimiter`]( wasmtime::ResourceLimiter )
171 /// embedded in the plugin context.
172 ///
173 /// The limiter is installed into the wasmtime [`Store`]( wasmtime::Store ) once at instantiation
174 /// and controls memory and table growth for the lifetime of the plugin.
175 ///
176 /// The [`ResourceLimiter`]( wasmtime::ResourceLimiter ) must be stored inside the context type `Ctx`
177 /// so that wasmtime can access it through a `&mut Ctx` reference.
178 ///
179 /// ```
180 /// # use wasm_link::{ Plugin, PluginContext, ResourceTable, Component, Engine };
181 /// # struct Ctx { resource_table: ResourceTable, limiter: MyLimiter }
182 /// # impl PluginContext for Ctx {
183 /// # fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
184 /// # }
185 /// # struct MyLimiter;
186 /// # impl wasmtime::ResourceLimiter for MyLimiter {
187 /// # fn memory_growing( &mut self, _: usize, _: usize, _: Option<usize> ) -> wasmtime::Result<bool> { Ok( true ) }
188 /// # fn table_growing( &mut self, _: usize, _: usize, _: Option<usize> ) -> wasmtime::Result<bool> { Ok( true ) }
189 /// # }
190 /// # fn example( component: Component ) {
191 /// let plugin = Plugin::new( component, Ctx { resource_table: ResourceTable::new(), limiter: MyLimiter })
192 /// .with_memory_limiter(| ctx | &mut ctx.limiter );
193 /// # }
194 /// ```
195 pub fn with_memory_limiter(
196 mut self,
197 limiter: impl (FnMut( &mut Ctx ) -> &mut dyn wasmtime::ResourceLimiter) + Send + Sync + 'static,
198 ) -> Self {
199 self.memory_limiter = Some( Box::new( limiter ));
200 self
201 }
202
203 /// Sets interface export remaps for this plugin.
204 ///
205 /// Use this when a plugin implements the same interface types as its binding
206 /// but exports one or more interfaces or functions under different names.
207 ///
208 /// The outer map is a lookup table from requested interface name to [`Remap`].
209 /// Each [`Remap`] describes where that requested interface, and optionally
210 /// requested items inside it, are found in this plugin's exports.
211 ///
212 /// All remap tables use the same direction:
213 ///
214 /// ```text
215 /// requested name -> exported name
216 /// ```
217 ///
218 /// ```
219 /// # use std::collections::HashMap ;
220 /// # use wasm_link::{ Plugin, PluginContext, ResourceTable, Component, Engine, Remap };
221 /// # struct Ctx { resource_table: ResourceTable }
222 /// # impl PluginContext for Ctx {
223 /// # fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
224 /// # }
225 /// # fn example( engine: &Engine ) -> Result<(), Box<dyn std::error::Error>> {
226 /// let plugin = Plugin::new(
227 /// Component::new( engine, "(component)" )?,
228 /// Ctx { resource_table: ResourceTable::new() },
229 /// ).remap_interfaces( HashMap::from([
230 /// ( "root".to_string(), Remap::found_as( "legacy-root" )),
231 /// ]));
232 /// # let _ = plugin ;
233 /// # Ok(())
234 /// # }
235 /// ```
236 pub fn remap_interfaces( mut self, interface_remaps: HashMap<String, Remap> ) -> Self {
237 self.interface_remaps = interface_remaps ;
238 self
239 }
240
241 /// Links this plugin with its socket bindings and instantiates it.
242 ///
243 /// Takes ownership of the `linker` because socket bindings are added to it. If you need
244 /// to reuse the same linker for multiple plugins, clone it before passing it in.
245 ///
246 /// # Type Parameters
247 /// - `PluginId`: Must implement `Into<Val>` so plugin IDs can be passed to WASM when
248 /// dispatching to multi-plugin sockets (the ID identifies which plugin produced each result).
249 ///
250 /// # Errors
251 /// Returns an error if linking or instantiation fails.
252 pub fn link<PluginId, Sockets>(
253 self,
254 engine: &Engine,
255 mut linker: Linker<Ctx>,
256 sockets: Sockets,
257 ) -> Result<PluginInstance<Ctx>, wasmtime::Error>
258 where
259 PluginId: Eq + std::hash::Hash + Clone + std::fmt::Debug + Send + Sync + Into<Val> + 'static,
260 Sockets: IntoIterator,
261 Sockets::Item: Into<BindingAny<PluginId, Ctx>>,
262 {
263 sockets.into_iter()
264 .map( Into::into )
265 .try_for_each(| binding | binding.add_to_linker( &mut linker ))?;
266 Self::instantiate( self, engine, &linker )
267 }
268
269 /// A convenience alias for [`Plugin::link`] with 0 sockets
270 ///
271 /// # Errors
272 /// Returns an error if instantiation fails.
273 pub fn instantiate(
274 self,
275 engine: &Engine,
276 linker: &Linker<Ctx>
277 ) -> Result<PluginInstance<Ctx>, wasmtime::Error> {
278 let mut store = Store::new( engine, self.context );
279 if let Some( limiter ) = self.memory_limiter { store.limiter( limiter ); }
280 let instance = linker.instantiate( &mut store, &self.component )?;
281 Ok( PluginInstance {
282 store,
283 instance,
284 interface_remaps: self.interface_remaps,
285 fuel_limiter: self.fuel_limiter,
286 epoch_limiter: self.epoch_limiter,
287 })
288 }
289
290}
291
292impl<Ctx: std::fmt::Debug + 'static> std::fmt::Debug for Plugin<Ctx> {
293 fn fmt( &self, f: &mut std::fmt::Formatter<'_> ) -> std::fmt::Result {
294 f.debug_struct( "Plugin" )
295 .field( "component", &"<Component>" )
296 .field( "context", &self.context )
297 .field( "interface_remaps", &self.interface_remaps )
298 .field( "fuel_limiter", &self.fuel_limiter.as_ref().map(| _ | "<closure>" ))
299 .field( "epoch_limiter", &self.epoch_limiter.as_ref().map(| _ | "<closure>" ))
300 .field( "memory_limiter", &self.memory_limiter.as_ref().map(| _ | "<closure>" ))
301 .finish_non_exhaustive()
302 }
303}