From a605175abe3e3578bdda2d91e565959dab908df2 Mon Sep 17 00:00:00 2001 From: Wei Tang Date: Tue, 10 Jul 2018 00:06:44 +0800 Subject: [PATCH] Resumable function invocation (#110) * Move call_stack to Interpreter struct * Accept func and args when creating the Interpreter * Create a RunState to indicate whether the current interpreter is recoverable * Add functionality to resume execution in Interpreter level * Implement resumable execution in func * Expose FuncInvocation and ResumableError * Fix missing docs for FuncInvocation * Add test for resumable invoke and move external parameter passing to start/resume_invocation * Add comments why assert is always true * Add note why value stack is always empty after execution * Use as_func * Document `resume_execution` on conditions for `is_resumable` and `resumable_value_type` * Document conditions where NotResumable and AlreadyStarted error is returned * Warn user that invoke_resumable is experimental --- src/func.rs | 156 +++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 12 +++- src/runner.rs | 131 ++++++++++++++++++++++++++++++-------- src/tests/host.rs | 66 +++++++++++++++++++- 4 files changed, 334 insertions(+), 31 deletions(-) diff --git a/src/func.rs b/src/func.rs index beeacea..109354e 100644 --- a/src/func.rs +++ b/src/func.rs @@ -3,8 +3,9 @@ use std::fmt; use parity_wasm::elements::Local; use {Trap, TrapKind, Signature}; use host::Externals; -use runner::{check_function_args, Interpreter}; +use runner::{check_function_args, Interpreter, InterpreterState}; use value::RuntimeValue; +use types::ValueType; use module::ModuleInstance; use isa; @@ -144,8 +145,8 @@ impl FuncInstance { check_function_args(func.signature(), &args).map_err(|_| TrapKind::UnexpectedSignature)?; match *func.as_internal() { FuncInstanceInternal::Internal { .. } => { - let mut interpreter = Interpreter::new(externals); - interpreter.start_execution(func, args) + let mut interpreter = Interpreter::new(func, args)?; + interpreter.start_execution(externals) } FuncInstanceInternal::Host { ref host_func_index, @@ -153,6 +154,155 @@ impl FuncInstance { } => externals.invoke_index(*host_func_index, args.into()), } } + + /// Invoke the function, get a resumable handle. This handle can then be used to [`start_execution`]. If a + /// Host trap happens, caller can use [`resume_execution`] to feed the expected return value back in, and then + /// continue the execution. + /// + /// This is an experimental API, and this functionality may not be available in other WebAssembly engines. + /// + /// # Errors + /// + /// Returns `Err` if `args` types is not match function [`signature`]. + /// + /// [`signature`]: #method.signature + /// [`Trap`]: #enum.Trap.html + /// [`start_execution`]: struct.FuncInvocation.html#method.start_execution + /// [`resume_execution`]: struct.FuncInvocation.html#method.resume_execution + pub fn invoke_resumable<'args>( + func: &FuncRef, + args: &'args [RuntimeValue], + ) -> Result, Trap> { + check_function_args(func.signature(), &args).map_err(|_| TrapKind::UnexpectedSignature)?; + match *func.as_internal() { + FuncInstanceInternal::Internal { .. } => { + let interpreter = Interpreter::new(func, args)?; + Ok(FuncInvocation { + kind: FuncInvocationKind::Internal(interpreter), + }) + } + FuncInstanceInternal::Host { + ref host_func_index, + .. + } => { + Ok(FuncInvocation { + kind: FuncInvocationKind::Host { + args, + host_func_index: *host_func_index, + finished: false, + }, + }) + }, + } + } +} + +/// A resumable invocation error. +#[derive(Debug)] +pub enum ResumableError { + /// Trap happened. + Trap(Trap), + /// The invocation is not resumable. + /// + /// Invocations are only resumable if a host function is called, and the host function returns a trap of `Host` kind. For other cases, this error will be returned. This includes: + /// - The invocation is directly a host function. + /// - The invocation has not been started. + /// - The invocation returns normally or returns any trap other than `Host` kind. + /// + /// This error is returned by [`resume_execution`]. + /// + /// [`resume_execution`]: struct.FuncInvocation.html#method.resume_execution + NotResumable, + /// The invocation has already been started. + /// + /// This error is returned by [`start_execution`]. + /// + /// [`start_execution`]: struct.FuncInvocation.html#method.start_execution + AlreadyStarted, +} + +impl From for ResumableError { + fn from(trap: Trap) -> Self { + ResumableError::Trap(trap) + } +} + +/// A resumable invocation handle. This struct is returned by `FuncInstance::invoke_resumable`. +pub struct FuncInvocation<'args> { + kind: FuncInvocationKind<'args>, +} + +enum FuncInvocationKind<'args> { + Internal(Interpreter), + Host { + args: &'args [RuntimeValue], + host_func_index: usize, + finished: bool + }, +} + +impl<'args> FuncInvocation<'args> { + /// Whether this invocation is currently resumable. + pub fn is_resumable(&self) -> bool { + match &self.kind { + &FuncInvocationKind::Internal(ref interpreter) => interpreter.state().is_resumable(), + &FuncInvocationKind::Host { .. } => false, + } + } + + /// If the invocation is resumable, the expected return value type to be feed back in. + pub fn resumable_value_type(&self) -> Option { + match &self.kind { + &FuncInvocationKind::Internal(ref interpreter) => { + match interpreter.state() { + &InterpreterState::Resumable(ref value_type) => value_type.clone(), + _ => None, + } + }, + &FuncInvocationKind::Host { .. } => None, + } + } + + /// Start the invocation execution. + pub fn start_execution<'externals, E: Externals + 'externals>(&mut self, externals: &'externals mut E) -> Result, ResumableError> { + match self.kind { + FuncInvocationKind::Internal(ref mut interpreter) => { + if interpreter.state() != &InterpreterState::Initialized { + return Err(ResumableError::AlreadyStarted); + } + Ok(interpreter.start_execution(externals)?) + }, + FuncInvocationKind::Host { ref args, ref mut finished, ref host_func_index } => { + if *finished { + return Err(ResumableError::AlreadyStarted); + } + *finished = true; + Ok(externals.invoke_index(*host_func_index, args.clone().into())?) + }, + } + } + + /// Resume an execution if a previous trap of Host kind happened. + /// + /// `return_val` must be of the value type [`resumable_value_type`], defined by the host function import. Otherwise, + /// `UnexpectedSignature` trap will be returned. The current invocation must also be resumable + /// [`is_resumable`]. Otherwise, a `NotResumable` error will be returned. + /// + /// [`resumable_value_type`]: #method.resumable_value_type + /// [`is_resumable`]: #method.is_resumable + pub fn resume_execution<'externals, E: Externals + 'externals>(&mut self, return_val: Option, externals: &'externals mut E) -> Result, ResumableError> { + match self.kind { + FuncInvocationKind::Internal(ref mut interpreter) => { + if !interpreter.state().is_resumable() { + return Err(ResumableError::AlreadyStarted); + } + Ok(interpreter.resume_execution(return_val, externals)?) + }, + FuncInvocationKind::Host { .. } => { + return Err(ResumableError::NotResumable); + }, + } + } } #[derive(Clone, Debug)] diff --git a/src/lib.rs b/src/lib.rs index c3eb592..430be84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -221,6 +221,16 @@ pub enum TrapKind { Host(Box), } +impl TrapKind { + /// Whether this trap is specified by the host. + pub fn is_host(&self) -> bool { + match self { + &TrapKind::Host(_) => true, + _ => false, + } + } +} + /// Internal interpreter error. #[derive(Debug)] pub enum Error { @@ -368,7 +378,7 @@ pub use self::host::{Externals, NopExternals, HostError, RuntimeArgs}; pub use self::imports::{ModuleImportResolver, ImportResolver, ImportsBuilder}; pub use self::module::{ModuleInstance, ModuleRef, ExternVal, NotStartedModuleRef}; pub use self::global::{GlobalInstance, GlobalRef}; -pub use self::func::{FuncInstance, FuncRef}; +pub use self::func::{FuncInstance, FuncRef, FuncInvocation, ResumableError}; pub use self::types::{Signature, ValueType, GlobalDescriptor, TableDescriptor, MemoryDescriptor}; /// WebAssembly-specific sizes and units. diff --git a/src/runner.rs b/src/runner.rs index e57dbce..f907673 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -14,6 +14,7 @@ use value::{ }; use host::Externals; use common::{DEFAULT_MEMORY_INDEX, DEFAULT_TABLE_INDEX}; +use types::ValueType; use memory_units::Pages; use nan_preserving_float::{F32, F64}; use isa; @@ -36,6 +37,27 @@ pub enum InstructionOutcome { Return(isa::DropKeep), } +#[derive(PartialEq, Eq)] +/// Function execution state, related to pause and resume. +pub enum InterpreterState { + /// The interpreter has been created, but has not been executed. + Initialized, + /// The interpreter has started execution, and cannot be called again if it exits normally, or no Host traps happened. + Started, + /// The interpreter has been executed, and returned a Host trap. It can resume execution by providing back a return + /// value. + Resumable(Option), +} + +impl InterpreterState { + pub fn is_resumable(&self) -> bool { + match self { + &InterpreterState::Resumable(_) => true, + _ => false, + } + } +} + /// Function run result. enum RunResult { /// Function has returned. @@ -45,23 +67,18 @@ enum RunResult { } /// Function interpreter. -pub struct Interpreter<'a, E: Externals + 'a> { - externals: &'a mut E, +pub struct Interpreter { value_stack: ValueStack, + call_stack: Vec, + return_type: Option, + state: InterpreterState, } -impl<'a, E: Externals> Interpreter<'a, E> { - pub fn new(externals: &'a mut E) -> Interpreter<'a, E> { - let value_stack = ValueStack::with_limit(DEFAULT_VALUE_STACK_LIMIT); - Interpreter { - externals, - value_stack, - } - } - - pub fn start_execution(&mut self, func: &FuncRef, args: &[RuntimeValue]) -> Result, Trap> { +impl Interpreter { + pub fn new(func: &FuncRef, args: &[RuntimeValue]) -> Result { + let mut value_stack = ValueStack::with_limit(DEFAULT_VALUE_STACK_LIMIT); for arg in args { - self.value_stack + value_stack .push(*arg) .map_err( // There is not enough space for pushing initial arguments. @@ -70,26 +87,78 @@ impl<'a, E: Externals> Interpreter<'a, E> { )?; } - let initial_frame = FunctionContext::new(func.clone()); - let mut call_stack = Vec::new(); + let initial_frame = FunctionContext::new(func.clone()); call_stack.push(initial_frame); - self.run_interpreter_loop(&mut call_stack)?; + let return_type = func.signature().return_type(); - let opt_return_value = func.signature().return_type().map(|_vt| { + Ok(Interpreter { + value_stack, + call_stack, + return_type, + state: InterpreterState::Initialized, + }) + } + + pub fn state(&self) -> &InterpreterState { + &self.state + } + + pub fn start_execution<'a, E: Externals + 'a>(&mut self, externals: &'a mut E) -> Result, Trap> { + // Ensure that the VM has not been executed. This is checked in `FuncInvocation::start_execution`. + assert!(self.state == InterpreterState::Initialized); + + self.state = InterpreterState::Started; + self.run_interpreter_loop(externals)?; + + let opt_return_value = self.return_type.map(|_vt| { self.value_stack.pop() }); - // Ensure that stack is empty after the execution. + // Ensure that stack is empty after the execution. This is guaranteed by the validation properties. assert!(self.value_stack.len() == 0); Ok(opt_return_value) } - fn run_interpreter_loop(&mut self, call_stack: &mut Vec) -> Result<(), Trap> { + pub fn resume_execution<'a, E: Externals + 'a>(&mut self, return_val: Option, externals: &'a mut E) -> Result, Trap> { + use std::mem::swap; + + // Ensure that the VM is resumable. This is checked in `FuncInvocation::resume_execution`. + assert!(self.state.is_resumable()); + + let mut resumable_state = InterpreterState::Started; + swap(&mut self.state, &mut resumable_state); + let expected_ty = match resumable_state { + InterpreterState::Resumable(ty) => ty, + _ => unreachable!("Resumable arm is checked above is_resumable; qed"), + }; + + let value_ty = return_val.as_ref().map(|val| val.value_type()); + if value_ty != expected_ty { + return Err(TrapKind::UnexpectedSignature.into()); + } + + if let Some(return_val) = return_val { + self.value_stack.push(return_val).map_err(Trap::new)?; + } + + self.run_interpreter_loop(externals)?; + + let opt_return_value = self.return_type.map(|_vt| { + self.value_stack.pop() + }); + + // Ensure that stack is empty after the execution. This is guaranteed by the validation properties. + assert!(self.value_stack.len() == 0); + + Ok(opt_return_value) + } + + fn run_interpreter_loop<'a, E: Externals + 'a>(&mut self, externals: &'a mut E) -> Result<(), Trap> { loop { - let mut function_context = call_stack + let mut function_context = self.call_stack .pop() .expect("on loop entry - not empty; on loop continue - checking for emptiness; qed"); let function_ref = function_context.function.clone(); @@ -112,26 +181,37 @@ impl<'a, E: Externals> Interpreter<'a, E> { match function_return { RunResult::Return => { - if call_stack.last().is_none() { + if self.call_stack.last().is_none() { // This was the last frame in the call stack. This means we // are done executing. return Ok(()); } }, RunResult::NestedCall(nested_func) => { - if call_stack.len() + 1 >= DEFAULT_CALL_STACK_LIMIT { + if self.call_stack.len() + 1 >= DEFAULT_CALL_STACK_LIMIT { return Err(TrapKind::StackOverflow.into()); } match *nested_func.as_internal() { FuncInstanceInternal::Internal { .. } => { let nested_context = FunctionContext::new(nested_func.clone()); - call_stack.push(function_context); - call_stack.push(nested_context); + self.call_stack.push(function_context); + self.call_stack.push(nested_context); }, FuncInstanceInternal::Host { ref signature, .. } => { let args = prepare_function_args(signature, &mut self.value_stack); - let return_val = FuncInstance::invoke(&nested_func, &args, self.externals)?; + // We push the function context first. If the VM is not resumable, it does no harm. If it is, we then save the context here. + self.call_stack.push(function_context); + + let return_val = match FuncInstance::invoke(&nested_func, &args, externals) { + Ok(val) => val, + Err(trap) => { + if trap.kind().is_host() { + self.state = InterpreterState::Resumable(nested_func.signature().return_type()); + } + return Err(trap); + }, + }; // Check if `return_val` matches the signature. let value_ty = return_val.as_ref().map(|val| val.value_type()); @@ -143,7 +223,6 @@ impl<'a, E: Externals> Interpreter<'a, E> { if let Some(return_val) = return_val { self.value_stack.push(return_val).map_err(Trap::new)?; } - call_stack.push(function_context); } } }, diff --git a/src/tests/host.rs b/src/tests/host.rs index 9bbb284..c0b5916 100644 --- a/src/tests/host.rs +++ b/src/tests/host.rs @@ -1,7 +1,7 @@ use { Error, Signature, Externals, FuncInstance, FuncRef, HostError, ImportsBuilder, MemoryInstance, MemoryRef, TableInstance, TableRef, ModuleImportResolver, ModuleInstance, ModuleRef, - RuntimeValue, RuntimeArgs, TableDescriptor, MemoryDescriptor, Trap, TrapKind, + RuntimeValue, RuntimeArgs, TableDescriptor, MemoryDescriptor, Trap, TrapKind, ResumableError, }; use types::ValueType; use memory_units::Pages; @@ -32,6 +32,8 @@ impl HostError for HostErrorWithCode {} struct TestHost { memory: Option, instance: Option, + + trap_sub_result: Option, } impl TestHost { @@ -39,6 +41,8 @@ impl TestHost { TestHost { memory: Some(MemoryInstance::alloc(Pages(1), Some(Pages(1))).unwrap()), instance: None, + + trap_sub_result: None, } } } @@ -75,6 +79,11 @@ const GET_MEM_FUNC_INDEX: usize = 3; /// This function requires attached module instance. const RECURSE_FUNC_INDEX: usize = 4; +/// trap_sub(a: i32, b: i32) -> i32 +/// +/// This function is the same as sub(a, b), but it will send a Host trap which pauses the interpreter execution. +const TRAP_SUB_FUNC_INDEX: usize = 5; + impl Externals for TestHost { fn invoke_index( &mut self, @@ -136,6 +145,14 @@ impl Externals for TestHost { } Ok(Some(result)) } + TRAP_SUB_FUNC_INDEX => { + let a: i32 = args.nth(0); + let b: i32 = args.nth(1); + + let result: RuntimeValue = (a - b).into(); + self.trap_sub_result = Some(result); + return Err(TrapKind::Host(Box::new(HostErrorWithCode { error_code: 301 })).into()); + } _ => panic!("env doesn't provide function at index {}", index), } } @@ -157,6 +174,7 @@ impl TestHost { ERR_FUNC_INDEX => (&[ValueType::I32], None), INC_MEM_FUNC_INDEX => (&[ValueType::I32], None), GET_MEM_FUNC_INDEX => (&[ValueType::I32], Some(ValueType::I32)), + TRAP_SUB_FUNC_INDEX => (&[ValueType::I32, ValueType::I32], Some(ValueType::I32)), _ => return false, }; @@ -172,6 +190,7 @@ impl ModuleImportResolver for TestHost { "inc_mem" => INC_MEM_FUNC_INDEX, "get_mem" => GET_MEM_FUNC_INDEX, "recurse" => RECURSE_FUNC_INDEX, + "trap_sub" => TRAP_SUB_FUNC_INDEX, _ => { return Err(Error::Instantiation( format!("Export {} not found", field_name), @@ -232,6 +251,49 @@ fn call_host_func() { ); } +#[test] +fn resume_call_host_func() { + let module = parse_wat( + r#" +(module + (import "env" "trap_sub" (func $trap_sub (param i32 i32) (result i32))) + + (func (export "test") (result i32) + (call $trap_sub + (i32.const 5) + (i32.const 7) + ) + ) +) +"#, + ); + + let mut env = TestHost::new(); + + let instance = ModuleInstance::new(&module, &ImportsBuilder::new().with_resolver("env", &env)) + .expect("Failed to instantiate module") + .assert_no_start(); + + let export = instance.export_by_name("test").unwrap(); + let func_instance = export.as_func().unwrap(); + + let mut invocation = FuncInstance::invoke_resumable(&func_instance, &[]).unwrap(); + let result = invocation.start_execution(&mut env); + match result { + Err(ResumableError::Trap(_)) => {}, + _ => panic!(), + } + + assert!(invocation.is_resumable()); + let trap_sub_result = env.trap_sub_result.take(); + assert_eq!( + invocation.resume_execution(trap_sub_result, &mut env).expect( + "Failed to invoke 'test' function", + ), + Some(RuntimeValue::I32(-2)) + ); +} + #[test] fn host_err() { let module = parse_wat( @@ -325,6 +387,8 @@ fn pull_internal_mem_from_module() { let mut env = TestHost { memory: None, instance: None, + + trap_sub_result: None, }; let instance = ModuleInstance::new(&module, &ImportsBuilder::new().with_resolver("env", &env))