From 9b5cfa1e4aa76866b37eb61c1fcc27e2fc83609e Mon Sep 17 00:00:00 2001 From: Vadim Smirnov Date: Wed, 24 Apr 2024 11:21:45 +0200 Subject: [PATCH] feat(sp-wasm-interface): Gear sandbox (gear-tech/substrate/pull/35) * move allocator to primitives * move HostState to sp-wasm-interface * move StoreData to sp-wasm-interface * caller method * move MemoryWrapper to sp-wasm-interface * add sp-wasm-interface util module * refactor with_caller_mut method --- Cargo.lock | 36 +- Cargo.toml | 2 +- substrate/client/allocator/Cargo.toml | 20 - substrate/client/executor/common/Cargo.toml | 2 +- substrate/client/executor/common/src/error.rs | 2 +- substrate/client/executor/common/src/util.rs | 8 - .../executor/common/src/wasm_runtime.rs | 2 +- substrate/client/executor/wasmi/Cargo.toml | 2 +- substrate/client/executor/wasmtime/Cargo.toml | 2 +- .../client/executor/wasmtime/src/host.rs | 93 +---- .../executor/wasmtime/src/instance_wrapper.rs | 45 +- .../client/executor/wasmtime/src/runtime.rs | 72 ++-- .../client/executor/wasmtime/src/util.rs | 37 +- substrate/primitives/allocator/Cargo.toml | 24 ++ .../allocator/README.md | 0 .../allocator/src/error.rs | 0 .../allocator/src/freeing_bump.rs | 25 +- .../allocator/src/lib.rs | 11 + substrate/primitives/core/Cargo.toml | 1 + substrate/primitives/core/src/lib.rs | 5 +- .../wasm-interface-common/Cargo.toml | 23 ++ .../wasm-interface-common/README.md | 3 + .../wasm-interface-common/src/lib.rs | 346 ++++++++++++++++ .../wasm-interface-common/src/util.rs | 26 ++ .../wasm-interface-common/src/wasmi_impl.rs | 80 ++++ .../primitives/wasm-interface/Cargo.toml | 5 +- substrate/primitives/wasm-interface/lib.rs | 385 ++++++++++++++++++ .../wasm-interface/src/host_state.rs | 52 +++ .../primitives/wasm-interface/src/lib.rs | 340 +++------------- .../wasm-interface/src/memory_wrapper.rs | 56 +++ .../wasm-interface/src/store_data.rs | 24 ++ .../primitives/wasm-interface/src/util.rs | 100 +++++ 32 files changed, 1308 insertions(+), 521 deletions(-) delete mode 100644 substrate/client/allocator/Cargo.toml create mode 100644 substrate/primitives/allocator/Cargo.toml rename substrate/{client => primitives}/allocator/README.md (100%) rename substrate/{client => primitives}/allocator/src/error.rs (100%) rename substrate/{client => primitives}/allocator/src/freeing_bump.rs (98%) rename substrate/{client => primitives}/allocator/src/lib.rs (86%) create mode 100644 substrate/primitives/wasm-interface-common/Cargo.toml create mode 100644 substrate/primitives/wasm-interface-common/README.md create mode 100644 substrate/primitives/wasm-interface-common/src/lib.rs create mode 100644 substrate/primitives/wasm-interface-common/src/util.rs create mode 100644 substrate/primitives/wasm-interface-common/src/wasmi_impl.rs create mode 100644 substrate/primitives/wasm-interface/lib.rs create mode 100644 substrate/primitives/wasm-interface/src/host_state.rs create mode 100644 substrate/primitives/wasm-interface/src/memory_wrapper.rs create mode 100644 substrate/primitives/wasm-interface/src/store_data.rs create mode 100644 substrate/primitives/wasm-interface/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index e2eb5b562a04..d8cb163f0833 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14559,16 +14559,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "sc-allocator" -version = "4.1.0-dev" -dependencies = [ - "log", - "sp-core", - "sp-wasm-interface", - "thiserror", -] - [[package]] name = "sc-authority-discovery" version = "0.10.0-dev" @@ -15178,7 +15168,7 @@ dependencies = [ name = "sc-executor-common" version = "0.10.0-dev" dependencies = [ - "sc-allocator", + "sp-allocator", "sp-maybe-compressed-blob", "sp-wasm-interface", "thiserror", @@ -15198,9 +15188,9 @@ dependencies = [ "parking_lot 0.12.1", "paste", "rustix 0.36.15", - "sc-allocator", "sc-executor-common", "sc-runtime-test", + "sp-allocator", "sp-io", "sp-runtime-interface", "sp-wasm-interface", @@ -16736,6 +16726,16 @@ dependencies = [ "sha-1 0.9.8", ] +[[package]] +name = "sp-allocator" +version = "4.1.0-dev" +dependencies = [ + "log", + "parity-scale-codec", + "sp-wasm-interface-common", + "thiserror", +] + [[package]] name = "sp-api" version = "4.0.0-dev" @@ -17040,6 +17040,7 @@ dependencies = [ "secrecy", "serde", "serde_json", + "sp-allocator", "sp-core-hashing", "sp-core-hashing-proc-macro", "sp-debug-derive", @@ -17600,10 +17601,21 @@ dependencies = [ "impl-trait-for-tuples", "log", "parity-scale-codec", + "sp-allocator", "sp-std", + "sp-wasm-interface-common", "wasmtime", ] +[[package]] +name = "sp-wasm-interface-common" +version = "7.0.0" +dependencies = [ + "parity-scale-codec", + "sp-std", + "wasmi 0.13.2", +] + [[package]] name = "sp-weights" version = "20.0.0" diff --git a/Cargo.toml b/Cargo.toml index c98fe6d1a3ac..b56882840651 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -193,7 +193,6 @@ members = [ "substrate/bin/node/testing", "substrate/bin/utils/chain-spec-builder", "substrate/bin/utils/subkey", - "substrate/client/allocator", "substrate/client/api", "substrate/client/authority-discovery", "substrate/client/basic-authorship", @@ -370,6 +369,7 @@ members = [ "substrate/frame/utility", "substrate/frame/vesting", "substrate/frame/whitelist", + "substrate/primitives/allocator", "substrate/primitives/api", "substrate/primitives/api/proc-macro", "substrate/primitives/api/test", diff --git a/substrate/client/allocator/Cargo.toml b/substrate/client/allocator/Cargo.toml deleted file mode 100644 index 31c714180ce5..000000000000 --- a/substrate/client/allocator/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "sc-allocator" -version = "4.1.0-dev" -authors.workspace = true -edition.workspace = true -license = "Apache-2.0" -homepage = "https://substrate.io" -repository.workspace = true -description = "Collection of allocator implementations." -documentation = "https://docs.rs/sc-allocator" -readme = "README.md" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[dependencies] -log = "0.4.17" -thiserror = "1.0.48" -sp-core = { path = "../../primitives/core" } -sp-wasm-interface = { path = "../../primitives/wasm-interface" } diff --git a/substrate/client/executor/common/Cargo.toml b/substrate/client/executor/common/Cargo.toml index 5118279b43b4..501bfd55dd92 100644 --- a/substrate/client/executor/common/Cargo.toml +++ b/substrate/client/executor/common/Cargo.toml @@ -16,7 +16,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] thiserror = "1.0.48" wasm-instrument = "0.3" -sc-allocator = { path = "../../allocator" } +sp-allocator = { path = "../../../primitives/allocator" } sp-maybe-compressed-blob = { path = "../../../primitives/maybe-compressed-blob" } sp-wasm-interface = { path = "../../../primitives/wasm-interface" } diff --git a/substrate/client/executor/common/src/error.rs b/substrate/client/executor/common/src/error.rs index 2a0dc364b410..62d2283a678e 100644 --- a/substrate/client/executor/common/src/error.rs +++ b/substrate/client/executor/common/src/error.rs @@ -59,7 +59,7 @@ pub enum Error { Other(String), #[error(transparent)] - Allocator(#[from] sc_allocator::Error), + Allocator(#[from] sp_allocator::Error), #[error("Host function {0} execution failed with: {1}")] FunctionExecution(String, String), diff --git a/substrate/client/executor/common/src/util.rs b/substrate/client/executor/common/src/util.rs index 34967f86595d..ac6e957ebb04 100644 --- a/substrate/client/executor/common/src/util.rs +++ b/substrate/client/executor/common/src/util.rs @@ -20,14 +20,6 @@ use crate::error::Result; use sp_wasm_interface::Pointer; -use std::ops::Range; - -/// Construct a range from an offset to a data length after the offset. -/// Returns None if the end of the range would exceed some maximum offset. -pub fn checked_range(offset: usize, len: usize, max: usize) -> Option> { - let end = offset.checked_add(len)?; - (end <= max).then(|| offset..end) -} /// Provides safe memory access interface using an external buffer pub trait MemoryTransfer { diff --git a/substrate/client/executor/common/src/wasm_runtime.rs b/substrate/client/executor/common/src/wasm_runtime.rs index d8e142b9d559..bbe20cc60584 100644 --- a/substrate/client/executor/common/src/wasm_runtime.rs +++ b/substrate/client/executor/common/src/wasm_runtime.rs @@ -21,7 +21,7 @@ use crate::error::Error; use sp_wasm_interface::Value; -pub use sc_allocator::AllocationStats; +pub use sp_allocator::AllocationStats; /// Default heap allocation strategy. pub const DEFAULT_HEAP_ALLOC_STRATEGY: HeapAllocStrategy = diff --git a/substrate/client/executor/wasmi/Cargo.toml b/substrate/client/executor/wasmi/Cargo.toml index 0b00bb6cead7..943b65101748 100644 --- a/substrate/client/executor/wasmi/Cargo.toml +++ b/substrate/client/executor/wasmi/Cargo.toml @@ -17,6 +17,6 @@ targets = ["x86_64-unknown-linux-gnu"] log = "0.4.17" wasmi = { git = "https://github.com/gear-tech/wasmi", branch = "v0.13.2-sign-ext", features = ["virtual_memory"] } sc-executor-common = { version = "0.10.0-dev", path = "../common" } -sp-allocator = { version = "4.1.0-dev", path = "../../../primitives/allocator" } +sp-allocator = { path = "../../primitives/allocator" } sp-runtime-interface = { version = "7.0.0", path = "../../../primitives/runtime-interface" } sp-wasm-interface = { version = "7.0.0", path = "../../../primitives/wasm-interface" } diff --git a/substrate/client/executor/wasmtime/Cargo.toml b/substrate/client/executor/wasmtime/Cargo.toml index 261d52c0ede3..37981154b24f 100644 --- a/substrate/client/executor/wasmtime/Cargo.toml +++ b/substrate/client/executor/wasmtime/Cargo.toml @@ -28,7 +28,7 @@ wasmtime = { version = "8.0.1", default-features = false, features = [ "pooling-allocator" ] } anyhow = "1.0.68" -sc-allocator = { path = "../../allocator" } +sp-allocator = { path = "../../../primitives/allocator" } sc-executor-common = { path = "../common" } sp-runtime-interface = { path = "../../../primitives/runtime-interface" } sp-wasm-interface = { path = "../../../primitives/wasm-interface", features = ["wasmtime"] } diff --git a/substrate/client/executor/wasmtime/src/host.rs b/substrate/client/executor/wasmtime/src/host.rs index f8c78cbb660e..2cc6c7ba7063 100644 --- a/substrate/client/executor/wasmtime/src/host.rs +++ b/substrate/client/executor/wasmtime/src/host.rs @@ -21,41 +21,10 @@ use wasmtime::Caller; -use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator}; -use sp_wasm_interface::{Pointer, WordSize}; +pub use sp_wasm_interface::HostState; +use sp_wasm_interface::{util, FunctionContextToken, Pointer, WordSize}; -use crate::{instance_wrapper::MemoryWrapper, runtime::StoreData, util}; - -/// The state required to construct a HostContext context. The context only lasts for one host -/// call, whereas the state is maintained for the duration of a Wasm runtime call, which may make -/// many different host calls that must share state. -pub struct HostState { - /// The allocator instance to keep track of allocated memory. - /// - /// This is stored as an `Option` as we need to temporarily set this to `None` when we are - /// allocating/deallocating memory. The problem being that we can only mutable access `caller` - /// once. - allocator: Option, - panic_message: Option, -} - -impl HostState { - /// Constructs a new `HostState`. - pub fn new(allocator: FreeingBumpHeapAllocator) -> Self { - HostState { allocator: Some(allocator), panic_message: None } - } - - /// Takes the error message out of the host state, leaving a `None` in its place. - pub fn take_panic_message(&mut self) -> Option { - self.panic_message.take() - } - - pub(crate) fn allocation_stats(&self) -> AllocationStats { - self.allocator.as_ref() - .expect("Allocator is always set and only unavailable when doing an allocation/deallocation; qed") - .stats() - } -} +use crate::runtime::StoreData; /// A `HostContext` implements `FunctionContext` for making host calls from a Wasmtime /// runtime. The `HostContext` exists only for the lifetime of the call and borrows state from @@ -64,15 +33,6 @@ pub(crate) struct HostContext<'a> { pub(crate) caller: Caller<'a, StoreData>, } -impl<'a> HostContext<'a> { - fn host_state_mut(&mut self) -> &mut HostState { - self.caller - .data_mut() - .host_state_mut() - .expect("host state is not empty when calling a function in wasm; qed") - } -} - impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> { fn read_memory_into( &self, @@ -87,42 +47,27 @@ impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> { } fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result> { - let memory = self.caller.data().memory(); - let mut allocator = self - .host_state_mut() - .allocator - .take() - .expect("allocator is not empty when calling a function in wasm; qed"); - - // We can not return on error early, as we need to store back allocator. - let res = allocator - .allocate(&mut MemoryWrapper(&memory, &mut self.caller), size) - .map_err(|e| e.to_string()); - - self.host_state_mut().allocator = Some(allocator); - - res + util::allocate_memory(&mut self.caller, size) } fn deallocate_memory(&mut self, ptr: Pointer) -> sp_wasm_interface::Result<()> { - let memory = self.caller.data().memory(); - let mut allocator = self - .host_state_mut() - .allocator - .take() - .expect("allocator is not empty when calling a function in wasm; qed"); - - // We can not return on error early, as we need to store back allocator. - let res = allocator - .deallocate(&mut MemoryWrapper(&memory, &mut self.caller), ptr) - .map_err(|e| e.to_string()); - - self.host_state_mut().allocator = Some(allocator); - - res + util::deallocate_memory(&mut self.caller, ptr) } fn register_panic_error_message(&mut self, message: &str) { - self.host_state_mut().panic_message = Some(message.to_owned()); + self.caller + .data_mut() + .host_state_mut() + .expect("host state is not empty when calling a function in wasm; qed") + .panic_message = Some(message.to_owned()); + } + + fn with_caller_mut_impl( + &mut self, + _: FunctionContextToken, + context: *mut (), + callback: fn(*mut (), &mut Caller), + ) { + callback(context, &mut self.caller) } } diff --git a/substrate/client/executor/wasmtime/src/instance_wrapper.rs b/substrate/client/executor/wasmtime/src/instance_wrapper.rs index 8852532adbca..00eda1f3c35b 100644 --- a/substrate/client/executor/wasmtime/src/instance_wrapper.rs +++ b/substrate/client/executor/wasmtime/src/instance_wrapper.rs @@ -65,10 +65,12 @@ impl EntryPoint { let data_len = u32::from(data_len); match self.call_type { - EntryPointType::Direct { ref entrypoint } => - entrypoint.call(&mut *store, (data_ptr, data_len)), - EntryPointType::Wrapped { func, ref dispatcher } => - dispatcher.call(&mut *store, (func, data_ptr, data_len)), + EntryPointType::Direct { ref entrypoint } => { + entrypoint.call(&mut *store, (data_ptr, data_len)) + }, + EntryPointType::Wrapped { func, ref dispatcher } => { + dispatcher.call(&mut *store, (func, data_ptr, data_len)) + }, } .map_err(|trap| { let host_state = store @@ -114,41 +116,6 @@ impl EntryPoint { } } -/// Wrapper around [`Memory`] that implements [`sc_allocator::Memory`]. -pub(crate) struct MemoryWrapper<'a, C>(pub &'a wasmtime::Memory, pub &'a mut C); - -impl sc_allocator::Memory for MemoryWrapper<'_, C> { - fn with_access_mut(&mut self, run: impl FnOnce(&mut [u8]) -> R) -> R { - run(self.0.data_mut(&mut self.1)) - } - - fn with_access(&self, run: impl FnOnce(&[u8]) -> R) -> R { - run(self.0.data(&self.1)) - } - - fn grow(&mut self, additional: u32) -> std::result::Result<(), ()> { - self.0 - .grow(&mut self.1, additional as u64) - .map_err(|e| { - log::error!( - target: "wasm-executor", - "Failed to grow memory by {} pages: {}", - additional, - e, - ) - }) - .map(drop) - } - - fn pages(&self) -> u32 { - self.0.size(&self.1) as u32 - } - - fn max_pages(&self) -> Option { - self.0.ty(&self.1).maximum().map(|p| p as _) - } -} - /// Wrap the given WebAssembly Instance of a wasm module with Substrate-runtime. /// /// This struct is a handy wrapper around a wasmtime `Instance` that provides substrate specific diff --git a/substrate/client/executor/wasmtime/src/runtime.rs b/substrate/client/executor/wasmtime/src/runtime.rs index ac88663f4e79..beb8c8b37299 100644 --- a/substrate/client/executor/wasmtime/src/runtime.rs +++ b/substrate/client/executor/wasmtime/src/runtime.rs @@ -20,20 +20,22 @@ use crate::{ host::HostState, - instance_wrapper::{EntryPoint, InstanceWrapper, MemoryWrapper}, - util::{self, replace_strategy_if_broken}, + instance_wrapper::{EntryPoint, InstanceWrapper}, + util::replace_strategy_if_broken, }; use parking_lot::Mutex; -use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator}; use sc_executor_common::{ - error::{Error, Result, WasmError}, + error::{Result, WasmError}, runtime_blob::RuntimeBlob, - util::checked_range, wasm_runtime::{HeapAllocStrategy, InvokeMethod, WasmInstance, WasmModule}, }; +use sp_allocator::{AllocationStats, FreeingBumpHeapAllocator}; use sp_runtime_interface::unpack_ptr_and_len; -use sp_wasm_interface::{HostFunctions, Pointer, Value, WordSize}; +pub use sp_wasm_interface::StoreData; +use sp_wasm_interface::{ + util as memory_util, HostFunctions, MemoryWrapper, Pointer, Value, WordSize, +}; use std::{ path::{Path, PathBuf}, sync::{ @@ -45,28 +47,6 @@ use wasmtime::{AsContext, Engine, Memory, Table}; const MAX_INSTANCE_COUNT: u32 = 64; -#[derive(Default)] -pub(crate) struct StoreData { - /// This will only be set when we call into the runtime. - pub(crate) host_state: Option, - /// This will be always set once the store is initialized. - pub(crate) memory: Option, - /// This will be set only if the runtime actually contains a table. - pub(crate) table: Option, -} - -impl StoreData { - /// Returns a mutable reference to the host state. - pub fn host_state_mut(&mut self) -> Option<&mut HostState> { - self.host_state.as_mut() - } - - /// Returns the host memory. - pub fn memory(&self) -> Memory { - self.memory.expect("memory is always set; qed") - } -} - pub(crate) type Store = wasmtime::Store; enum Strategy { @@ -194,8 +174,9 @@ impl WasmInstance for WasmtimeInstance { fn get_global_const(&mut self, name: &str) -> Result> { match &mut self.strategy { - Strategy::RecreateInstance(ref mut instance_creator) => - instance_creator.instantiate()?.get_global_val(name), + Strategy::RecreateInstance(ref mut instance_creator) => { + instance_creator.instantiate()?.get_global_val(name) + }, } } } @@ -298,8 +279,9 @@ fn common_config(semantics: &Semantics) -> std::result::Result - maximum_pages.map(|p| p as u64 * WASM_PAGE_SIZE).unwrap_or(u64::MAX), + HeapAllocStrategy::Dynamic { maximum_pages } => { + maximum_pages.map(|p| p as u64 * WASM_PAGE_SIZE).unwrap_or(u64::MAX) + }, HeapAllocStrategy::Static { .. } => u64::MAX, }); @@ -307,8 +289,9 @@ fn common_config(semantics: &Semantics) -> std::result::Result - maximum_pages.map(|p| p as u64).unwrap_or(MAX_WASM_PAGES), + HeapAllocStrategy::Dynamic { maximum_pages } => { + maximum_pages.map(|p| p as u64).unwrap_or(MAX_WASM_PAGES) + }, HeapAllocStrategy::Static { .. } => MAX_WASM_PAGES, }; @@ -604,11 +587,12 @@ where .map_err(|e| WasmError::Other(format!("cannot create module: {:#}", e)))?; match config.semantics.instantiation_strategy { - InstantiationStrategy::Pooling | - InstantiationStrategy::PoolingCopyOnWrite | - InstantiationStrategy::RecreateInstance | - InstantiationStrategy::RecreateInstanceCopyOnWrite => - (module, InternalInstantiationStrategy::Builtin), + InstantiationStrategy::Pooling + | InstantiationStrategy::PoolingCopyOnWrite + | InstantiationStrategy::RecreateInstance + | InstantiationStrategy::RecreateInstanceCopyOnWrite => { + (module, InternalInstantiationStrategy::Builtin) + }, } }, CodeSupplyMode::Precompiled(compiled_artifact_path) => { @@ -723,8 +707,8 @@ fn inject_input_data( let mut ctx = instance.store_mut(); let memory = ctx.data().memory(); let data_len = data.len() as WordSize; - let data_ptr = allocator.allocate(&mut MemoryWrapper(&memory, &mut ctx), data_len)?; - util::write_memory_from(instance.store_mut(), data_ptr, data)?; + let data_ptr = allocator.allocate(&mut MemoryWrapper::from((&memory, &mut ctx)), data_len)?; + memory_util::write_memory_from(instance.store_mut(), data_ptr, data)?; Ok((data_ptr, data_len)) } @@ -741,11 +725,11 @@ fn extract_output_data( // // Get the size of the WASM memory in bytes. let memory_size = ctx.as_context().data().memory().data_size(ctx); - if checked_range(output_ptr as usize, output_len as usize, memory_size).is_none() { - Err(Error::OutputExceedsBounds)? + if memory_util::checked_range(output_ptr as usize, output_len as usize, memory_size).is_none() { + Err(WasmError::Other("output exceeds bounds of wasm memory".into()))? } let mut output = vec![0; output_len as usize]; - util::read_memory_into(ctx, Pointer::new(output_ptr), &mut output)?; + memory_util::read_memory_into(ctx, Pointer::new(output_ptr), &mut output)?; Ok(output) } diff --git a/substrate/client/executor/wasmtime/src/util.rs b/substrate/client/executor/wasmtime/src/util.rs index 7af554c35e1b..d63eedaf3e4d 100644 --- a/substrate/client/executor/wasmtime/src/util.rs +++ b/substrate/client/executor/wasmtime/src/util.rs @@ -19,11 +19,35 @@ use crate::{runtime::StoreData, InstantiationStrategy}; use sc_executor_common::{ error::{Error, Result}, - util::checked_range, }; -use sp_wasm_interface::Pointer; +use sp_wasm_interface::util as memory_util; +use sp_wasm_interface::{Pointer, Value}; use wasmtime::{AsContext, AsContextMut}; +/// Converts a [`wasmtime::Val`] into a substrate runtime interface [`Value`]. +/// +/// Panics if the given value doesn't have a corresponding variant in `Value`. +pub fn from_wasmtime_val(val: wasmtime::Val) -> Value { + match val { + wasmtime::Val::I32(v) => Value::I32(v), + wasmtime::Val::I64(v) => Value::I64(v), + wasmtime::Val::F32(f_bits) => Value::F32(f_bits), + wasmtime::Val::F64(f_bits) => Value::F64(f_bits), + v => panic!("Given value type is unsupported by Substrate: {:?}", v), + } +} + +/// Converts a sp_wasm_interface's [`Value`] into the corresponding variant in wasmtime's +/// [`wasmtime::Val`]. +pub fn into_wasmtime_val(value: Value) -> wasmtime::Val { + match value { + Value::I32(v) => wasmtime::Val::I32(v), + Value::I64(v) => wasmtime::Val::I64(v), + Value::F32(f_bits) => wasmtime::Val::F32(f_bits), + Value::F64(f_bits) => wasmtime::Val::F64(f_bits), + } +} + /// Read data from the instance memory into a slice. /// /// Returns an error if the read would go out of the memory bounds. @@ -34,7 +58,7 @@ pub(crate) fn read_memory_into( ) -> Result<()> { let memory = ctx.as_context().data().memory().data(&ctx); - let range = checked_range(address.into(), dest.len(), memory.len()) + let range = memory_util::checked_range(address.into(), dest.len(), memory.len()) .ok_or_else(|| Error::Other("memory read is out of bounds".into()))?; dest.copy_from_slice(&memory[range]); Ok(()) @@ -51,7 +75,7 @@ pub(crate) fn write_memory_from( let memory = ctx.as_context().data().memory(); let memory = memory.data_mut(&mut ctx); - let range = checked_range(address.into(), data.len(), memory.len()) + let range = memory_util::checked_range(address.into(), data.len(), memory.len()) .ok_or_else(|| Error::Other("memory write is out of bounds".into()))?; memory[range].copy_from_slice(data); Ok(()) @@ -116,8 +140,9 @@ pub(crate) fn replace_strategy_if_broken(strategy: &mut InstantiationStrategy) { // These strategies require a working `madvise` to be sound. InstantiationStrategy::PoolingCopyOnWrite => InstantiationStrategy::Pooling, - InstantiationStrategy::RecreateInstanceCopyOnWrite => - InstantiationStrategy::RecreateInstance, + InstantiationStrategy::RecreateInstanceCopyOnWrite => { + InstantiationStrategy::RecreateInstance + }, }; use std::sync::OnceLock; diff --git a/substrate/primitives/allocator/Cargo.toml b/substrate/primitives/allocator/Cargo.toml new file mode 100644 index 000000000000..18241f38901b --- /dev/null +++ b/substrate/primitives/allocator/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "sp-allocator" +version = "4.1.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "Collection of allocator implementations." +documentation = "https://docs.rs/sc-allocator" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +log = { version = "0.4.17", default-features = false } +thiserror = { version = "1.0.30", optional = true } +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +sp-wasm-interface-common = { version = "7.0.0", path = "../wasm-interface-common", default-features = false } + +[features] +default = [ "std" ] +std = [ "codec/std", "log/std", "sp-wasm-interface-common/std", "thiserror" ] diff --git a/substrate/client/allocator/README.md b/substrate/primitives/allocator/README.md similarity index 100% rename from substrate/client/allocator/README.md rename to substrate/primitives/allocator/README.md diff --git a/substrate/client/allocator/src/error.rs b/substrate/primitives/allocator/src/error.rs similarity index 100% rename from substrate/client/allocator/src/error.rs rename to substrate/primitives/allocator/src/error.rs diff --git a/substrate/client/allocator/src/freeing_bump.rs b/substrate/primitives/allocator/src/freeing_bump.rs similarity index 98% rename from substrate/client/allocator/src/freeing_bump.rs rename to substrate/primitives/allocator/src/freeing_bump.rs index 144c0764540d..f380196bd97f 100644 --- a/substrate/client/allocator/src/freeing_bump.rs +++ b/substrate/primitives/allocator/src/freeing_bump.rs @@ -67,9 +67,8 @@ //! wasted. This is more pronounced (in terms of absolute heap amounts) with larger allocation //! sizes. -use crate::{Error, Memory, MAX_WASM_PAGES, PAGE_SIZE}; -pub use sp_core::MAX_POSSIBLE_ALLOCATION; -use sp_wasm_interface::{Pointer, WordSize}; +use crate::{Error, Memory, MAX_POSSIBLE_ALLOCATION, MAX_WASM_PAGES, PAGE_SIZE}; +use sp_wasm_interface_common::{Pointer, WordSize}; use std::{ cmp::{max, min}, mem, @@ -142,7 +141,7 @@ impl Order { fn from_size(size: u32) -> Result { let clamped_size = if size > MAX_POSSIBLE_ALLOCATION { log::warn!(target: LOG_TARGET, "going to fail due to allocating {:?}", size); - return Err(Error::RequestedAllocationTooLarge) + return Err(Error::RequestedAllocationTooLarge); } else if size < MIN_POSSIBLE_ALLOCATION { MIN_POSSIBLE_ALLOCATION } else { @@ -411,7 +410,7 @@ impl FreeingBumpHeapAllocator { size: WordSize, ) -> Result, Error> { if self.poisoned { - return Err(error("the allocator has been poisoned")) + return Err(error("the allocator has been poisoned")); } let bomb = PoisonBomb { poisoned: &mut self.poisoned }; @@ -421,10 +420,10 @@ impl FreeingBumpHeapAllocator { let header_ptr: u32 = match self.free_lists[order] { Link::Ptr(header_ptr) => { - if (u64::from(header_ptr) + u64::from(order.size()) + u64::from(HEADER_SIZE)) > - mem.size() + if (u64::from(header_ptr) + u64::from(order.size()) + u64::from(HEADER_SIZE)) + > mem.size() { - return Err(error("Invalid header pointer detected")) + return Err(error("Invalid header pointer detected")); } // Remove this header from the free list. @@ -469,7 +468,7 @@ impl FreeingBumpHeapAllocator { /// - `ptr` - pointer to the allocated chunk pub fn deallocate(&mut self, mem: &mut impl Memory, ptr: Pointer) -> Result<(), Error> { if self.poisoned { - return Err(error("the allocator has been poisoned")) + return Err(error("the allocator has been poisoned")); } let bomb = PoisonBomb { poisoned: &mut self.poisoned }; @@ -529,14 +528,14 @@ impl FreeingBumpHeapAllocator { "Wasm pages ({current_pages}) are already at the maximum.", ); - return Err(Error::AllocatorOutOfSpace) + return Err(Error::AllocatorOutOfSpace); } else if required_pages > max_pages { log::debug!( target: LOG_TARGET, "Failed to grow memory from {current_pages} pages to at least {required_pages}\ pages due to the maximum limit of {max_pages} pages", ); - return Err(Error::AllocatorOutOfSpace) + return Err(Error::AllocatorOutOfSpace); } // Ideally we want to double our current number of pages, @@ -551,7 +550,7 @@ impl FreeingBumpHeapAllocator { "Failed to grow memory from {current_pages} pages to {next_pages} pages", ); - return Err(Error::AllocatorOutOfSpace) + return Err(Error::AllocatorOutOfSpace); } debug_assert_eq!(memory.pages(), next_pages, "Number of pages should have increased!"); @@ -567,7 +566,7 @@ impl FreeingBumpHeapAllocator { mem: &mut impl Memory, ) -> Result<(), Error> { if mem.size() < *last_observed_memory_size { - return Err(Error::MemoryShrinked) + return Err(Error::MemoryShrinked); } *last_observed_memory_size = mem.size(); Ok(()) diff --git a/substrate/client/allocator/src/lib.rs b/substrate/primitives/allocator/src/lib.rs similarity index 86% rename from substrate/client/allocator/src/lib.rs rename to substrate/primitives/allocator/src/lib.rs index e50d7d54c8e9..d8fc749b5546 100644 --- a/substrate/client/allocator/src/lib.rs +++ b/substrate/primitives/allocator/src/lib.rs @@ -20,12 +20,17 @@ //! This crate provides the following allocator implementations: //! - A freeing-bump allocator: [`FreeingBumpHeapAllocator`](freeing_bump::FreeingBumpHeapAllocator) +#![cfg_attr(not(feature = "std"), no_std)] #![warn(missing_docs)] +#[cfg(feature = "std")] mod error; +#[cfg(feature = "std")] mod freeing_bump; +#[cfg(feature = "std")] pub use error::Error; +#[cfg(feature = "std")] pub use freeing_bump::{AllocationStats, FreeingBumpHeapAllocator}; /// The size of one wasm page in bytes. @@ -59,3 +64,9 @@ pub trait Memory { /// If `None` is returned, there is no maximum (besides the maximum defined in the wasm spec). fn max_pages(&self) -> Option; } + +/// The maximum number of bytes that can be allocated at one time. +// The maximum possible allocation size was chosen rather arbitrary, 32 MiB should be enough for +// everybody. +// 2^25 bytes, 32 MiB +pub const MAX_POSSIBLE_ALLOCATION: u32 = 33_554_432; diff --git a/substrate/primitives/core/Cargo.toml b/substrate/primitives/core/Cargo.toml index 1b6b10eeeedf..5ce5d460e5dd 100644 --- a/substrate/primitives/core/Cargo.toml +++ b/substrate/primitives/core/Cargo.toml @@ -51,6 +51,7 @@ libsecp256k1 = { version = "0.7", default-features = false, features = ["static- schnorrkel = { version = "0.9.1", features = ["preaudit_deprecated", "u64_backend"], default-features = false } merlin = { version = "2.0", default-features = false } secp256k1 = { version = "0.24.0", default-features = false, features = ["recovery", "alloc"], optional = true } +sp-allocator = { version = "4.1.0-dev", path = "../allocator", default-features = false } sp-core-hashing = { path = "hashing", default-features = false, optional = true } sp-runtime-interface = { path = "../runtime-interface", default-features = false} diff --git a/substrate/primitives/core/src/lib.rs b/substrate/primitives/core/src/lib.rs index ec0641c54668..773b413d235d 100644 --- a/substrate/primitives/core/src/lib.rs +++ b/substrate/primitives/core/src/lib.rs @@ -399,10 +399,7 @@ macro_rules! impl_maybe_marker_std_or_serde { } } -/// The maximum number of bytes that can be allocated at one time. -// The maximum possible allocation size was chosen rather arbitrary, 32 MiB should be enough for -// everybody. -pub const MAX_POSSIBLE_ALLOCATION: u32 = 33554432; // 2^25 bytes, 32 MiB +pub use sp_allocator::MAX_POSSIBLE_ALLOCATION; /// Generates a macro for checking if a certain feature is enabled. /// diff --git a/substrate/primitives/wasm-interface-common/Cargo.toml b/substrate/primitives/wasm-interface-common/Cargo.toml new file mode 100644 index 000000000000..0d319e9591b6 --- /dev/null +++ b/substrate/primitives/wasm-interface-common/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sp-wasm-interface-common" +version = "7.0.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "Types and traits for interfacing between the host and the wasm runtime." +documentation = "https://docs.rs/sp-wasm-interface" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +wasmi = { git = "https://github.com/gear-tech/wasmi", branch = "v0.13.2-sign-ext", default-features = false, optional = true } +sp-std = { version = "8.0.0", default-features = false, path = "../std" } + +[features] +default = [ "std" ] +std = [ "codec/std", "sp-std/std", "wasmi/std" ] diff --git a/substrate/primitives/wasm-interface-common/README.md b/substrate/primitives/wasm-interface-common/README.md new file mode 100644 index 000000000000..7e6c46581ae4 --- /dev/null +++ b/substrate/primitives/wasm-interface-common/README.md @@ -0,0 +1,3 @@ +Types and traits for interfacing between the host and the wasm runtime. + +License: Apache-2.0 \ No newline at end of file diff --git a/substrate/primitives/wasm-interface-common/src/lib.rs b/substrate/primitives/wasm-interface-common/src/lib.rs new file mode 100644 index 000000000000..18d5a9759203 --- /dev/null +++ b/substrate/primitives/wasm-interface-common/src/lib.rs @@ -0,0 +1,346 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types and traits for interfacing between the host and the wasm runtime. + +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::borrow::Cow; +use core::{mem, marker::PhantomData}; + +pub mod util; + +#[cfg(feature = "wasmi")] +pub use wasmi; + +#[cfg(feature = "wasmi")] +pub mod wasmi_impl; + +/// Value types supported by Substrate on the boundary between host/Wasm. +#[derive(Copy, Clone, PartialEq, Debug, Eq)] +pub enum ValueType { + /// An `i32` value type. + I32, + /// An `i64` value type. + I64, + /// An `f32` value type. + F32, + /// An `f64` value type. + F64, +} + +impl From for u8 { + fn from(val: ValueType) -> u8 { + match val { + ValueType::I32 => 0, + ValueType::I64 => 1, + ValueType::F32 => 2, + ValueType::F64 => 3, + } + } +} + +impl TryFrom for ValueType { + type Error = (); + + fn try_from(val: u8) -> core::result::Result { + match val { + 0 => Ok(Self::I32), + 1 => Ok(Self::I64), + 2 => Ok(Self::F32), + 3 => Ok(Self::F64), + _ => Err(()), + } + } +} + +/// Values supported by Substrate on the boundary between host/Wasm. +#[derive(PartialEq, Debug, Clone, Copy, codec::Encode, codec::Decode)] +pub enum Value { + /// A 32-bit integer. + I32(i32), + /// A 64-bit integer. + I64(i64), + /// A 32-bit floating-point number stored as raw bit pattern. + /// + /// You can materialize this value using `f32::from_bits`. + F32(u32), + /// A 64-bit floating-point number stored as raw bit pattern. + /// + /// You can materialize this value using `f64::from_bits`. + F64(u64), +} + +impl Value { + /// Returns the type of this value. + pub fn value_type(&self) -> ValueType { + match self { + Value::I32(_) => ValueType::I32, + Value::I64(_) => ValueType::I64, + Value::F32(_) => ValueType::F32, + Value::F64(_) => ValueType::F64, + } + } + + /// Return `Self` as `i32`. + pub fn as_i32(&self) -> Option { + match self { + Self::I32(val) => Some(*val), + _ => None, + } + } +} + +/// Something that can be converted into a wasm compatible `Value`. +pub trait IntoValue { + /// The type of the value in wasm. + const VALUE_TYPE: ValueType; + + /// Convert `self` into a wasm `Value`. + fn into_value(self) -> Value; +} + +/// Something that can may be created from a wasm `Value`. +pub trait TryFromValue: Sized { + /// Try to convert the given `Value` into `Self`. + fn try_from_value(val: Value) -> Option; +} + +macro_rules! impl_into_and_from_value { + ( + $( + $type:ty, $( < $gen:ident >, )? $value_variant:ident, + )* + ) => { + $( + impl $( <$gen> )? IntoValue for $type { + const VALUE_TYPE: ValueType = ValueType::$value_variant; + fn into_value(self) -> Value { Value::$value_variant(self as _) } + } + + impl $( <$gen> )? TryFromValue for $type { + fn try_from_value(val: Value) -> Option { + match val { + Value::$value_variant(val) => Some(val as _), + _ => None, + } + } + } + )* + } +} + +impl_into_and_from_value! { + u8, I32, + u16, I32, + u32, I32, + u64, I64, + i8, I32, + i16, I32, + i32, I32, + i64, I64, +} + +/// Provides `Sealed` trait to prevent implementing trait `PointerType` and `WasmTy` outside of this +/// crate. +mod private { + pub trait Sealed {} + + impl Sealed for u8 {} + impl Sealed for u16 {} + impl Sealed for u32 {} + impl Sealed for u64 {} + + impl Sealed for i32 {} + impl Sealed for i64 {} +} + +/// Something that can be wrapped in a wasm `Pointer`. +/// +/// This trait is sealed. +pub trait PointerType: Sized + private::Sealed { + /// The size of the type in wasm. + const SIZE: u32 = mem::size_of::() as u32; +} + +impl PointerType for u8 {} +impl PointerType for u16 {} +impl PointerType for u32 {} +impl PointerType for u64 {} + +/// Type to represent a pointer in wasm at the host. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Pointer { + ptr: u32, + _marker: PhantomData, +} + +impl Pointer { + /// Create a new instance of `Self`. + pub fn new(ptr: u32) -> Self { + Self { ptr, _marker: Default::default() } + } + + /// Calculate the offset from this pointer. + /// + /// `offset` is in units of `T`. So, `3` means `3 * mem::size_of::()` as offset to the + /// pointer. + /// + /// Returns an `Option` to respect that the pointer could probably overflow. + pub fn offset(self, offset: u32) -> Option { + offset + .checked_mul(T::SIZE) + .and_then(|o| self.ptr.checked_add(o)) + .map(|ptr| Self { ptr, _marker: Default::default() }) + } + + /// Create a null pointer. + pub fn null() -> Self { + Self::new(0) + } + + /// Cast this pointer of type `T` to a pointer of type `R`. + pub fn cast(self) -> Pointer { + Pointer::new(self.ptr) + } +} + +impl From for Pointer { + fn from(ptr: u32) -> Self { + Pointer::new(ptr) + } +} + +impl From> for u32 { + fn from(ptr: Pointer) -> Self { + ptr.ptr + } +} + +impl From> for u64 { + fn from(ptr: Pointer) -> Self { + u64::from(ptr.ptr) + } +} + +impl From> for usize { + fn from(ptr: Pointer) -> Self { + ptr.ptr as _ + } +} + +// trait implementations for Pointer. +impl IntoValue for Pointer { + const VALUE_TYPE: ValueType = ValueType::I32; + fn into_value(self) -> Value { + Value::I32(self.ptr as _) + } +} + +impl TryFromValue for Pointer { + fn try_from_value(val: Value) -> Option { + match val { + Value::I32(val) => Some(Self::new(val as _)), + _ => None, + } + } +} + +/// The Signature of a function +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct Signature { + /// The arguments of a function. + pub args: Cow<'static, [ValueType]>, + /// The optional return value of a function. + pub return_value: Option, +} + +impl Signature { + /// Create a new instance of `Signature`. + pub fn new>>( + args: T, + return_value: Option, + ) -> Self { + Self { args: args.into(), return_value } + } + + /// Create a new instance of `Signature` with the given `args` and without any return value. + pub fn new_with_args>>(args: T) -> Self { + Self { args: args.into(), return_value: None } + } +} + +/// The word size used in wasm. Normally known as `usize` in Rust. +pub type WordSize = u32; + +/// Sandbox memory identifier. +pub type MemoryId = u32; + +/// Host pointer: suit both for 32-bit and 64-bit archs. +pub type HostPointer = u64; + +/// Typed value that can be returned from a function. +/// +/// Basically a `TypedValue` plus `Unit`, for functions which return nothing. +#[derive(Clone, Copy, PartialEq, codec::Encode, codec::Decode, Debug)] +pub enum ReturnValue { + /// For returning nothing. + Unit, + /// For returning some concrete value. + Value(Value), +} + +impl From for ReturnValue { + fn from(v: Value) -> ReturnValue { + ReturnValue::Value(v) + } +} + +impl ReturnValue { + /// Maximum number of bytes `ReturnValue` might occupy when serialized with `SCALE`. + /// + /// Breakdown: + /// 1 byte for encoding unit/value variant + /// 1 byte for encoding value type + /// 8 bytes for encoding the biggest value types available in wasm: f64, i64. + pub const ENCODED_MAX_SIZE: usize = 10; +} + +#[cfg(test)] +mod tests { + use super::*; + use codec::Encode; + + #[test] + fn pointer_offset_works() { + let ptr = Pointer::::null(); + + assert_eq!(ptr.offset(10).unwrap(), Pointer::new(40)); + assert_eq!(ptr.offset(32).unwrap(), Pointer::new(128)); + + let ptr = Pointer::::null(); + + assert_eq!(ptr.offset(10).unwrap(), Pointer::new(80)); + assert_eq!(ptr.offset(32).unwrap(), Pointer::new(256)); + } + + #[test] + fn return_value_encoded_max_size() { + let encoded = ReturnValue::Value(Value::I64(-1)).encode(); + assert_eq!(encoded.len(), ReturnValue::ENCODED_MAX_SIZE); + } +} diff --git a/substrate/primitives/wasm-interface-common/src/util.rs b/substrate/primitives/wasm-interface-common/src/util.rs new file mode 100644 index 000000000000..2b4ebd6e6651 --- /dev/null +++ b/substrate/primitives/wasm-interface-common/src/util.rs @@ -0,0 +1,26 @@ +// This file is part of Gear. +// +// Copyright (C) 2021-2023 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use core::ops::Range; + +/// Construct a range from an offset to a data length after the offset. +/// Returns None if the end of the range would exceed some maximum offset. +pub fn checked_range(offset: usize, len: usize, max: usize) -> Option> { + let end = offset.checked_add(len)?; + (end <= max).then(|| offset..end) +} diff --git a/substrate/primitives/wasm-interface-common/src/wasmi_impl.rs b/substrate/primitives/wasm-interface-common/src/wasmi_impl.rs new file mode 100644 index 000000000000..7394e3455130 --- /dev/null +++ b/substrate/primitives/wasm-interface-common/src/wasmi_impl.rs @@ -0,0 +1,80 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation of conversions between Substrate and wasmi types. +use crate::{Signature, Value, ValueType}; +use sp_std::vec::Vec; + +impl From for wasmi::RuntimeValue { + fn from(value: Value) -> Self { + match value { + Value::I32(val) => Self::I32(val), + Value::I64(val) => Self::I64(val), + Value::F32(val) => Self::F32(val.into()), + Value::F64(val) => Self::F64(val.into()), + } + } +} + +impl From for Value { + fn from(value: wasmi::RuntimeValue) -> Self { + match value { + wasmi::RuntimeValue::I32(val) => Self::I32(val), + wasmi::RuntimeValue::I64(val) => Self::I64(val), + wasmi::RuntimeValue::F32(val) => Self::F32(val.into()), + wasmi::RuntimeValue::F64(val) => Self::F64(val.into()), + } + } +} + +impl From for wasmi::ValueType { + fn from(value: ValueType) -> Self { + match value { + ValueType::I32 => Self::I32, + ValueType::I64 => Self::I64, + ValueType::F32 => Self::F32, + ValueType::F64 => Self::F64, + } + } +} + +impl From for ValueType { + fn from(value: wasmi::ValueType) -> Self { + match value { + wasmi::ValueType::I32 => Self::I32, + wasmi::ValueType::I64 => Self::I64, + wasmi::ValueType::F32 => Self::F32, + wasmi::ValueType::F64 => Self::F64, + } + } +} + +impl From for wasmi::Signature { + fn from(sig: Signature) -> Self { + let args = sig.args.iter().map(|a| (*a).into()).collect::>(); + wasmi::Signature::new(args, sig.return_value.map(Into::into)) + } +} + +impl From<&wasmi::Signature> for Signature { + fn from(sig: &wasmi::Signature) -> Self { + Signature::new( + sig.params().iter().copied().map(Into::into).collect::>(), + sig.return_type().map(Into::into), + ) + } +} diff --git a/substrate/primitives/wasm-interface/Cargo.toml b/substrate/primitives/wasm-interface/Cargo.toml index c7413fec43c4..4f305268ac08 100644 --- a/substrate/primitives/wasm-interface/Cargo.toml +++ b/substrate/primitives/wasm-interface/Cargo.toml @@ -20,8 +20,11 @@ log = { version = "0.4.17", optional = true } wasmtime = { version = "8.0.1", default-features = false, optional = true } anyhow = { version = "1.0.68", optional = true } sp-std = { path = "../std", default-features = false} +sp-wasm-interface-common = { version = "7.0.0", path = "../wasm-interface-common", default-features = false } +sp-allocator = { version = "4.1.0-dev", path = "../allocator", default-features = false } [features] default = [ "std" ] -std = [ "codec/std", "log/std", "sp-std/std", "wasmtime" ] +std = ["codec/std", "log/std", "sp-allocator/std", "sp-std/std", "wasmtime"] wasmtime = [ "anyhow", "dep:wasmtime" ] +wasmi = [ "sp-wasm-interface-common/wasmi" ] diff --git a/substrate/primitives/wasm-interface/lib.rs b/substrate/primitives/wasm-interface/lib.rs new file mode 100644 index 000000000000..b6e85b5894da --- /dev/null +++ b/substrate/primitives/wasm-interface/lib.rs @@ -0,0 +1,385 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types and traits for interfacing between the host and the wasm runtime. + +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::{iter::Iterator, marker::PhantomData, result, vec, vec::Vec}; +pub use sp_wasm_interface_common::{ + self as common, HostPointer, IntoValue, MemoryId, Pointer, PointerType, ReturnValue, Signature, + TryFromValue, Value, ValueType, WordSize, +}; + +if_wasmtime_is_enabled! { + mod host_state; + pub use host_state::HostState; + + mod store_data; + pub use store_data::StoreData; + + mod memory_wrapper; + pub use memory_wrapper::MemoryWrapper; + + pub mod util; +} + +#[cfg(not(all(feature = "std", feature = "wasmtime")))] +pub struct StoreData; + +#[cfg(not(all(feature = "std", feature = "wasmtime")))] +#[macro_export] +macro_rules! if_wasmtime_is_enabled { + ($($token:tt)*) => {}; +} + +#[cfg(all(feature = "std", feature = "wasmtime"))] +#[macro_export] +macro_rules! if_wasmtime_is_enabled { + ($($token:tt)*) => { + $($token)* + } +} + +if_wasmtime_is_enabled! { + // Reexport wasmtime so that its types are accessible from the procedural macro. + pub use wasmtime; + + // Wasmtime uses anyhow types but doesn't reexport them. + pub use anyhow; +} + +/// Result type used by traits in this crate. +#[cfg(feature = "std")] +pub type Result = result::Result; +#[cfg(not(feature = "std"))] +pub type Result = result::Result; + +/// Provides `Sealed` trait to prevent implementing trait `PointerType` and `WasmTy` outside of this +/// crate. +mod private { + pub trait Sealed {} + + impl Sealed for u8 {} + impl Sealed for u16 {} + impl Sealed for u32 {} + impl Sealed for u64 {} + + impl Sealed for i32 {} + impl Sealed for i64 {} + + pub(super) struct Token; +} + +/// A trait that requires `RefUnwindSafe` when `feature = std`. +#[cfg(feature = "std")] +pub trait MaybeRefUnwindSafe: std::panic::RefUnwindSafe {} +#[cfg(feature = "std")] +impl MaybeRefUnwindSafe for T {} + +/// A trait that requires `RefUnwindSafe` when `feature = std`. +#[cfg(not(feature = "std"))] +pub trait MaybeRefUnwindSafe {} +#[cfg(not(feature = "std"))] +impl MaybeRefUnwindSafe for T {} + +/// Something that provides a function implementation on the host for a wasm function. +pub trait Function: MaybeRefUnwindSafe + Send + Sync { + /// Returns the name of this function. + fn name(&self) -> &str; + /// Returns the signature of this function. + fn signature(&self) -> Signature; + /// Execute this function with the given arguments. + fn execute( + &self, + context: &mut dyn FunctionContext, + args: &mut dyn Iterator, + ) -> Result>; +} + +impl PartialEq for dyn Function { + fn eq(&self, other: &Self) -> bool { + other.name() == self.name() && other.signature() == self.signature() + } +} + +#[cfg(not(all(feature = "std", feature = "wasmtime")))] +pub struct Caller<'a, T>(PhantomData<&'a T>); + +#[cfg(all(feature = "std", feature = "wasmtime"))] +pub use wasmtime::Caller; + +pub struct FunctionContextToken(private::Token); + +impl FunctionContextToken { + fn new() -> Self { + Self(private::Token) + } +} + +/// Context used by `Function` to interact with the allocator and the memory of the wasm instance. +pub trait FunctionContext { + /// Read memory from `address` into a vector. + fn read_memory(&self, address: Pointer, size: WordSize) -> Result> { + let mut vec = vec![0; size as usize]; + self.read_memory_into(address, &mut vec)?; + Ok(vec) + } + /// Read memory into the given `dest` buffer from `address`. + fn read_memory_into(&self, address: Pointer, dest: &mut [u8]) -> Result<()>; + /// Write the given data at `address` into the memory. + fn write_memory(&mut self, address: Pointer, data: &[u8]) -> Result<()>; + /// Allocate a memory instance of `size` bytes. + fn allocate_memory(&mut self, size: WordSize) -> Result>; + /// Deallocate a given memory instance. + fn deallocate_memory(&mut self, ptr: Pointer) -> Result<()>; + /// Registers a panic error message within the executor. + /// + /// This is meant to be used in situations where the runtime + /// encounters an unrecoverable error and intends to panic. + /// + /// Panicking in WASM is done through the [`unreachable`](https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-control) + /// instruction which causes an unconditional trap and immediately aborts + /// the execution. It does not however allow for any diagnostics to be + /// passed through to the host, so while we do know that *something* went + /// wrong we don't have any direct indication of what *exactly* went wrong. + /// + /// As a workaround we use this method right before the execution is + /// actually aborted to pass an error message to the host so that it + /// can associate it with the next trap, and return that to the caller. + /// + /// A WASM trap should be triggered immediately after calling this method; + /// otherwise the error message might be associated with a completely + /// unrelated trap. + /// + /// It should only be called once, however calling it more than once + /// is harmless and will overwrite the previously set error message. + fn register_panic_error_message(&mut self, message: &str); + + fn with_caller_mut_impl( + &mut self, + _: FunctionContextToken, + context: *mut (), + callback: fn(*mut (), &mut Caller), + ); +} + +pub fn with_caller_mut)>( + context: &mut dyn FunctionContext, + mut callback: T, +) { + let callback: *mut T = &mut callback; + context.with_caller_mut_impl(FunctionContextToken::new(), callback.cast(), |callback, caller| { + let callback: *mut T = callback.cast(); + let callback: &mut T = unsafe { callback.as_mut().expect("we own the value, obtain mutable reference to it and cast to pointer (correct (not null) and aligned properly); qed") }; + + callback(caller); + }) +} + +if_wasmtime_is_enabled! { + /// A trait used to statically register host callbacks with the WASM executor, + /// so that they call be called from within the runtime with minimal overhead. + /// + /// This is used internally to interface the wasmtime-based executor with the + /// host functions' definitions generated through the runtime interface macro, + /// and is not meant to be used directly. + pub trait HostFunctionRegistry { + type State; + type Error; + type FunctionContext: FunctionContext; + + /// Wraps the given `caller` in a type which implements `FunctionContext` + /// and calls the given `callback`. + fn with_function_context( + caller: wasmtime::Caller, + callback: impl FnOnce(&mut dyn FunctionContext) -> R, + ) -> R; + + /// Registers a given host function with the WASM executor. + /// + /// The function has to be statically callable, and all of its arguments + /// and its return value have to be compatible with WASM FFI. + fn register_static( + &mut self, + fn_name: &str, + func: impl wasmtime::IntoFunc + 'static, + ) -> core::result::Result<(), Self::Error>; + } +} + +/// Something that provides implementations for host functions. +pub trait HostFunctions: 'static + Send + Sync { + /// Returns the host functions `Self` provides. + fn host_functions() -> Vec<&'static dyn Function>; + + if_wasmtime_is_enabled! { + /// Statically registers the host functions. + fn register_static(registry: &mut T) -> core::result::Result<(), T::Error> + where + T: HostFunctionRegistry; + } +} + +#[impl_trait_for_tuples::impl_for_tuples(30)] +impl HostFunctions for Tuple { + fn host_functions() -> Vec<&'static dyn Function> { + let mut host_functions = Vec::new(); + + for_tuples!( #( host_functions.extend(Tuple::host_functions()); )* ); + + host_functions + } + + #[cfg(all(feature = "std", feature = "wasmtime"))] + fn register_static(registry: &mut T) -> core::result::Result<(), T::Error> + where + T: HostFunctionRegistry, + { + for_tuples!( + #( Tuple::register_static(registry)?; )* + ); + + Ok(()) + } +} + +/// A wrapper which merges two sets of host functions, and allows the second set to override +/// the host functions from the first set. +pub struct ExtendedHostFunctions { + phantom: PhantomData<(Base, Overlay)>, +} + +impl HostFunctions for ExtendedHostFunctions +where + Base: HostFunctions, + Overlay: HostFunctions, +{ + fn host_functions() -> Vec<&'static dyn Function> { + let mut base = Base::host_functions(); + let overlay = Overlay::host_functions(); + base.retain(|host_fn| { + !overlay.iter().any(|ext_host_fn| host_fn.name() == ext_host_fn.name()) + }); + base.extend(overlay); + base + } + + if_wasmtime_is_enabled! { + fn register_static(registry: &mut T) -> core::result::Result<(), T::Error> + where + T: HostFunctionRegistry, + { + struct Proxy<'a, T> { + registry: &'a mut T, + seen_overlay: std::collections::HashSet, + seen_base: std::collections::HashSet, + overlay_registered: bool, + } + + impl<'a, T> HostFunctionRegistry for Proxy<'a, T> + where + T: HostFunctionRegistry, + { + type State = T::State; + type Error = T::Error; + type FunctionContext = T::FunctionContext; + + fn with_function_context( + caller: wasmtime::Caller, + callback: impl FnOnce(&mut dyn FunctionContext) -> R, + ) -> R { + T::with_function_context(caller, callback) + } + + fn register_static( + &mut self, + fn_name: &str, + func: impl wasmtime::IntoFunc + 'static, + ) -> core::result::Result<(), Self::Error> { + if self.overlay_registered { + if !self.seen_base.insert(fn_name.to_owned()) { + log::warn!( + target: "extended_host_functions", + "Duplicate base host function: '{}'", + fn_name, + ); + + // TODO: Return an error here? + return Ok(()) + } + + if self.seen_overlay.contains(fn_name) { + // Was already registered when we went through the overlay, so just ignore it. + log::debug!( + target: "extended_host_functions", + "Overriding base host function: '{}'", + fn_name, + ); + + return Ok(()) + } + } else if !self.seen_overlay.insert(fn_name.to_owned()) { + log::warn!( + target: "extended_host_functions", + "Duplicate overlay host function: '{}'", + fn_name, + ); + + // TODO: Return an error here? + return Ok(()) + } + + self.registry.register_static(fn_name, func) + } + } + + let mut proxy = Proxy { + registry, + seen_overlay: Default::default(), + seen_base: Default::default(), + overlay_registered: false, + }; + + // The functions from the `Overlay` can override those from the `Base`, + // so `Overlay` is registered first, and then we skip those functions + // in `Base` if they were already registered from the `Overlay`. + Overlay::register_static(&mut proxy)?; + proxy.overlay_registered = true; + Base::register_static(&mut proxy)?; + + Ok(()) + } + } +} + +/// A trait for types directly usable at the WASM FFI boundary without any conversion at all. +/// +/// This trait is sealed and should not be implemented downstream. +#[cfg(all(feature = "std", feature = "wasmtime"))] +pub trait WasmTy: wasmtime::WasmTy + private::Sealed {} + +/// A trait for types directly usable at the WASM FFI boundary without any conversion at all. +/// +/// This trait is sealed and should not be implemented downstream. +#[cfg(not(all(feature = "std", feature = "wasmtime")))] +pub trait WasmTy: private::Sealed {} + +impl WasmTy for i32 {} +impl WasmTy for u32 {} +impl WasmTy for i64 {} +impl WasmTy for u64 {} diff --git a/substrate/primitives/wasm-interface/src/host_state.rs b/substrate/primitives/wasm-interface/src/host_state.rs new file mode 100644 index 000000000000..440c9271dbf1 --- /dev/null +++ b/substrate/primitives/wasm-interface/src/host_state.rs @@ -0,0 +1,52 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This module defines `HostState` struct which provide logic and state +//! required for execution of host. + +use sp_allocator::{AllocationStats, FreeingBumpHeapAllocator}; + +/// The state required to construct a HostContext context. The context only lasts for one host +/// call, whereas the state is maintained for the duration of a Wasm runtime call, which may make +/// many different host calls that must share state. +pub struct HostState { + /// The allocator instance to keep track of allocated memory. + /// + /// This is stored as an `Option` as we need to temporarly set this to `None` when we are + /// allocating/deallocating memory. The problem being that we can only mutable access `caller` + /// once. + pub allocator: Option, + pub panic_message: Option, +} + +impl HostState { + /// Constructs a new `HostState`. + pub fn new(allocator: FreeingBumpHeapAllocator) -> Self { + HostState { allocator: Some(allocator), panic_message: None } + } + + /// Takes the error message out of the host state, leaving a `None` in its place. + pub fn take_panic_message(&mut self) -> Option { + self.panic_message.take() + } + + pub fn allocation_stats(&self) -> AllocationStats { + self.allocator.as_ref() + .expect("Allocator is always set and only unavailable when doing an allocation/deallocation; qed") + .stats() + } +} diff --git a/substrate/primitives/wasm-interface/src/lib.rs b/substrate/primitives/wasm-interface/src/lib.rs index 9d5d2bb358d5..f70ccc934c0f 100644 --- a/substrate/primitives/wasm-interface/src/lib.rs +++ b/substrate/primitives/wasm-interface/src/lib.rs @@ -19,7 +19,24 @@ #![cfg_attr(not(feature = "std"), no_std)] -use sp_std::{borrow::Cow, iter::Iterator, marker::PhantomData, mem, result, vec, vec::Vec}; +use sp_std::{iter::Iterator, marker::PhantomData, result, vec, vec::Vec}; +pub use sp_wasm_interface_common::{self as common, Value, ValueType, Pointer, PointerType, IntoValue, TryFromValue, ReturnValue, WordSize, MemoryId, HostPointer, Signature}; + +if_wasmtime_is_enabled! { + mod host_state; + pub use host_state::HostState; + + mod store_data; + pub use store_data::StoreData; + + mod memory_wrapper; + pub use memory_wrapper::MemoryWrapper; + + pub mod util; +} + +#[cfg(not(all(feature = "std", feature = "wasmtime")))] +pub struct StoreData; #[cfg(not(all(feature = "std", feature = "wasmtime")))] #[macro_export] @@ -49,81 +66,6 @@ pub type Result = result::Result; #[cfg(not(feature = "std"))] pub type Result = result::Result; -/// Value types supported by Substrate on the boundary between host/Wasm. -#[derive(Copy, Clone, PartialEq, Debug, Eq)] -pub enum ValueType { - /// An `i32` value type. - I32, - /// An `i64` value type. - I64, - /// An `f32` value type. - F32, - /// An `f64` value type. - F64, -} - -impl From for u8 { - fn from(val: ValueType) -> u8 { - match val { - ValueType::I32 => 0, - ValueType::I64 => 1, - ValueType::F32 => 2, - ValueType::F64 => 3, - } - } -} - -impl TryFrom for ValueType { - type Error = (); - - fn try_from(val: u8) -> sp_std::result::Result { - match val { - 0 => Ok(Self::I32), - 1 => Ok(Self::I64), - 2 => Ok(Self::F32), - 3 => Ok(Self::F64), - _ => Err(()), - } - } -} - -/// Values supported by Substrate on the boundary between host/Wasm. -#[derive(PartialEq, Debug, Clone, Copy, codec::Encode, codec::Decode)] -pub enum Value { - /// A 32-bit integer. - I32(i32), - /// A 64-bit integer. - I64(i64), - /// A 32-bit floating-point number stored as raw bit pattern. - /// - /// You can materialize this value using `f32::from_bits`. - F32(u32), - /// A 64-bit floating-point number stored as raw bit pattern. - /// - /// You can materialize this value using `f64::from_bits`. - F64(u64), -} - -impl Value { - /// Returns the type of this value. - pub fn value_type(&self) -> ValueType { - match self { - Value::I32(_) => ValueType::I32, - Value::I64(_) => ValueType::I64, - Value::F32(_) => ValueType::F32, - Value::F64(_) => ValueType::F64, - } - } - - /// Return `Self` as `i32`. - pub fn as_i32(&self) -> Option { - match self { - Self::I32(val) => Some(*val), - _ => None, - } - } -} - /// Provides `Sealed` trait to prevent implementing trait `PointerType` and `WasmTy` outside of this /// crate. mod private { @@ -136,123 +78,8 @@ mod private { impl Sealed for i32 {} impl Sealed for i64 {} -} - -/// Something that can be wrapped in a wasm `Pointer`. -/// -/// This trait is sealed. -pub trait PointerType: Sized + private::Sealed { - /// The size of the type in wasm. - const SIZE: u32 = mem::size_of::() as u32; -} - -impl PointerType for u8 {} -impl PointerType for u16 {} -impl PointerType for u32 {} -impl PointerType for u64 {} - -/// Type to represent a pointer in wasm at the host. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct Pointer { - ptr: u32, - _marker: PhantomData, -} - -impl Pointer { - /// Create a new instance of `Self`. - pub fn new(ptr: u32) -> Self { - Self { ptr, _marker: Default::default() } - } - - /// Calculate the offset from this pointer. - /// - /// `offset` is in units of `T`. So, `3` means `3 * mem::size_of::()` as offset to the - /// pointer. - /// - /// Returns an `Option` to respect that the pointer could probably overflow. - pub fn offset(self, offset: u32) -> Option { - offset - .checked_mul(T::SIZE) - .and_then(|o| self.ptr.checked_add(o)) - .map(|ptr| Self { ptr, _marker: Default::default() }) - } - - /// Create a null pointer. - pub fn null() -> Self { - Self::new(0) - } - /// Cast this pointer of type `T` to a pointer of type `R`. - pub fn cast(self) -> Pointer { - Pointer::new(self.ptr) - } -} - -impl From for Pointer { - fn from(ptr: u32) -> Self { - Pointer::new(ptr) - } -} - -impl From> for u32 { - fn from(ptr: Pointer) -> Self { - ptr.ptr - } -} - -impl From> for u64 { - fn from(ptr: Pointer) -> Self { - u64::from(ptr.ptr) - } -} - -impl From> for usize { - fn from(ptr: Pointer) -> Self { - ptr.ptr as _ - } -} - -impl IntoValue for Pointer { - const VALUE_TYPE: ValueType = ValueType::I32; - fn into_value(self) -> Value { - Value::I32(self.ptr as _) - } -} - -impl TryFromValue for Pointer { - fn try_from_value(val: Value) -> Option { - match val { - Value::I32(val) => Some(Self::new(val as _)), - _ => None, - } - } -} - -/// The word size used in wasm. Normally known as `usize` in Rust. -pub type WordSize = u32; - -/// The Signature of a function -#[derive(Eq, PartialEq, Debug, Clone)] -pub struct Signature { - /// The arguments of a function. - pub args: Cow<'static, [ValueType]>, - /// The optional return value of a function. - pub return_value: Option, -} - -impl Signature { - /// Create a new instance of `Signature`. - pub fn new>>( - args: T, - return_value: Option, - ) -> Self { - Self { args: args.into(), return_value } - } - - /// Create a new instance of `Signature` with the given `args` and without any return value. - pub fn new_with_args>>(args: T) -> Self { - Self { args: args.into(), return_value: None } - } + pub(super) struct Token; } /// A trait that requires `RefUnwindSafe` when `feature = std`. @@ -287,6 +114,20 @@ impl PartialEq for dyn Function { } } +#[cfg(not(all(feature = "std", feature = "wasmtime")))] +pub struct Caller<'a, T>(PhantomData<&'a T>); + +#[cfg(all(feature = "std", feature = "wasmtime"))] +pub use wasmtime::Caller; + +pub struct FunctionContextToken(private::Token); + +impl FunctionContextToken { + fn new() -> Self { + Self(private::Token) + } +} + /// Context used by `Function` to interact with the allocator and the memory of the wasm instance. pub trait FunctionContext { /// Read memory from `address` into a vector. @@ -325,6 +166,19 @@ pub trait FunctionContext { /// It should only be called once, however calling it more than once /// is harmless and will overwrite the previously set error message. fn register_panic_error_message(&mut self, message: &str); + + fn with_caller_mut_impl(&mut self, _: FunctionContextToken, context: *mut (), callback: fn(*mut (), &mut Caller)); + +} + +pub fn with_caller_mut)>(context: &mut dyn FunctionContext, mut callback: T) { + let callback: *mut T = &mut callback; + context.with_caller_mut_impl(FunctionContextToken::new(), callback.cast(), |callback, caller| { + let callback: *mut T = callback.cast(); + let callback: &mut T = unsafe { callback.as_mut().expect("we own the value, obtain mutable reference to it and cast to pointer (correct (not null) and aligned properly); qed") }; + + callback(caller); + }) } if_wasmtime_is_enabled! { @@ -519,105 +373,3 @@ impl WasmTy for i32 {} impl WasmTy for u32 {} impl WasmTy for i64 {} impl WasmTy for u64 {} - -/// Something that can be converted into a wasm compatible `Value`. -pub trait IntoValue { - /// The type of the value in wasm. - const VALUE_TYPE: ValueType; - - /// Convert `self` into a wasm `Value`. - fn into_value(self) -> Value; -} - -/// Something that can may be created from a wasm `Value`. -pub trait TryFromValue: Sized { - /// Try to convert the given `Value` into `Self`. - fn try_from_value(val: Value) -> Option; -} - -macro_rules! impl_into_and_from_value { - ( - $( - $type:ty, $( < $gen:ident >, )? $value_variant:ident, - )* - ) => { - $( - impl $( <$gen> )? IntoValue for $type { - const VALUE_TYPE: ValueType = ValueType::$value_variant; - fn into_value(self) -> Value { Value::$value_variant(self as _) } - } - - impl $( <$gen> )? TryFromValue for $type { - fn try_from_value(val: Value) -> Option { - match val { - Value::$value_variant(val) => Some(val as _), - _ => None, - } - } - } - )* - } -} - -impl_into_and_from_value! { - u8, I32, - u16, I32, - u32, I32, - u64, I64, - i8, I32, - i16, I32, - i32, I32, - i64, I64, -} - -/// Typed value that can be returned from a function. -/// -/// Basically a `TypedValue` plus `Unit`, for functions which return nothing. -#[derive(Clone, Copy, PartialEq, codec::Encode, codec::Decode, Debug)] -pub enum ReturnValue { - /// For returning nothing. - Unit, - /// For returning some concrete value. - Value(Value), -} - -impl From for ReturnValue { - fn from(v: Value) -> ReturnValue { - ReturnValue::Value(v) - } -} - -impl ReturnValue { - /// Maximum number of bytes `ReturnValue` might occupy when serialized with `SCALE`. - /// - /// Breakdown: - /// 1 byte for encoding unit/value variant - /// 1 byte for encoding value type - /// 8 bytes for encoding the biggest value types available in wasm: f64, i64. - pub const ENCODED_MAX_SIZE: usize = 10; -} - -#[cfg(test)] -mod tests { - use super::*; - use codec::Encode; - - #[test] - fn pointer_offset_works() { - let ptr = Pointer::::null(); - - assert_eq!(ptr.offset(10).unwrap(), Pointer::new(40)); - assert_eq!(ptr.offset(32).unwrap(), Pointer::new(128)); - - let ptr = Pointer::::null(); - - assert_eq!(ptr.offset(10).unwrap(), Pointer::new(80)); - assert_eq!(ptr.offset(32).unwrap(), Pointer::new(256)); - } - - #[test] - fn return_value_encoded_max_size() { - let encoded = ReturnValue::Value(Value::I64(-1)).encode(); - assert_eq!(encoded.len(), ReturnValue::ENCODED_MAX_SIZE); - } -} diff --git a/substrate/primitives/wasm-interface/src/memory_wrapper.rs b/substrate/primitives/wasm-interface/src/memory_wrapper.rs new file mode 100644 index 000000000000..5a3960f6ded3 --- /dev/null +++ b/substrate/primitives/wasm-interface/src/memory_wrapper.rs @@ -0,0 +1,56 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Wrapper around [`Memory`] that implements [`sp_allocator::Memory`]. +pub struct MemoryWrapper<'a, C>(&'a wasmtime::Memory, &'a mut C); + +impl<'a, C> From<(&'a wasmtime::Memory, &'a mut C)> for MemoryWrapper<'a, C> { + fn from((memory, caller): (&'a wasmtime::Memory, &'a mut C)) -> Self { + Self(memory, caller) + } +} + +impl sp_allocator::Memory for MemoryWrapper<'_, C> { + fn with_access(&self, run: impl FnOnce(&[u8]) -> R) -> R { + run(self.0.data(&self.1)) + } + + fn with_access_mut(&mut self, run: impl FnOnce(&mut [u8]) -> R) -> R { + run(self.0.data_mut(&mut self.1)) + } + + fn grow(&mut self, additional: u32) -> std::result::Result<(), ()> { + self.0 + .grow(&mut self.1, additional as u64) + .map_err(|e| { + log::error!( + "Failed to grow memory by {} pages: {}", + additional, + e, + ) + }) + .map(drop) + } + + fn pages(&self) -> u32 { + self.0.size(&self.1) as u32 + } + + fn max_pages(&self) -> Option { + self.0.ty(&self.1).maximum().map(|p| p as _) + } +} diff --git a/substrate/primitives/wasm-interface/src/store_data.rs b/substrate/primitives/wasm-interface/src/store_data.rs new file mode 100644 index 000000000000..570429f21e75 --- /dev/null +++ b/substrate/primitives/wasm-interface/src/store_data.rs @@ -0,0 +1,24 @@ +use crate::HostState; +use wasmtime::{Memory, Table}; + +#[derive(Default)] +pub struct StoreData { + /// This will only be set when we call into the runtime. + pub host_state: Option, + /// This will be always set once the store is initialized. + pub memory: Option, + /// This will be set only if the runtime actually contains a table. + pub table: Option
, +} + +impl StoreData { + /// Returns a mutable reference to the host state. + pub fn host_state_mut(&mut self) -> Option<&mut HostState> { + self.host_state.as_mut() + } + + /// Returns the host memory. + pub fn memory(&self) -> Memory { + self.memory.expect("memory is always set; qed") + } +} diff --git a/substrate/primitives/wasm-interface/src/util.rs b/substrate/primitives/wasm-interface/src/util.rs new file mode 100644 index 000000000000..72969b0c716c --- /dev/null +++ b/substrate/primitives/wasm-interface/src/util.rs @@ -0,0 +1,100 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use wasmtime::{AsContext, AsContextMut}; +pub use sp_wasm_interface_common::util::checked_range; + +pub fn write_memory_from( + mut ctx: impl AsContextMut, + address: Pointer, + data: &[u8], +) -> Result<()> { + let memory = ctx.as_context().data().memory(); + let memory = memory.data_mut(&mut ctx); + + let range = checked_range(address.into(), data.len(), memory.len()) + .ok_or_else(|| String::from("memory write is out of bounds"))?; + memory[range].copy_from_slice(data); + Ok(()) +} + +pub fn read_memory_into( + ctx: impl AsContext, + address: Pointer, + dest: &mut [u8], +) -> Result<()> { + let memory = ctx.as_context().data().memory().data(&ctx); + + let range = checked_range(address.into(), dest.len(), memory.len()) + .ok_or_else(|| String::from("memory read is out of bounds"))?; + dest.copy_from_slice(&memory[range]); + Ok(()) +} + +pub fn read_memory( + ctx: impl AsContext, + address: Pointer, + size: WordSize, +) -> Result> { + let mut vec = vec![0; size as usize]; + read_memory_into(ctx, address, &mut vec)?; + Ok(vec) +} + +#[track_caller] +fn host_state_mut<'a>(caller: &'a mut Caller<'_, StoreData>) -> &'a mut HostState { + caller + .data_mut() + .host_state_mut() + .expect("host state is not empty when calling a function in wasm; qed") +} + +pub fn allocate_memory(caller: &mut Caller<'_, StoreData>, size: WordSize) -> Result> { + let mut allocator = host_state_mut(caller) + .allocator + .take() + .expect("allocator is not empty when calling a function in wasm; qed"); + + let memory = caller.data().memory(); + // We can not return on error early, as we need to store back allocator. + let res = allocator + .allocate(&mut MemoryWrapper::from((&memory, &mut caller.as_context_mut())), size) + .map_err(|e| e.to_string()); + + host_state_mut(caller).allocator = Some(allocator); + + res +} + +pub fn deallocate_memory(caller: &mut Caller<'_, StoreData>, ptr: Pointer) -> Result<()> { + let mut allocator = host_state_mut(caller) + .allocator + .take() + .expect("allocator is not empty when calling a function in wasm; qed"); + + let memory = caller.data().memory(); + + // We can not return on error early, as we need to store back allocator. + let res = allocator + .deallocate(&mut MemoryWrapper::from((&memory, &mut caller.as_context_mut())), ptr) + .map_err(|e| e.to_string()); + + host_state_mut(caller).allocator = Some(allocator); + + res +}