wapc/wapchost/
host_async.rs

1use std::sync::{atomic::Ordering, Arc};
2
3use tokio::sync::Mutex;
4
5use crate::{
6  wapchost::{
7    errors, modulestate_async::ModuleStateAsync, traits::WebAssemblyEngineProviderAsync, Invocation, Result,
8    GLOBAL_MODULE_COUNT,
9  },
10  HostCallbackAsync,
11};
12
13/// A WebAssembly host runtime for waPC-compliant modules that can be used in async contexts
14///
15/// Use an instance of this struct to provide a means of invoking procedure calls by
16/// specifying an operation name and a set of bytes representing the opaque operation payload.
17/// `WapcHostAsync` makes no assumptions about the contents or format of either the payload or the
18/// operation name, other than that the operation name is a UTF-8 encoded string.
19#[must_use]
20pub struct WapcHostAsync {
21  engine: Mutex<Box<dyn WebAssemblyEngineProviderAsync + Send>>,
22  state: Arc<ModuleStateAsync>,
23}
24
25impl std::fmt::Debug for WapcHostAsync {
26  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27    f.debug_struct("WapcHostAsync").field("state", &self.state).finish()
28  }
29}
30
31impl WapcHostAsync {
32  /// Creates a new instance of a waPC-compliant host runtime paired with a given
33  /// low-level engine provider
34  pub async fn new(
35    engine: Box<dyn WebAssemblyEngineProviderAsync + Send>,
36    host_callback: Option<Box<HostCallbackAsync>>,
37  ) -> Result<Self> {
38    let id = GLOBAL_MODULE_COUNT.fetch_add(1, Ordering::SeqCst);
39
40    let state = Arc::new(ModuleStateAsync::new(host_callback, id));
41
42    let mh = WapcHostAsync {
43      engine: Mutex::new(engine),
44      state: state.clone(),
45    };
46
47    mh.initialize(state).await?;
48
49    Ok(mh)
50  }
51
52  async fn initialize(&self, state: Arc<ModuleStateAsync>) -> Result<()> {
53    match self.engine.lock().await.init(state).await {
54      Ok(_) => Ok(()),
55      Err(e) => Err(errors::Error::InitFailed(e.to_string())),
56    }
57  }
58
59  /// Returns a reference to the unique identifier of this module. If a parent process
60  /// has instantiated multiple `WapcHost`s, then the single static host callback function
61  /// will contain this value to allow disambiguation of modules
62  pub fn id(&self) -> u64 {
63    self.state.id
64  }
65
66  /// Invokes the `__guest_call` function within the guest module as per the waPC specification.
67  /// Provide an operation name and an opaque payload of bytes and the function returns a `Result`
68  /// containing either an error or an opaque reply of bytes.
69  ///
70  /// It is worth noting that the _first_ time `call` is invoked, the WebAssembly module
71  /// might incur a "cold start" penalty, depending on which underlying engine you're using. This
72  /// might be due to lazy initialization or JIT-compilation.
73  pub async fn call(&self, op: &str, payload: &[u8]) -> Result<Vec<u8>> {
74    let inv = Invocation::new(op, payload.to_vec());
75    let op_len = inv.operation.len();
76    let msg_len = inv.msg.len();
77
78    {
79      *self.state.guest_response.write().await = None;
80      *self.state.guest_request.write().await = Some(inv);
81      *self.state.guest_error.write().await = None;
82      *self.state.host_response.write().await = None;
83      *self.state.host_error.write().await = None;
84    }
85
86    let callresult = match self.engine.lock().await.call(op_len as i32, msg_len as i32).await {
87      Ok(c) => c,
88      Err(e) => {
89        return Err(errors::Error::GuestCallFailure(e.to_string()));
90      }
91    };
92
93    if callresult == 0 {
94      // invocation failed
95      let lock = self.state.guest_error.read().await;
96      lock.as_ref().map_or_else(
97        || {
98          Err(errors::Error::GuestCallFailure(
99            "No error message set for call failure".to_owned(),
100          ))
101        },
102        |s| Err(errors::Error::GuestCallFailure(s.clone())),
103      )
104    } else {
105      // invocation succeeded
106      match self.state.guest_response.read().await.as_ref() {
107        Some(r) => Ok(r.clone()),
108        None => {
109          let lock = self.state.guest_error.read().await;
110          lock.as_ref().map_or_else(
111            || {
112              Err(errors::Error::GuestCallFailure(
113                "No error message OR response set for call success".to_owned(),
114              ))
115            },
116            |s| Err(errors::Error::GuestCallFailure(s.clone())),
117          )
118        }
119      }
120    }
121  }
122
123  /// Performs a live "hot swap" of the WebAssembly module. Since all internal waPC execution is assumed to be
124  /// single-threaded and non-reentrant, this call is synchronous and so
125  /// you should never attempt to invoke `call` from another thread while performing this hot swap.
126  ///
127  /// **Note**: if the underlying engine you've chosen is a JITting engine, then performing a swap
128  /// will re-introduce a "cold start" delay upon the next function call.
129  ///
130  /// If you perform a hot swap of a WASI module, you cannot alter the parameters used to create the WASI module
131  /// like the environment variables, mapped directories, pre-opened files, etc. Not abiding by this could lead
132  /// to privilege escalation attacks or non-deterministic behavior after the swap.
133  pub async fn replace_module(&self, module: &[u8]) -> Result<()> {
134    match self.engine.lock().await.replace(module).await {
135      Ok(_) => Ok(()),
136      Err(e) => Err(errors::Error::ReplacementFailed(e.to_string())),
137    }
138  }
139}