diff --git a/Cargo.lock b/Cargo.lock index 47718667..8f3c72a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3597,6 +3597,7 @@ dependencies = [ "alloy-transport", "alloy-transport-ws", "anyhow", + "async-trait", "bytes", "futures", "http", diff --git a/Cargo.toml b/Cargo.toml index 43f68852..2519fd97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,9 @@ anyhow = "1" thiserror = "2" tokio = { version = "1", features = ["full"] } futures = "0.3" +# Cold `dyn` boot paths only (`ProviderKind::install`); hot guest traits +# use native async-fn-in-trait. +async-trait = "0.1" # Serde + config. serde = { version = "1", features = ["derive"] } diff --git a/crates/nexum-runtime/Cargo.toml b/crates/nexum-runtime/Cargo.toml index 7abab005..7b66aa90 100644 --- a/crates/nexum-runtime/Cargo.toml +++ b/crates/nexum-runtime/Cargo.toml @@ -22,6 +22,7 @@ wasmtime-wasi-http.workspace = true # Async + error plumbing. anyhow.workspace = true thiserror.workspace = true +async-trait.workspace = true # `strum::IntoStaticStr` on error enums gives metric labels (`error_kind`) # free via a snake_case `&'static str` for every variant. Used at # `tracing::warn!(error_kind = .into(), ...)` sites and diff --git a/crates/nexum-runtime/src/bootstrap.rs b/crates/nexum-runtime/src/bootstrap.rs index 532e5582..4e0c3c18 100644 --- a/crates/nexum-runtime/src/bootstrap.rs +++ b/crates/nexum-runtime/src/bootstrap.rs @@ -10,6 +10,7 @@ //! [`LaunchRuntime`] directly. use std::path::Path; +use std::sync::Arc; use crate::addons::RuntimeAddOn; use crate::builder::{AssembledRuntime, LaunchContext, LaunchRuntime}; @@ -34,7 +35,7 @@ pub async fn run( wasm: Option<&Path>, manifest: Option<&Path>, components: &Components, - extensions: &[Extension], + extensions: &[Arc>], add_ons: &[&dyn RuntimeAddOn], ) -> anyhow::Result<()> { let runtime = AssembledRuntime { diff --git a/crates/nexum-runtime/src/builder.rs b/crates/nexum-runtime/src/builder.rs index 34ad617b..412c4d8f 100644 --- a/crates/nexum-runtime/src/builder.rs +++ b/crates/nexum-runtime/src/builder.rs @@ -18,6 +18,7 @@ use std::future::{Future, IntoFuture}; use std::marker::PhantomData; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::time::Duration; use nexum_tasks::{DrainOutcome, TaskExit, TaskHandle, TaskManager, TaskSet}; @@ -127,8 +128,9 @@ fn finish_wait(joined: Option) -> anyhow::Result<()> { pub struct AssembledRuntime<'a, T: RuntimeTypes> { /// Shared backends threaded into every module store. pub components: Components, - /// Linker hooks and capability namespaces. - pub extensions: Vec>, + /// Extensions: namespaces, capabilities, linker hooks, services, and + /// provider kinds. + pub extensions: Vec>>, /// Cross-cutting facilities installed before the engine boots. pub add_ons: &'a [&'a dyn RuntimeAddOn], /// Single-module source override; `None` runs `[[modules]]`. @@ -386,7 +388,7 @@ impl<'a> RuntimeBuilder<'a> { /// optional extension hooks and module source before [`launch`](Self::launch). pub struct PresetBuilder<'a, R: Runtime> { config: &'a EngineConfig, - extensions: Vec>, + extensions: Vec>>, wasm: Option, manifest: Option, clocks: Option, @@ -394,11 +396,10 @@ pub struct PresetBuilder<'a, R: Runtime> { } impl<'a, R: Runtime> PresetBuilder<'a, R> { - /// Add extension linker hooks and capability namespaces on top of the - /// preset. The default preset carries none. + /// Add extensions on top of the preset. The default preset carries none. pub fn with_extensions( mut self, - extensions: impl IntoIterator>, + extensions: impl IntoIterator>>, ) -> Self { self.extensions.extend(extensions); self @@ -459,7 +460,7 @@ impl<'a, R: Runtime> PresetBuilder<'a, R> { /// may be added before the component builders. pub struct TypedBuilder<'a, T: RuntimeTypes> { config: &'a EngineConfig, - extensions: Vec>, + extensions: Vec>>, wasm: Option, manifest: Option, clocks: Option, @@ -467,8 +468,11 @@ pub struct TypedBuilder<'a, T: RuntimeTypes> { } impl<'a, T: RuntimeTypes> TypedBuilder<'a, T> { - /// Add the extension linker hooks and capability namespaces. - pub fn with_extensions(mut self, extensions: impl IntoIterator>) -> Self { + /// Add the extensions. + pub fn with_extensions( + mut self, + extensions: impl IntoIterator>>, + ) -> Self { self.extensions.extend(extensions); self } @@ -509,7 +513,7 @@ impl<'a, T: RuntimeTypes> TypedBuilder<'a, T> { /// The component builders are bound; the add-on set remains. pub struct ComponentsStage<'a, T: RuntimeTypes, C, S, E> { config: &'a EngineConfig, - extensions: Vec>, + extensions: Vec>>, wasm: Option, manifest: Option, clocks: Option, @@ -536,7 +540,7 @@ impl<'a, T: RuntimeTypes, C, S, E> ComponentsStage<'a, T, C, S, E> { /// runs. pub struct ReadyBuilder<'a, T: RuntimeTypes, C, S, E> { config: &'a EngineConfig, - extensions: Vec>, + extensions: Vec>>, wasm: Option, manifest: Option, clocks: Option, diff --git a/crates/nexum-runtime/src/host/extension.rs b/crates/nexum-runtime/src/host/extension.rs index f5b1b806..6f57e6a8 100644 --- a/crates/nexum-runtime/src/host/extension.rs +++ b/crates/nexum-runtime/src/host/extension.rs @@ -1,39 +1,196 @@ -//! The extension seam: a linker hook plus the capability namespace an -//! extension contributes, assembled at the composition root and threaded -//! into every module linker. +//! The extension seam: what one extension contributes to the host - a +//! namespace, a capability namespace, a linker hook, an optional host +//! service, and an optional provider kind. Assembled at the composition +//! root and threaded into every module linker. +use std::any::Any; +use std::collections::BTreeMap; use std::sync::Arc; -use wasmtime::component::Linker; +use async_trait::async_trait; +use wasmtime::Store; +use wasmtime::component::{Component, Linker}; use crate::host::component::RuntimeTypes; use crate::host::state::HostState; use crate::manifest::NamespaceCaps; -/// Adds an extension's WIT interfaces to a module linker. Runs after the -/// core interfaces and before instantiation. Takes only `&mut Linker`, so -/// the seam stays compatible with a future per-extension router that -/// serialises access to the non-`Sync` wasmtime `Store`. -pub type LinkerHook = Arc>) -> anyhow::Result<()> + Send + Sync>; - -/// One runtime extension: how to wire its interfaces into a module linker, -/// and the capability namespace enforcement must recognise for it. The two -/// travel together: a module that imports an extension interface boots only -/// if the linker entry AND the capability namespace are both registered -/// before instantiation. -pub struct Extension { - /// Linker contribution: adds the extension's imports to a module linker. - pub link: LinkerHook, - /// Capability namespace this extension owns, merged into enforcement so - /// a module importing the extension's interfaces still validates. - pub capabilities: NamespaceCaps, +/// One runtime extension. A module that imports an extension interface +/// boots only if the linker entry AND the capability namespace are both +/// registered before instantiation. +pub trait Extension: Send + Sync + 'static { + /// Namespace this extension owns; keys its service in [`HostServices`]. + fn namespace(&self) -> &'static str; + + /// Capability namespace merged into enforcement so a module importing + /// the extension's interfaces still validates. + fn capabilities(&self) -> NamespaceCaps; + + /// Adds the extension's imports to a worker linker. Runs after the + /// core interfaces and before instantiation. Takes only `&mut Linker`, + /// so the seam stays compatible with a future per-extension router + /// that serializes access to the non-`Sync` wasmtime `Store`. + fn link(&self, linker: &mut Linker>) -> anyhow::Result<()>; + + /// Host service this extension owns, published under its namespace on + /// [`HostServices`]. + fn service(&self) -> Option> { + None + } + + /// Provider kind this extension installs. + fn provider(&self) -> Option>> { + None + } +} + +/// A type-erased host service an extension owns. Held per namespace on +/// `HostState::services` and downcast at the call site. Kept synchronous +/// so it stays `dyn`-compatible. +pub trait HostService: Any + Send + Sync + 'static {} + +/// A provider component kind: the host holds an instance behind the owning +/// extension's serialized service; others call it. `async_trait` carries +/// the one cold `dyn` boot path until `async_fn_in_dyn_trait` stabilizes. +#[async_trait] +pub trait ProviderKind: Send + Sync + 'static { + /// Manifest kind this provider answers for. + fn kind(&self) -> &'static str; + + /// Adds the provider's imports to a provider linker. + fn link(&self, linker: &mut Linker>) -> anyhow::Result<()>; + + /// Install one instantiated provider behind the extension's service. + async fn install( + &self, + component: &Component, + store: Store>, + service: &Arc, + ) -> anyhow::Result<()>; +} + +/// Immutable per-namespace service map: each extension's [`HostService`] +/// under its [`Extension::namespace`], built once at boot and shared by +/// every module store. +#[derive(Clone, Default)] +pub struct HostServices(Arc>>); + +impl std::fmt::Debug for HostServices { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_set().entries(self.0.keys()).finish() + } +} + +impl HostServices { + /// Collect each extension's service under its namespace. Refuses a + /// duplicate namespace. + pub fn from_extensions( + extensions: &[Arc>], + ) -> anyhow::Result { + let mut map = BTreeMap::new(); + for ext in extensions { + let Some(service) = ext.service() else { + continue; + }; + let namespace = ext.namespace(); + if map.insert(namespace, service).is_some() { + anyhow::bail!("duplicate extension service namespace {namespace}"); + } + } + Ok(Self(Arc::new(map))) + } + + /// The service under `namespace`, downcast to its concrete type. + /// `None` when the namespace is absent or the type does not match. + pub fn get(&self, namespace: &str) -> Option> { + let service = Arc::clone(self.0.get(namespace)?); + let erased: Arc = service; + erased.downcast().ok() + } + + /// The raw type-erased service under `namespace`. + pub fn raw(&self, namespace: &str) -> Option<&Arc> { + self.0.get(namespace) + } } -impl Clone for Extension { - fn clone(&self) -> Self { - Self { - link: Arc::clone(&self.link), - capabilities: self.capabilities, +#[cfg(test)] +mod tests { + use super::*; + use crate::supervisor::TestTypes; + + struct Registry(u64); + impl HostService for Registry {} + + struct Clockwork; + impl HostService for Clockwork {} + + struct ServiceExt { + namespace: &'static str, + service: Option>, + } + + impl Extension for ServiceExt { + fn namespace(&self) -> &'static str { + self.namespace + } + fn capabilities(&self) -> NamespaceCaps { + NamespaceCaps { + prefix: "test:ext/", + ifaces: &[], + } + } + fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { + Ok(()) } + fn service(&self) -> Option> { + self.service.as_ref().map(Arc::clone) + } + } + + fn ext( + namespace: &'static str, + service: Arc, + ) -> Arc> { + Arc::new(ServiceExt { + namespace, + service: Some(service), + }) + } + + /// A registered service comes back under its namespace, downcast to + /// its concrete type; a wrong type or an absent namespace is `None`. + #[test] + fn get_downcasts_by_namespace() { + let services = + HostServices::from_extensions(&[ext("videre", Arc::new(Registry(7)))]).expect("build"); + + let registry = services.get::("videre").expect("registered"); + assert_eq!(registry.0, 7); + assert!(services.get::("videre").is_none()); + assert!(services.get::("absent").is_none()); + assert!(services.raw("videre").is_some()); + } + + /// A serviceless extension contributes nothing to the map. + #[test] + fn serviceless_extension_is_absent() { + let serviceless: Arc> = Arc::new(ServiceExt { + namespace: "quiet", + service: None, + }); + let services = HostServices::from_extensions(&[serviceless]).expect("build"); + assert!(services.raw("quiet").is_none()); + } + + /// Two services under one namespace refuse to build. + #[test] + fn duplicate_namespace_is_refused() { + let err = HostServices::from_extensions(&[ + ext("videre", Arc::new(Registry(1))), + ext("videre", Arc::new(Clockwork)), + ]) + .expect_err("duplicate namespace"); + assert!(err.to_string().contains("videre"), "{err}"); } } diff --git a/crates/nexum-runtime/src/host/state.rs b/crates/nexum-runtime/src/host/state.rs index 5dd07d85..3d85117d 100644 --- a/crates/nexum-runtime/src/host/state.rs +++ b/crates/nexum-runtime/src/host/state.rs @@ -11,6 +11,7 @@ use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; use wasmtime_wasi_http::WasiHttpCtx; use super::component::{Handle, RuntimeTypes}; +use super::extension::HostServices; use super::http::HttpGate; use super::logs::{LogRouter, RunId}; use super::venue_registry::VenueRegistry; @@ -55,6 +56,9 @@ pub struct HostState { /// Every module store carries the same shared handle; an adapter store, /// which cannot call the client face, carries an empty one. pub venue_registry: VenueRegistry, + /// Extension-owned host services, keyed by extension namespace and + /// downcast at the call site. One shared map across every store. + pub services: HostServices, } // `WasiView: Send`, so the backends must be `Send` too; the lattice diff --git a/crates/nexum-runtime/src/supervisor.rs b/crates/nexum-runtime/src/supervisor.rs index 87f1fac7..f31a9a52 100644 --- a/crates/nexum-runtime/src/supervisor.rs +++ b/crates/nexum-runtime/src/supervisor.rs @@ -43,7 +43,7 @@ use crate::engine_config::{ AdapterEntry, EngineConfig, ModuleEntry, ModuleLimits, OutboundHttpLimits, }; use crate::host::component::{Components, RuntimeTypes, StateHandle, StateStore}; -use crate::host::extension::Extension; +use crate::host::extension::{Extension, HostServices}; use crate::host::http::HttpGate; #[cfg(test)] use crate::host::local_store_redb::LocalStore; @@ -81,7 +81,10 @@ pub struct Supervisor { /// Extensions wired at boot. Cached so the module-restart path can /// rebuild an identical linker (core interfaces plus every extension /// hook) without re-consulting the composition root. - extensions: Vec>, + extensions: Vec>>, + /// Extension-owned host services, built once at boot from the same + /// extension set and carried by every store. + services: HostServices, /// Poison-pill thresholds resolved from `[limits.poison]` at boot /// (production defaults: 5 failures / 10 min). poison_policy: crate::runtime::poison_policy::PoisonPolicy, @@ -277,10 +280,11 @@ impl Supervisor { linker: &Linker>, engine_cfg: &EngineConfig, components: &Components, - extensions: &[Extension], + extensions: &[Arc>], clocks: Option, ) -> Result { let registry = capability_registry(extensions); + let services = HostServices::from_extensions(extensions)?; // Adapters instantiate first: the venue registry must contain them // before any module store (which carries the built registry) is // built. Adapters link only their scoped transport, against a @@ -302,6 +306,7 @@ impl Supervisor { &engine_cfg.limits, &adapter_registry, clocks.as_ref(), + services.clone(), ) .await .with_context(|| format!("load adapter {}", entry.path.display()))?; @@ -330,6 +335,7 @@ impl Supervisor { ®istry, clocks.as_ref(), venue_registry.clone(), + services.clone(), ) .await .with_context(|| format!("load module {}", entry.path.display()))?; @@ -351,6 +357,7 @@ impl Supervisor { engine: engine.clone(), components: components.clone(), extensions: extensions.to_vec(), + services, poison_policy: engine_cfg.limits.poison(), clocks, }) @@ -370,10 +377,11 @@ impl Supervisor { manifest: Option<&Path>, components: &Components, limits: &ModuleLimits, - extensions: &[Extension], + extensions: &[Arc>], clocks: Option, ) -> Result { let registry = capability_registry(extensions); + let services = HostServices::from_extensions(extensions)?; let entry = ModuleEntry { path: wasm.to_path_buf(), manifest: manifest.map(Path::to_path_buf), @@ -391,6 +399,7 @@ impl Supervisor { ®istry, clocks.as_ref(), venue_registry.clone(), + services.clone(), ) .await?; Ok(Self { @@ -401,6 +410,7 @@ impl Supervisor { engine: engine.clone(), components: components.clone(), extensions: extensions.to_vec(), + services, poison_policy: limits.poison(), clocks, }) @@ -426,6 +436,7 @@ impl Supervisor { state_quota: u64, clocks: Option<&WasiClockOverride>, venue_registry: VenueRegistry, + services: HostServices, ) -> Result> { let namespace: &str = &run.module; // Capture guest stdout/stderr per store instead of inheriting the @@ -484,6 +495,7 @@ impl Supervisor { chain_response_max_bytes, store: module_store, venue_registry, + services, }, ); store.limiter(|state| &mut state.limits); @@ -503,6 +515,7 @@ impl Supervisor { registry: &CapabilityRegistry, clocks: Option<&WasiClockOverride>, venue_registry: VenueRegistry, + services: HostServices, ) -> Result> { let manifest_path = resolve_manifest_path(&entry.path, entry.manifest.as_deref()); let loaded_manifest: LoadedManifest = match manifest_path.as_deref() { @@ -568,6 +581,7 @@ impl Supervisor { state_bytes, clocks, venue_registry, + services, )?; let bindings = EventModule::instantiate_async(&mut store, &component, linker) .await @@ -664,6 +678,9 @@ impl Supervisor { /// HTTP and messaging grants, instantiate the `VenueAdapter` bindings /// against the adapter linker, and run `init`. Nothing dispatches to /// the result yet; it boots so the registry can later reach it. + // One flat argument per shared input threaded onto the store, matching + // the module load path. + #[allow(clippy::too_many_arguments)] async fn load_adapter( engine: &Engine, linker: &Linker>, @@ -672,6 +689,7 @@ impl Supervisor { limits_cfg: &ModuleLimits, registry: &CapabilityRegistry, clocks: Option<&WasiClockOverride>, + services: HostServices, ) -> Result> { let manifest_path = resolve_manifest_path(&entry.path, entry.manifest.as_deref()); let loaded_manifest: LoadedManifest = match manifest_path.as_deref() { @@ -751,6 +769,7 @@ impl Supervisor { limits_cfg.state_bytes(), clocks, VenueRegistry::empty(), + services, )?; let bindings = VenueAdapter::instantiate_async(&mut store, &component, linker) .await @@ -921,6 +940,7 @@ impl Supervisor { // as the initial boot. let clocks = self.clocks.clone(); let venue_registry = self.venue_registry.clone(); + let services = self.services.clone(); let module = &mut self.modules[idx]; // A restart is a new run: bump the sequence so its logs key // apart from the dead run's, which stays readable until evicted. @@ -938,6 +958,7 @@ impl Supervisor { module.local_store_bytes, clocks.as_ref(), venue_registry, + services, )?; let bindings = EventModule::instantiate_async(&mut store, &module.component, &linker) .await @@ -1422,7 +1443,7 @@ impl Supervisor { /// and capability enforcement via the crate-internal `capability_registry`. pub fn build_linker( engine: &Engine, - extensions: &[Extension], + extensions: &[Arc>], ) -> anyhow::Result>> { let mut linker = Linker::>::new(engine); EventModule::add_to_linker::, HasSelf>>(&mut linker, |state| state)?; @@ -1438,7 +1459,7 @@ pub fn build_linker( // wasi:io/wasi:clocks interfaces. wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?; for ext in extensions { - (ext.link)(&mut linker)?; + ext.link(&mut linker)?; } Ok(linker) } @@ -1502,11 +1523,11 @@ fn resolve_manifest_path(component: &Path, explicit: Option<&Path>) -> Option( - extensions: &[Extension], + extensions: &[Arc>], ) -> CapabilityRegistry { let mut registry = CapabilityRegistry::core(); for ext in extensions { - registry.register(ext.capabilities); + registry.register(ext.capabilities()); } registry } diff --git a/crates/nexum-runtime/src/supervisor/tests.rs b/crates/nexum-runtime/src/supervisor/tests.rs index 0de3d908..6d814a16 100644 --- a/crates/nexum-runtime/src/supervisor/tests.rs +++ b/crates/nexum-runtime/src/supervisor/tests.rs @@ -328,7 +328,7 @@ fn make_wasmtime_engine() -> wasmtime::Engine { /// The core-only extension set: no domain extensions. Domain-extension /// boot coverage lives in the extension crate that owns the backend. -fn core_extensions() -> Vec> { +fn core_extensions() -> Vec>> { Vec::new() } diff --git a/crates/nexum-runtime/src/test_utils/harness.rs b/crates/nexum-runtime/src/test_utils/harness.rs index 31f2a8d7..10d808d6 100644 --- a/crates/nexum-runtime/src/test_utils/harness.rs +++ b/crates/nexum-runtime/src/test_utils/harness.rs @@ -22,6 +22,7 @@ //! crate's backend through the same harness. use std::path::PathBuf; +use std::sync::Arc; use std::time::Duration; use alloy_rpc_types_eth::{Header, Log}; @@ -54,7 +55,7 @@ where { wasm: PathBuf, manifest: ManifestSource, - extensions: Vec>>, + extensions: Vec>>>, ext: E, limits: ModuleLimits, chain: MockChainProvider, @@ -100,8 +101,8 @@ impl TestRuntimeBuilder { self } - /// Register an extension's linker hook and capability namespace. - pub fn extension(mut self, extension: Extension>) -> Self { + /// Register an extension. + pub fn extension(mut self, extension: Arc>>) -> Self { self.extensions.push(extension); self } @@ -109,7 +110,7 @@ impl TestRuntimeBuilder { /// Register several extensions at once. pub fn extensions( mut self, - extensions: impl IntoIterator>>, + extensions: impl IntoIterator>>>, ) -> Self { self.extensions.extend(extensions); self @@ -425,18 +426,31 @@ chain_id = {chain_id} return; }; - let calls = Arc::new(AtomicUsize::new(0)); - let hooked = calls.clone(); - let extension = Extension::>> { - link: Arc::new(move |_linker| { - hooked.fetch_add(1, Ordering::SeqCst); + struct CountingExtension(Arc); + + impl Extension>> for CountingExtension { + fn namespace(&self) -> &'static str { + "test" + } + fn capabilities(&self) -> NamespaceCaps { + NamespaceCaps { + prefix: "test:ext/", + ifaces: &[], + } + } + fn link( + &self, + _linker: &mut wasmtime::component::Linker< + crate::host::state::HostState>>, + >, + ) -> anyhow::Result<()> { + self.0.fetch_add(1, Ordering::SeqCst); Ok(()) - }), - capabilities: NamespaceCaps { - prefix: "test:ext/", - ifaces: &[], - }, - }; + } + } + + let calls = Arc::new(AtomicUsize::new(0)); + let extension = Arc::new(CountingExtension(calls.clone())); let mut rt = TestRuntime::builder_with_ext(wasm, calls.clone()) .extension(extension) diff --git a/crates/shepherd-cow-host/src/ext_cow.rs b/crates/shepherd-cow-host/src/ext_cow.rs index c98acb31..b7c93271 100644 --- a/crates/shepherd-cow-host/src/ext_cow.rs +++ b/crates/shepherd-cow-host/src/ext_cow.rs @@ -4,12 +4,14 @@ //! Shape: a local `bindgen!` for the extension world, a `Host` impl for //! the foreign `HostState` reached through [`ExtState`], a payload //! trait ([`CowBackend`]) the lattice `Ext` member satisfies, and an -//! [`Extension`] bundling the linker hook with the capability namespace. +//! [`Extension`] impl carrying the linker hook and capability namespace. //! //! The bindgen shares `nexum:host/types` with the core bindings via //! `with`, so the `fault` the extension's `cow-api-error` embeds is the //! same type the core host constructs. +use std::marker::PhantomData; +use std::sync::Arc; use std::time::Instant; use alloy_chains::Chain; @@ -18,7 +20,7 @@ use nexum_runtime::host::component::{BuilderContext, ComponentBuilder, RuntimeTy use nexum_runtime::host::extension::Extension; use nexum_runtime::host::state::{ExtState, HostState}; use nexum_runtime::manifest::NamespaceCaps; -use wasmtime::component::HasSelf; +use wasmtime::component::{HasSelf, Linker}; use crate::cow::CowApi; use crate::cow_orderbook::{CowApiError, OrderBookPool}; @@ -84,28 +86,45 @@ impl ComponentBuilder for ReferenceExtBuilder { } } +/// The cow-api extension over a lattice whose `Ext` payload carries a cow +/// backend. +struct CowExtension(PhantomData T>); + +impl Extension for CowExtension +where + T: RuntimeTypes, + T::Ext: CowBackend, +{ + fn namespace(&self) -> &'static str { + "cow" + } + + fn capabilities(&self) -> NamespaceCaps { + COW_CAPABILITIES + } + + fn link(&self, linker: &mut Linker>) -> anyhow::Result<()> { + // Link only the cow-api interface. The whole-world + // `CowExt::add_to_linker` would also re-add the shared + // `nexum:host/types` instance, which the core event-module + // linker already provides, tripping a "defined twice" error. + bindings::shepherd::cow::cow_api::add_to_linker::, HasSelf>>( + linker, + |s| s, + )?; + Ok(()) + } +} + /// Build the cow extension for a lattice whose `Ext` payload carries a cow /// backend. Wired at the composition root into `build_linker` and /// capability enforcement. -pub fn extension() -> Extension +pub fn extension() -> Arc> where T: RuntimeTypes, T::Ext: CowBackend, { - Extension { - link: std::sync::Arc::new(|linker| { - // Link only the cow-api interface. The whole-world - // `CowExt::add_to_linker` would also re-add the shared - // `nexum:host/types` instance, which the core event-module - // linker already provides, tripping a "defined twice" error. - bindings::shepherd::cow::cow_api::add_to_linker::, HasSelf>>( - linker, - |s| s, - )?; - Ok(()) - }), - capabilities: COW_CAPABILITIES, - } + Arc::new(CowExtension(PhantomData)) } /// Project the backend [`CowApiError`] into the WIT `cow-api-error`. diff --git a/crates/shepherd-cow-host/tests/cow_boot.rs b/crates/shepherd-cow-host/tests/cow_boot.rs index f3a75d9a..559cf28e 100644 --- a/crates/shepherd-cow-host/tests/cow_boot.rs +++ b/crates/shepherd-cow-host/tests/cow_boot.rs @@ -7,6 +7,7 @@ //! wasm artefacts and skip gracefully when the artefact is absent. use std::path::{Path, PathBuf}; +use std::sync::Arc; use alloy_chains::Chain; use nexum_runtime::bindings::nexum; @@ -33,7 +34,7 @@ impl RuntimeTypes for CowTestTypes { type Ext = ReferenceExt; } -fn cow_extensions() -> Vec> { +fn cow_extensions() -> Vec>> { vec![extension::()] }