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