Skip to main content

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}