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