Skip to main content

wasm_link/
lib.rs

1//! A WebAssembly plugin runtime for building modular applications.
2//!
3//! Plugins are small, single-purpose WASM components that connect through abstract
4//! bindings. Each plugin declares a **plug** (the binding it implements) and
5//! zero or more **sockets** (bindings it depends on). `wasm_link` links these
6//! into a directed acyclic graph (DAG) and handles cross-plugin dispatch.
7//!
8//! # Core Concepts
9//!
10//! - [`Binding`]: An abstract contract declaring what an implementer exports and what a
11//! 	consumer may import. Contains a package name, a set of interfaces, and plugged-in
12//! 	plugin instances.
13//!
14//! - [`Interface`]: A single WIT interface with functions and resources. Note that
15//! 	interfaces don't have a name field; their names are provided as keys of a `HashMap`
16//! 	when constructing a [`Binding`]. This prevents duplicate interface names.
17//!
18//! - [`Plugin`]: A struct containing a wasm component and the runtime context made available
19//! 	to host exports; their ids are provided as keys of a `HashMap` when constructing a
20//! 	[`Binding`]. This prevents duplicate ids.
21//!
22//! - [`PluginInstance`]: An instantiated plugin with its store and instance, ready for dispatch.
23//!
24//! - **Plug**: A plugin's declaration that it implements a [`Binding`].
25//!
26//! - **Socket**: A plugin's declaration that it depends on a [`Binding`]. Cardinality is
27//! 	expressed with wrapper types in [`crate::cardinality`]:
28//! 	- [`cardinality::ExactlyOne`]`( Id, T )` - exactly one plugin, guaranteed present
29//! 	- [`cardinality::AtMostOne`]`( Option<( Id, T )> )` - zero or one plugin
30//! 	- [`cardinality::AtLeastOne`]`( nonempty_collections::NEMap<Id, T> )` - one or more plugins
31//! 	- [`cardinality::Any`]`( HashMap<Id, T> )` - zero or more plugins
32//!
33//! # Re-exports
34//!
35//! `wasm_link` re-exports a small set of types from `wasmtime` for convenience
36//! (`Engine`, `Component`, `Linker`, `ResourceTable`, `Val`). These types are
37//! defined by wasmtime; see the [wasmtime docs](https://docs.rs/wasmtime/latest/wasmtime/)
38//! for details.
39//!
40//! # Example
41//!
42//! ```
43//! use std::collections::{ HashMap, HashSet };
44//! use wasm_link::{
45//! 	Binding, Interface, Function, FunctionKind, ReturnKind,
46//! 	Plugin, PluginContext, Engine, Component, Linker, ResourceTable, Val,
47//! };
48//! use wasm_link::cardinality::ExactlyOne ;
49//!
50//! // First, declare a plugin context, the data stored inside wasmtime `Store<T>`.
51//! // It must contain a resource table to implement `PluginContext` which is needed
52//! // for ownership tracking of wasm component model resources.
53//! struct Context { resource_table: ResourceTable }
54//!
55//! impl PluginContext for Context {
56//! 	fn resource_table( &mut self ) -> &mut ResourceTable {
57//! 		&mut self.resource_table
58//! 	}
59//! }
60//!
61//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
62//! // You create your own engine. This allows you to define your config but note that
63//! // not all options are compatible. As a general rule of thumb, if an option changes
64//! // the way you interact with wasm, it is likely not compatible since this is managed
65//! // by `wasm_link` directly. If the option makes sense, it will likely be supported
66//! // in the future through wasm_link options.
67//! let engine = Engine::default();
68//!
69//! // Similarly you may create your own linker, which you can add any exports into.
70//! // Such exports will be available to all the plugins. It is your responsibility to
71//! // make sure these don't conflict with re-exports of plugins that some other plugin
72//! // depends on as these too have to be added to the same linker.
73//! let linker = Linker::new( &engine );
74//!
75//! // Build the DAG bottom-up: start with plugins that have no dependencies.
76//! // Note that for plugins that don't require linking, you only need to pass in
77//! // a reference to a linker. For plugins that have dependencies, the linker is mutated.
78//! // Plugin IDs are specified in the cardinality wrapper to prevent duplicate ids.
79//! let leaf = Plugin::new(
80//! 	Component::new( &engine, "(component)" )?,
81//! 	Context { resource_table: ResourceTable::new() },
82//! ).instantiate( &engine, &linker )?;
83//!
84//! // Bindings expose a plugin's exports to other plugins.
85//! // Wrapper sets cardinality: ExactlyOne, AtMostOne (0-1), AtLeastOne (1+), Any (0+).
86//! let leaf_binding = Binding::new(
87//! 	"empty:package",
88//! 	HashMap::new(),
89//! 	ExactlyOne( "leaf".to_string(), leaf ),
90//! );
91//!
92//! // `link()` wires up dependencies - this plugin can now import from leaf_binding.
93//! let root = Plugin::new(
94//! 	Component::new( &engine, r#"(component
95//! 		(core module $m (func (export "f") (result i32) i32.const 42))
96//! 		(core instance $i (instantiate $m))
97//! 		(func $f (export "get-value") (result u32) (canon lift (core func $i "f")))
98//! 		(instance $inst (export "get-value" (func $f)))
99//! 		(export "my:package/example" (instance $inst))
100//! 	)"# )?,
101//! 	Context { resource_table: ResourceTable::new() },
102//! ).link( &engine, linker, vec![ leaf_binding ])?;
103//!
104//! // Interface tells `wasm_link` which functions exist and how to handle returns.
105//! let root_binding = Binding::new(
106//! 	"my:package",
107//! 	HashMap::from([( "example".to_string(), Interface::new(
108//! 		HashMap::from([( "get-value".into(), Function::new(
109//! 			FunctionKind::Freestanding, ReturnKind::MayContainResources,
110//! 		))]),
111//! 		HashSet::new(),
112//! 	))]),
113//! 	ExactlyOne( "root".to_string(), root ),
114//! );
115//!
116//! // Now you can call into the plugin graph from the host.
117//! let result = root_binding.dispatch( "example", "get-value", &[ /* args */ ] )?;
118//! match result {
119//! 	ExactlyOne( _id, Ok( Val::U32( n ))) => assert_eq!( n, 42 ),
120//! 	ExactlyOne( _id, Ok( _ )) => panic!( "unexpected response" ),
121//! 	ExactlyOne( _id, Err( err )) => panic!( "dispatch error: {}", err ),
122//! }
123//! # Ok(())
124//! # }
125//! ```
126//!
127//! # Shared Dependencies
128//!
129//! Sometimes multiple plugins need to depend on the same binding. Since `Binding`
130//! is a handle type, cloning it creates another reference to the same underlying
131//! binding rather than duplicating it.
132//!
133//! ```
134//! # use std::collections::HashMap ;
135//! # use wasm_link::{ Binding, Plugin, PluginContext, Engine, Component, Linker, ResourceTable };
136//! # use wasm_link::cardinality::ExactlyOne ;
137//! # struct Context { resource_table: ResourceTable }
138//! # impl PluginContext for Context {
139//! # 	fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
140//! # }
141//! # impl Context {
142//! # 	pub fn new() -> Self { Self { resource_table: ResourceTable::new() } }
143//! # }
144//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
145//! # let engine = Engine::default();
146//! # let linker = Linker::new( &engine );
147//! let plugin_d = Plugin::new( Component::new( &engine, "(component)" )?, Context::new())
148//! 	.instantiate( &engine, &linker )?;
149//! let binding_d = Binding::new( "d:pkg", HashMap::new(), ExactlyOne( "D".to_string(), plugin_d ));
150//!
151//! // Both B and C import from D. Clone the binding handle so both can reference it.
152//! let plugin_b = Plugin::new( Component::new( &engine, "(component)" )?, Context::new())
153//! 	.link( &engine, linker.clone(), vec![ binding_d.clone() ])?;
154//! let plugin_c = Plugin::new( Component::new( &engine, "(component)" )?, Context::new())
155//! 	.link( &engine, linker.clone(), vec![ binding_d ])?;
156//!
157//! let binding_b = Binding::new( "b:pkg", HashMap::new(), ExactlyOne( "B".to_string(), plugin_b ));
158//! let binding_c = Binding::new( "c:pkg", HashMap::new(), ExactlyOne( "C".to_string(), plugin_c ));
159//!
160//! let plugin_a = Plugin::new( Component::new( &engine, "(component)" )?, Context::new())
161//! 	.link( &engine, linker, vec![ binding_b, binding_c ])?;
162//! # let _ = plugin_a ;
163//! # Ok(())
164//! # }
165//! ```
166//!
167//! # Multiple Plugins Per Binding
168//!
169//! A single binding can have multiple plugin implementations. Use [`cardinality::AtLeastOne`]
170//! when at least one implementation is required, or [`cardinality::Any`] when zero is acceptable.
171//! When you dispatch to such a binding, you get results from all plugins.
172//!
173//! ```
174//! # use std::collections::{ HashMap, HashSet };
175//! # use wasm_link::{
176//! # 	Binding, Interface, Function, FunctionKind, ReturnKind, Plugin, PluginContext,
177//! # 	Engine, Component, Linker, ResourceTable, Val,
178//! # };
179//! # use wasm_link::cardinality::Any ;
180//! # struct Context { resource_table: ResourceTable }
181//! # impl Context {
182//! # 	pub fn new() -> Self { Self { resource_table: ResourceTable::new() } }
183//! # }
184//! # impl PluginContext for Context {
185//! # 	fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
186//! # }
187//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
188//! # let engine = Engine::default();
189//! # let linker = Linker::new( &engine );
190//! // Plugin IDs are specified through the HashMap keys for Any.
191//! let plugin1 = Plugin::new( Component::new( &engine, r#"(component
192//! 	(core module $m (func (export "f") (result i32) i32.const 1))
193//! 	(core instance $i (instantiate $m))
194//! 	(func $f (result u32) (canon lift (core func $i "f")))
195//! 	(instance $inst (export "get-value" (func $f)))
196//! 	(export "pkg:interface/root" (instance $inst))
197//! )"# )?, Context::new()).instantiate( &engine, &linker )?;
198//!
199//! let plugin2 = Plugin::new( Component::new( &engine, r#"(component
200//! 	(core module $m (func (export "f") (result i32) i32.const 2))
201//! 	(core instance $i (instantiate $m))
202//! 	(func $f (result u32) (canon lift (core func $i "f")))
203//! 	(instance $inst (export "get-value" (func $f)))
204//! 	(export "pkg:interface/root" (instance $inst))
205//! )"# )?, Context::new()).instantiate( &engine, &linker )?;
206//!
207//! let binding = Binding::new(
208//! 	"pkg:interface",
209//! 	HashMap::from([( "root".to_string(), Interface::new(
210//! 		HashMap::from([( "get-value".into(), Function::new(
211//!				FunctionKind::Freestanding,
212//!				ReturnKind::MayContainResources,
213//!			))]),
214//! 		HashSet::new(),
215//! 	))]),
216//! 	Any( HashMap::from([
217//! 		( "p1".to_string(), plugin1 ),
218//! 		( "p2".to_string(), plugin2 ),
219//! 	])),
220//! );
221//!
222//! // Dispatch calls all plugins; the result wrapper matches what you passed in.
223//! let Any( map ) = binding.dispatch( "root", "get-value", &[] )?;
224//! assert_eq!( map.len(), 2 );
225//! assert!( matches!( map.get( "p1" ), Some( Ok( Val::U32( 1 )))));
226//! assert!( matches!( map.get( "p2" ), Some( Ok( Val::U32( 2 )))));
227//! # Ok(())
228//! # }
229//! ```
230//!
231//! # Resource Limits
232//!
233//! Plugins may run untrusted code. `wasm_link` exposes three mechanisms to control
234//! resource usage:
235//!
236//! - **Fuel** counts WebAssembly instructions. When fuel runs out, execution traps.
237//! 	Enable with [`Config::consume_fuel`]( wasmtime::Config::consume_fuel ).
238//! 	Set per-call via [`Plugin::with_fuel_limiter`].
239//!
240//! - **Epoch deadline** counts external timer ticks. When the deadline is reached,
241//! 	execution traps. Enable with [`Config::epoch_interruption`]( wasmtime::Config::epoch_interruption ).
242//! 	Set per-call via [`Plugin::with_epoch_limiter`].
243//!
244//! - **Memory** limits linear memory and table growth via wasmtime's
245//! 	[`ResourceLimiter`]( wasmtime::ResourceLimiter ). No engine configuration required.
246//! 	Set once at instantiation via [`Plugin::with_memory_limiter`].
247//!
248//! ## Fuel and Epoch Limits
249//!
250//! Fuel and epoch limits are set per-plugin via closures that receive the store,
251//! WIT interface path, function name, and function metadata. This gives you full
252//! control over the limit per call.
253//!
254//! ```
255//! # use std::collections::{ HashMap, HashSet };
256//! # use wasm_link::{ Binding, Interface, Function, FunctionKind, ReturnKind, Plugin, PluginContext, Component, Linker, ResourceTable };
257//! # use wasm_link::cardinality::ExactlyOne ;
258//! # use wasmtime::{ Config, Engine };
259//! # struct Context { resource_table: ResourceTable }
260//! # impl Context { fn new() -> Self { Self { resource_table: ResourceTable::new() }}}
261//! # impl PluginContext for Context {
262//! # 	fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
263//! # }
264//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
265//! // Enable fuel consumption in the engine
266//! let mut config = Config::new();
267//! config.consume_fuel( true );
268//! let engine = Engine::new( &config )?;
269//! let linker = Linker::new( &engine );
270//!
271//! # let component = Component::new( &engine, "(component)" )?;
272//! // Give this plugin a flat fuel budget per call
273//! let plugin = Plugin::new( component, Context::new() )
274//! 	.with_fuel_limiter(| _store, _interface, _function, _metadata | 100_000 )
275//! 	.instantiate( &engine, &linker )?;
276//!
277//! let binding = Binding::<String, _>::new(
278//! 	"my:pkg",
279//! 	HashMap::from([( "api".into(), Interface::new(
280//! 		HashMap::from([
281//! 			( "cheap-fn".into(), Function::new( FunctionKind::Freestanding, ReturnKind::Void )),
282//! 			( "expensive-fn".into(), Function::new( FunctionKind::Freestanding, ReturnKind::Void )),
283//! 		]),
284//! 		HashSet::new(),
285//! 	))]),
286//! 	ExactlyOne( "plugin".into(), plugin ),
287//! );
288//! # Ok(())
289//! # }
290//! ```
291//! ## Important Notes
292//!
293//! **Engine configuration is required.** Fuel and epoch deadline limits only work when enabled
294//! in the [`Engine`] configuration. Memory limits require no engine configuration.
295//! For more information, see the [wasmtime docs](https://docs.rs/wasmtime/latest/wasmtime/).
296//!
297//! **Fuel and epoch deadlines are independent.** A function can have both a fuel limit and an
298//! epoch deadline. They are applied separately; whichever is exhausted first causes
299//! a trap.
300//!
301//! **Engine enabled but no limiter set.** If you enable fuel/epoch deadlines in the [`Engine`]
302//! but don't set a limiter on the [`Plugin`], the behavior mimics the wasmtime default.
303//! - *Fuel*: A fresh [`Store`]( wasmtime::Store ) starts with 0 fuel, so the first
304//! 	instruction immediately traps. This is likely not what you want.
305//! - *Epoch deadlines*: No deadline is set, so execution runs indefinitely regardless of epoch
306//! 	ticks.
307//!
308//! ## Memory Limits
309//!
310//! Memory limits are implemented via wasmtime's [`ResourceLimiter`]( wasmtime::ResourceLimiter ),
311//! which you implement and store inside your plugin context. The limiter is installed
312//! once at instantiation and controls memory and table growth for the plugin's lifetime.
313//! No engine configuration is required.
314//!
315//! ```
316//! # use wasm_link::{ Plugin, PluginContext, ResourceTable, Component, Engine, Linker };
317//! # use wasmtime::ResourceLimiter;
318//! struct Ctx {
319//! 	resource_table: ResourceTable,
320//! 	limiter: MemoryLimiter,
321//! }
322//! impl PluginContext for Ctx {
323//! 	fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
324//! }
325//!
326//! struct MemoryLimiter { max_bytes: usize }
327//! impl ResourceLimiter for MemoryLimiter {
328//! 	fn memory_growing( &mut self, _current: usize, desired: usize, _max: Option<usize> ) -> wasmtime::Result<bool> {
329//! 		Ok( desired <= self.max_bytes )
330//! 	}
331//! 	fn table_growing( &mut self, _current: usize, _desired: usize, _max: Option<usize> ) -> wasmtime::Result<bool> {
332//! 		Ok( true )
333//! 	}
334//! }
335//!
336//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
337//! let engine = Engine::default();
338//! let linker = Linker::new( &engine );
339//! # let component = Component::new( &engine, "(component)" )?;
340//! let plugin = Plugin::new( component, Ctx {
341//! 	resource_table: ResourceTable::new(),
342//! 	limiter: MemoryLimiter { max_bytes: 10 * 1024 * 1024 }, // 10 MiB
343//! }).with_memory_limiter(| ctx | &mut ctx.limiter )
344//! 	.instantiate( &engine, &linker )?;
345//! # let _ = plugin;
346//! # Ok(())
347//! # }
348//! ```
349
350mod binding ;
351mod interface ;
352mod plugin ;
353mod plugin_instance ;
354pub mod cardinality ;
355mod linker ;
356mod resource_wrapper ;
357
358#[doc( no_inline )]
359pub use wasmtime::Engine ;
360#[doc( no_inline )]
361pub use wasmtime::component::{ Component, Linker, ResourceTable, Val };
362#[doc( no_inline )]
363pub use nonempty_collections::{ NEMap, nem };
364
365pub use binding::Binding ;
366pub use interface::{ Interface, Function, FunctionKind, ReturnKind };
367pub use plugin::{ PluginContext, Plugin };
368pub use plugin_instance::{ PluginInstance, DispatchError };
369pub use binding::BindingAny ;
370pub use resource_wrapper::{ ResourceCreationError, ResourceReceiveError };