wasmtime/runtime/vm/
interpreter.rs

1use crate::prelude::*;
2use crate::runtime::vm::vmcontext::VMArrayCallNative;
3use crate::runtime::vm::{tls, TrapRegisters, TrapTest, VMContext, VMOpaqueContext};
4use crate::{Engine, ValRaw};
5use core::ptr::NonNull;
6use pulley_interpreter::interp::{DoneReason, RegType, TrapKind, Val, Vm, XRegVal};
7use pulley_interpreter::{FReg, Reg, XReg};
8use wasmtime_environ::{BuiltinFunctionIndex, HostCall, Trap};
9
10/// Interpreter state stored within a `Store<T>`.
11#[repr(transparent)]
12pub struct Interpreter {
13    /// Pulley VM state, stored behind a `Box<T>` to make the storage in
14    /// `Store<T>` only pointer-sized (that way if you enable pulley but don't
15    /// use it it's low-overhead).
16    pulley: Box<Vm>,
17}
18
19impl Interpreter {
20    /// Creates a new interpreter ready to interpret code.
21    pub fn new(engine: &Engine) -> Interpreter {
22        let ret = Interpreter {
23            pulley: Box::new(Vm::with_stack(engine.config().max_wasm_stack)),
24        };
25        engine.profiler().register_interpreter(&ret);
26        ret
27    }
28
29    /// Returns the `InterpreterRef` structure which can be used to actually
30    /// execute interpreted code.
31    pub fn as_interpreter_ref(&mut self) -> InterpreterRef<'_> {
32        InterpreterRef(&mut self.pulley)
33    }
34
35    pub fn pulley(&self) -> &Vm {
36        &self.pulley
37    }
38}
39
40/// Wrapper around `&mut pulley_interpreter::Vm` to enable compiling this to a
41/// zero-sized structure when pulley is disabled at compile time.
42#[repr(transparent)]
43pub struct InterpreterRef<'a>(&'a mut Vm);
44
45/// Equivalent of a native platform's `jmp_buf` (sort of).
46///
47/// This structure ensures that all callee-save state in Pulley is saved at wasm
48/// function boundaries. This handles the case for example where a function is
49/// executed but it traps halfway through. The trap will unwind the Pulley stack
50/// and reset it back to what it was when the function started. This means that
51/// Pulley function prologues don't execute and callee-saved registers aren't
52/// restored. This structure is used to restore all that state to as it was
53/// when the function started.
54///
55/// Note that this is a blind copy of all callee-saved state which is kept in
56/// sync with `pulley_shared/abi.rs` in Cranelift. This includes the upper 16
57/// x-regs, the upper 16 f-regs, the frame pointer, and the link register. The
58/// stack pointer is included in the upper 16 x-regs. This representation is
59/// explicitly chosen over an alternative such as only saving a bare minimum
60/// amount of state and using function ABIs to auto-save registers. For example
61/// we could, in Cranelift, indicate that the Pulley-to-host function call
62/// clobbered all registers forcing the function prologue to save all
63/// xregs/fregs. This means though that every wasm->host call would save/restore
64/// all this state, even when a trap didn't happen. Alternatively this structure
65/// being large means that the state is only saved once per host->wasm call
66/// instead which is currently what's being optimized for.
67///
68/// If saving this structure is a performance hot spot in the future it might be
69/// worth reevaluating this decision or perhaps shrinking the register file of
70/// Pulley so less state need be saved.
71#[derive(Clone, Copy)]
72struct Setjmp {
73    xregs: [u64; 16],
74    fregs: [f64; 16],
75    fp: *mut u8,
76    lr: *mut u8,
77}
78
79impl InterpreterRef<'_> {
80    /// Invokes interpreted code.
81    ///
82    /// The `bytecode` pointer should previously have been produced by Cranelift
83    /// and `callee` / `caller` / `args_and_results` are normal array-call
84    /// arguments being passed around.
85    pub unsafe fn call(
86        mut self,
87        mut bytecode: NonNull<u8>,
88        callee: NonNull<VMOpaqueContext>,
89        caller: NonNull<VMOpaqueContext>,
90        args_and_results: NonNull<[ValRaw]>,
91    ) -> bool {
92        // Initialize argument registers with the ABI arguments.
93        let args = [
94            XRegVal::new_ptr(callee.as_ptr()).into(),
95            XRegVal::new_ptr(caller.as_ptr()).into(),
96            XRegVal::new_ptr(args_and_results.cast::<u8>().as_ptr()).into(),
97            XRegVal::new_u64(args_and_results.len() as u64).into(),
98        ];
99
100        // Fake a "poor man's setjmp" for now by saving some critical context to
101        // get restored when a trap happens. This pseudo-implements the stack
102        // unwinding necessary for a trap.
103        //
104        // See more comments in `trap` below about how this isn't actually
105        // correct as it's not saving all callee-save state.
106        let setjmp = self.setjmp();
107
108        let old_lr = self.0.call_start(&args);
109
110        // Run the interpreter as much as possible until it finishes, and then
111        // handle each finish condition differently.
112        let ret = loop {
113            match self.0.call_run(bytecode) {
114                // If the VM returned entirely then read the return value and
115                // return that (it indicates whether a trap happened or not.
116                DoneReason::ReturnToHost(()) => {
117                    match self.0.call_end(old_lr, [RegType::XReg]).next().unwrap() {
118                        #[allow(
119                            clippy::cast_possible_truncation,
120                            reason = "intentionally reading the lower bits only"
121                        )]
122                        Val::XReg(xreg) => break (xreg.get_u32() as u8) != 0,
123                        _ => unreachable!(),
124                    }
125                }
126                // If the VM wants to call out to the host then dispatch that
127                // here based on `sig`. Once that returns we can resume
128                // execution at `resume`.
129                //
130                // Note that the `raise` libcall is handled specially here since
131                // longjmp/setjmp is handled differently than on the host.
132                DoneReason::CallIndirectHost { id, resume } => {
133                    if u32::from(id) == HostCall::Builtin(BuiltinFunctionIndex::raise()).index() {
134                        self.longjmp(setjmp);
135                        break false;
136                    } else {
137                        self.call_indirect_host(id);
138                        bytecode = resume;
139                    }
140                }
141                // If the VM trapped then process that here and return `false`.
142                DoneReason::Trap { pc, kind } => {
143                    self.trap(pc, kind, setjmp);
144                    break false;
145                }
146            }
147        };
148
149        if cfg!(debug_assertions) {
150            for (i, reg) in callee_save_xregs() {
151                assert!(self.0[reg].get_u64() == setjmp.xregs[i]);
152            }
153            for (i, reg) in callee_save_fregs() {
154                assert!(self.0[reg].get_f64().to_bits() == setjmp.fregs[i].to_bits());
155            }
156            assert!(self.0.fp() == setjmp.fp);
157            assert!(self.0.lr() == setjmp.lr);
158        }
159        ret
160    }
161
162    /// Handles an interpreter trap. This will initialize the trap state stored
163    /// in TLS via the `test_if_trap` helper below by reading the pc/fp of the
164    /// interpreter and seeing if that's a valid opcode to trap at.
165    fn trap(&mut self, pc: NonNull<u8>, kind: Option<TrapKind>, setjmp: Setjmp) {
166        let regs = TrapRegisters {
167            pc: pc.as_ptr() as usize,
168            fp: self.0.fp() as usize,
169        };
170        tls::with(|s| {
171            let s = s.unwrap();
172            match kind {
173                Some(kind) => {
174                    let trap = match kind {
175                        TrapKind::IntegerOverflow => Trap::IntegerOverflow,
176                        TrapKind::DivideByZero => Trap::IntegerDivisionByZero,
177                        TrapKind::BadConversionToInteger => Trap::BadConversionToInteger,
178                        TrapKind::MemoryOutOfBounds => Trap::MemoryOutOfBounds,
179                    };
180                    s.set_jit_trap(regs, None, trap);
181                }
182                None => {
183                    match s.test_if_trap(regs, None, |_| false) {
184                        // This shouldn't be possible, so this is a fatal error
185                        // if it happens.
186                        TrapTest::NotWasm => {
187                            panic!("pulley trap at {pc:?} without trap code registered")
188                        }
189
190                        // Not possible with our closure above returning `false`.
191                        #[cfg(has_host_compiler_backend)]
192                        TrapTest::HandledByEmbedder => unreachable!(),
193
194                        // Trap was handled, yay! We don't use `jmp_buf`.
195                        TrapTest::Trap { .. } => {}
196                    }
197                }
198            }
199        });
200
201        self.longjmp(setjmp);
202    }
203
204    fn setjmp(&self) -> Setjmp {
205        let mut xregs = [0; 16];
206        let mut fregs = [0.0; 16];
207        for (i, reg) in callee_save_xregs() {
208            xregs[i] = self.0[reg].get_u64();
209        }
210        for (i, reg) in callee_save_fregs() {
211            fregs[i] = self.0[reg].get_f64();
212        }
213        Setjmp {
214            xregs,
215            fregs,
216            fp: self.0.fp(),
217            lr: self.0.lr(),
218        }
219    }
220
221    /// Perform a "longjmp" by restoring the "setjmp" context saved when this
222    /// started.
223    fn longjmp(&mut self, setjmp: Setjmp) {
224        let Setjmp {
225            xregs,
226            fregs,
227            fp,
228            lr,
229        } = setjmp;
230        unsafe {
231            for (i, reg) in callee_save_xregs() {
232                self.0[reg].set_u64(xregs[i]);
233            }
234            for (i, reg) in callee_save_fregs() {
235                self.0[reg].set_f64(fregs[i]);
236            }
237            self.0.set_fp(fp);
238            self.0.set_lr(lr);
239        }
240    }
241
242    /// Handles the `call_indirect_host` instruction, dispatching the `sig`
243    /// number here which corresponds to `wasmtime_environ::HostCall`.
244    #[allow(
245        clippy::cast_possible_truncation,
246        clippy::cast_sign_loss,
247        unused_macro_rules,
248        reason = "macro-generated code"
249    )]
250    #[cfg_attr(
251        not(feature = "component-model"),
252        expect(unused_macro_rules, reason = "macro-code")
253    )]
254    unsafe fn call_indirect_host(&mut self, id: u8) {
255        let id = u32::from(id);
256        let fnptr = self.0[XReg::x0].get_ptr();
257        let mut arg_reg = 1;
258
259        /// Helper macro to invoke a builtin.
260        ///
261        /// Used as:
262        ///
263        /// `call(@builtin(ty1, ty2, ...) -> retty)` - invoke a core or
264        /// component builtin with the macro-defined signature.
265        ///
266        /// `call(@host Ty(ty1, ty2, ...) -> retty)` - invoke a host function
267        /// with the type `Ty`. The other types in the macro are checked by
268        /// rustc to match the actual `Ty` definition in Rust.
269        macro_rules! call {
270            (@builtin($($param:ident),*) $(-> $result:ident)?) => {{
271                type T = unsafe extern "C" fn($(call!(@ty $param)),*) $(-> call!(@ty $result))?;
272                call!(@host T($($param),*) $(-> $result)?);
273            }};
274            (@host $ty:ident($($param:ident),*) $(-> $result:ident)?) => {{
275                // Convert the pointer from pulley to a native function pointer.
276                union GetNative {
277                    fnptr: *mut u8,
278                    host: $ty,
279                }
280                let host = GetNative { fnptr }.host;
281
282                // Decode each argument according to this macro, pulling
283                // arguments from successive registers.
284                let ret = host($({
285                    let reg = XReg::new(arg_reg).unwrap();
286                    arg_reg += 1;
287                    call!(@get $param reg)
288                }),*);
289                let _ = arg_reg; // silence last dead arg_reg increment warning
290
291                // Store the return value, if one is here, in x0.
292                $(
293                    let dst = XReg::x0;
294                    call!(@set $result dst ret);
295                )?
296                let _ = ret; // silence warning if no return value
297
298                // Return from the outer `call_indirect_host` host function as
299                // it's been processed.
300                return;
301            }};
302
303            // Conversion from macro-defined types to Rust host types.
304            (@ty bool) => (bool);
305            (@ty u8) => (u8);
306            (@ty u32) => (u32);
307            (@ty i32) => (i32);
308            (@ty u64) => (u64);
309            (@ty i64) => (i64);
310            (@ty vmctx) => (*mut VMContext);
311            (@ty pointer) => (*mut u8);
312            (@ty ptr_u8) => (*mut u8);
313            (@ty ptr_u16) => (*mut u16);
314            (@ty ptr_size) => (*mut usize);
315            (@ty size) => (usize);
316
317            // Conversion from a pulley register value to the macro-defined
318            // type.
319            (@get u8 $reg:ident) => (self.0[$reg].get_i32() as u8);
320            (@get u32 $reg:ident) => (self.0[$reg].get_u32());
321            (@get u64 $reg:ident) => (self.0[$reg].get_u64());
322            (@get vmctx $reg:ident) => (self.0[$reg].get_ptr());
323            (@get pointer $reg:ident) => (self.0[$reg].get_ptr());
324            (@get ptr $reg:ident) => (self.0[$reg].get_ptr());
325            (@get nonnull $reg:ident) => (NonNull::new(self.0[$reg].get_ptr()).unwrap());
326            (@get ptr_u8 $reg:ident) => (self.0[$reg].get_ptr());
327            (@get ptr_u16 $reg:ident) => (self.0[$reg].get_ptr());
328            (@get ptr_size $reg:ident) => (self.0[$reg].get_ptr());
329            (@get size $reg:ident) => (self.0[$reg].get_ptr::<u8>() as usize);
330
331            // Conversion from a Rust value back into a macro-defined type,
332            // stored in a pulley register.
333            (@set bool $reg:ident $val:ident) => (self.0[$reg].set_i32(i32::from($val)));
334            (@set u32 $reg:ident $val:ident) => (self.0[$reg].set_u32($val));
335            (@set u64 $reg:ident $val:ident) => (self.0[$reg].set_u64($val));
336            (@set pointer $reg:ident $val:ident) => (self.0[$reg].set_ptr($val));
337            (@set size $reg:ident $val:ident) => (self.0[$reg].set_ptr($val as *mut u8));
338        }
339
340        // With the helper macro above structure this into:
341        //
342        // foreach [core, component]
343        //   * dispatch the call-the-host function pointer type
344        //   * dispatch all builtins by their index.
345        //
346        // The hope is that this is relatively easy for LLVM to optimize since
347        // it's a bunch of:
348        //
349        //  if id == 0 { ...;  return; }
350        //  if id == 1 { ...;  return; }
351        //  if id == 2 { ...;  return; }
352        //  ...
353        //
354
355        if id == const { HostCall::ArrayCall.index() } {
356            call!(@host VMArrayCallNative(nonnull, nonnull, nonnull, size) -> bool);
357        }
358
359        macro_rules! core {
360            (
361                $(
362                    $( #[cfg($attr:meta)] )?
363                    $name:ident($($pname:ident: $param:ident ),* ) $(-> $result:ident)?;
364                )*
365            ) => {
366                $(
367                    $( #[cfg($attr)] )?
368                    if id == const { HostCall::Builtin(BuiltinFunctionIndex::$name()).index() } {
369                        call!(@builtin($($param),*) $(-> $result)?);
370                    }
371                )*
372            }
373        }
374        wasmtime_environ::foreach_builtin_function!(core);
375
376        #[cfg(feature = "component-model")]
377        {
378            use crate::runtime::vm::component::VMLoweringCallee;
379            use wasmtime_environ::component::ComponentBuiltinFunctionIndex;
380
381            if id == const { HostCall::ComponentLowerImport.index() } {
382                call!(@host VMLoweringCallee(nonnull, nonnull, u32, u32, nonnull, ptr, ptr, u8, u8, nonnull, size) -> bool);
383            }
384
385            macro_rules! component {
386                (
387                    $(
388                        $( #[cfg($attr:meta)] )?
389                        $name:ident($($pname:ident: $param:ident ),* ) $(-> $result:ident)?;
390                    )*
391                ) => {
392                    $(
393                        $( #[cfg($attr)] )?
394                        if id == const { HostCall::ComponentBuiltin(ComponentBuiltinFunctionIndex::$name()).index() } {
395                            call!(@builtin($($param),*) $(-> $result)?);
396                        }
397                    )*
398                }
399            }
400            wasmtime_environ::foreach_builtin_component_function!(component);
401        }
402
403        // if we got this far then something has gone seriously wrong.
404        unreachable!()
405    }
406}
407
408fn callee_save_xregs() -> impl Iterator<Item = (usize, XReg)> {
409    (0..16).map(|i| (i.into(), XReg::new(i + 16).unwrap()))
410}
411
412fn callee_save_fregs() -> impl Iterator<Item = (usize, FReg)> {
413    (0..16).map(|i| (i.into(), FReg::new(i + 16).unwrap()))
414}