Skip to main content
Version: v2.0.0-rc

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

rust
#[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:

  1. Registration: Plugins are registered via HostBuilder::with_plugin(). Each plugin must have a unique ID.
  2. Start: When the host starts, all plugins' start() methods are called. If any plugin fails to start, the host startup fails.
  3. Workload binding: When a workload starts, the host calls on_workload_bind() once per plugin, then calls on_workload_item_bind() for each component and service in that workload.
  4. Resolution: After successful binding, on_workload_resolved() is called.
  5. Unbinding: When workloads stop, on_workload_unbind() is called for cleanup.
  6. 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 - Contains imports and exports as HashSet<WitInterface>, representing the WIT interfaces the plugin provides.
  • WitInterface - Describes a specific interface with namespace, package, interfaces (a set of interface names), an optional version, and a config map.
  • UnresolvedWorkload - A workload that has been initialized but not yet bound to plugins.
  • WorkloadItem<'a> - An enum passed to on_workload_item_bind. Variants are WorkloadItem::Component(&mut WorkloadComponent) and WorkloadItem::Service(&mut WorkloadService). Implements Deref<Target = WorkloadMetadata>, so item.linker() is accessible directly without pattern matching. Use item.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:

rust
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:

rust
let logger = CustomLogger::new("my-app");

let host = HostBuilder::new()
    .with_engine(engine)
    .with_plugin(Arc::new(logger))?
    .build()?;

Keep reading