Expand description
A WebAssembly plugin runtime for building modular applications.
Plugins are small, single-purpose WASM components that connect through abstract
bindings. Each plugin declares a plug (the binding it implements) and
zero or more sockets (bindings it depends on). wasm_link links these
into a directed acyclic graph (DAG) and handles cross-plugin dispatch.
§Core Concepts
-
Binding: An abstract contract declaring what an implementer exports and what a consumer may import. Contains a package name, a set of interfaces, and plugged-in plugin instances. -
Interface: A single WIT interface with functions and resources. Note that interfaces don’t have a name field; their names are provided as keys of aHashMapwhen constructing aBinding. This prevents duplicate interface names. -
Plugin: A struct containing a wasm component and the runtime context made available to host exports; their ids are provided as keys of aHashMapwhen constructing aBinding. This prevents duplicate ids. -
PluginInstance: An instantiated plugin with its store and instance, ready for dispatch. -
Plug: A plugin’s declaration that it implements a
Binding. -
Socket: A plugin’s declaration that it depends on a
Binding. Cardinality is expressed with wrapper types incrate::cardinality:cardinality::ExactlyOne( Id, T )- exactly one plugin, guaranteed presentcardinality::AtMostOne( Option<( Id, T )> )- zero or one plugincardinality::AtLeastOne( nonempty_collections::NEMap<Id, T> )- one or more pluginscardinality::Any( HashMap<Id, T> )- zero or more plugins
§Re-exports
wasm_link re-exports a small set of types from wasmtime for convenience
(Engine, Component, Linker, ResourceTable, Val). These types are
defined by wasmtime; see the wasmtime docs
for details.
§Example
use std::collections::{ HashMap, HashSet };
use wasm_link::{
Binding, Interface, Function, FunctionKind, ReturnKind,
Plugin, PluginContext, Engine, Component, Linker, ResourceTable, Val,
};
use wasm_link::cardinality::ExactlyOne ;
// First, declare a plugin context, the data stored inside wasmtime `Store<T>`.
// It must contain a resource table to implement `PluginContext` which is needed
// for ownership tracking of wasm component model resources.
struct Context { resource_table: ResourceTable }
impl PluginContext for Context {
fn resource_table( &mut self ) -> &mut ResourceTable {
&mut self.resource_table
}
}
// You create your own engine. This allows you to define your config but note that
// not all options are compatible. As a general rule of thumb, if an option changes
// the way you interact with wasm, it is likely not compatible since this is managed
// by `wasm_link` directly. If the option makes sense, it will likely be supported
// in the future through wasm_link options.
let engine = Engine::default();
// Similarly you may create your own linker, which you can add any exports into.
// Such exports will be available to all the plugins. It is your responsibility to
// make sure these don't conflict with re-exports of plugins that some other plugin
// depends on as these too have to be added to the same linker.
let linker = Linker::new( &engine );
// Build the DAG bottom-up: start with plugins that have no dependencies.
// Note that for plugins that don't require linking, you only need to pass in
// a reference to a linker. For plugins that have dependencies, the linker is mutated.
// Plugin IDs are specified in the cardinality wrapper to prevent duplicate ids.
let leaf = Plugin::new(
Component::new( &engine, "(component)" )?,
Context { resource_table: ResourceTable::new() },
).instantiate( &engine, &linker )?;
// Bindings expose a plugin's exports to other plugins.
// Wrapper sets cardinality: ExactlyOne, AtMostOne (0-1), AtLeastOne (1+), Any (0+).
let leaf_binding = Binding::new(
"empty:package",
HashMap::new(),
ExactlyOne( "leaf".to_string(), leaf ),
);
// `link()` wires up dependencies - this plugin can now import from leaf_binding.
let root = Plugin::new(
Component::new( &engine, r#"(component
(core module $m (func (export "f") (result i32) i32.const 42))
(core instance $i (instantiate $m))
(func $f (export "get-value") (result u32) (canon lift (core func $i "f")))
(instance $inst (export "get-value" (func $f)))
(export "my:package/example" (instance $inst))
)"# )?,
Context { resource_table: ResourceTable::new() },
).link( &engine, linker, vec![ leaf_binding ])?;
// Interface tells `wasm_link` which functions exist and how to handle returns.
let root_binding = Binding::new(
"my:package",
HashMap::from([( "example".to_string(), Interface::new(
HashMap::from([( "get-value".into(), Function::new(
FunctionKind::Freestanding, ReturnKind::MayContainResources,
))]),
HashSet::new(),
))]),
ExactlyOne( "root".to_string(), root ),
);
// Now you can call into the plugin graph from the host.
let result = root_binding.dispatch( "example", "get-value", &[ /* args */ ] )?;
match result {
ExactlyOne( _id, Ok( Val::U32( n ))) => assert_eq!( n, 42 ),
ExactlyOne( _id, Ok( _ )) => panic!( "unexpected response" ),
ExactlyOne( _id, Err( err )) => panic!( "dispatch error: {}", err ),
}§Shared Dependencies
Sometimes multiple plugins need to depend on the same binding. Since Binding
is a handle type, cloning it creates another reference to the same underlying
binding rather than duplicating it.
let plugin_d = Plugin::new( Component::new( &engine, "(component)" )?, Context::new())
.instantiate( &engine, &linker )?;
let binding_d = Binding::new( "d:pkg", HashMap::new(), ExactlyOne( "D".to_string(), plugin_d ));
// Both B and C import from D. Clone the binding handle so both can reference it.
let plugin_b = Plugin::new( Component::new( &engine, "(component)" )?, Context::new())
.link( &engine, linker.clone(), vec![ binding_d.clone() ])?;
let plugin_c = Plugin::new( Component::new( &engine, "(component)" )?, Context::new())
.link( &engine, linker.clone(), vec![ binding_d ])?;
let binding_b = Binding::new( "b:pkg", HashMap::new(), ExactlyOne( "B".to_string(), plugin_b ));
let binding_c = Binding::new( "c:pkg", HashMap::new(), ExactlyOne( "C".to_string(), plugin_c ));
let plugin_a = Plugin::new( Component::new( &engine, "(component)" )?, Context::new())
.link( &engine, linker, vec![ binding_b, binding_c ])?;§Multiple Plugins Per Binding
A single binding can have multiple plugin implementations. Use cardinality::AtLeastOne
when at least one implementation is required, or cardinality::Any when zero is acceptable.
When you dispatch to such a binding, you get results from all plugins.
// Plugin IDs are specified through the HashMap keys for Any.
let plugin1 = Plugin::new( Component::new( &engine, r#"(component
(core module $m (func (export "f") (result i32) i32.const 1))
(core instance $i (instantiate $m))
(func $f (result u32) (canon lift (core func $i "f")))
(instance $inst (export "get-value" (func $f)))
(export "pkg:interface/root" (instance $inst))
)"# )?, Context::new()).instantiate( &engine, &linker )?;
let plugin2 = Plugin::new( Component::new( &engine, r#"(component
(core module $m (func (export "f") (result i32) i32.const 2))
(core instance $i (instantiate $m))
(func $f (result u32) (canon lift (core func $i "f")))
(instance $inst (export "get-value" (func $f)))
(export "pkg:interface/root" (instance $inst))
)"# )?, Context::new()).instantiate( &engine, &linker )?;
let binding = Binding::new(
"pkg:interface",
HashMap::from([( "root".to_string(), Interface::new(
HashMap::from([( "get-value".into(), Function::new(
FunctionKind::Freestanding,
ReturnKind::MayContainResources,
))]),
HashSet::new(),
))]),
Any( HashMap::from([
( "p1".to_string(), plugin1 ),
( "p2".to_string(), plugin2 ),
])),
);
// Dispatch calls all plugins; the result wrapper matches what you passed in.
let Any( map ) = binding.dispatch( "root", "get-value", &[] )?;
assert_eq!( map.len(), 2 );
assert!( matches!( map.get( "p1" ), Some( Ok( Val::U32( 1 )))));
assert!( matches!( map.get( "p2" ), Some( Ok( Val::U32( 2 )))));§Resource Limits
Plugins may run untrusted code. wasm_link exposes three mechanisms to control
resource usage:
-
Fuel counts WebAssembly instructions. When fuel runs out, execution traps. Enable with
Config::consume_fuel. Set per-call viaPlugin::with_fuel_limiter. -
Epoch deadline counts external timer ticks. When the deadline is reached, execution traps. Enable with
Config::epoch_interruption. Set per-call viaPlugin::with_epoch_limiter. -
Memory limits linear memory and table growth via wasmtime’s
ResourceLimiter. No engine configuration required. Set once at instantiation viaPlugin::with_memory_limiter.
§Fuel and Epoch Limits
Fuel and epoch limits are set per-plugin via closures that receive the store, WIT interface path, function name, and function metadata. This gives you full control over the limit per call.
// Enable fuel consumption in the engine
let mut config = Config::new();
config.consume_fuel( true );
let engine = Engine::new( &config )?;
let linker = Linker::new( &engine );
// Give this plugin a flat fuel budget per call
let plugin = Plugin::new( component, Context::new() )
.with_fuel_limiter(| _store, _interface, _function, _metadata | 100_000 )
.instantiate( &engine, &linker )?;
let binding = Binding::<String, _>::new(
"my:pkg",
HashMap::from([( "api".into(), Interface::new(
HashMap::from([
( "cheap-fn".into(), Function::new( FunctionKind::Freestanding, ReturnKind::Void )),
( "expensive-fn".into(), Function::new( FunctionKind::Freestanding, ReturnKind::Void )),
]),
HashSet::new(),
))]),
ExactlyOne( "plugin".into(), plugin ),
);§Important Notes
Engine configuration is required. Fuel and epoch deadline limits only work when enabled
in the Engine configuration. Memory limits require no engine configuration.
For more information, see the wasmtime docs.
Fuel and epoch deadlines are independent. A function can have both a fuel limit and an epoch deadline. They are applied separately; whichever is exhausted first causes a trap.
Engine enabled but no limiter set. If you enable fuel/epoch deadlines in the Engine
but don’t set a limiter on the Plugin, the behavior mimics the wasmtime default.
- Fuel: A fresh
Storestarts with 0 fuel, so the first instruction immediately traps. This is likely not what you want. - Epoch deadlines: No deadline is set, so execution runs indefinitely regardless of epoch ticks.
§Memory Limits
Memory limits are implemented via wasmtime’s ResourceLimiter,
which you implement and store inside your plugin context. The limiter is installed
once at instantiation and controls memory and table growth for the plugin’s lifetime.
No engine configuration is required.
struct Ctx {
resource_table: ResourceTable,
limiter: MemoryLimiter,
}
impl PluginContext for Ctx {
fn resource_table( &mut self ) -> &mut ResourceTable { &mut self.resource_table }
}
struct MemoryLimiter { max_bytes: usize }
impl ResourceLimiter for MemoryLimiter {
fn memory_growing( &mut self, _current: usize, desired: usize, _max: Option<usize> ) -> wasmtime::Result<bool> {
Ok( desired <= self.max_bytes )
}
fn table_growing( &mut self, _current: usize, _desired: usize, _max: Option<usize> ) -> wasmtime::Result<bool> {
Ok( true )
}
}
let engine = Engine::default();
let linker = Linker::new( &engine );
let plugin = Plugin::new( component, Ctx {
resource_table: ResourceTable::new(),
limiter: MemoryLimiter { max_bytes: 10 * 1024 * 1024 }, // 10 MiB
}).with_memory_limiter(| ctx | &mut ctx.limiter )
.instantiate( &engine, &linker )?;Re-exports§
pub use wasmtime::Engine;pub use wasmtime::component::Component;pub use wasmtime::component::Linker;pub use wasmtime::component::ResourceTable;pub use wasmtime::component::Val;pub use nonempty_collections::NEMap;pub use nonempty_collections::nem;
Modules§
- cardinality
- Cardinality wrappers for plugin collections.
Structs§
- Binding
- An abstract contract specifying what plugins must implement (via plugs) or what
they could depend on (via sockets). It bundles one or more WIT
Interfaces under a single package name. - Function
- Metadata about a function declared by an interface.
- Interface
- A single WIT interface within a
Binding. - Plugin
- A WASM component bundled with its runtime context, ready for instantiation.
- Plugin
Instance - An instantiated plugin with its store and instance, ready for dispatch.
Enums§
- Binding
Any - Type-erased binding wrapper for heterogeneous socket lists.
- Dispatch
Error - Errors that can occur when dispatching a function call to plugins.
- Function
Kind - Denotes whether a function is freestanding or a resource method. Constructors are treated as freestanding functions.
- Resource
Creation Error - Errors that occur when creating a resource handle for cross-plugin transfer.
- Resource
Receive Error - Errors that occur when unwrapping a resource handle received from another plugin.
- Return
Kind - Categorizes a function’s return for dispatch handling.
Traits§
- Plugin
Context - Trait for accessing a
ResourceTablefrom the store’s data type.