Creating Host Plugins for wasmCloud
Extend wasmCloud hosts with additional capabilities.
The wash-runtime crate includes built-in support for common WASI interfaces like HTTP, key-value, and configuration. When you need to provide capabilities that aren't covered by the built-ins—such as integrating with a proprietary database, exposing specialized hardware, or implementing a custom protocol—you can create a custom plugin.
A host plugin is primarily responsible for implementing a specific WIT world as a collection of imports and exports that will be directly linked to the workload's wasmtime::component::Linker.
The HostPlugin trait
#[async_trait]
pub trait HostPlugin: Any + Send + Sync + 'static {
/// Returns a unique identifier for the plugin.
/// Plugin IDs must be unique across all registered plugins.
fn id(&self) -> &'static str;
/// Returns the WIT world this plugin implements.
/// The binding process only occurs if workloads require these interfaces.
fn world(&self) -> WitWorld;
/// Called during host initialization before accepting workloads.
/// Use this for setup tasks like establishing connections.
async fn start(&self) -> anyhow::Result<()> {
Ok(())
}
/// Called when a workload begins binding to the plugin.
/// Enables pre-binding validation and setup.
async fn on_workload_bind(
&self,
workload: &UnresolvedWorkload,
interfaces: WitInterfaces<'_>,
) -> anyhow::Result<()> {
Ok(())
}
/// Called when configuring a specific component or service's linker.
/// `item` is an enum — match on `WorkloadItem::Component` or `WorkloadItem::Service`.
/// This is where you add your implementations to the linker.
async fn on_workload_item_bind<'a>(
&self,
item: &mut WorkloadItem<'a>,
interfaces: WitInterfaces<'_>,
) -> anyhow::Result<()> {
Ok(())
}
/// Called after successful workload binding and resolution.
async fn on_workload_resolved(
&self,
workload: &ResolvedWorkload,
component_id: &str,
) -> anyhow::Result<()> {
Ok(())
}
/// Called during unbinding or shutdown for cleanup.
async fn on_workload_unbind(
&self,
workload_id: &str,
interfaces: WitInterfaces<'_>,
) -> anyhow::Result<()> {
Ok(())
}
/// Called during host shutdown for final cleanup.
async fn stop(&self) -> anyhow::Result<()> {
Ok(())
}
}Plugin lifecycle
Plugins follow a defined lifecycle within the host:
- Registration: Plugins are registered via
HostBuilder::with_plugin(). Each plugin must have a unique ID. - Start: When the host starts, all plugins'
start()methods are called. If any plugin fails to start, the host startup fails. - Workload binding: When a workload starts, the host calls
on_workload_bind()once per plugin, then callson_workload_item_bind()for each component and service in that workload. - Resolution: After successful binding,
on_workload_resolved()is called. - Unbinding: When workloads stop,
on_workload_unbind()is called for cleanup. - Stop: During host shutdown, all plugins'
stop()methods are called (with a 3-second timeout per plugin).
Key types
The HostPlugin trait uses several types from the wash_runtime crate:
WitWorld- ContainsimportsandexportsasHashSet<WitInterface>, representing the WIT interfaces the plugin provides.WitInterface- Describes a specific interface withnamespace,package,interfaces(a set of interface names), an optionalversion, an optionalname(used for multi-backend binding), and aconfigmap.WitInterfaces<'a>- A borrowed wrapper around&HashSetpassed toon_workload_bind,on_workload_item_bind, andon_workload_unbind. Providesiter(),get(namespace, package, interfaces), andcontains(namespace, package, interfaces)lookup helpers, avoiding a clone of the interface set per callback. Introduced in 2.4.0; earlier releases passed an ownedHashSet<WitInterface>by value.UnresolvedWorkload- A workload that has been initialized but not yet bound to plugins.WorkloadItem<'a>- An enum passed toon_workload_item_bind. Variants areWorkloadItem::Component(&mut WorkloadComponent)andWorkloadItem::Service(&mut WorkloadService). ImplementsDeref<Target = WorkloadMetadata>, soitem.linker()is accessible directly without pattern matching. Useitem.is_component()/item.is_service()when you need to handle the two variants differently.ResolvedWorkload- A fully bound workload ready for execution.
Looking up other plugins
Plugins that delegate to another plugin (for example, a custom backend that wraps wasi:keyvalue storage) can resolve siblings by ID from the runtime Ctx:
use wash_runtime::plugin::wasi_keyvalue::InMemoryKeyValue;
// Fallible: composes with `?` and returns a descriptive error if the
// plugin isn't registered or the type doesn't match.
let kv = ctx.try_get_plugin::<InMemoryKeyValue>("wasi-keyvalue")?;
// Infallible: panics on missing plugin or type mismatch. Use only
// when the plugin is guaranteed by your host's registration logic.
let kv = ctx.get_plugin::<InMemoryKeyValue>("wasi-keyvalue");The type parameter is the concrete backend your host registered for the looked-up plugin. The built-in wasi:keyvalue plugin ships four interchangeable backends — InMemoryKeyValue, FilesystemKeyValue, NatsKeyValue, and RedisKeyValue — all under the same plugin ID "wasi-keyvalue"; substitute whichever your HostBuilder::with_plugin() call registered. A type mismatch surfaces through the descriptive error from try_get_plugin.
Introduced in 2.4.0 (#5237). Earlier releases exposed only get_plugin(id) -> Option<Arc<T>>, which forced callers to write a guard block on every lookup; the new pair is the recommended pattern going forward.
Example: Custom logging plugin
Here's a simplified example of a custom plugin that implements logging:
use std::collections::HashSet;
use async_trait::async_trait;
use wash_runtime::{
engine::workload::WorkloadItem,
plugin::{HostPlugin, WitInterfaces},
wit::{WitInterface, WitWorld},
};
pub struct CustomLogger {
prefix: String,
}
impl CustomLogger {
pub fn new(prefix: impl Into<String>) -> Self {
Self { prefix: prefix.into() }
}
}
#[async_trait]
impl HostPlugin for CustomLogger {
fn id(&self) -> &'static str {
"custom-logger"
}
fn world(&self) -> WitWorld {
WitWorld {
imports: HashSet::new(),
exports: HashSet::from([WitInterface {
namespace: "wasi".to_string(),
package: "logging".to_string(),
interfaces: HashSet::from(["logging".to_string()]),
version: None,
name: None,
config: std::collections::HashMap::new(),
}]),
}
}
async fn start(&self) -> anyhow::Result<()> {
println!("[{}] Logger plugin started", self.prefix);
Ok(())
}
async fn on_workload_item_bind<'a>(
&self,
item: &mut WorkloadItem<'a>,
interfaces: WitInterfaces<'_>,
) -> anyhow::Result<()> {
// WorkloadItem implements Deref<Target = WorkloadMetadata>, so linker()
// is accessible directly without pattern matching:
// item.linker().func_wrap(...)?;
//
// Use item.is_component() / item.is_service() if you need to
// handle components and services differently.
//
// Use interfaces.contains("wasi", "logging", &["logging"]) to gate
// bind logic on which interfaces the workload actually requires.
Ok(())
}
async fn stop(&self) -> anyhow::Result<()> {
println!("[{}] Logger plugin stopped", self.prefix);
Ok(())
}
}Register the custom plugin with your host:
let logger = CustomLogger::new("my-app");
let host = HostBuilder::new()
.with_engine(engine)
.with_plugin(Arc::new(logger))?
.build()?;Keep reading
- Building Custom Hosts - Embed the wasmCloud runtime in your own application
- Host Plugins Overview - Built-in plugins and plugin architecture
- wash-runtime source code - Reference implementations of built-in plugins