Skip to main content

wasmer_wasix/runtime/
mod.rs

1pub mod module_cache;
2pub mod package_loader;
3pub mod resolver;
4pub mod task_manager;
5
6use self::module_cache::CacheError;
7pub use self::task_manager::{SpawnType, VirtualTaskManager};
8use module_cache::HashedModuleData;
9use wasmer_types::{CompilationProgressCallback, ModuleHash};
10
11use std::{
12    borrow::Cow,
13    fmt,
14    ops::Deref,
15    sync::{Arc, Mutex},
16};
17
18use anyhow::Context as _;
19use futures::future::BoxFuture;
20use virtual_mio::block_on;
21use virtual_net::{DynVirtualNetworking, VirtualNetworking};
22use wasmer::{Engine, Module, RuntimeError};
23use wasmer_wasix_types::wasi::ExitCode;
24
25#[cfg(feature = "journal")]
26use crate::journal::{DynJournal, DynReadableJournal};
27use crate::{
28    SpawnError, WasiTtyState,
29    bin_factory::BinaryPackageCommand,
30    http::{DynHttpClient, HttpClient},
31    os::TtyBridge,
32    runtime::{
33        module_cache::{
34            ModuleCache, ThreadLocalCache,
35            progress::{ModuleLoadProgress, ModuleLoadProgressReporter},
36        },
37        package_loader::{PackageLoader, UnsupportedPackageLoader},
38        resolver::{BackendSource, MultiSource, Source},
39    },
40};
41
42/// Opaque per-instantiation state, created by
43/// [`InstantiationHook::prepare_imports`] and handed back to
44/// [`InstantiationHook::configure_new_instance`] for the instance built with
45/// those imports.
46///
47/// Hooks put the data they need to carry between the two phases in with
48/// [`InstantiationState::new`] and get it back out with
49/// [`InstantiationState::take`]. Callers only pass the value along, unmodified.
50// Carrying the state through the instantiation, instead of parking it in the
51// hook, is what makes concurrent instantiations safe: an implementation never
52// has to guess which pending instantiation a configure_new_instance call
53// belongs to, so two threads cold-starting the same module in different
54// stores cannot receive each other's state.
55#[derive(Default)]
56pub struct InstantiationState {
57    state: Option<Box<dyn std::any::Any + Send>>,
58}
59
60impl InstantiationState {
61    /// State that carries no data, for hooks that need nothing from the import
62    /// phase.
63    pub fn empty() -> Self {
64        Self { state: None }
65    }
66
67    /// Carries `state` from the import phase to the instance setup phase.
68    pub fn new<T: std::any::Any + Send>(state: T) -> Self {
69        Self {
70            state: Some(Box::new(state)),
71        }
72    }
73
74    /// Whether this state carries no data.
75    pub fn is_empty(&self) -> bool {
76        self.state.is_none()
77    }
78
79    /// Takes back the data stored by [`InstantiationState::new`].
80    ///
81    /// Fails if the state is empty or holds a different type, both of which
82    /// mean it did not come from the matching import phase.
83    pub fn take<T: std::any::Any + Send>(self) -> anyhow::Result<T> {
84        let state = self
85            .state
86            .context("missing instantiation state from the import phase")?;
87        state
88            .downcast::<T>()
89            .map(|state| *state)
90            .map_err(|_| anyhow::anyhow!("instantiation state does not belong to this hook"))
91    }
92}
93
94impl fmt::Debug for InstantiationState {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        if self.is_empty() {
97            f.write_str("InstantiationState::empty()")
98        } else {
99            f.write_str("InstantiationState(..)")
100        }
101    }
102}
103
104/// A hook into the instantiation of WASIX module instances.
105///
106/// Registered on [`PluggableRuntime::with_instantiation_hook`] or
107/// [`OverriddenRuntime::with_instantiation_hook`], and invoked once per
108/// instance the runtime creates (process bootstrap, thread spawn, dynamically
109/// linked side module).
110///
111/// Every method has a compatible default, so an implementation only needs to
112/// provide the phases it cares about.
113// Keeping both phases on one trait is what lets each hook route its own
114// InstantiationState from its import phase to its own setup phase when
115// several hooks are registered on the same runtime.
116pub trait InstantiationHook: fmt::Debug + Send + Sync + 'static {
117    /// Creates additional imports for an instance about to be created in
118    /// `store`.
119    ///
120    /// The returned [`InstantiationState`] is handed back to
121    /// [`InstantiationHook::configure_new_instance`] for the instance built
122    /// with these imports. If instantiation fails, the state is dropped.
123    fn additional_imports(
124        &self,
125        module: &wasmer::Module,
126        store: &mut wasmer::StoreMut,
127    ) -> anyhow::Result<(wasmer::Imports, InstantiationState)> {
128        let _ = (module, store);
129        Ok((wasmer::Imports::new(), InstantiationState::empty()))
130    }
131
132    /// Prepares the complete import object immediately before instantiation.
133    ///
134    /// The default implementation preserves the original
135    /// [`InstantiationHook::additional_imports`] contract by merging its
136    /// returned imports without replacing any entry already present in
137    /// `imports`. This includes imports supplied by WASIX and imports inserted
138    /// by earlier hooks. Unlike [`wasmer::Imports::extend`], duplicate entries
139    /// from `additional_imports` do not overwrite existing ones. Hooks that
140    /// need to inspect or reuse an existing import, such as an imported memory,
141    /// can override this method and update `imports` in place. Overrides must
142    /// preserve existing entries.
143    fn prepare_imports(
144        &self,
145        module: &wasmer::Module,
146        store: &mut wasmer::StoreMut,
147        imports: &mut wasmer::Imports,
148    ) -> anyhow::Result<InstantiationState> {
149        let (additional_imports, state) = self.additional_imports(module, store)?;
150        merge_missing_imports(imports, &additional_imports);
151        Ok(state)
152    }
153
154    /// Configures an instance after successful instantiation and startup.
155    ///
156    /// `state` is the [`InstantiationState`] this hook returned from the
157    /// [`InstantiationHook::prepare_imports`] call whose imports the
158    /// instance was created with.
159    fn configure_new_instance(
160        &self,
161        module: &wasmer::Module,
162        store: &mut wasmer::StoreMut,
163        instance: &wasmer::Instance,
164        imported_memory: Option<&wasmer::Memory>,
165        state: InstantiationState,
166    ) -> anyhow::Result<()> {
167        let _ = (module, store, instance, imported_memory, state);
168        Ok(())
169    }
170}
171
172impl<H: InstantiationHook + ?Sized> InstantiationHook for Arc<H> {
173    fn additional_imports(
174        &self,
175        module: &wasmer::Module,
176        store: &mut wasmer::StoreMut,
177    ) -> anyhow::Result<(wasmer::Imports, InstantiationState)> {
178        (**self).additional_imports(module, store)
179    }
180
181    fn prepare_imports(
182        &self,
183        module: &wasmer::Module,
184        store: &mut wasmer::StoreMut,
185        imports: &mut wasmer::Imports,
186    ) -> anyhow::Result<InstantiationState> {
187        (**self).prepare_imports(module, store, imports)
188    }
189
190    fn configure_new_instance(
191        &self,
192        module: &wasmer::Module,
193        store: &mut wasmer::StoreMut,
194        instance: &wasmer::Instance,
195        imported_memory: Option<&wasmer::Memory>,
196        state: InstantiationState,
197    ) -> anyhow::Result<()> {
198        (**self).configure_new_instance(module, store, instance, imported_memory, state)
199    }
200}
201
202/// Adapts an import-creation closure to [`InstantiationHook`].
203struct ImportsOnlyHook<F>(F);
204
205impl<F> fmt::Debug for ImportsOnlyHook<F> {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        f.write_str("ImportsOnlyHook(..)")
208    }
209}
210
211impl<F> InstantiationHook for ImportsOnlyHook<F>
212where
213    F: Fn(&wasmer::Module, &mut wasmer::StoreMut) -> anyhow::Result<wasmer::Imports>
214        + Send
215        + Sync
216        + 'static,
217{
218    fn additional_imports(
219        &self,
220        module: &wasmer::Module,
221        store: &mut wasmer::StoreMut,
222    ) -> anyhow::Result<(wasmer::Imports, InstantiationState)> {
223        Ok(((self.0)(module, store)?, InstantiationState::empty()))
224    }
225}
226
227/// Adapts an instance-setup closure to [`InstantiationHook`].
228struct InstanceSetupHook<F>(F);
229
230impl<F> fmt::Debug for InstanceSetupHook<F> {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        f.write_str("InstanceSetupHook(..)")
233    }
234}
235
236impl<F> InstantiationHook for InstanceSetupHook<F>
237where
238    F: Fn(
239            &wasmer::Module,
240            &mut wasmer::StoreMut,
241            &wasmer::Instance,
242            Option<&wasmer::Memory>,
243        ) -> anyhow::Result<()>
244        + Send
245        + Sync
246        + 'static,
247{
248    fn configure_new_instance(
249        &self,
250        module: &wasmer::Module,
251        store: &mut wasmer::StoreMut,
252        instance: &wasmer::Instance,
253        imported_memory: Option<&wasmer::Memory>,
254        _state: InstantiationState,
255    ) -> anyhow::Result<()> {
256        (self.0)(module, store, instance, imported_memory)
257    }
258}
259
260#[derive(Clone)]
261pub enum TaintReason {
262    UnknownWasiVersion,
263    NonZeroExitCode(ExitCode),
264    RuntimeError(RuntimeError),
265    DlSymbolResolutionFailed(String),
266}
267
268/// The input to load a module.
269///
270/// Exists because the semantics for resolving modules can vary between
271/// different sources.
272///
273/// All variants are wrapped in `Cow` to allow for zero-copy usage when possible.
274#[allow(clippy::large_enum_variant)]
275pub enum ModuleInput<'a> {
276    /// Raw bytes.
277    Bytes(Cow<'a, [u8]>),
278    /// Pre-hashed module data.
279    Hashed(Cow<'a, HashedModuleData>),
280    /// A binary package command.
281    Command(Cow<'a, BinaryPackageCommand>),
282}
283
284impl<'a> ModuleInput<'a> {
285    /// Convert to an owned version of the module input.
286    pub fn to_owned(&'a self) -> ModuleInput<'static> {
287        // The manual code below is needed due to compiler issues with the lifetime.
288        match self {
289            Self::Bytes(Cow::Borrowed(b)) => {
290                let v: Vec<u8> = (*b).to_owned();
291                let c: Cow<'static, [u8]> = Cow::from(v);
292                ModuleInput::Bytes(c)
293            }
294            Self::Bytes(Cow::Owned(b)) => ModuleInput::Bytes(Cow::Owned((*b).clone())),
295            Self::Hashed(Cow::Borrowed(h)) => ModuleInput::Hashed(Cow::Owned((*h).clone())),
296            Self::Hashed(Cow::Owned(h)) => ModuleInput::Hashed(Cow::Owned(h.clone())),
297            Self::Command(Cow::Borrowed(c)) => ModuleInput::Command(Cow::Owned((*c).clone())),
298            Self::Command(Cow::Owned(c)) => ModuleInput::Command(Cow::Owned(c.clone())),
299        }
300    }
301
302    /// Get the module hash.
303    ///
304    /// NOTE: may be expensive, depending on the variant.
305    pub fn hash(&self) -> ModuleHash {
306        match self {
307            Self::Bytes(b) => {
308                // Hash on the fly
309                ModuleHash::new(b)
310            }
311            Self::Hashed(hashed) => *hashed.hash(),
312            Self::Command(cmd) => *cmd.hash(),
313        }
314    }
315
316    /// Get the raw WebAssembly bytes.
317    pub fn wasm(&self) -> &[u8] {
318        match self {
319            Self::Bytes(b) => b,
320            Self::Hashed(hashed) => hashed.wasm().as_ref(),
321            Self::Command(cmd) => cmd.atom_ref().as_ref(),
322        }
323    }
324
325    /// Convert to a `HashedModuleData`.
326    ///
327    /// May involve cloning and hashing.
328    pub fn to_hashed(&self) -> HashedModuleData {
329        match self {
330            Self::Bytes(b) => HashedModuleData::new(b.as_ref()),
331            Self::Hashed(hashed) => hashed.as_ref().clone(),
332            Self::Command(cmd) => HashedModuleData::from_command(cmd),
333        }
334    }
335}
336
337/// Runtime components used when running WebAssembly programs.
338///
339/// Think of this as the "System" in "WebAssembly Systems Interface".
340#[allow(unused_variables)]
341pub trait Runtime
342where
343    Self: fmt::Debug,
344{
345    /// Provides access to all the networking related functions such as sockets.
346    fn networking(&self) -> &DynVirtualNetworking;
347
348    /// Retrieve the active [`VirtualTaskManager`].
349    fn task_manager(&self) -> &Arc<dyn VirtualTaskManager>;
350
351    /// A package loader.
352    fn package_loader(&self) -> Arc<dyn PackageLoader + Send + Sync> {
353        Arc::new(UnsupportedPackageLoader)
354    }
355
356    /// A cache for compiled modules.
357    fn module_cache(&self) -> Arc<dyn ModuleCache + Send + Sync> {
358        // Return a cache that uses a thread-local variable. This isn't ideal
359        // because it allows silently sharing state, possibly between runtimes.
360        //
361        // That said, it means people will still get *some* level of caching
362        // because each cache returned by this default implementation will go
363        // through the same thread-local variable.
364        Arc::new(ThreadLocalCache::default())
365    }
366
367    /// The package registry.
368    fn source(&self) -> Arc<dyn Source + Send + Sync>;
369
370    /// Get a [`wasmer::Engine`] for module compilation.
371    fn engine(&self) -> Engine {
372        Engine::default()
373    }
374
375    /// Create a new [`wasmer::Store`].
376    fn new_store(&self) -> wasmer::Store {
377        cfg_select! {
378            feature = "sys" => {
379                wasmer::Store::new(self.engine())
380            }
381            _ => {
382                wasmer::Store::default()
383            }
384        }
385    }
386
387    /// Create additional imports for a new WASIX instance in the provided store.
388    ///
389    /// This callback may be invoked multiple times (e.g. process bootstrap,
390    /// thread spawn), so implementations should create imports that are valid
391    /// for the given store each time.
392    ///
393    /// The returned [`InstantiationState`] is per-instantiation state that the
394    /// caller must pass to [`Runtime::configure_new_instance`] once the
395    /// instance built from these imports exists. If instantiation fails, the
396    /// state is simply dropped.
397    fn additional_imports(
398        &self,
399        _module: &wasmer::Module,
400        _store: &mut wasmer::StoreMut,
401    ) -> anyhow::Result<(wasmer::Imports, InstantiationState)> {
402        Ok((wasmer::Imports::new(), InstantiationState::empty()))
403    }
404
405    /// Prepare the complete import object immediately before instantiation.
406    ///
407    /// Existing implementations can continue overriding
408    /// [`Runtime::additional_imports`]. Runtimes that need to inspect or reuse
409    /// imports created by WASIX can override this method instead. Overrides
410    /// must preserve existing entries.
411    fn prepare_imports(
412        &self,
413        module: &wasmer::Module,
414        store: &mut wasmer::StoreMut,
415        imports: &mut wasmer::Imports,
416    ) -> anyhow::Result<InstantiationState> {
417        let (additional_imports, state) = self.additional_imports(module, store)?;
418        merge_missing_imports(imports, &additional_imports);
419        Ok(state)
420    }
421
422    /// Configure an instance after successful instantiation and startup.
423    ///
424    /// `state` must be the [`InstantiationState`] returned by the
425    /// [`Runtime::prepare_imports`] call whose imports this instance was
426    /// created with.
427    fn configure_new_instance(
428        &self,
429        _module: &wasmer::Module,
430        _store: &mut wasmer::StoreMut,
431        _instance: &wasmer::Instance,
432        _imported_memory: Option<&wasmer::Memory>,
433        _state: InstantiationState,
434    ) -> anyhow::Result<()> {
435        Ok(())
436    }
437
438    /// Get a custom HTTP client
439    fn http_client(&self) -> Option<&DynHttpClient> {
440        None
441    }
442
443    /// Get access to the TTY used by the environment.
444    fn tty(&self) -> Option<&(dyn TtyBridge + Send + Sync)> {
445        None
446    }
447
448    /// The primary way to load a module given a module input.
449    ///
450    /// The engine to use can be optionally provided, otherwise the most appropriate engine
451    /// should be selected.
452    ///
453    /// An optional progress reporter callback can be provided to report progress during module loading.
454    fn resolve_module<'a>(
455        &'a self,
456        input: ModuleInput<'a>,
457        engine: Option<&Engine>,
458        on_progress: Option<ModuleLoadProgressReporter>,
459    ) -> BoxFuture<'a, Result<Module, SpawnError>> {
460        let data = input.to_hashed();
461
462        let engine = if let Some(e) = engine {
463            e.clone()
464        } else {
465            match &input {
466                ModuleInput::Bytes(_) => self.engine(),
467                ModuleInput::Hashed(_) => self.engine(),
468                ModuleInput::Command(cmd) => self.engine(),
469            }
470        };
471
472        let module_cache = self.module_cache();
473
474        let task = async move { load_module(&engine, &module_cache, input, on_progress).await };
475        Box::pin(task)
476    }
477
478    /// Sync variant of [`Self::resolve_module`].
479    fn resolve_module_sync(
480        &self,
481        input: ModuleInput<'_>,
482        engine: Option<&Engine>,
483        on_progress: Option<ModuleLoadProgressReporter>,
484    ) -> Result<Module, SpawnError> {
485        block_on(self.resolve_module(input, engine, on_progress))
486    }
487
488    /// Load the module for a command.
489    ///
490    /// Will load the module from the cache if possible, otherwise will compile.
491    ///
492    /// NOTE: This always be preferred over [`Self::load_module`] to avoid
493    /// re-hashing the module!
494    #[deprecated(since = "0.601.0", note = "Use `resolve_module` instead")]
495    fn load_command_module(
496        &self,
497        cmd: &BinaryPackageCommand,
498    ) -> BoxFuture<'_, Result<Module, SpawnError>> {
499        self.resolve_module(ModuleInput::Command(Cow::Owned(cmd.clone())), None, None)
500    }
501
502    /// Sync version of [`Self::load_command_module`].
503    #[deprecated(since = "0.601.0", note = "Use `resolve_module_sync` instead")]
504    fn load_command_module_sync(&self, cmd: &BinaryPackageCommand) -> Result<Module, SpawnError> {
505        block_on(self.resolve_module(ModuleInput::Command(Cow::Borrowed(cmd)), None, None))
506    }
507
508    /// Load a WebAssembly module from raw bytes.
509    ///
510    /// Will load the module from the cache if possible, otherwise will compile.
511    #[deprecated(since = "0.601.0", note = "Use `resolve_module` instead")]
512    fn load_module<'a>(&'a self, wasm: &'a [u8]) -> BoxFuture<'a, Result<Module, SpawnError>> {
513        self.resolve_module(ModuleInput::Bytes(Cow::Borrowed(wasm)), None, None)
514    }
515
516    /// Synchronous version of [`Self::load_module`].
517    #[deprecated(
518        since = "0.601.0",
519        note = "Use `load_command_module` or `load_hashed_module` instead - this method can have high overhead"
520    )]
521    fn load_module_sync(&self, wasm: &[u8]) -> Result<Module, SpawnError> {
522        block_on(self.resolve_module(ModuleInput::Bytes(Cow::Borrowed(wasm)), None, None))
523    }
524
525    /// Load a WebAssembly module from pre-hashed data.
526    ///
527    /// Will load the module from the cache if possible, otherwise will compile.
528    fn load_hashed_module(
529        &self,
530        module: HashedModuleData,
531        engine: Option<&Engine>,
532    ) -> BoxFuture<'_, Result<Module, SpawnError>> {
533        self.resolve_module(ModuleInput::Hashed(Cow::Owned(module)), engine, None)
534    }
535
536    /// Synchronous version of [`Self::load_hashed_module`].
537    fn load_hashed_module_sync(
538        &self,
539        wasm: HashedModuleData,
540        engine: Option<&Engine>,
541    ) -> Result<Module, SpawnError> {
542        block_on(self.resolve_module(ModuleInput::Hashed(Cow::Owned(wasm)), engine, None))
543    }
544
545    /// Callback thats invokes whenever the instance is tainted, tainting can occur
546    /// for multiple reasons however the most common is a panic within the process
547    fn on_taint(&self, _reason: TaintReason) {}
548
549    /// The list of all read-only journals which will be used to restore the state of the
550    /// runtime at a particular point in time
551    #[cfg(feature = "journal")]
552    fn read_only_journals<'a>(&'a self) -> Box<dyn Iterator<Item = Arc<DynReadableJournal>> + 'a> {
553        Box::new(std::iter::empty())
554    }
555
556    /// The list of writable journals which will be appended to
557    #[cfg(feature = "journal")]
558    fn writable_journals<'a>(&'a self) -> Box<dyn Iterator<Item = Arc<DynJournal>> + 'a> {
559        Box::new(std::iter::empty())
560    }
561
562    /// The snapshot capturer takes and restores snapshots of the WASM process at specific
563    /// points in time by reading and writing log entries
564    #[cfg(feature = "journal")]
565    fn active_journal(&self) -> Option<&'_ DynJournal> {
566        None
567    }
568}
569
570pub type DynRuntime = dyn Runtime + Send + Sync;
571
572/// Load a Webassembly module, trying to use a pre-compiled version if possible.
573///
574// This function exists to provide a reusable baseline implementation for
575// implementing [`Runtime::load_module`], so custom logic can be added on top.
576#[tracing::instrument(level = "debug", skip_all)]
577pub async fn load_module(
578    engine: &Engine,
579    module_cache: &(dyn ModuleCache + Send + Sync),
580    input: ModuleInput<'_>,
581    on_progress: Option<ModuleLoadProgressReporter>,
582) -> Result<Module, crate::SpawnError> {
583    let wasm_hash = input.hash();
584
585    let result = if let Some(on_progress) = &on_progress {
586        module_cache
587            .load_with_progress(wasm_hash, engine, on_progress.clone())
588            .await
589    } else {
590        module_cache.load(wasm_hash, engine).await
591    };
592
593    match result {
594        Ok(module) => return Ok(module),
595        Err(CacheError::NotFound) => {}
596        Err(other) => {
597            tracing::warn!(
598                %wasm_hash,
599                error=&other as &dyn std::error::Error,
600                "Unable to load the cached module",
601            );
602        }
603    }
604
605    let res = if let Some(progress) = on_progress {
606        #[allow(unused_variables)]
607        let p = CompilationProgressCallback::new(move |p| {
608            progress.notify(ModuleLoadProgress::CompilingModule(p))
609        });
610        #[cfg(feature = "sys")]
611        {
612            if engine.is_sys() {
613                use wasmer::sys::NativeEngineExt;
614                engine.new_module_with_progress(input.wasm(), p)
615            } else {
616                Module::new(&engine, input.wasm())
617            }
618        }
619        #[cfg(not(feature = "sys"))]
620        {
621            Module::new(&engine, input.wasm())
622        }
623    } else {
624        Module::new(&engine, input.wasm())
625    };
626
627    let module = res.map_err(|err| crate::SpawnError::CompileError {
628        module_hash: wasm_hash,
629        error: err,
630    })?;
631
632    // TODO: pass a [`HashedModule`] struct that is safe by construction.
633    if let Err(e) = module_cache.save(wasm_hash, engine, &module).await {
634        tracing::warn!(
635            %wasm_hash,
636            error=&e as &dyn std::error::Error,
637            "Unable to cache the compiled module",
638        );
639    }
640
641    Ok(module)
642}
643
644#[derive(Debug, Default)]
645pub struct DefaultTty {
646    state: Mutex<WasiTtyState>,
647}
648
649impl TtyBridge for DefaultTty {
650    fn reset(&self) {
651        let mut state = self.state.lock().unwrap();
652        state.echo = false;
653        state.line_buffered = false;
654        state.line_feeds = false
655    }
656
657    fn tty_get(&self) -> WasiTtyState {
658        let state = self.state.lock().unwrap();
659        state.clone()
660    }
661
662    fn tty_set(&self, tty_state: WasiTtyState) {
663        let mut state = self.state.lock().unwrap();
664        *state = tty_state;
665    }
666}
667
668#[derive(Debug, Clone)]
669pub struct PluggableRuntime {
670    pub rt: Arc<dyn VirtualTaskManager>,
671    pub networking: DynVirtualNetworking,
672    pub http_client: Option<DynHttpClient>,
673    pub package_loader: Arc<dyn PackageLoader + Send + Sync>,
674    pub source: Arc<dyn Source + Send + Sync>,
675    pub engine: Engine,
676    pub module_cache: Arc<dyn ModuleCache + Send + Sync>,
677    pub tty: Option<Arc<dyn TtyBridge + Send + Sync>>,
678    #[cfg(feature = "journal")]
679    pub read_only_journals: Vec<Arc<DynReadableJournal>>,
680    #[cfg(feature = "journal")]
681    pub writable_journals: Vec<Arc<DynJournal>>,
682    pub instantiation_hooks: Vec<Arc<dyn InstantiationHook>>,
683}
684
685impl PluggableRuntime {
686    pub fn new(rt: Arc<dyn VirtualTaskManager>) -> Self {
687        // TODO: the cfg flags below should instead be handled by separate implementations.
688        cfg_select! {
689            feature = "host-vnet" => {
690                let networking = Arc::new(virtual_net::host::LocalNetworking::default());
691            }
692            _ => {
693                let networking = Arc::new(virtual_net::UnsupportedVirtualNetworking::default());
694            }
695        }
696        let http_client =
697            crate::http::default_http_client().map(|client| Arc::new(client) as DynHttpClient);
698
699        let loader = UnsupportedPackageLoader;
700
701        let mut source = MultiSource::default();
702        if let Some(client) = &http_client {
703            source.add_source(BackendSource::new(
704                BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap(),
705                client.clone(),
706            ));
707        }
708
709        Self {
710            rt,
711            networking,
712            http_client,
713            engine: Default::default(),
714            tty: None,
715            source: Arc::new(source),
716            package_loader: Arc::new(loader),
717            module_cache: Arc::new(module_cache::in_memory()),
718            #[cfg(feature = "journal")]
719            read_only_journals: Vec::new(),
720            #[cfg(feature = "journal")]
721            writable_journals: Vec::new(),
722            instantiation_hooks: Vec::new(),
723        }
724    }
725
726    pub fn set_networking_implementation<I>(&mut self, net: I) -> &mut Self
727    where
728        I: VirtualNetworking + Sync,
729    {
730        self.networking = Arc::new(net);
731        self
732    }
733
734    pub fn set_engine(&mut self, engine: Engine) -> &mut Self {
735        self.engine = engine;
736        self
737    }
738
739    pub fn set_tty(&mut self, tty: Arc<dyn TtyBridge + Send + Sync>) -> &mut Self {
740        self.tty = Some(tty);
741        self
742    }
743
744    pub fn set_module_cache(
745        &mut self,
746        module_cache: impl ModuleCache + Send + Sync + 'static,
747    ) -> &mut Self {
748        self.module_cache = Arc::new(module_cache);
749        self
750    }
751
752    pub fn set_source(&mut self, source: impl Source + Send + 'static) -> &mut Self {
753        self.source = Arc::new(source);
754        self
755    }
756
757    pub fn set_package_loader(
758        &mut self,
759        package_loader: impl PackageLoader + 'static,
760    ) -> &mut Self {
761        self.package_loader = Arc::new(package_loader);
762        self
763    }
764
765    pub fn set_http_client(
766        &mut self,
767        client: impl HttpClient + Send + Sync + 'static,
768    ) -> &mut Self {
769        self.http_client = Some(Arc::new(client));
770        self
771    }
772
773    #[cfg(feature = "journal")]
774    pub fn add_read_only_journal(&mut self, journal: Arc<DynReadableJournal>) -> &mut Self {
775        self.read_only_journals.push(journal);
776        self
777    }
778
779    #[cfg(feature = "journal")]
780    pub fn add_writable_journal(&mut self, journal: Arc<DynJournal>) -> &mut Self {
781        self.writable_journals.push(journal);
782        self
783    }
784
785    /// Registers a hook that only creates additional imports.
786    pub fn with_additional_imports(
787        &mut self,
788        imports: impl Fn(&wasmer::Module, &mut wasmer::StoreMut) -> anyhow::Result<wasmer::Imports>
789        + Send
790        + Sync
791        + 'static,
792    ) -> &mut Self {
793        self.with_instantiation_hook(ImportsOnlyHook(imports))
794    }
795
796    /// Registers a hook that only configures newly created instances.
797    pub fn with_instance_setup(
798        &mut self,
799        callback: impl Fn(
800            &wasmer::Module,
801            &mut wasmer::StoreMut,
802            &wasmer::Instance,
803            Option<&wasmer::Memory>,
804        ) -> anyhow::Result<()>
805        + Send
806        + Sync
807        + 'static,
808    ) -> &mut Self {
809        self.with_instantiation_hook(InstanceSetupHook(callback))
810    }
811
812    /// Registers a hook that takes part in both phases of instantiation, so it
813    /// can carry [`InstantiationState`] from its imports to its instance setup.
814    pub fn with_instantiation_hook(&mut self, hook: impl InstantiationHook) -> &mut Self {
815        self.instantiation_hooks.push(Arc::new(hook));
816        self
817    }
818}
819
820fn merge_missing_imports(imports: &mut wasmer::Imports, additional_imports: &wasmer::Imports) {
821    for (namespace, name, value) in additional_imports.iter() {
822        if imports.exists(namespace, name) {
823            tracing::warn!(
824                "Skipping duplicate additional import {}.{}",
825                namespace,
826                name
827            );
828        } else {
829            imports.define(namespace, name, value.clone());
830        }
831    }
832}
833
834/// Runs the import phase of `hooks`, updating the complete import object and
835/// returning the per-hook states aligned by index with `hooks`.
836fn run_import_hooks(
837    hooks: &[Arc<dyn InstantiationHook>],
838    module: &wasmer::Module,
839    store: &mut wasmer::StoreMut,
840    imports: &mut wasmer::Imports,
841) -> anyhow::Result<Vec<InstantiationState>> {
842    let mut states = Vec::with_capacity(hooks.len());
843    for hook in hooks {
844        let state = hook.prepare_imports(module, store, imports)?;
845        states.push(state);
846    }
847    Ok(states)
848}
849
850/// Composite state used by [`OverriddenRuntime`] to carry the inner
851/// runtime's state alongside its own hooks' states.
852struct OverriddenInstantiationState {
853    inner: InstantiationState,
854    own: Vec<InstantiationState>,
855}
856
857/// Runs the setup phase of `hooks`, handing each hook the state it produced
858/// during the import phase.
859fn run_setup_hooks(
860    hooks: &[Arc<dyn InstantiationHook>],
861    states: Vec<InstantiationState>,
862    module: &wasmer::Module,
863    store: &mut wasmer::StoreMut,
864    instance: &wasmer::Instance,
865    imported_memory: Option<&wasmer::Memory>,
866) -> anyhow::Result<()> {
867    anyhow::ensure!(
868        states.len() == hooks.len(),
869        "instance setup state does not match the registered instantiation hooks \
870         (got {} states for {} hooks)",
871        states.len(),
872        hooks.len(),
873    );
874    for (hook, state) in hooks.iter().zip(states) {
875        hook.configure_new_instance(module, store, instance, imported_memory, state)?;
876    }
877    Ok(())
878}
879
880impl Runtime for PluggableRuntime {
881    fn networking(&self) -> &DynVirtualNetworking {
882        &self.networking
883    }
884
885    fn http_client(&self) -> Option<&DynHttpClient> {
886        self.http_client.as_ref()
887    }
888
889    fn package_loader(&self) -> Arc<dyn PackageLoader + Send + Sync> {
890        Arc::clone(&self.package_loader)
891    }
892
893    fn source(&self) -> Arc<dyn Source + Send + Sync> {
894        Arc::clone(&self.source)
895    }
896
897    fn engine(&self) -> Engine {
898        self.engine.clone()
899    }
900
901    fn new_store(&self) -> wasmer::Store {
902        wasmer::Store::new(self.engine.clone())
903    }
904
905    fn task_manager(&self) -> &Arc<dyn VirtualTaskManager> {
906        &self.rt
907    }
908
909    fn tty(&self) -> Option<&(dyn TtyBridge + Send + Sync)> {
910        self.tty.as_deref()
911    }
912
913    fn module_cache(&self) -> Arc<dyn ModuleCache + Send + Sync> {
914        self.module_cache.clone()
915    }
916
917    fn additional_imports(
918        &self,
919        module: &wasmer::Module,
920        store: &mut wasmer::StoreMut,
921    ) -> anyhow::Result<(wasmer::Imports, InstantiationState)> {
922        if self.instantiation_hooks.is_empty() {
923            return Ok((wasmer::Imports::new(), InstantiationState::empty()));
924        }
925        let mut imports = wasmer::Imports::new();
926        let states = run_import_hooks(&self.instantiation_hooks, module, store, &mut imports)?;
927        Ok((imports, InstantiationState::new(states)))
928    }
929
930    fn prepare_imports(
931        &self,
932        module: &wasmer::Module,
933        store: &mut wasmer::StoreMut,
934        imports: &mut wasmer::Imports,
935    ) -> anyhow::Result<InstantiationState> {
936        if self.instantiation_hooks.is_empty() {
937            return Ok(InstantiationState::empty());
938        }
939        let states = run_import_hooks(&self.instantiation_hooks, module, store, imports)?;
940        Ok(InstantiationState::new(states))
941    }
942
943    fn configure_new_instance(
944        &self,
945        module: &wasmer::Module,
946        store: &mut wasmer::StoreMut,
947        instance: &wasmer::Instance,
948        imported_memory: Option<&wasmer::Memory>,
949        state: InstantiationState,
950    ) -> anyhow::Result<()> {
951        if self.instantiation_hooks.is_empty() {
952            return Ok(());
953        }
954        let states = state
955            .take::<Vec<InstantiationState>>()
956            .context("invalid instance setup state from import preparation")?;
957        run_setup_hooks(
958            &self.instantiation_hooks,
959            states,
960            module,
961            store,
962            instance,
963            imported_memory,
964        )
965    }
966
967    #[cfg(feature = "journal")]
968    fn read_only_journals<'a>(&'a self) -> Box<dyn Iterator<Item = Arc<DynReadableJournal>> + 'a> {
969        Box::new(self.read_only_journals.iter().cloned())
970    }
971
972    #[cfg(feature = "journal")]
973    fn writable_journals<'a>(&'a self) -> Box<dyn Iterator<Item = Arc<DynJournal>> + 'a> {
974        Box::new(self.writable_journals.iter().cloned())
975    }
976
977    #[cfg(feature = "journal")]
978    fn active_journal(&self) -> Option<&DynJournal> {
979        self.writable_journals.iter().last().map(|a| a.as_ref())
980    }
981}
982
983/// Runtime that allows for certain things to be overridden
984/// such as the active journals
985#[derive(Clone, Debug)]
986pub struct OverriddenRuntime {
987    inner: Arc<DynRuntime>,
988    task_manager: Option<Arc<dyn VirtualTaskManager>>,
989    networking: Option<DynVirtualNetworking>,
990    http_client: Option<DynHttpClient>,
991    package_loader: Option<Arc<dyn PackageLoader + Send + Sync>>,
992    source: Option<Arc<dyn Source + Send + Sync>>,
993    engine: Option<Engine>,
994    module_cache: Option<Arc<dyn ModuleCache + Send + Sync>>,
995    tty: Option<Arc<dyn TtyBridge + Send + Sync>>,
996    instantiation_hooks: Vec<Arc<dyn InstantiationHook>>,
997    #[cfg(feature = "journal")]
998    pub read_only_journals: Option<Vec<Arc<DynReadableJournal>>>,
999    #[cfg(feature = "journal")]
1000    pub writable_journals: Option<Vec<Arc<DynJournal>>>,
1001}
1002
1003impl OverriddenRuntime {
1004    pub fn new(inner: Arc<DynRuntime>) -> Self {
1005        Self {
1006            inner,
1007            task_manager: None,
1008            networking: None,
1009            http_client: None,
1010            package_loader: None,
1011            source: None,
1012            engine: None,
1013            module_cache: None,
1014            tty: None,
1015            instantiation_hooks: Vec::new(),
1016            #[cfg(feature = "journal")]
1017            read_only_journals: None,
1018            #[cfg(feature = "journal")]
1019            writable_journals: None,
1020        }
1021    }
1022
1023    pub fn with_task_manager(mut self, task_manager: Arc<dyn VirtualTaskManager>) -> Self {
1024        self.task_manager.replace(task_manager);
1025        self
1026    }
1027
1028    pub fn with_networking(mut self, networking: DynVirtualNetworking) -> Self {
1029        self.networking.replace(networking);
1030        self
1031    }
1032
1033    pub fn with_http_client(mut self, http_client: DynHttpClient) -> Self {
1034        self.http_client.replace(http_client);
1035        self
1036    }
1037
1038    pub fn with_package_loader(
1039        mut self,
1040        package_loader: Arc<dyn PackageLoader + Send + Sync>,
1041    ) -> Self {
1042        self.package_loader.replace(package_loader);
1043        self
1044    }
1045
1046    pub fn with_source(mut self, source: Arc<dyn Source + Send + Sync>) -> Self {
1047        self.source.replace(source);
1048        self
1049    }
1050
1051    pub fn with_engine(mut self, engine: Engine) -> Self {
1052        self.engine.replace(engine);
1053        self
1054    }
1055
1056    pub fn with_module_cache(mut self, module_cache: Arc<dyn ModuleCache + Send + Sync>) -> Self {
1057        self.module_cache.replace(module_cache);
1058        self
1059    }
1060
1061    pub fn with_tty(mut self, tty: Arc<dyn TtyBridge + Send + Sync>) -> Self {
1062        self.tty.replace(tty);
1063        self
1064    }
1065
1066    /// Registers a hook that only creates additional imports.
1067    pub fn with_additional_imports(
1068        self,
1069        imports: impl Fn(&wasmer::Module, &mut wasmer::StoreMut) -> anyhow::Result<wasmer::Imports>
1070        + Send
1071        + Sync
1072        + 'static,
1073    ) -> Self {
1074        self.with_instantiation_hook(ImportsOnlyHook(imports))
1075    }
1076
1077    /// Registers a hook that only configures newly created instances.
1078    pub fn with_instance_setup(
1079        self,
1080        callback: impl Fn(
1081            &wasmer::Module,
1082            &mut wasmer::StoreMut,
1083            &wasmer::Instance,
1084            Option<&wasmer::Memory>,
1085        ) -> anyhow::Result<()>
1086        + Send
1087        + Sync
1088        + 'static,
1089    ) -> Self {
1090        self.with_instantiation_hook(InstanceSetupHook(callback))
1091    }
1092
1093    /// Registers a hook that takes part in both phases of instantiation, so it
1094    /// can carry [`InstantiationState`] from its imports to its instance setup.
1095    pub fn with_instantiation_hook(mut self, hook: impl InstantiationHook) -> Self {
1096        self.instantiation_hooks.push(Arc::new(hook));
1097        self
1098    }
1099
1100    #[cfg(feature = "journal")]
1101    pub fn with_read_only_journals(mut self, journals: Vec<Arc<DynReadableJournal>>) -> Self {
1102        self.read_only_journals.replace(journals);
1103        self
1104    }
1105
1106    #[cfg(feature = "journal")]
1107    pub fn with_writable_journals(mut self, journals: Vec<Arc<DynJournal>>) -> Self {
1108        self.writable_journals.replace(journals);
1109        self
1110    }
1111}
1112
1113impl Runtime for OverriddenRuntime {
1114    fn networking(&self) -> &DynVirtualNetworking {
1115        if let Some(net) = self.networking.as_ref() {
1116            net
1117        } else {
1118            self.inner.networking()
1119        }
1120    }
1121
1122    fn task_manager(&self) -> &Arc<dyn VirtualTaskManager> {
1123        if let Some(rt) = self.task_manager.as_ref() {
1124            rt
1125        } else {
1126            self.inner.task_manager()
1127        }
1128    }
1129
1130    fn source(&self) -> Arc<dyn Source + Send + Sync> {
1131        if let Some(source) = self.source.clone() {
1132            source
1133        } else {
1134            self.inner.source()
1135        }
1136    }
1137
1138    fn package_loader(&self) -> Arc<dyn PackageLoader + Send + Sync> {
1139        if let Some(loader) = self.package_loader.clone() {
1140            loader
1141        } else {
1142            self.inner.package_loader()
1143        }
1144    }
1145
1146    fn module_cache(&self) -> Arc<dyn ModuleCache + Send + Sync> {
1147        if let Some(cache) = self.module_cache.clone() {
1148            cache
1149        } else {
1150            self.inner.module_cache()
1151        }
1152    }
1153
1154    fn engine(&self) -> Engine {
1155        if let Some(engine) = self.engine.clone() {
1156            engine
1157        } else {
1158            self.inner.engine()
1159        }
1160    }
1161
1162    fn new_store(&self) -> wasmer::Store {
1163        if let Some(engine) = self.engine.clone() {
1164            wasmer::Store::new(engine)
1165        } else {
1166            self.inner.new_store()
1167        }
1168    }
1169
1170    fn additional_imports(
1171        &self,
1172        module: &wasmer::Module,
1173        store: &mut wasmer::StoreMut,
1174    ) -> anyhow::Result<(wasmer::Imports, InstantiationState)> {
1175        let (mut imports, inner_state) = self.inner.additional_imports(module, store)?;
1176        if self.instantiation_hooks.is_empty() && inner_state.is_empty() {
1177            return Ok((imports, InstantiationState::empty()));
1178        }
1179        let own_states = run_import_hooks(&self.instantiation_hooks, module, store, &mut imports)?;
1180        Ok((
1181            imports,
1182            InstantiationState::new(OverriddenInstantiationState {
1183                inner: inner_state,
1184                own: own_states,
1185            }),
1186        ))
1187    }
1188
1189    fn prepare_imports(
1190        &self,
1191        module: &wasmer::Module,
1192        store: &mut wasmer::StoreMut,
1193        imports: &mut wasmer::Imports,
1194    ) -> anyhow::Result<InstantiationState> {
1195        let inner_state = self.inner.prepare_imports(module, store, imports)?;
1196        if self.instantiation_hooks.is_empty() && inner_state.is_empty() {
1197            return Ok(InstantiationState::empty());
1198        }
1199        let own_states = run_import_hooks(&self.instantiation_hooks, module, store, imports)?;
1200        Ok(InstantiationState::new(OverriddenInstantiationState {
1201            inner: inner_state,
1202            own: own_states,
1203        }))
1204    }
1205
1206    fn configure_new_instance(
1207        &self,
1208        module: &wasmer::Module,
1209        store: &mut wasmer::StoreMut,
1210        instance: &wasmer::Instance,
1211        imported_memory: Option<&wasmer::Memory>,
1212        state: InstantiationState,
1213    ) -> anyhow::Result<()> {
1214        let state = if state.is_empty() {
1215            anyhow::ensure!(
1216                self.instantiation_hooks.is_empty(),
1217                "missing instance setup state from import preparation"
1218            );
1219            OverriddenInstantiationState {
1220                inner: InstantiationState::empty(),
1221                own: Vec::new(),
1222            }
1223        } else {
1224            state
1225                .take::<OverriddenInstantiationState>()
1226                .context("invalid instance setup state from import preparation")?
1227        };
1228        self.inner
1229            .configure_new_instance(module, store, instance, imported_memory, state.inner)?;
1230        run_setup_hooks(
1231            &self.instantiation_hooks,
1232            state.own,
1233            module,
1234            store,
1235            instance,
1236            imported_memory,
1237        )
1238    }
1239
1240    fn http_client(&self) -> Option<&DynHttpClient> {
1241        if let Some(client) = self.http_client.as_ref() {
1242            Some(client)
1243        } else {
1244            self.inner.http_client()
1245        }
1246    }
1247
1248    fn tty(&self) -> Option<&(dyn TtyBridge + Send + Sync)> {
1249        if let Some(tty) = self.tty.as_ref() {
1250            Some(tty.deref())
1251        } else {
1252            self.inner.tty()
1253        }
1254    }
1255
1256    #[cfg(feature = "journal")]
1257    fn read_only_journals<'a>(&'a self) -> Box<dyn Iterator<Item = Arc<DynReadableJournal>> + 'a> {
1258        if let Some(journals) = self.read_only_journals.as_ref() {
1259            Box::new(journals.iter().cloned())
1260        } else {
1261            self.inner.read_only_journals()
1262        }
1263    }
1264
1265    #[cfg(feature = "journal")]
1266    fn writable_journals<'a>(&'a self) -> Box<dyn Iterator<Item = Arc<DynJournal>> + 'a> {
1267        if let Some(journals) = self.writable_journals.as_ref() {
1268            Box::new(journals.iter().cloned())
1269        } else {
1270            self.inner.writable_journals()
1271        }
1272    }
1273
1274    #[cfg(feature = "journal")]
1275    fn active_journal(&self) -> Option<&'_ DynJournal> {
1276        if let Some(journals) = self.writable_journals.as_ref() {
1277            journals.iter().last().map(|a| a.as_ref())
1278        } else {
1279            self.inner.active_journal()
1280        }
1281    }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286    use super::{InstantiationHook, InstantiationState};
1287    use wasmer::{AsStoreMut, Extern, Global, Imports, Module, Store, Value};
1288
1289    #[derive(Debug)]
1290    struct LegacyImportsHook;
1291
1292    impl InstantiationHook for LegacyImportsHook {
1293        fn additional_imports(
1294            &self,
1295            _module: &Module,
1296            store: &mut wasmer::StoreMut,
1297        ) -> anyhow::Result<(Imports, InstantiationState)> {
1298            let mut imports = Imports::new();
1299            imports.define("host", "existing", Global::new(store, Value::I32(2)));
1300            imports.define("host", "added", Global::new(store, Value::I32(3)));
1301            Ok((imports, InstantiationState::empty()))
1302        }
1303    }
1304
1305    #[test]
1306    fn prepare_imports_preserves_the_legacy_additional_imports_contract() {
1307        let mut store = Store::default();
1308        let module = Module::new(&store, "(module)").unwrap();
1309        let mut imports = Imports::new();
1310        imports.define("host", "existing", Global::new(&mut store, Value::I32(1)));
1311
1312        LegacyImportsHook
1313            .prepare_imports(&module, &mut store.as_store_mut(), &mut imports)
1314            .unwrap();
1315
1316        let Some(Extern::Global(existing)) = imports.get_export("host", "existing") else {
1317            panic!("existing import is not a global");
1318        };
1319        let Some(Extern::Global(added)) = imports.get_export("host", "added") else {
1320            panic!("added import is not a global");
1321        };
1322        assert_eq!(existing.get(&mut store), Value::I32(1));
1323        assert_eq!(added.get(&mut store), Value::I32(3));
1324    }
1325
1326    #[test]
1327    fn instantiation_state_round_trips_the_hook_data() {
1328        let state = InstantiationState::new(42u32);
1329        assert!(!state.is_empty());
1330        assert_eq!(state.take::<u32>().unwrap(), 42);
1331    }
1332
1333    #[test]
1334    fn empty_instantiation_state_carries_nothing() {
1335        let state = InstantiationState::empty();
1336        assert!(state.is_empty());
1337        let err = state.take::<u32>().unwrap_err();
1338        assert!(err.to_string().contains("missing instantiation state"));
1339    }
1340
1341    #[test]
1342    fn instantiation_state_from_another_hook_is_rejected() {
1343        // What a hook receiving state that isn't its own must see, rather than
1344        // silently operating on another instantiation's data.
1345        let state = InstantiationState::new("some other hook's state");
1346        let err = state.take::<u32>().unwrap_err();
1347        assert!(err.to_string().contains("does not belong to this hook"));
1348    }
1349}