Implement the remaining valid spec tests.

And lots of other miscellaneous changes. Rename InstanceWorld to
InstancePlus and reorganize its contents. This still isn't a great name,
but at least now it has a clear purpose.
This commit is contained in:
Dan Gohman
2018-12-11 17:12:33 -08:00
parent 6dd39dee6a
commit 3f24098edc
34 changed files with 1572 additions and 1262 deletions

View File

@@ -6,6 +6,7 @@ use std::fmt;
use std::string::String;
use std::vec::Vec;
use wasmtime_environ::CompileError;
use wasmtime_runtime::InstantiationError;
/// A runtime value.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
@@ -110,10 +111,6 @@ pub enum ActionError {
#[fail(display = "Unknown field: {}", _0)]
Field(String),
/// An index was out of bounds.
#[fail(display = "Index out of bounds: {}", _0)]
Index(u64),
/// The field was present but was the wrong kind (eg. function, table, global, or memory).
#[fail(display = "Kind error: {}", _0)]
Kind(String),
@@ -126,9 +123,10 @@ pub enum ActionError {
#[fail(display = "WebAssembly compilation error: {}", _0)]
Compile(CompileError),
/// Some runtime resource was unavailable or insufficient.
#[fail(display = "Runtime resource error: {}", _0)]
Resource(String),
/// Some runtime resource was unavailable or insufficient, or the start function
/// trapped.
#[fail(display = "Instantiation error: {}", _0)]
Instantiate(InstantiationError),
/// Link error.
#[fail(display = "Link error: {}", _0)]

View File

@@ -1,97 +0,0 @@
use cranelift_codegen::ir;
use cranelift_wasm::Global;
use wasmtime_environ::{MemoryPlan, TablePlan};
use wasmtime_runtime::{
VMContext, VMFunctionBody, VMGlobalDefinition, VMMemoryDefinition, VMTableDefinition,
};
/// An exported function.
pub struct FunctionExport {
/// The address of the native-code function.
pub address: *const VMFunctionBody,
/// The function signature declaration, used for compatibilty checking.
pub signature: ir::Signature,
}
/// The value of an export passed from one instance to another.
pub enum Export {
/// A function export value.
Function(FunctionExport),
/// A table export value.
Table {
/// The address of the table descriptor.
address: *mut VMTableDefinition,
/// Pointer to the containing VMContext.
vmctx: *mut VMContext,
/// The table declaration, used for compatibilty checking.
table: TablePlan,
},
/// A memory export value.
Memory {
/// The address of the memory descriptor.
address: *mut VMMemoryDefinition,
/// Pointer to the containing VMContext.
vmctx: *mut VMContext,
/// The memory declaration, used for compatibilty checking.
memory: MemoryPlan,
},
/// A global export value.
Global {
/// The address of the global storage.
address: *mut VMGlobalDefinition,
/// The global declaration, used for compatibilty checking.
global: Global,
},
}
impl Export {
/// Construct a function export value.
pub fn function(address: *const VMFunctionBody, signature: ir::Signature) -> Self {
Export::Function(FunctionExport { address, signature })
}
/// Construct a table export value.
pub fn table(address: *mut VMTableDefinition, vmctx: *mut VMContext, table: TablePlan) -> Self {
Export::Table {
address,
vmctx,
table,
}
}
/// Construct a memory export value.
pub fn memory(
address: *mut VMMemoryDefinition,
vmctx: *mut VMContext,
memory: MemoryPlan,
) -> Self {
Export::Memory {
address,
vmctx,
memory,
}
}
/// Construct a global export value.
pub fn global(address: *mut VMGlobalDefinition, global: Global) -> Self {
Export::Global { address, global }
}
}
/// Import resolver connects imports with available exported values.
pub trait Resolver {
/// Resolve the given module/field combo.
fn resolve(&mut self, module: &str, field: &str) -> Option<Export>;
}
/// `Resolver` implementation that always resolves to `None`.
pub struct NullResolver {}
impl Resolver for NullResolver {
fn resolve(&mut self, _module: &str, _field: &str) -> Option<Export> {
None
}
}

View File

@@ -0,0 +1,287 @@
use action::{ActionError, ActionOutcome, RuntimeValue};
use cranelift_codegen::{ir, isa};
use cranelift_entity::{BoxedSlice, PrimaryMap};
use cranelift_wasm::DefinedFuncIndex;
use jit_code::JITCode;
use link::link_module;
use resolver::Resolver;
use std::cmp::max;
use std::rc::Rc;
use std::slice;
use std::string::String;
use std::vec::Vec;
use std::{mem, ptr};
use trampoline_park::TrampolinePark;
use wasmtime_environ::{
compile_module, Compilation, CompileError, DataInitializer, Module, ModuleEnvironment, Tunables,
};
use wasmtime_runtime::{
wasmtime_call_trampoline, Export, Imports, Instance, InstantiationError, VMFunctionBody,
};
/// `InstancePlus` holds an `Instance` and adds support for performing actions
/// such as the "invoke" command in wast.
///
/// TODO: Think of a better name.
#[derive(Debug)]
pub struct InstancePlus {
/// The contained instance.
pub instance: Box<Instance>,
/// Trampolines for calling into JIT code.
trampolines: TrampolinePark,
}
impl InstancePlus {
/// Create a new `InstancePlus` by compiling the wasm module in `data` and instatiating it.
pub fn new(
jit_code: &mut JITCode,
isa: &isa::TargetIsa,
data: &[u8],
resolver: &mut Resolver,
) -> Result<Self, ActionError> {
let mut module = Module::new();
// TODO: Allow the tunables to be overridden.
let tunables = Tunables::default();
let (lazy_function_body_inputs, lazy_data_initializers) = {
let environ = ModuleEnvironment::new(isa, &mut module, tunables);
let translation = environ
.translate(&data)
.map_err(|error| ActionError::Compile(CompileError::Wasm(error)))?;
(
translation.lazy.function_body_inputs,
translation.lazy.data_initializers,
)
};
let (compilation, relocations) = compile_module(&module, &lazy_function_body_inputs, isa)
.map_err(ActionError::Compile)?;
let allocated_functions = allocate_functions(jit_code, compilation).map_err(|message| {
ActionError::Instantiate(InstantiationError::Resource(format!(
"failed to allocate memory for functions: {}",
message
)))
})?;
let imports = link_module(&module, &allocated_functions, relocations, resolver)
.map_err(ActionError::Link)?;
// Gather up the pointers to the compiled functions.
let finished_functions: BoxedSlice<DefinedFuncIndex, *const VMFunctionBody> =
allocated_functions
.into_iter()
.map(|(_index, allocated)| {
let fatptr: *const [VMFunctionBody] = *allocated;
fatptr as *const VMFunctionBody
})
.collect::<PrimaryMap<_, _>>()
.into_boxed_slice();
// Make all code compiled thus far executable.
jit_code.publish();
Self::with_parts(
Rc::new(module),
finished_functions,
imports,
lazy_data_initializers,
)
}
/// Construct a new `InstancePlus` from the parts needed to produce an `Instance`.
pub fn with_parts(
module: Rc<Module>,
finished_functions: BoxedSlice<DefinedFuncIndex, *const VMFunctionBody>,
imports: Imports,
data_initializers: Vec<DataInitializer>,
) -> Result<Self, ActionError> {
let instance = Instance::new(module, finished_functions, imports, data_initializers)
.map_err(ActionError::Instantiate)?;
Ok(Self::with_instance(instance))
}
/// Construct a new `InstancePlus` from an existing instance.
pub fn with_instance(instance: Box<Instance>) -> Self {
Self {
instance,
trampolines: TrampolinePark::new(),
}
}
/// Invoke a function in this `Instance` identified by an export name.
pub fn invoke(
&mut self,
jit_code: &mut JITCode,
isa: &isa::TargetIsa,
function_name: &str,
args: &[RuntimeValue],
) -> Result<ActionOutcome, ActionError> {
let (address, signature, callee_vmctx) = match self.instance.lookup(function_name) {
Some(Export::Function {
address,
signature,
vmctx,
}) => (address, signature, vmctx),
Some(_) => {
return Err(ActionError::Kind(format!(
"exported item \"{}\" is not a function",
function_name
)))
}
None => {
return Err(ActionError::Field(format!(
"no export named \"{}\"",
function_name
)))
}
};
for (index, value) in args.iter().enumerate() {
assert_eq!(value.value_type(), signature.params[index].value_type);
}
// TODO: Support values larger than u64.
let mut values_vec: Vec<u64> = Vec::new();
let value_size = mem::size_of::<u64>();
values_vec.resize(max(signature.params.len(), signature.returns.len()), 0u64);
// Store the argument values into `values_vec`.
for (index, arg) in args.iter().enumerate() {
unsafe {
let ptr = values_vec.as_mut_ptr().add(index);
match arg {
RuntimeValue::I32(x) => ptr::write(ptr as *mut i32, *x),
RuntimeValue::I64(x) => ptr::write(ptr as *mut i64, *x),
RuntimeValue::F32(x) => ptr::write(ptr as *mut u32, *x),
RuntimeValue::F64(x) => ptr::write(ptr as *mut u64, *x),
}
}
}
// Get the trampoline to call for this function.
let exec_code_buf = self
.trampolines
.get(jit_code, isa, address, &signature, value_size)?;
// Make all JIT code produced thus far executable.
jit_code.publish();
// Call the trampoline.
if let Err(message) = unsafe {
wasmtime_call_trampoline(
exec_code_buf,
values_vec.as_mut_ptr() as *mut u8,
callee_vmctx,
)
} {
return Ok(ActionOutcome::Trapped { message });
}
// Load the return values out of `values_vec`.
let values = signature
.returns
.iter()
.enumerate()
.map(|(index, abi_param)| unsafe {
let ptr = values_vec.as_ptr().add(index);
match abi_param.value_type {
ir::types::I32 => RuntimeValue::I32(ptr::read(ptr as *const i32)),
ir::types::I64 => RuntimeValue::I64(ptr::read(ptr as *const i64)),
ir::types::F32 => RuntimeValue::F32(ptr::read(ptr as *const u32)),
ir::types::F64 => RuntimeValue::F64(ptr::read(ptr as *const u64)),
other => panic!("unsupported value type {:?}", other),
}
})
.collect();
Ok(ActionOutcome::Returned { values })
}
/// Returns a slice of the contents of allocated linear memory.
pub fn inspect_memory(
&self,
memory_name: &str,
start: usize,
len: usize,
) -> Result<&[u8], ActionError> {
let address = match unsafe { self.instance.lookup_immutable(memory_name) } {
Some(Export::Memory {
address,
memory: _memory,
vmctx: _vmctx,
}) => address,
Some(_) => {
return Err(ActionError::Kind(format!(
"exported item \"{}\" is not a linear memory",
memory_name
)))
}
None => {
return Err(ActionError::Field(format!(
"no export named \"{}\"",
memory_name
)))
}
};
Ok(unsafe {
let memory_def = &*address;
&slice::from_raw_parts(memory_def.base, memory_def.current_length)[start..start + len]
})
}
/// Read a global in this `Instance` identified by an export name.
pub fn get(&self, global_name: &str) -> Result<RuntimeValue, ActionError> {
let (address, global) = match unsafe { self.instance.lookup_immutable(global_name) } {
Some(Export::Global { address, global }) => (address, global),
Some(_) => {
return Err(ActionError::Kind(format!(
"exported item \"{}\" is not a global variable",
global_name
)))
}
None => {
return Err(ActionError::Field(format!(
"no export named \"{}\"",
global_name
)))
}
};
unsafe {
let global_def = &*address;
Ok(match global.ty {
ir::types::I32 => RuntimeValue::I32(*global_def.as_i32()),
ir::types::I64 => RuntimeValue::I64(*global_def.as_i64()),
ir::types::F32 => RuntimeValue::F32(*global_def.as_f32_bits()),
ir::types::F64 => RuntimeValue::F64(*global_def.as_f64_bits()),
other => {
return Err(ActionError::Type(format!(
"global with type {} not supported",
other
)))
}
})
}
}
}
fn allocate_functions(
jit_code: &mut JITCode,
compilation: Compilation,
) -> Result<PrimaryMap<DefinedFuncIndex, *mut [VMFunctionBody]>, String> {
let mut result = PrimaryMap::with_capacity(compilation.functions.len());
for (_, body) in compilation.functions.into_iter() {
let fatptr: *mut [VMFunctionBody] = jit_code.allocate_copy_of_byte_slice(body)?;
result.push(fatptr);
}
Ok(result)
}

View File

@@ -7,15 +7,15 @@ use std::{cmp, mem};
use wasmtime_runtime::{Mmap, VMFunctionBody};
/// Memory manager for executable code.
pub struct Code {
pub struct JITCode {
current: Mmap,
mmaps: Vec<Mmap>,
position: usize,
published: usize,
}
impl Code {
/// Create a new `Code` instance.
impl JITCode {
/// Create a new `JITCode` instance.
pub fn new() -> Self {
Self {
current: Mmap::new(),

View File

@@ -39,16 +39,17 @@ extern crate failure;
extern crate failure_derive;
mod action;
mod code;
mod export;
mod instance_plus;
mod jit_code;
mod link;
mod world;
mod resolver;
mod trampoline_park;
pub use action::{ActionError, ActionOutcome, RuntimeValue};
pub use code::Code;
pub use export::{Export, NullResolver, Resolver};
pub use instance_plus::InstancePlus;
pub use jit_code::JITCode;
pub use link::link_module;
pub use world::InstanceWorld;
pub use resolver::{NullResolver, Resolver};
#[cfg(not(feature = "std"))]
mod std {

View File

@@ -1,16 +1,18 @@
use cranelift_codegen::binemit::Reloc;
use cranelift_entity::PrimaryMap;
use cranelift_wasm::{DefinedFuncIndex, Global, GlobalInit, Memory, Table, TableElementType};
use export::{Export, FunctionExport, Resolver};
use resolver::Resolver;
use std::ptr::write_unaligned;
use std::string::String;
use std::vec::Vec;
use wasmtime_environ::{
MemoryPlan, MemoryStyle, Module, Relocation, RelocationTarget, Relocations, TablePlan,
TableStyle,
};
use wasmtime_runtime::libcalls;
use wasmtime_runtime::{Imports, VMFunctionBody, VMGlobalImport, VMMemoryImport, VMTableImport};
use wasmtime_runtime::{
Export, Imports, VMFunctionBody, VMFunctionImport, VMGlobalImport, VMMemoryImport,
VMTableImport,
};
/// A link error, such as incompatible or unmatched imports/exports.
#[derive(Fail, Debug)]
@@ -28,7 +30,11 @@ pub fn link_module(
for (index, (ref module_name, ref field)) in module.imported_funcs.iter() {
match resolver.resolve(module_name, field) {
Some(export_value) => match export_value {
Export::Function(FunctionExport { address, signature }) => {
Export::Function {
address,
signature,
vmctx,
} => {
let import_signature = &module.signatures[module.functions[index]];
if signature != *import_signature {
// TODO: If the difference is in the calling convention,
@@ -39,7 +45,10 @@ pub fn link_module(
signature, import_signature)
));
}
function_imports.push(address);
function_imports.push(VMFunctionImport {
body: address,
vmctx,
});
}
Export::Table { .. } | Export::Memory { .. } | Export::Global { .. } => {
return Err(LinkError(format!(
@@ -104,12 +113,28 @@ pub fn link_module(
memory,
} => {
let import_memory = &module.memory_plans[index];
if is_memory_compatible(&memory, import_memory) {
if !is_memory_compatible(&memory, import_memory) {
return Err(LinkError(format!(
"{}/{}: exported memory incompatible with memory import",
module_name, field
)));
}
// Sanity-check: Ensure that the imported memory has at least
// guard-page protections the importing module expects it to have.
match (memory.style, &import_memory.style) {
(
MemoryStyle::Static { bound },
MemoryStyle::Static {
bound: import_bound,
},
) => {
assert!(bound >= *import_bound);
}
_ => (),
}
assert!(memory.offset_guard_size >= import_memory.offset_guard_size);
memory_imports.push(VMMemoryImport {
from: address,
vmctx,
@@ -161,17 +186,15 @@ pub fn link_module(
}
}
let imports = Imports::new(
// Apply relocations, now that we have virtual addresses for everything.
relocate(allocated_functions, relocations, &module);
Ok(Imports::new(
function_imports,
table_imports,
memory_imports,
global_imports,
);
// Apply relocations, now that we have virtual addresses for everything.
relocate(&imports, allocated_functions, relocations, &module);
Ok(imports)
))
}
fn is_global_compatible(exported: &Global, imported: &Global) -> bool {
@@ -193,14 +216,6 @@ fn is_global_compatible(exported: &Global, imported: &Global) -> bool {
exported_ty == imported_ty && imported_mutability == exported_mutability
}
fn is_table_style_compatible(exported_style: &TableStyle, imported_style: &TableStyle) -> bool {
match exported_style {
TableStyle::CallerChecksSignature => match imported_style {
TableStyle::CallerChecksSignature => true,
},
}
}
fn is_table_element_type_compatible(
exported_type: TableElementType,
imported_type: TableElementType,
@@ -225,7 +240,7 @@ fn is_table_compatible(exported: &TablePlan, imported: &TablePlan) -> bool {
minimum: exported_minimum,
maximum: exported_maximum,
},
style: exported_style,
style: _exported_style,
} = exported;
let TablePlan {
table:
@@ -234,30 +249,14 @@ fn is_table_compatible(exported: &TablePlan, imported: &TablePlan) -> bool {
minimum: imported_minimum,
maximum: imported_maximum,
},
style: imported_style,
style: _imported_style,
} = imported;
is_table_element_type_compatible(*exported_ty, *imported_ty)
&& imported_minimum >= exported_minimum
&& imported_maximum <= exported_maximum
&& is_table_style_compatible(imported_style, exported_style)
}
fn is_memory_style_compatible(exported_style: &MemoryStyle, imported_style: &MemoryStyle) -> bool {
match exported_style {
MemoryStyle::Dynamic => match imported_style {
MemoryStyle::Dynamic => true,
_ => false,
},
MemoryStyle::Static {
bound: imported_bound,
} => match imported_style {
MemoryStyle::Static {
bound: exported_bound,
} => exported_bound >= imported_bound,
_ => false,
},
}
&& imported_minimum <= exported_minimum
&& (imported_maximum.is_none()
|| (!exported_maximum.is_none()
&& imported_maximum.unwrap() >= exported_maximum.unwrap()))
}
fn is_memory_compatible(exported: &MemoryPlan, imported: &MemoryPlan) -> bool {
@@ -268,8 +267,8 @@ fn is_memory_compatible(exported: &MemoryPlan, imported: &MemoryPlan) -> bool {
maximum: exported_maximum,
shared: exported_shared,
},
style: exported_style,
offset_guard_size: exported_offset_guard_size,
style: _exported_style,
offset_guard_size: _exported_offset_guard_size,
} = exported;
let MemoryPlan {
memory:
@@ -278,20 +277,19 @@ fn is_memory_compatible(exported: &MemoryPlan, imported: &MemoryPlan) -> bool {
maximum: imported_maximum,
shared: imported_shared,
},
style: imported_style,
offset_guard_size: imported_offset_guard_size,
style: _imported_style,
offset_guard_size: _imported_offset_guard_size,
} = imported;
imported_minimum >= exported_minimum
&& imported_maximum <= exported_maximum
imported_minimum <= exported_minimum
&& (imported_maximum.is_none()
|| (!exported_maximum.is_none()
&& imported_maximum.unwrap() >= exported_maximum.unwrap()))
&& exported_shared == imported_shared
&& is_memory_style_compatible(exported_style, imported_style)
&& exported_offset_guard_size >= imported_offset_guard_size
}
/// Performs the relocations inside the function bytecode, provided the necessary metadata.
fn relocate(
imports: &Imports,
allocated_functions: &PrimaryMap<DefinedFuncIndex, *mut [VMFunctionBody]>,
relocations: PrimaryMap<DefinedFuncIndex, Vec<Relocation>>,
module: &Module,
@@ -305,7 +303,7 @@ fn relocate(
let fatptr: *const [VMFunctionBody] = allocated_functions[f];
fatptr as *const VMFunctionBody as usize
}
None => imports.functions[index] as usize,
None => panic!("direct call to import"),
},
RelocationTarget::Memory32Grow => wasmtime_memory32_grow as usize,
RelocationTarget::Memory32Size => wasmtime_memory32_size as usize,

View File

@@ -0,0 +1,16 @@
use wasmtime_runtime::Export;
/// Import resolver connects imports with available exported values.
pub trait Resolver {
/// Resolve the given module/field combo.
fn resolve(&mut self, module: &str, field: &str) -> Option<Export>;
}
/// `Resolver` implementation that always resolves to `None`.
pub struct NullResolver {}
impl Resolver for NullResolver {
fn resolve(&mut self, _module: &str, _field: &str) -> Option<Export> {
None
}
}

View File

@@ -0,0 +1,152 @@
use action::ActionError;
use cranelift_codegen::ir::InstBuilder;
use cranelift_codegen::Context;
use cranelift_codegen::{binemit, ir, isa};
use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
use jit_code::JITCode;
use std::collections::HashMap;
use std::fmt;
use wasmtime_environ::{CompileError, RelocSink};
use wasmtime_runtime::{InstantiationError, VMFunctionBody};
pub struct TrampolinePark {
/// Memoized per-function trampolines.
memoized: HashMap<*const VMFunctionBody, *const VMFunctionBody>,
/// The `FunctionBuilderContext`, shared between function compilations.
fn_builder_ctx: FunctionBuilderContext,
}
impl TrampolinePark {
pub fn new() -> Self {
Self {
memoized: HashMap::new(),
fn_builder_ctx: FunctionBuilderContext::new(),
}
}
pub fn get(
&mut self,
jit_code: &mut JITCode,
isa: &isa::TargetIsa,
callee_address: *const VMFunctionBody,
signature: &ir::Signature,
value_size: usize,
) -> Result<*const VMFunctionBody, ActionError> {
use std::collections::hash_map::Entry::{Occupied, Vacant};
Ok(match self.memoized.entry(callee_address) {
Occupied(entry) => *entry.get(),
Vacant(entry) => {
let body = make_trampoline(
&mut self.fn_builder_ctx,
jit_code,
isa,
callee_address,
signature,
value_size,
)?;
entry.insert(body);
body
}
})
}
}
impl fmt::Debug for TrampolinePark {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// The `fn_builder_ctx` field is just a cache and has no logical state.
write!(f, "{:?}", self.memoized)
}
}
fn make_trampoline(
fn_builder_ctx: &mut FunctionBuilderContext,
jit_code: &mut JITCode,
isa: &isa::TargetIsa,
callee_address: *const VMFunctionBody,
signature: &ir::Signature,
value_size: usize,
) -> Result<*const VMFunctionBody, ActionError> {
let pointer_type = isa.pointer_type();
let mut wrapper_sig = ir::Signature::new(isa.frontend_config().default_call_conv);
// Add the `values_vec` parameter.
wrapper_sig.params.push(ir::AbiParam::new(pointer_type));
// Add the `vmctx` parameter.
wrapper_sig.params.push(ir::AbiParam::special(
pointer_type,
ir::ArgumentPurpose::VMContext,
));
let mut context = Context::new();
context.func = ir::Function::with_name_signature(ir::ExternalName::user(0, 0), wrapper_sig);
{
let mut builder = FunctionBuilder::new(&mut context.func, fn_builder_ctx);
let block0 = builder.create_ebb();
builder.append_ebb_params_for_function_params(block0);
builder.switch_to_block(block0);
builder.seal_block(block0);
let mut callee_args = Vec::new();
let pointer_type = isa.pointer_type();
let (values_vec_ptr_val, vmctx_ptr_val) = {
let params = builder.func.dfg.ebb_params(block0);
(params[0], params[1])
};
// Load the argument values out of `values_vec`.
let mflags = ir::MemFlags::trusted();
for (i, r) in signature.params.iter().enumerate() {
let value = match r.purpose {
ir::ArgumentPurpose::Normal => builder.ins().load(
r.value_type,
mflags,
values_vec_ptr_val,
(i * value_size) as i32,
),
ir::ArgumentPurpose::VMContext => vmctx_ptr_val,
other => panic!("unsupported argument purpose {}", other),
};
callee_args.push(value);
}
let new_sig = builder.import_signature(signature.clone());
// TODO: It's possible to make this a direct call. We just need Cranelift
// to support functions declared with an immediate integer address.
// ExternalName::Absolute(u64). Let's do it.
let callee_value = builder.ins().iconst(pointer_type, callee_address as i64);
let call = builder
.ins()
.call_indirect(new_sig, callee_value, &callee_args);
let results = builder.func.dfg.inst_results(call).to_vec();
// Store the return values into `values_vec`.
let mflags = ir::MemFlags::trusted();
for (i, r) in results.iter().enumerate() {
builder
.ins()
.store(mflags, *r, values_vec_ptr_val, (i * value_size) as i32);
}
builder.ins().return_(&[]);
builder.finalize()
}
let mut code_buf: Vec<u8> = Vec::new();
let mut reloc_sink = RelocSink::new();
let mut trap_sink = binemit::NullTrapSink {};
context
.compile_and_emit(isa, &mut code_buf, &mut reloc_sink, &mut trap_sink)
.map_err(|error| ActionError::Compile(CompileError::Codegen(error)))?;
assert!(reloc_sink.func_relocs.is_empty());
Ok(jit_code
.allocate_copy_of_byte_slice(&code_buf)
.map_err(|message| ActionError::Instantiate(InstantiationError::Resource(message)))?
.as_ptr())
}

View File

@@ -1,553 +0,0 @@
use action::{ActionError, ActionOutcome, RuntimeValue};
use code::Code;
use cranelift_codegen::ir::InstBuilder;
use cranelift_codegen::Context;
use cranelift_codegen::{binemit, ir, isa};
use cranelift_entity::{BoxedSlice, EntityRef, PrimaryMap};
use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
use cranelift_wasm::{
DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, FuncIndex,
GlobalIndex, MemoryIndex, TableIndex,
};
use export::Resolver;
use link::link_module;
use std::cmp::max;
use std::collections::HashMap;
use std::slice;
use std::string::String;
use std::vec::Vec;
use std::{mem, ptr};
use wasmtime_environ::{
compile_module, Compilation, CompileError, Export, Module, ModuleEnvironment, RelocSink,
Tunables,
};
use wasmtime_runtime::{
wasmtime_call_trampoline, wasmtime_init_eager, wasmtime_init_finish, Instance, VMContext,
VMFunctionBody, VMGlobalDefinition, VMGlobalImport, VMMemoryDefinition, VMMemoryImport,
VMTableDefinition, VMTableImport,
};
/// A module, an instance of that module, and accompanying compilation artifacts.
///
/// TODO: Rename and reorganize this.
pub struct InstanceWorld {
module: Module,
instance: Instance,
/// Pointers to functions in executable memory.
finished_functions: BoxedSlice<DefinedFuncIndex, *const VMFunctionBody>,
/// Trampolines for calling into JIT code.
trampolines: TrampolinePark,
}
impl InstanceWorld {
/// Create a new `InstanceWorld` by compiling the wasm module in `data` and instatiating it.
///
/// `finished_functions` holds the function bodies
/// which have been placed in executable memory and linked.
pub fn new(
code: &mut Code,
isa: &isa::TargetIsa,
data: &[u8],
resolver: &mut Resolver,
) -> Result<Self, ActionError> {
let mut module = Module::new();
// TODO: Allow the tunables to be overridden.
let tunables = Tunables::default();
let (lazy_function_body_inputs, lazy_data_initializers) = {
let environ = ModuleEnvironment::new(isa, &mut module, tunables);
let translation = environ
.translate(&data)
.map_err(|error| ActionError::Compile(CompileError::Wasm(error)))?;
(
translation.lazy.function_body_inputs,
translation.lazy.data_initializers,
)
};
let (compilation, relocations) = compile_module(&module, &lazy_function_body_inputs, isa)
.map_err(ActionError::Compile)?;
let allocated_functions =
allocate_functions(code, compilation).map_err(ActionError::Resource)?;
let imports = link_module(&module, &allocated_functions, relocations, resolver)
.map_err(ActionError::Link)?;
let finished_functions: BoxedSlice<DefinedFuncIndex, *const VMFunctionBody> =
allocated_functions
.into_iter()
.map(|(_index, allocated)| {
let fatptr: *const [VMFunctionBody] = *allocated;
fatptr as *const VMFunctionBody
})
.collect::<PrimaryMap<_, _>>()
.into_boxed_slice();
let instance = Instance::new(
&module,
&finished_functions,
imports,
&lazy_data_initializers,
)
.map_err(ActionError::Resource)?;
let fn_builder_ctx = FunctionBuilderContext::new();
let mut result = Self {
module,
instance,
finished_functions,
trampolines: TrampolinePark {
memo: HashMap::new(),
fn_builder_ctx,
},
};
// The WebAssembly spec specifies that the start function is
// invoked automatically at instantiation time.
match result.invoke_start_function(code, isa)? {
ActionOutcome::Returned { .. } => {}
ActionOutcome::Trapped { message } => {
// Instantiation fails if the start function traps.
return Err(ActionError::Start(message));
}
}
Ok(result)
}
fn get_imported_function(&self, index: FuncIndex) -> Option<*const VMFunctionBody> {
if index.index() < self.module.imported_funcs.len() {
Some(unsafe { self.instance.vmctx().imported_function(index) })
} else {
None
}
}
// TODO: Add an accessor for table elements.
#[allow(dead_code)]
fn get_imported_table(&self, index: TableIndex) -> Option<&VMTableImport> {
if index.index() < self.module.imported_tables.len() {
Some(unsafe { self.instance.vmctx().imported_table(index) })
} else {
None
}
}
fn get_imported_memory(&self, index: MemoryIndex) -> Option<&VMMemoryImport> {
if index.index() < self.module.imported_memories.len() {
Some(unsafe { self.instance.vmctx().imported_memory(index) })
} else {
None
}
}
fn get_imported_global(&self, index: GlobalIndex) -> Option<&VMGlobalImport> {
if index.index() < self.module.imported_globals.len() {
Some(unsafe { self.instance.vmctx().imported_global(index) })
} else {
None
}
}
fn get_finished_function(&self, index: DefinedFuncIndex) -> Option<*const VMFunctionBody> {
self.finished_functions.get(index).cloned()
}
// TODO: Add an accessor for table elements.
#[allow(dead_code)]
fn get_defined_table(&self, index: DefinedTableIndex) -> Option<&VMTableDefinition> {
if self.module.table_index(index).index() < self.module.table_plans.len() {
Some(unsafe { self.instance.vmctx().table(index) })
} else {
None
}
}
fn get_defined_memory(&self, index: DefinedMemoryIndex) -> Option<&VMMemoryDefinition> {
if self.module.memory_index(index).index() < self.module.memory_plans.len() {
Some(unsafe { self.instance.vmctx().memory(index) })
} else {
None
}
}
fn get_defined_global(&self, index: DefinedGlobalIndex) -> Option<&VMGlobalDefinition> {
if self.module.global_index(index).index() < self.module.globals.len() {
Some(unsafe { self.instance.vmctx().global(index) })
} else {
None
}
}
/// Invoke a function in this `InstanceWorld` by name.
pub fn invoke(
&mut self,
code: &mut Code,
isa: &isa::TargetIsa,
function_name: &str,
args: &[RuntimeValue],
) -> Result<ActionOutcome, ActionError> {
let fn_index = match self.module.exports.get(function_name) {
Some(Export::Function(index)) => *index,
Some(_) => {
return Err(ActionError::Kind(format!(
"exported item \"{}\" is not a function",
function_name
)))
}
None => {
return Err(ActionError::Field(format!(
"no export named \"{}\"",
function_name
)))
}
};
self.invoke_by_index(code, isa, fn_index, args)
}
/// Invoke the WebAssembly start function of the instance, if one is present.
fn invoke_start_function(
&mut self,
code: &mut Code,
isa: &isa::TargetIsa,
) -> Result<ActionOutcome, ActionError> {
if let Some(start_index) = self.module.start_func {
self.invoke_by_index(code, isa, start_index, &[])
} else {
// No start function, just return nothing.
Ok(ActionOutcome::Returned { values: vec![] })
}
}
/// Calls the given indexed function, passing its return values and returning
/// its results.
fn invoke_by_index(
&mut self,
code: &mut Code,
isa: &isa::TargetIsa,
fn_index: FuncIndex,
args: &[RuntimeValue],
) -> Result<ActionOutcome, ActionError> {
let callee_address = match self.module.defined_func_index(fn_index) {
Some(def_fn_index) => self
.get_finished_function(def_fn_index)
.ok_or_else(|| ActionError::Index(def_fn_index.index() as u64))?,
None => self
.get_imported_function(fn_index)
.ok_or_else(|| ActionError::Index(fn_index.index() as u64))?,
};
// Rather than writing inline assembly to jump to the code region, we use the fact that
// the Rust ABI for calling a function with no arguments and no return values matches the one
// of the generated code. Thanks to this, we can transmute the code region into a first-class
// Rust function and call it.
// Ensure that our signal handlers are ready for action.
wasmtime_init_eager();
wasmtime_init_finish(self.instance.vmctx_mut());
let signature = &self.module.signatures[self.module.functions[fn_index]];
let vmctx: *mut VMContext = self.instance.vmctx_mut();
for (index, value) in args.iter().enumerate() {
assert_eq!(value.value_type(), signature.params[index].value_type);
}
// TODO: Support values larger than u64.
let mut values_vec: Vec<u64> = Vec::new();
let value_size = mem::size_of::<u64>();
values_vec.resize(max(signature.params.len(), signature.returns.len()), 0u64);
// Store the argument values into `values_vec`.
for (index, arg) in args.iter().enumerate() {
unsafe {
let ptr = values_vec.as_mut_ptr().add(index);
match arg {
RuntimeValue::I32(x) => ptr::write(ptr as *mut i32, *x),
RuntimeValue::I64(x) => ptr::write(ptr as *mut i64, *x),
RuntimeValue::F32(x) => ptr::write(ptr as *mut u32, *x),
RuntimeValue::F64(x) => ptr::write(ptr as *mut u64, *x),
}
}
}
// Store the vmctx value into `values_vec`.
unsafe {
let ptr = values_vec.as_mut_ptr().add(args.len());
ptr::write(ptr as *mut usize, vmctx as usize)
}
// Get the trampoline to call for this function.
let exec_code_buf =
self.trampolines
.get(code, isa, callee_address, &signature, value_size)?;
// Make all JIT code produced thus far executable.
code.publish();
// Call the trampoline.
if let Err(message) = unsafe {
wasmtime_call_trampoline(
exec_code_buf,
values_vec.as_mut_ptr() as *mut u8,
self.instance.vmctx_mut(),
)
} {
return Ok(ActionOutcome::Trapped { message });
}
// Load the return values out of `values_vec`.
let values = signature
.returns
.iter()
.enumerate()
.map(|(index, abi_param)| unsafe {
let ptr = values_vec.as_ptr().add(index);
match abi_param.value_type {
ir::types::I32 => RuntimeValue::I32(ptr::read(ptr as *const i32)),
ir::types::I64 => RuntimeValue::I64(ptr::read(ptr as *const i64)),
ir::types::F32 => RuntimeValue::F32(ptr::read(ptr as *const u32)),
ir::types::F64 => RuntimeValue::F64(ptr::read(ptr as *const u64)),
other => panic!("unsupported value type {:?}", other),
}
})
.collect();
Ok(ActionOutcome::Returned { values })
}
/// Read a global in this `InstanceWorld` by name.
pub fn get(&self, global_name: &str) -> Result<RuntimeValue, ActionError> {
let global_index = match self.module.exports.get(global_name) {
Some(Export::Global(index)) => *index,
Some(_) => {
return Err(ActionError::Kind(format!(
"exported item \"{}\" is not a global",
global_name
)))
}
None => {
return Err(ActionError::Field(format!(
"no export named \"{}\"",
global_name
)))
}
};
self.get_by_index(global_index)
}
/// Reads the value of the indexed global variable in `module`.
pub fn get_by_index(&self, global_index: GlobalIndex) -> Result<RuntimeValue, ActionError> {
let global_address = match self.module.defined_global_index(global_index) {
Some(def_global_index) => self
.get_defined_global(def_global_index)
.ok_or_else(|| ActionError::Index(def_global_index.index() as u64))?,
None => {
let from: *const VMGlobalDefinition = self
.get_imported_global(global_index)
.ok_or_else(|| ActionError::Index(global_index.index() as u64))?
.from;
from
}
};
let global_def = unsafe { &*global_address };
unsafe {
Ok(
match self
.module
.globals
.get(global_index)
.ok_or_else(|| ActionError::Index(global_index.index() as u64))?
.ty
{
ir::types::I32 => RuntimeValue::I32(*global_def.as_i32()),
ir::types::I64 => RuntimeValue::I64(*global_def.as_i64()),
ir::types::F32 => RuntimeValue::F32(*global_def.as_f32_bits()),
ir::types::F64 => RuntimeValue::F64(*global_def.as_f64_bits()),
other => {
return Err(ActionError::Type(format!(
"global with type {} not supported",
other
)))
}
},
)
}
}
/// Returns a slice of the contents of allocated linear memory.
pub fn inspect_memory(
&self,
memory_index: MemoryIndex,
address: usize,
len: usize,
) -> Result<&[u8], ActionError> {
let memory_address = match self.module.defined_memory_index(memory_index) {
Some(def_memory_index) => self
.get_defined_memory(def_memory_index)
.ok_or_else(|| ActionError::Index(def_memory_index.index() as u64))?,
None => {
let from: *const VMMemoryDefinition = self
.get_imported_memory(memory_index)
.ok_or_else(|| ActionError::Index(memory_index.index() as u64))?
.from;
from
}
};
let memory_def = unsafe { &*memory_address };
Ok(unsafe {
&slice::from_raw_parts(memory_def.base, memory_def.current_length)
[address..address + len]
})
}
}
fn allocate_functions(
code: &mut Code,
compilation: Compilation,
) -> Result<PrimaryMap<DefinedFuncIndex, *mut [VMFunctionBody]>, String> {
let mut result = PrimaryMap::with_capacity(compilation.functions.len());
for (_, body) in compilation.functions.into_iter() {
let fatptr: *mut [VMFunctionBody] = code.allocate_copy_of_byte_slice(body)?;
result.push(fatptr);
}
Ok(result)
}
struct TrampolinePark {
/// Memorized per-function trampolines.
memo: HashMap<*const VMFunctionBody, *const VMFunctionBody>,
/// The `FunctionBuilderContext`, shared between function compilations.
fn_builder_ctx: FunctionBuilderContext,
}
impl TrampolinePark {
fn get(
&mut self,
code: &mut Code,
isa: &isa::TargetIsa,
callee_address: *const VMFunctionBody,
signature: &ir::Signature,
value_size: usize,
) -> Result<*const VMFunctionBody, ActionError> {
use std::collections::hash_map::Entry::{Occupied, Vacant};
Ok(match self.memo.entry(callee_address) {
Occupied(entry) => *entry.get(),
Vacant(entry) => {
let body = make_trampoline(
&mut self.fn_builder_ctx,
code,
isa,
callee_address,
signature,
value_size,
)?;
entry.insert(body);
body
}
})
}
}
fn make_trampoline(
fn_builder_ctx: &mut FunctionBuilderContext,
code: &mut Code,
isa: &isa::TargetIsa,
callee_address: *const VMFunctionBody,
signature: &ir::Signature,
value_size: usize,
) -> Result<*const VMFunctionBody, ActionError> {
let pointer_type = isa.pointer_type();
let mut wrapper_sig = ir::Signature::new(isa.frontend_config().default_call_conv);
// Add the `values_vec` parameter.
wrapper_sig.params.push(ir::AbiParam::new(pointer_type));
// Add the `vmctx` parameter.
wrapper_sig.params.push(ir::AbiParam::special(
pointer_type,
ir::ArgumentPurpose::VMContext,
));
let mut context = Context::new();
context.func = ir::Function::with_name_signature(ir::ExternalName::user(0, 0), wrapper_sig);
{
let mut builder = FunctionBuilder::new(&mut context.func, fn_builder_ctx);
let block0 = builder.create_ebb();
builder.append_ebb_params_for_function_params(block0);
builder.switch_to_block(block0);
builder.seal_block(block0);
let mut callee_args = Vec::new();
let pointer_type = isa.pointer_type();
let (values_vec_ptr_val, vmctx_ptr_val) = {
let params = builder.func.dfg.ebb_params(block0);
(params[0], params[1])
};
// Load the argument values out of `values_vec`.
let mflags = ir::MemFlags::trusted();
for (i, r) in signature.params.iter().enumerate() {
let value = match r.purpose {
ir::ArgumentPurpose::Normal => builder.ins().load(
r.value_type,
mflags,
values_vec_ptr_val,
(i * value_size) as i32,
),
ir::ArgumentPurpose::VMContext => vmctx_ptr_val,
other => panic!("unsupported argument purpose {}", other),
};
callee_args.push(value);
}
let new_sig = builder.import_signature(signature.clone());
// TODO: It's possible to make this a direct call. We just need Cranelift
// to support functions declared with an immediate integer address.
// ExternalName::Absolute(u64). Let's do it.
let callee_value = builder.ins().iconst(pointer_type, callee_address as i64);
let call = builder
.ins()
.call_indirect(new_sig, callee_value, &callee_args);
let results = builder.func.dfg.inst_results(call).to_vec();
// Store the return values into `values_vec`.
let mflags = ir::MemFlags::trusted();
for (i, r) in results.iter().enumerate() {
builder
.ins()
.store(mflags, *r, values_vec_ptr_val, (i * value_size) as i32);
}
builder.ins().return_(&[]);
builder.finalize()
}
let mut code_buf: Vec<u8> = Vec::new();
let mut reloc_sink = RelocSink::new();
let mut trap_sink = binemit::NullTrapSink {};
context
.compile_and_emit(isa, &mut code_buf, &mut reloc_sink, &mut trap_sink)
.map_err(|error| ActionError::Compile(CompileError::Codegen(error)))?;
assert!(reloc_sink.func_relocs.is_empty());
Ok(code
.allocate_copy_of_byte_slice(&code_buf)
.map_err(ActionError::Resource)?
.as_ptr())
}