From 5b32886efe471fc7aefcf38987647ad0a61332fb Mon Sep 17 00:00:00 2001 From: Maofeng Date: Wed, 29 Jul 2026 19:13:35 +0800 Subject: [PATCH] feat(runtime): modularize host capabilities - add an exact-version Capability Registry and structured descriptors - move Clock bindings, host state, and Linker installation into its own module - derive required capabilities from Component imports and link only requested interfaces - cover exact-version resolution and minimal per-component capability sets --- crates/wasmeld-runtime/src/bindings.rs | 9 -- .../wasmeld-runtime/src/capability/clock.rs | 44 +++++++ crates/wasmeld-runtime/src/capability/mod.rs | 29 +++++ .../src/capability/registry.rs | 120 ++++++++++++++++++ crates/wasmeld-runtime/src/lib.rs | 2 + crates/wasmeld-runtime/src/runtime.rs | 76 +++++------ .../tests/runtime_components.rs | 8 ++ 7 files changed, 238 insertions(+), 50 deletions(-) create mode 100644 crates/wasmeld-runtime/src/capability/clock.rs create mode 100644 crates/wasmeld-runtime/src/capability/mod.rs create mode 100644 crates/wasmeld-runtime/src/capability/registry.rs diff --git a/crates/wasmeld-runtime/src/bindings.rs b/crates/wasmeld-runtime/src/bindings.rs index 012a19d..82d2a93 100644 --- a/crates/wasmeld-runtime/src/bindings.rs +++ b/crates/wasmeld-runtime/src/bindings.rs @@ -5,12 +5,3 @@ wasmtime::component::bindgen!({ path: "../../wit/service", world: "service-component", }); - -pub(crate) mod clock { - // Host capabilities are generated separately so adding a capability does - // not expand the baseline service contract for every component. - wasmtime::component::bindgen!({ - path: "../../wit/clock", - world: "clock-host", - }); -} diff --git a/crates/wasmeld-runtime/src/capability/clock.rs b/crates/wasmeld-runtime/src/capability/clock.rs new file mode 100644 index 0000000..c49ca23 --- /dev/null +++ b/crates/wasmeld-runtime/src/capability/clock.rs @@ -0,0 +1,44 @@ +//! `wasmeld:clock/monotonic-clock@0.1.0` host implementation. + +use std::time::Instant; + +use wasmtime::component::Linker; + +use crate::{RuntimeError, runtime::HostState}; + +wasmtime::component::bindgen!({ + path: "../../wit/clock", + world: "clock-host", +}); + +pub(crate) const INTERFACE: &str = "wasmeld:clock/monotonic-clock@0.1.0"; +pub(crate) const PACKAGE: &str = "wasmeld:clock"; +pub(crate) const NAME: &str = "monotonic-clock"; +pub(crate) const VERSION: &str = "0.1.0"; + +pub(crate) struct ClockState { + origin: Instant, +} + +impl ClockState { + pub(crate) fn new() -> Self { + Self { + origin: Instant::now(), + } + } + + pub(crate) fn now(&self) -> u64 { + u64::try_from(self.origin.elapsed().as_nanos()).unwrap_or(u64::MAX) + } +} + +impl wasmeld::clock::monotonic_clock::Host for HostState { + fn now(&mut self) -> u64 { + self.capabilities.monotonic_clock_now() + } +} + +pub(crate) fn add_to_linker(linker: &mut Linker) -> Result<(), RuntimeError> { + wasmeld::clock::monotonic_clock::add_to_linker::(linker, |state| state) + .map_err(|error| RuntimeError::ActorInitialization(error.to_string())) +} diff --git a/crates/wasmeld-runtime/src/capability/mod.rs b/crates/wasmeld-runtime/src/capability/mod.rs new file mode 100644 index 0000000..83d51d1 --- /dev/null +++ b/crates/wasmeld-runtime/src/capability/mod.rs @@ -0,0 +1,29 @@ +//! Versioned host capabilities available to WebAssembly Components. + +mod clock; +mod registry; + +pub use registry::CapabilityDescriptor; +pub(crate) use registry::{Capability, CapabilityRegistry}; + +/// Per-Actor state owned by enabled host capabilities. +pub(crate) struct HostCapabilities { + clock: Option, +} + +impl HostCapabilities { + pub(crate) fn new(capabilities: &[Capability]) -> Self { + Self { + clock: capabilities + .contains(&Capability::MonotonicClock) + .then(clock::ClockState::new), + } + } + + fn monotonic_clock_now(&self) -> u64 { + self.clock + .as_ref() + .expect("the Clock interface is linked only when its state is enabled") + .now() + } +} diff --git a/crates/wasmeld-runtime/src/capability/registry.rs b/crates/wasmeld-runtime/src/capability/registry.rs new file mode 100644 index 0000000..eee337a --- /dev/null +++ b/crates/wasmeld-runtime/src/capability/registry.rs @@ -0,0 +1,120 @@ +//! Exact interface lookup and Linker installation for host capabilities. + +use wasmtime::component::Linker; + +use super::clock; +use crate::{RuntimeError, runtime::HostState}; + +/// Stable description of one versioned WIT interface used by a Component. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct CapabilityDescriptor { + interface: &'static str, + package: &'static str, + name: &'static str, + version: &'static str, +} + +impl CapabilityDescriptor { + const fn new( + interface: &'static str, + package: &'static str, + name: &'static str, + version: &'static str, + ) -> Self { + Self { + interface, + package, + name, + version, + } + } + + /// Returns the fully qualified and versioned WIT interface identity. + pub const fn interface(&self) -> &'static str { + self.interface + } + + /// Returns the versioned WIT package identity without the version suffix. + pub const fn package(&self) -> &'static str { + self.package + } + + /// Returns the WIT interface name inside the package. + pub const fn name(&self) -> &'static str { + self.name + } + + /// Returns the exact semantic version implemented by the Runtime. + pub const fn version(&self) -> &'static str { + self.version + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) enum Capability { + MonotonicClock, +} + +impl Capability { + pub(crate) const fn descriptor(self) -> CapabilityDescriptor { + match self { + Self::MonotonicClock => CapabilityDescriptor::new( + clock::INTERFACE, + clock::PACKAGE, + clock::NAME, + clock::VERSION, + ), + } + } + + pub(crate) fn add_to_linker(self, linker: &mut Linker) -> Result<(), RuntimeError> { + match self { + Self::MonotonicClock => clock::add_to_linker(linker), + } + } +} + +/// Registry of the exact WIT interface versions implemented by this Runtime. +pub(crate) struct CapabilityRegistry; + +impl CapabilityRegistry { + /// Resolves an exact Component import to a supported host capability. + pub(crate) fn resolve(interface: &str) -> Option { + match interface { + clock::INTERFACE => Some(Capability::MonotonicClock), + _ => None, + } + } + + /// Installs only the capabilities requested by one Component. + pub(crate) fn add_to_linker( + linker: &mut Linker, + capabilities: &[Capability], + ) -> Result<(), RuntimeError> { + for capability in capabilities { + capability.add_to_linker(linker)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::{Capability, CapabilityRegistry}; + + #[test] + fn resolves_only_the_exact_supported_interface_version() { + assert_eq!( + CapabilityRegistry::resolve("wasmeld:clock/monotonic-clock@0.1.0"), + Some(Capability::MonotonicClock) + ); + assert_eq!( + CapabilityRegistry::resolve("wasmeld:clock/monotonic-clock@0.2.0"), + None + ); + assert_eq!( + CapabilityRegistry::resolve("wasmeld:clock/other@0.1.0"), + None + ); + } +} diff --git a/crates/wasmeld-runtime/src/lib.rs b/crates/wasmeld-runtime/src/lib.rs index 2667d3b..9b29bdb 100644 --- a/crates/wasmeld-runtime/src/lib.rs +++ b/crates/wasmeld-runtime/src/lib.rs @@ -6,10 +6,12 @@ //! service exports, and invokes it through one serial Actor. mod bindings; +mod capability; mod error; mod manifest; mod runtime; +pub use capability::CapabilityDescriptor; pub use error::RuntimeError; pub use manifest::{ResourceLimits, ServiceKey, ServiceManifest}; pub use runtime::{ActorHandle, Runtime, RuntimeConfig, RuntimeStats}; diff --git a/crates/wasmeld-runtime/src/runtime.rs b/crates/wasmeld-runtime/src/runtime.rs index 83bcda0..d0b5fc8 100644 --- a/crates/wasmeld-runtime/src/runtime.rs +++ b/crates/wasmeld-runtime/src/runtime.rs @@ -30,8 +30,9 @@ use wasmtime_wasi::{ }; use crate::{ - RuntimeError, ServiceKey, ServiceManifest, - bindings::{ServiceComponent, ServiceError, clock as clock_bindings}, + CapabilityDescriptor, RuntimeError, ServiceKey, ServiceManifest, + bindings::{ServiceComponent, ServiceError}, + capability::{Capability, CapabilityRegistry, HostCapabilities}, manifest::ResourceLimits, }; @@ -40,7 +41,6 @@ const MAX_EPOCH_TICK: Duration = Duration::from_millis(10); const DEFAULT_MAX_WASM_STACK: usize = 512 * 1024; const ACTOR_RESOURCE_COUNT_LIMIT: usize = 16; const ACTOR_TABLE_ELEMENT_LIMIT: usize = 16 * 1024; -const MONOTONIC_CLOCK_IMPORT: &str = "wasmeld:clock/monotonic-clock@0.1.0"; const ALLOWED_WASI_IMPORTS: &[&str] = &[ "wasi:cli/environment@", "wasi:cli/exit@", @@ -162,12 +162,7 @@ impl Drop for EpochTicker { struct RegisteredService { manifest: ServiceManifest, component: Arc, - capabilities: Vec, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum HostCapability { - MonotonicClock, + capabilities: Vec, } impl Runtime { @@ -327,6 +322,26 @@ impl Runtime { actor.invoke(input) } + /// Returns the exact versioned host capabilities imported by a service. + pub fn capabilities( + &self, + key: &ServiceKey, + ) -> Result, RuntimeError> { + let services = self + .inner + .services + .lock() + .map_err(|_| RuntimeError::LockPoisoned)?; + let service = services + .get(key) + .ok_or_else(|| RuntimeError::ServiceNotRegistered(key.clone()))?; + Ok(service + .capabilities + .iter() + .map(|capability| capability.descriptor()) + .collect()) + } + /// Stops one Actor and releases its Store and Component Instance. pub fn stop(&self, key: &ServiceKey) -> Result<(), RuntimeError> { let _lifecycle = self @@ -557,15 +572,15 @@ impl ActorStatus { } } -struct HostState { +pub(crate) struct HostState { store_limits: StoreLimits, wasi: WasiCtx, wasi_table: ResourceTable, - clock_origin: Instant, + pub(crate) capabilities: HostCapabilities, } impl HostState { - fn new(limits: &ResourceLimits) -> Self { + fn new(limits: &ResourceLimits, capabilities: &[Capability]) -> Self { let store_limits = StoreLimitsBuilder::new() .memory_size(limits.memory_bytes) .table_elements(ACTOR_TABLE_ELEMENT_LIMIT) @@ -585,7 +600,7 @@ impl HostState { store_limits, wasi: wasi.build(), wasi_table: ResourceTable::new(), - clock_origin: Instant::now(), + capabilities: HostCapabilities::new(capabilities), } } } @@ -594,12 +609,6 @@ impl HasData for HostState { type Data<'a> = &'a mut HostState; } -impl clock_bindings::wasmeld::clock::monotonic_clock::Host for HostState { - fn now(&mut self) -> u64 { - u64::try_from(self.clock_origin.elapsed().as_nanos()).unwrap_or(u64::MAX) - } -} - impl WasiView for HostState { fn ctx(&mut self) -> WasiCtxView<'_> { WasiCtxView { @@ -639,12 +648,12 @@ impl ActorWorker { }); } - let mut store = Store::new(&engine, HostState::new(&limits)); + let mut store = Store::new(&engine, HostState::new(&limits, &service.capabilities)); store.limiter(|state| &mut state.store_limits); let mut linker = Linker::new(&engine); add_restricted_wasi(&mut linker)?; - add_host_capabilities(&mut linker, &service.capabilities)?; + CapabilityRegistry::add_to_linker(&mut linker, &service.capabilities)?; let epoch_ticks = ticks_for(limits.deadline(), epoch_tick); configure_call_budget(&mut store, &limits, epoch_ticks)?; @@ -758,33 +767,16 @@ fn link_wasi(result: wasmtime::Result<()>) -> Result<(), RuntimeError> { result.map_err(|error| RuntimeError::ActorInitialization(error.to_string())) } -fn add_host_capabilities( - linker: &mut Linker, - capabilities: &[HostCapability], -) -> Result<(), RuntimeError> { - for capability in capabilities { - match capability { - HostCapability::MonotonicClock => link_wasi( - clock_bindings::wasmeld::clock::monotonic_clock::add_to_linker::< - HostState, - HostState, - >(linker, |state| state), - )?, - } - } - Ok(()) -} - fn validate_component_imports( component: &Component, engine: &Engine, -) -> Result, RuntimeError> { +) -> Result, RuntimeError> { // Import validation is deny-by-default. A host function is linked only // after its exact WIT identity or WASI family has passed this allowlist. let mut capabilities = Vec::new(); for (name, _) in component.component_type().imports(engine) { - if name == MONOTONIC_CLOCK_IMPORT { - capabilities.push(HostCapability::MonotonicClock); + if let Some(capability) = CapabilityRegistry::resolve(name) { + capabilities.push(capability); } else if !ALLOWED_WASI_IMPORTS .iter() .any(|allowed| name.starts_with(allowed)) @@ -793,6 +785,8 @@ fn validate_component_imports( } } + capabilities.sort_unstable(); + capabilities.dedup(); Ok(capabilities) } diff --git a/crates/wasmeld-runtime/tests/runtime_components.rs b/crates/wasmeld-runtime/tests/runtime_components.rs index e6cef3a..e8aae78 100644 --- a/crates/wasmeld-runtime/tests/runtime_components.rs +++ b/crates/wasmeld-runtime/tests/runtime_components.rs @@ -16,6 +16,7 @@ fn echo_component_round_trips_bytes() { let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start"); let (key, actor) = start_component(&runtime, "echo", "echo_component.wasm"); + assert!(runtime.capabilities(&key).unwrap().is_empty()); assert_eq!(actor.invoke(b"hello wasm".to_vec()).unwrap(), b"hello wasm"); runtime.stop(&key).unwrap(); @@ -171,6 +172,13 @@ fn explicit_clock_capability_is_linked() { let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start"); let (key, actor) = start_component(&runtime, "clock-probe", "clock_probe_component.wasm"); + let capabilities = runtime.capabilities(&key).unwrap(); + assert_eq!(capabilities.len(), 1); + assert_eq!( + capabilities[0].interface(), + "wasmeld:clock/monotonic-clock@0.1.0" + ); + let first = actor.invoke(b"first".to_vec()).unwrap(); let second = actor.invoke(b"second".to_vec()).unwrap(); assert_eq!(&first[8..], b"first");