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:
@@ -5,12 +5,3 @@ wasmtime::component::bindgen!({
|
|||||||
path: "../../wit/service",
|
path: "../../wit/service",
|
||||||
world: "service-component",
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,12 @@
|
|||||||
//! service exports, and invokes it through one serial Actor.
|
//! service exports, and invokes it through one serial Actor.
|
||||||
|
|
||||||
mod bindings;
|
mod bindings;
|
||||||
|
mod capability;
|
||||||
mod error;
|
mod error;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
|
|
||||||
|
pub use capability::CapabilityDescriptor;
|
||||||
pub use error::RuntimeError;
|
pub use error::RuntimeError;
|
||||||
pub use manifest::{ResourceLimits, ServiceKey, ServiceManifest};
|
pub use manifest::{ResourceLimits, ServiceKey, ServiceManifest};
|
||||||
pub use runtime::{ActorHandle, Runtime, RuntimeConfig, RuntimeStats};
|
pub use runtime::{ActorHandle, Runtime, RuntimeConfig, RuntimeStats};
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ use wasmtime_wasi::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
RuntimeError, ServiceKey, ServiceManifest,
|
CapabilityDescriptor, RuntimeError, ServiceKey, ServiceManifest,
|
||||||
bindings::{ServiceComponent, ServiceError, clock as clock_bindings},
|
bindings::{ServiceComponent, ServiceError},
|
||||||
|
capability::{Capability, CapabilityRegistry, HostCapabilities},
|
||||||
manifest::ResourceLimits,
|
manifest::ResourceLimits,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,7 +41,6 @@ const MAX_EPOCH_TICK: Duration = Duration::from_millis(10);
|
|||||||
const DEFAULT_MAX_WASM_STACK: usize = 512 * 1024;
|
const DEFAULT_MAX_WASM_STACK: usize = 512 * 1024;
|
||||||
const ACTOR_RESOURCE_COUNT_LIMIT: usize = 16;
|
const ACTOR_RESOURCE_COUNT_LIMIT: usize = 16;
|
||||||
const ACTOR_TABLE_ELEMENT_LIMIT: usize = 16 * 1024;
|
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] = &[
|
const ALLOWED_WASI_IMPORTS: &[&str] = &[
|
||||||
"wasi:cli/environment@",
|
"wasi:cli/environment@",
|
||||||
"wasi:cli/exit@",
|
"wasi:cli/exit@",
|
||||||
@@ -162,12 +162,7 @@ impl Drop for EpochTicker {
|
|||||||
struct RegisteredService {
|
struct RegisteredService {
|
||||||
manifest: ServiceManifest,
|
manifest: ServiceManifest,
|
||||||
component: Arc<Component>,
|
component: Arc<Component>,
|
||||||
capabilities: Vec<HostCapability>,
|
capabilities: Vec<Capability>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
enum HostCapability {
|
|
||||||
MonotonicClock,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Runtime {
|
impl Runtime {
|
||||||
@@ -327,6 +322,26 @@ impl Runtime {
|
|||||||
actor.invoke(input)
|
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.
|
/// Stops one Actor and releases its Store and Component Instance.
|
||||||
pub fn stop(&self, key: &ServiceKey) -> Result<(), RuntimeError> {
|
pub fn stop(&self, key: &ServiceKey) -> Result<(), RuntimeError> {
|
||||||
let _lifecycle = self
|
let _lifecycle = self
|
||||||
@@ -557,15 +572,15 @@ impl ActorStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HostState {
|
pub(crate) struct HostState {
|
||||||
store_limits: StoreLimits,
|
store_limits: StoreLimits,
|
||||||
wasi: WasiCtx,
|
wasi: WasiCtx,
|
||||||
wasi_table: ResourceTable,
|
wasi_table: ResourceTable,
|
||||||
clock_origin: Instant,
|
pub(crate) capabilities: HostCapabilities,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HostState {
|
impl HostState {
|
||||||
fn new(limits: &ResourceLimits) -> Self {
|
fn new(limits: &ResourceLimits, capabilities: &[Capability]) -> Self {
|
||||||
let store_limits = StoreLimitsBuilder::new()
|
let store_limits = StoreLimitsBuilder::new()
|
||||||
.memory_size(limits.memory_bytes)
|
.memory_size(limits.memory_bytes)
|
||||||
.table_elements(ACTOR_TABLE_ELEMENT_LIMIT)
|
.table_elements(ACTOR_TABLE_ELEMENT_LIMIT)
|
||||||
@@ -585,7 +600,7 @@ impl HostState {
|
|||||||
store_limits,
|
store_limits,
|
||||||
wasi: wasi.build(),
|
wasi: wasi.build(),
|
||||||
wasi_table: ResourceTable::new(),
|
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;
|
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 {
|
impl WasiView for HostState {
|
||||||
fn ctx(&mut self) -> WasiCtxView<'_> {
|
fn ctx(&mut self) -> WasiCtxView<'_> {
|
||||||
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);
|
store.limiter(|state| &mut state.store_limits);
|
||||||
|
|
||||||
let mut linker = Linker::new(&engine);
|
let mut linker = Linker::new(&engine);
|
||||||
add_restricted_wasi(&mut linker)?;
|
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);
|
let epoch_ticks = ticks_for(limits.deadline(), epoch_tick);
|
||||||
configure_call_budget(&mut store, &limits, epoch_ticks)?;
|
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()))
|
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(
|
fn validate_component_imports(
|
||||||
component: &Component,
|
component: &Component,
|
||||||
engine: &Engine,
|
engine: &Engine,
|
||||||
) -> Result<Vec<HostCapability>, RuntimeError> {
|
) -> Result<Vec<Capability>, RuntimeError> {
|
||||||
// Import validation is deny-by-default. A host function is linked only
|
// Import validation is deny-by-default. A host function is linked only
|
||||||
// after its exact WIT identity or WASI family has passed this allowlist.
|
// after its exact WIT identity or WASI family has passed this allowlist.
|
||||||
let mut capabilities = Vec::new();
|
let mut capabilities = Vec::new();
|
||||||
for (name, _) in component.component_type().imports(engine) {
|
for (name, _) in component.component_type().imports(engine) {
|
||||||
if name == MONOTONIC_CLOCK_IMPORT {
|
if let Some(capability) = CapabilityRegistry::resolve(name) {
|
||||||
capabilities.push(HostCapability::MonotonicClock);
|
capabilities.push(capability);
|
||||||
} else if !ALLOWED_WASI_IMPORTS
|
} else if !ALLOWED_WASI_IMPORTS
|
||||||
.iter()
|
.iter()
|
||||||
.any(|allowed| name.starts_with(allowed))
|
.any(|allowed| name.starts_with(allowed))
|
||||||
@@ -793,6 +785,8 @@ fn validate_component_imports(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
capabilities.sort_unstable();
|
||||||
|
capabilities.dedup();
|
||||||
Ok(capabilities)
|
Ok(capabilities)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ fn echo_component_round_trips_bytes() {
|
|||||||
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
||||||
let (key, actor) = start_component(&runtime, "echo", "echo_component.wasm");
|
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");
|
assert_eq!(actor.invoke(b"hello wasm".to_vec()).unwrap(), b"hello wasm");
|
||||||
|
|
||||||
runtime.stop(&key).unwrap();
|
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 runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
||||||
let (key, actor) = start_component(&runtime, "clock-probe", "clock_probe_component.wasm");
|
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 first = actor.invoke(b"first".to_vec()).unwrap();
|
||||||
let second = actor.invoke(b"second".to_vec()).unwrap();
|
let second = actor.invoke(b"second".to_vec()).unwrap();
|
||||||
assert_eq!(&first[8..], b"first");
|
assert_eq!(&first[8..], b"first");
|
||||||
|
|||||||
Reference in New Issue
Block a user