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
This commit is contained in:
Maofeng
2026-07-29 19:13:35 +08:00
parent 3f5bb344f7
commit 5b32886efe
7 changed files with 238 additions and 50 deletions
-9
View File
@@ -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",
});
}
@@ -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<HostState>) -> Result<(), RuntimeError> {
wasmeld::clock::monotonic_clock::add_to_linker::<HostState, HostState>(linker, |state| state)
.map_err(|error| RuntimeError::ActorInitialization(error.to_string()))
}
@@ -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<clock::ClockState>,
}
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()
}
}
@@ -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<HostState>) -> 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<Capability> {
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<HostState>,
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
);
}
}
+2
View File
@@ -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};
+35 -41
View File
@@ -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<Component>,
capabilities: Vec<HostCapability>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum HostCapability {
MonotonicClock,
capabilities: Vec<Capability>,
}
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<Vec<CapabilityDescriptor>, 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<HostState>,
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<Vec<HostCapability>, RuntimeError> {
) -> Result<Vec<Capability>, 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)
}