Skip to main content

Crate wasm_link

Crate wasm_link 

Source
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 a HashMap when constructing a Binding. 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 a HashMap when constructing a Binding. 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 in crate::cardinality:

§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 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 Store starts 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.
PluginInstance
An instantiated plugin with its store and instance, ready for dispatch.

Enums§

BindingAny
Type-erased binding wrapper for heterogeneous socket lists.
DispatchError
Errors that can occur when dispatching a function call to plugins.
FunctionKind
Denotes whether a function is freestanding or a resource method. Constructors are treated as freestanding functions.
ResourceCreationError
Errors that occur when creating a resource handle for cross-plugin transfer.
ResourceReceiveError
Errors that occur when unwrapping a resource handle received from another plugin.
ReturnKind
Categorizes a function’s return for dispatch handling.

Traits§

PluginContext
Trait for accessing a ResourceTable from the store’s data type.