Creating Host Plugins
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: HashSet<WitInterface>,
) -> 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: HashSet<WitInterface>,
) -> 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: HashSet<WitInterface>,
) -> 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, and aconfigmap.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.
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,
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,
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: HashSet<WitInterface>,
) -> 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.
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