Skip to main content

wasmi/engine/
resumable.rs

1use super::Func;
2use crate::{
3    engine::Stack,
4    func::{CallResultsTuple, FuncError},
5    ir::SlotSpan,
6    AsContext,
7    AsContextMut,
8    Engine,
9    Error,
10    TrapCode,
11    Val,
12    WasmResults,
13};
14use core::{fmt, marker::PhantomData, mem::replace, ops::Deref};
15
16/// Returned by [`Engine`] methods for calling a function in a resumable way.
17///
18/// # Note
19///
20/// This is the base type for resumable call results and can be converted into
21/// either the dynamically typed [`ResumableCall`] or the statically typed
22/// [`TypedResumableCall`] that act as user facing API. Therefore this type
23/// must provide all the information necessary to be properly converted into
24/// either user facing types.
25#[derive(Debug)]
26pub(crate) enum ResumableCallBase<T> {
27    /// The resumable call has finished properly and returned a result.
28    Finished(T),
29    /// The resumable call encountered a host error and can be resumed.
30    HostTrap(ResumableCallHostTrap),
31    /// The resumable call ran out of fuel and can be resumed.
32    OutOfFuel(ResumableCallOutOfFuel),
33}
34
35/// Any resumable error.
36#[derive(Debug)]
37pub enum ResumableError {
38    HostTrap(ResumableHostTrapError),
39    OutOfFuel(ResumableOutOfFuelError),
40}
41
42impl ResumableError {
43    /// Consumes `self` to return the underlying [`Error`].
44    pub fn into_error(self) -> Error {
45        match self {
46            ResumableError::HostTrap(error) => error.into_error(),
47            ResumableError::OutOfFuel(error) => error.into_error(),
48        }
49    }
50}
51
52/// Error returned from a called host function in a resumable state.
53#[derive(Debug)]
54pub struct ResumableHostTrapError {
55    /// The error returned by the called host function.
56    host_error: Error,
57    /// The host function that returned the error.
58    host_func: Func,
59    /// The result registers of the caller of the host function.
60    caller_results: SlotSpan,
61}
62
63impl core::error::Error for ResumableHostTrapError {}
64
65impl fmt::Display for ResumableHostTrapError {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        self.host_error.fmt(f)
68    }
69}
70
71impl ResumableHostTrapError {
72    /// Creates a new [`ResumableHostTrapError`].
73    #[cold]
74    pub(crate) fn new(host_error: Error, host_func: Func, caller_results: SlotSpan) -> Self {
75        Self {
76            host_error,
77            host_func,
78            caller_results,
79        }
80    }
81
82    /// Consumes `self` to return the underlying [`Error`].
83    pub(crate) fn into_error(self) -> Error {
84        self.host_error
85    }
86
87    /// Returns the [`Func`] of the [`ResumableHostTrapError`].
88    pub(crate) fn host_func(&self) -> &Func {
89        &self.host_func
90    }
91
92    /// Returns the caller results [`SlotSpan`] of the [`ResumableHostTrapError`].
93    pub(crate) fn caller_results(&self) -> &SlotSpan {
94        &self.caller_results
95    }
96}
97
98/// Error returned from a called host function in a resumable state.
99#[derive(Debug)]
100pub struct ResumableOutOfFuelError {
101    /// The minimum required amount of fuel to progress execution.
102    required_fuel: u64,
103}
104
105impl core::error::Error for ResumableOutOfFuelError {}
106
107impl fmt::Display for ResumableOutOfFuelError {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(
110            f,
111            "ran out of fuel while calling a resumable function: required_fuel={}",
112            self.required_fuel
113        )
114    }
115}
116
117impl ResumableOutOfFuelError {
118    /// Creates a new [`ResumableOutOfFuelError`].
119    #[cold]
120    pub(crate) fn new(required_fuel: u64) -> Self {
121        Self { required_fuel }
122    }
123
124    /// Consumes `self` to return the underlying [`Error`].
125    pub(crate) fn required_fuel(self) -> u64 {
126        self.required_fuel
127    }
128
129    /// Consumes `self` to return the underlying [`Error`].
130    pub(crate) fn into_error(self) -> Error {
131        Error::from(TrapCode::OutOfFuel)
132    }
133}
134
135/// Returned by calling a [`Func`] in a resumable way.
136#[derive(Debug)]
137pub enum ResumableCall {
138    /// The resumable call has finished properly and returned a result.
139    Finished,
140    /// The resumable call encountered a host error and can be resumed.
141    HostTrap(ResumableCallHostTrap),
142    /// The resumable call ran out of fuel but can be resumed.
143    OutOfFuel(ResumableCallOutOfFuel),
144}
145
146impl ResumableCall {
147    /// Creates a [`ResumableCall`] from the [`Engine`]'s base [`ResumableCallBase`].
148    pub(crate) fn new(call: ResumableCallBase<()>) -> Self {
149        match call {
150            ResumableCallBase::Finished(()) => Self::Finished,
151            ResumableCallBase::HostTrap(invocation) => Self::HostTrap(invocation),
152            ResumableCallBase::OutOfFuel(invocation) => Self::OutOfFuel(invocation),
153        }
154    }
155}
156
157/// Common state for resumable calls.
158#[derive(Debug)]
159pub struct ResumableCallCommon {
160    /// The engine in use for the function invocation.
161    ///
162    /// # Note
163    ///
164    /// - This handle is required to resolve function types
165    ///   of both `func` and `host_func` fields as well as in
166    ///   the `Drop` impl to recycle the stack.
167    engine: Engine,
168    /// The underlying root function to be executed.
169    ///
170    /// # Note
171    ///
172    /// The results of this function must always match with the
173    /// results given when resuming the call.
174    func: Func,
175    /// The value and call stack in use by the [`ResumableCallHostTrap`].
176    ///
177    /// # Note
178    ///
179    /// - We need to keep the stack around since the user might want to
180    ///   resume the execution.
181    /// - This stack is borrowed from the engine and needs to be given
182    ///   back to the engine when the [`ResumableCallHostTrap`] goes out
183    ///   of scope.
184    stack: Stack,
185}
186
187impl ResumableCallCommon {
188    /// Creates a new [`ResumableCallCommon`].
189    pub(super) fn new(engine: Engine, func: Func, stack: Stack) -> Self {
190        Self {
191            engine,
192            func,
193            stack,
194        }
195    }
196
197    /// Replaces the internal stack with an empty one that has no heap allocations.
198    pub(super) fn take_stack(&mut self) -> Stack {
199        replace(&mut self.stack, Stack::empty())
200    }
201
202    /// Returns an exclusive reference to the underlying [`Stack`].
203    pub(super) fn stack_mut(&mut self) -> &mut Stack {
204        &mut self.stack
205    }
206
207    /// Prepares the `outputs` buffer for call resumption.
208    ///
209    /// # Errors
210    ///
211    /// If the number of items in `outputs` does not match the number of results of the resumed function.
212    fn prepare_outputs<T>(
213        &self,
214        ctx: impl AsContext<Data = T>,
215        outputs: &mut [Val],
216    ) -> Result<(), Error> {
217        self.engine.resolve_func_type(
218            self.func.ty_dedup(ctx.as_context()),
219            |func_type| -> Result<(), Error> {
220                func_type.prepare_outputs(outputs)?;
221                Ok(())
222            },
223        )
224    }
225}
226
227impl Drop for ResumableCallCommon {
228    fn drop(&mut self) {
229        let stack = self.take_stack();
230        self.engine.recycle_stack(stack);
231    }
232}
233
234// # Safety
235//
236// `ResumableCallCommon` is not `Sync` because of the following sequence of fields:
237//
238// - `ResumableCallCommon`'s `Stack` is not `Sync`
239// - `Stack`'s `CallStack` is not `Sync`
240//     - `CallStack`'s `CallFrame` sequence is not `Sync`
241//     - `CallFrame`'s `InstructionPtr` is not `Sync`:
242//       Thi is because it is a raw pointer to an `Op` buffer owned by the [`Engine`].
243//
244// Since `Engine` is owned by `ResumableCallCommon` it cannot be outlived.
245// Also the `Op` buffers that are pointed to by the `InstructionPtr` are immutable.
246//
247// Therefore `ResumableCallCommon` can safely be assumed to be `Sync`.
248unsafe impl Sync for ResumableCallCommon {}
249
250/// State required to resume a [`Func`] invocation after a host trap.
251#[derive(Debug)]
252pub struct ResumableCallHostTrap {
253    /// Common state for resumable calls.
254    pub(super) common: ResumableCallCommon,
255    /// The host function that returned a host error.
256    ///
257    /// # Note
258    ///
259    /// - This is required to receive its result values that are
260    ///   needed to be fed back in manually by the user. This way we
261    ///   avoid heap memory allocations.
262    /// - The results of this function must always match with the
263    ///   arguments given when resuming the call.
264    host_func: Func,
265    /// The host error that was returned by the `host_func` which
266    /// caused the resumable function invocation to break.
267    ///
268    /// # Note
269    ///
270    /// This might be useful to users of this API to inspect the
271    /// actual host error. This is therefore guaranteed to never
272    /// be a Wasm trap.
273    host_error: Error,
274    /// The registers where to put provided host function results upon resumption.
275    ///
276    /// # Note
277    ///
278    /// This is only needed for the register-machine Wasmi engine backend.
279    caller_results: SlotSpan,
280}
281
282impl ResumableCallHostTrap {
283    /// Creates a new [`ResumableCallHostTrap`].
284    pub(super) fn new(
285        engine: Engine,
286        func: Func,
287        host_func: Func,
288        host_error: Error,
289        caller_results: SlotSpan,
290        stack: Stack,
291    ) -> Self {
292        Self {
293            common: ResumableCallCommon::new(engine, func, stack),
294            host_func,
295            host_error,
296            caller_results,
297        }
298    }
299
300    /// Updates the [`ResumableCallHostTrap`] with the new `host_func`, `host_error` and `caller_results`.
301    ///
302    /// # Note
303    ///
304    /// This should only be called from the register-machine Wasmi engine backend.
305    pub(super) fn update(&mut self, host_func: Func, host_error: Error, caller_results: SlotSpan) {
306        self.host_func = host_func;
307        self.host_error = host_error;
308        self.caller_results = caller_results;
309    }
310
311    /// Updates the [`ResumableCallHostTrap`] to a [`ResumableCallOutOfFuel`].
312    pub(super) fn update_to_out_of_fuel(self, required_fuel: u64) -> ResumableCallOutOfFuel {
313        ResumableCallOutOfFuel {
314            common: self.common,
315            required_fuel,
316        }
317    }
318
319    /// Returns the host [`Func`] that returned the host error.
320    ///
321    /// # Note
322    ///
323    /// When using [`ResumableCallHostTrap::resume`] the `inputs`
324    /// need to match the results of this host function so that
325    /// the function invocation can properly resume. For that
326    /// number and types of the values provided must match.
327    pub fn host_func(&self) -> Func {
328        self.host_func
329    }
330
331    /// Returns a shared reference to the encountered host error.
332    ///
333    /// # Note
334    ///
335    /// This is guaranteed to never be a Wasm trap.
336    pub fn host_error(&self) -> &Error {
337        &self.host_error
338    }
339
340    /// Consumes `self` and returns the encountered host error.
341    ///
342    /// # Note
343    ///
344    /// This is guaranteed to never be a Wasm trap.
345    pub fn into_host_error(self) -> Error {
346        self.host_error
347    }
348
349    /// Returns the caller results [`SlotSpan`].
350    ///
351    /// # Note
352    ///
353    /// This is `Some` only for [`ResumableCallHostTrap`] originating from the register-machine Wasmi engine.
354    pub(crate) fn caller_results(&self) -> SlotSpan {
355        self.caller_results
356    }
357
358    /// Validates that the `inputs` types are valid for the host function resumption.
359    ///
360    /// # Errors
361    ///
362    /// If the `inputs` types do not match.
363    fn validate_inputs<T>(
364        &self,
365        ctx: impl AsContext<Data = T>,
366        inputs: &[Val],
367    ) -> Result<(), FuncError> {
368        self.common
369            .engine
370            .resolve_func_type(self.host_func().ty_dedup(ctx.as_context()), |func_type| {
371                func_type.match_results(inputs)
372            })
373    }
374
375    /// Resumes the call to the [`Func`] with the given inputs.
376    ///
377    /// The result is written back into the `outputs` buffer upon success.
378    ///
379    /// Returns a resumable handle to the function invocation upon
380    /// encountering host errors with which it is possible to handle
381    /// the error and continue the execution as if no error occurred.
382    ///
383    /// # Errors
384    ///
385    /// - If the function resumption returned a Wasm [`Error`].
386    /// - If the types or the number of values in `inputs` does not match
387    ///   the types and number of result values of the erroneous host function.
388    /// - If the number of output values does not match the expected number of
389    ///   outputs required by the called function.
390    pub fn resume<T>(
391        self,
392        mut ctx: impl AsContextMut<Data = T>,
393        inputs: &[Val],
394        outputs: &mut [Val],
395    ) -> Result<ResumableCall, Error> {
396        self.validate_inputs(ctx.as_context(), inputs)?;
397        self.common.prepare_outputs(ctx.as_context(), outputs)?;
398        self.common
399            .engine
400            .clone()
401            .resume_func_host_trap(ctx.as_context_mut(), self, inputs, outputs)
402            .map(ResumableCall::new)
403    }
404}
405
406/// State required to resume a [`Func`] invocation after a host trap.
407#[derive(Debug)]
408pub struct ResumableCallOutOfFuel {
409    /// Common state for resumable calls.
410    pub(super) common: ResumableCallCommon,
411    /// The minimum fuel required to progress execution.
412    required_fuel: u64,
413}
414
415impl ResumableCallOutOfFuel {
416    /// Creates a new [`ResumableCallOutOfFuel`].
417    pub(super) fn new(engine: Engine, func: Func, stack: Stack, required_fuel: u64) -> Self {
418        Self {
419            common: ResumableCallCommon::new(engine, func, stack),
420            required_fuel,
421        }
422    }
423
424    /// Updates the [`ResumableCallOutOfFuel`] with the new `required_fuel`.
425    ///
426    /// # Note
427    ///
428    /// This should only be called from the register-machine Wasmi engine backend.
429    pub(super) fn update(&mut self, required_fuel: u64) {
430        self.required_fuel = required_fuel;
431    }
432
433    /// Updates the [`ResumableCallHostTrap`] to a [`ResumableCallOutOfFuel`].
434    pub(super) fn update_to_host_trap(
435        self,
436        host_func: Func,
437        host_error: Error,
438        caller_results: SlotSpan,
439    ) -> ResumableCallHostTrap {
440        ResumableCallHostTrap {
441            common: self.common,
442            host_func,
443            host_error,
444            caller_results,
445        }
446    }
447
448    /// Returns the minimum required fuel to progress execution.
449    pub fn required_fuel(&self) -> u64 {
450        self.required_fuel
451    }
452
453    /// Resumes the call to the [`Func`] with the given inputs.
454    ///
455    /// The result is written back into the `outputs` buffer upon success.
456    /// Returns a resumable handle to the function invocation.
457    ///
458    /// # Errors
459    ///
460    /// - If the function resumption returned a Wasm [`Error`].
461    /// - If the types or the number of values in `inputs` does not match
462    ///   the types and number of result values of the erroneous host function.
463    /// - If the number of output values does not match the expected number of
464    ///   outputs required by the called function.
465    pub fn resume<T>(
466        self,
467        mut ctx: impl AsContextMut<Data = T>,
468        outputs: &mut [Val],
469    ) -> Result<ResumableCall, Error> {
470        self.common.prepare_outputs(ctx.as_context(), outputs)?;
471        self.common
472            .engine
473            .clone()
474            .resume_func_out_of_fuel(ctx.as_context_mut(), self, outputs)
475            .map(ResumableCall::new)
476    }
477}
478
479/// Returned by calling a [`TypedFunc`] in a resumable way.
480///
481/// [`TypedFunc`]: [`crate::TypedFunc`]
482#[derive(Debug)]
483pub enum TypedResumableCall<T> {
484    /// The resumable call has finished properly and returned a result.
485    Finished(T),
486    /// The resumable call encountered a host error and can be resumed.
487    HostTrap(TypedResumableCallHostTrap<T>),
488    /// The resumable call ran out of fuel and can be resumed.
489    OutOfFuel(TypedResumableCallOutOfFuel<T>),
490}
491
492impl<Results> TypedResumableCall<Results> {
493    /// Creates a [`TypedResumableCall`] from the [`Engine`]'s base [`ResumableCallBase`].
494    pub(crate) fn new(call: ResumableCallBase<Results>) -> Self {
495        match call {
496            ResumableCallBase::Finished(results) => Self::Finished(results),
497            ResumableCallBase::HostTrap(invocation) => {
498                Self::HostTrap(TypedResumableCallHostTrap::new(invocation))
499            }
500            ResumableCallBase::OutOfFuel(invocation) => {
501                Self::OutOfFuel(TypedResumableCallOutOfFuel::new(invocation))
502            }
503        }
504    }
505}
506
507/// State required to resume a [`TypedFunc`] invocation.
508///
509/// [`TypedFunc`]: [`crate::TypedFunc`]
510pub struct TypedResumableCallHostTrap<Results> {
511    invocation: ResumableCallHostTrap,
512    /// The parameter and result typed encoded in Rust type system.
513    results: PhantomData<fn() -> Results>,
514}
515
516impl<Results> TypedResumableCallHostTrap<Results> {
517    /// Creates a [`TypedResumableCallHostTrap`] wrapper for the given [`ResumableCallHostTrap`].
518    pub(crate) fn new(invocation: ResumableCallHostTrap) -> Self {
519        Self {
520            invocation,
521            results: PhantomData,
522        }
523    }
524
525    /// Resumes the call to the [`TypedFunc`] with the given inputs.
526    ///
527    /// Returns a resumable handle to the function invocation upon
528    /// encountering host errors with which it is possible to handle
529    /// the error and continue the execution as if no error occurred.
530    ///
531    /// # Errors
532    ///
533    /// - If the function resumption returned a Wasm [`Error`].
534    /// - If the types or the number of values in `inputs` does not match
535    ///   the types and number of result values of the erroneous host function.
536    ///
537    /// [`TypedFunc`]: [`crate::TypedFunc`]
538    pub fn resume<T>(
539        self,
540        mut ctx: impl AsContextMut<Data = T>,
541        inputs: &[Val],
542    ) -> Result<TypedResumableCall<Results>, Error>
543    where
544        Results: WasmResults,
545    {
546        self.invocation.validate_inputs(ctx.as_context(), inputs)?;
547        self.common
548            .engine
549            .clone()
550            .resume_func_host_trap(
551                ctx.as_context_mut(),
552                self.invocation,
553                inputs,
554                <CallResultsTuple<Results>>::default(),
555            )
556            .map(TypedResumableCall::new)
557    }
558}
559
560impl<Results> Deref for TypedResumableCallHostTrap<Results> {
561    type Target = ResumableCallHostTrap;
562
563    fn deref(&self) -> &Self::Target {
564        &self.invocation
565    }
566}
567
568impl<Results> fmt::Debug for TypedResumableCallHostTrap<Results> {
569    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
570        f.debug_struct("TypedResumableCallHostTrap")
571            .field("invocation", &self.invocation)
572            .field("results", &self.results)
573            .finish()
574    }
575}
576
577/// State required to resume a [`TypedFunc`] invocation after running out of fuel.
578///
579/// [`TypedFunc`]: [`crate::TypedFunc`]
580pub struct TypedResumableCallOutOfFuel<Results> {
581    invocation: ResumableCallOutOfFuel,
582    /// The parameter and result typed encoded in Rust type system.
583    results: PhantomData<fn() -> Results>,
584}
585
586impl<Results> TypedResumableCallOutOfFuel<Results> {
587    /// Creates a [`TypedResumableCallHostTrap`] wrapper for the given [`ResumableCallOutOfFuel`].
588    pub(crate) fn new(invocation: ResumableCallOutOfFuel) -> Self {
589        Self {
590            invocation,
591            results: PhantomData,
592        }
593    }
594
595    /// Resumes the call to the [`TypedFunc`] with the given inputs.
596    ///
597    /// Returns a resumable handle to the function invocation upon
598    /// encountering host errors with which it is possible to handle
599    /// the error and continue the execution as if no error occurred.
600    ///
601    /// # Errors
602    ///
603    /// - If the function resumption returned a Wasm [`Error`].
604    /// - If the types or the number of values in `inputs` does not match
605    ///   the types and number of result values of the erroneous host function.
606    ///
607    /// [`TypedFunc`]: [`crate::TypedFunc`]
608    pub fn resume<T>(
609        self,
610        mut ctx: impl AsContextMut<Data = T>,
611    ) -> Result<TypedResumableCall<Results>, Error>
612    where
613        Results: WasmResults,
614    {
615        self.common
616            .engine
617            .clone()
618            .resume_func_out_of_fuel(
619                ctx.as_context_mut(),
620                self.invocation,
621                <CallResultsTuple<Results>>::default(),
622            )
623            .map(TypedResumableCall::new)
624    }
625}
626
627impl<Results> Deref for TypedResumableCallOutOfFuel<Results> {
628    type Target = ResumableCallOutOfFuel;
629
630    fn deref(&self) -> &Self::Target {
631        &self.invocation
632    }
633}
634
635impl<Results> fmt::Debug for TypedResumableCallOutOfFuel<Results> {
636    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
637        f.debug_struct("TypedResumableCallOutOfFuel")
638            .field("invocation", &self.invocation)
639            .field("results", &self.results)
640            .finish()
641    }
642}