Remove wasmi backend from sc-executor (#13800)

* refactor: use builder api for all executors

* improve a lot

* remove unused args

* cleanup deps

* fix inconsistency about heap alloc

* add `heap_pages` back to try-runtime

* fix

* chore: reduce duplicated code for sc-service-test

* cleanup code

* fmt

* improve test executor

* improve

* use #[deprecated]

* set runtime_cache_size: 4

* wip

* fix and improve

* remove sc-executor-wasmi deps

* clean up bench and tests

* delete "client/executor/wasmi"

* cleanup

* refactor builder

* fix

* fix bench

* fix tests

* fix warnings

* fix warnings

* fix

* fix

* remove wasmi and fix tests

* unused imports

* improve by suggestions

* Update client/cli/src/arg_enums.rs

---------

Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: parity-processbot <>
This commit is contained in:
yjh
2023-05-23 23:31:45 +08:00
committed by GitHub
parent ea21a495f8
commit b6a2948461
18 changed files with 36 additions and 795 deletions
+3 -31
View File
@@ -7840,14 +7840,14 @@ version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e30165d31df606f5726b090ec7592c308a0eaf61721ff64c9a3018e344a8753e"
dependencies = [
"portable-atomic 1.3.1",
"portable-atomic 1.3.2",
]
[[package]]
name = "portable-atomic"
version = "1.3.1"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bbda379e6e462c97ea6afe9f6233619b202bbc4968d7caa6917788d2070a044"
checksum = "dc59d1bcc64fc5d021d67521f818db868368028108d37f0e98d74e33f68297b5"
[[package]]
name = "ppv-lite86"
@@ -8413,18 +8413,6 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]]
name = "region"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76e189c2369884dce920945e2ddf79b3dff49e071a167dd1817fa9c4c00d512e"
dependencies = [
"bitflags",
"libc",
"mach",
"winapi",
]
[[package]]
name = "resolv-conf"
version = "0.7.0"
@@ -9316,7 +9304,6 @@ dependencies = [
"paste",
"regex",
"sc-executor-common",
"sc-executor-wasmi",
"sc-executor-wasmtime",
"sc-runtime-test",
"sc-tracing",
@@ -9337,7 +9324,6 @@ dependencies = [
"tempfile",
"tracing",
"tracing-subscriber 0.2.25",
"wasmi 0.13.2",
"wat",
]
@@ -9350,19 +9336,6 @@ dependencies = [
"sp-wasm-interface",
"thiserror",
"wasm-instrument 0.3.0",
"wasmi 0.13.2",
]
[[package]]
name = "sc-executor-wasmi"
version = "0.10.0-dev"
dependencies = [
"log",
"sc-allocator",
"sc-executor-common",
"sp-runtime-interface",
"sp-wasm-interface",
"wasmi 0.13.2",
]
[[package]]
@@ -13092,7 +13065,6 @@ dependencies = [
"memory_units",
"num-rational",
"num-traits",
"region",
]
[[package]]
-1
View File
@@ -38,7 +38,6 @@ members = [
"client/executor",
"client/executor/common",
"client/executor/runtime-test",
"client/executor/wasmi",
"client/executor/wasmtime",
"client/informant",
"client/keystore",
@@ -69,7 +69,6 @@ pub fn new_partial(
.transpose()?;
let executor = sc_service::new_native_or_wasm_executor(&config);
let (client, backend, keystore_container, task_manager) =
sc_service::new_full_parts::<Block, RuntimeApi, _>(
config,
@@ -182,7 +182,6 @@ fn bench_execute_block(c: &mut Criterion) {
let mut group = c.benchmark_group("execute blocks");
let execution_methods = vec![
ExecutionMethod::Native,
ExecutionMethod::Wasm(WasmExecutionMethod::Interpreted),
ExecutionMethod::Wasm(WasmExecutionMethod::Compiled {
instantiation_strategy: WasmtimeInstantiationStrategy::PoolingCopyOnWrite,
}),
+19 -16
View File
@@ -54,7 +54,7 @@ pub const DEFAULT_WASMTIME_INSTANTIATION_STRATEGY: WasmtimeInstantiationStrategy
#[derive(Debug, Clone, Copy, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum WasmExecutionMethod {
/// Uses an interpreter.
/// Uses an interpreter which now is deprecated.
#[clap(name = "interpreted-i-know-what-i-do")]
Interpreted,
/// Uses a compiled runtime.
@@ -76,21 +76,24 @@ pub fn execution_method_from_cli(
execution_method: WasmExecutionMethod,
instantiation_strategy: WasmtimeInstantiationStrategy,
) -> sc_service::config::WasmExecutionMethod {
match execution_method {
WasmExecutionMethod::Interpreted => sc_service::config::WasmExecutionMethod::Interpreted,
WasmExecutionMethod::Compiled => sc_service::config::WasmExecutionMethod::Compiled {
instantiation_strategy: match instantiation_strategy {
WasmtimeInstantiationStrategy::PoolingCopyOnWrite =>
sc_service::config::WasmtimeInstantiationStrategy::PoolingCopyOnWrite,
WasmtimeInstantiationStrategy::RecreateInstanceCopyOnWrite =>
sc_service::config::WasmtimeInstantiationStrategy::RecreateInstanceCopyOnWrite,
WasmtimeInstantiationStrategy::Pooling =>
sc_service::config::WasmtimeInstantiationStrategy::Pooling,
WasmtimeInstantiationStrategy::RecreateInstance =>
sc_service::config::WasmtimeInstantiationStrategy::RecreateInstance,
WasmtimeInstantiationStrategy::LegacyInstanceReuse =>
sc_service::config::WasmtimeInstantiationStrategy::LegacyInstanceReuse,
},
if let WasmExecutionMethod::Interpreted = execution_method {
log::warn!(
"`interpreted-i-know-what-i-do` is deprecated and will be removed in the future. Defaults to `compiled` execution mode."
);
}
sc_service::config::WasmExecutionMethod::Compiled {
instantiation_strategy: match instantiation_strategy {
WasmtimeInstantiationStrategy::PoolingCopyOnWrite =>
sc_service::config::WasmtimeInstantiationStrategy::PoolingCopyOnWrite,
WasmtimeInstantiationStrategy::RecreateInstanceCopyOnWrite =>
sc_service::config::WasmtimeInstantiationStrategy::RecreateInstanceCopyOnWrite,
WasmtimeInstantiationStrategy::Pooling =>
sc_service::config::WasmtimeInstantiationStrategy::Pooling,
WasmtimeInstantiationStrategy::RecreateInstance =>
sc_service::config::WasmtimeInstantiationStrategy::RecreateInstance,
WasmtimeInstantiationStrategy::LegacyInstanceReuse =>
sc_service::config::WasmtimeInstantiationStrategy::LegacyInstanceReuse,
},
}
}
-2
View File
@@ -17,11 +17,9 @@ targets = ["x86_64-unknown-linux-gnu"]
lru = "0.8.1"
parking_lot = "0.12.1"
tracing = "0.1.29"
wasmi = "0.13.2"
codec = { package = "parity-scale-codec", version = "3.2.2" }
sc-executor-common = { version = "0.10.0-dev", path = "common" }
sc-executor-wasmi = { version = "0.10.0-dev", path = "wasmi" }
sc-executor-wasmtime = { version = "0.10.0-dev", path = "wasmtime" }
sp-api = { version = "4.0.0-dev", path = "../../primitives/api" }
sp-core = { version = "7.0.0", path = "../../primitives/core" }
+1 -10
View File
@@ -33,7 +33,6 @@ use std::sync::{
#[derive(Clone)]
enum Method {
Interpreted,
Compiled { instantiation_strategy: InstantiationStrategy, precompile: bool },
}
@@ -50,17 +49,10 @@ fn initialize(
method: Method,
) -> Box<dyn WasmModule> {
let blob = RuntimeBlob::uncompress_if_needed(runtime).unwrap();
let host_functions = sp_io::SubstrateHostFunctions::host_functions();
let allow_missing_func_imports = true;
match method {
Method::Interpreted => sc_executor_wasmi::create_runtime(
blob,
DEFAULT_HEAP_ALLOC_STRATEGY,
host_functions,
allow_missing_func_imports,
)
.map(|runtime| -> Box<dyn WasmModule> { Box::new(runtime) }),
Method::Compiled { instantiation_strategy, precompile } => {
let config = sc_executor_wasmtime::Config {
allow_missing_func_imports,
@@ -215,7 +207,6 @@ fn bench_call_instance(c: &mut Criterion) {
precompile: true,
},
),
("interpreted", Method::Interpreted),
];
let runtimes = [("kusama_runtime", kusama_runtime()), ("test_runtime", test_runtime())];
@@ -16,7 +16,6 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
thiserror = "1.0.30"
wasm-instrument = "0.3"
wasmi = "0.13.2"
sc-allocator = { version = "4.1.0-dev", path = "../../allocator" }
sp-maybe-compressed-blob = { version = "4.1.0-dev", path = "../../../primitives/maybe-compressed-blob" }
sp-wasm-interface = { version = "7.0.0", path = "../../../primitives/wasm-interface" }
@@ -18,8 +18,6 @@
//! Rust executor possible errors.
use wasmi;
/// Result type alias.
pub type Result<T> = std::result::Result<T, Error>;
@@ -27,9 +25,6 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
Wasmi(#[from] wasmi::Error),
#[error("Error calling api function: {0}")]
ApiError(Box<dyn std::error::Error + Send + Sync>),
@@ -48,9 +43,6 @@ pub enum Error {
#[error("Invalid type returned (should be u64)")]
InvalidReturn,
#[error("Runtime error")]
Runtime,
#[error("Runtime panicked: {0}")]
RuntimePanicked(String),
@@ -109,8 +101,6 @@ pub enum Error {
OutputExceedsBounds,
}
impl wasmi::HostError for Error {}
impl From<&'static str> for Error {
fn from(err: &'static str) -> Error {
Error::Other(err.into())
@@ -26,9 +26,6 @@
//!
//! To give you some examples:
//!
//! - wasmi allows reaching to non-exported mutable globals so that we could reset them. Wasmtime
//! doesnt support that.
//!
//! We need to reset the globals because when we
//! execute the Substrate Runtime, we do not drop and create the instance anew, instead
//! we restore some selected parts of the state.
@@ -18,34 +18,15 @@
//! Tests that are only relevant for Linux.
mod smaps;
use super::mk_test_runtime;
use crate::WasmExecutionMethod;
use codec::Encode as _;
use sc_executor_common::wasm_runtime::DEFAULT_HEAP_ALLOC_STRATEGY;
mod smaps;
use self::smaps::Smaps;
#[test]
fn memory_consumption_interpreted() {
let _ = sp_tracing::try_init_simple();
if std::env::var("RUN_TEST").is_ok() {
memory_consumption(WasmExecutionMethod::Interpreted);
} else {
// We need to run the test in isolation, to not getting interfered by the other tests.
let executable = std::env::current_exe().unwrap();
let output = std::process::Command::new(executable)
.env("RUN_TEST", "1")
.args(&["--nocapture", "memory_consumption_interpreted"])
.output()
.unwrap();
assert!(output.status.success());
}
}
#[test]
fn memory_consumption_compiled() {
let _ = sp_tracing::try_init_simple();
@@ -24,7 +24,7 @@ use codec::{Decode, Encode};
use sc_executor_common::{
error::Error,
runtime_blob::RuntimeBlob,
wasm_runtime::{HeapAllocStrategy, WasmModule, DEFAULT_HEAP_ALLOC_STRATEGY},
wasm_runtime::{HeapAllocStrategy, WasmModule},
};
use sc_runtime_test::wasm_binary_unwrap;
use sp_core::{
@@ -50,12 +50,6 @@ type HostFunctions = sp_io::SubstrateHostFunctions;
macro_rules! test_wasm_execution {
($method_name:ident) => {
paste::item! {
#[test]
fn [<$method_name _interpreted>]() {
let _ = sp_tracing::try_init_simple();
$method_name(WasmExecutionMethod::Interpreted);
}
#[test]
fn [<$method_name _compiled_recreate_instance_cow>]() {
let _ = sp_tracing::try_init_simple();
@@ -97,15 +91,6 @@ macro_rules! test_wasm_execution {
}
}
};
(interpreted_only $method_name:ident) => {
paste::item! {
#[test]
fn [<$method_name _interpreted>]() {
$method_name(WasmExecutionMethod::Interpreted);
}
}
};
}
fn call_in_wasm<E: Externalities>(
@@ -144,8 +129,8 @@ fn call_not_existing_function(wasm_method: WasmExecutionMethod) {
match call_in_wasm("test_calling_missing_external", &[], wasm_method, &mut ext).unwrap_err() {
Error::AbortedDueToTrap(error) => {
let expected = match wasm_method {
WasmExecutionMethod::Interpreted => "Other: Function `missing_external` is only a stub. Calling a stub is not allowed.",
WasmExecutionMethod::Compiled { .. } => "call to a missing function env:missing_external"
WasmExecutionMethod::Compiled { .. } =>
"call to a missing function env:missing_external",
};
assert_eq!(error.message, expected);
},
@@ -163,8 +148,8 @@ fn call_yet_another_not_existing_function(wasm_method: WasmExecutionMethod) {
{
Error::AbortedDueToTrap(error) => {
let expected = match wasm_method {
WasmExecutionMethod::Interpreted => "Other: Function `yet_another_missing_external` is only a stub. Calling a stub is not allowed.",
WasmExecutionMethod::Compiled { .. } => "call to a missing function env:yet_another_missing_external"
WasmExecutionMethod::Compiled { .. } =>
"call to a missing function env:yet_another_missing_external",
};
assert_eq!(error.message, expected);
},
@@ -473,9 +458,6 @@ fn should_trap_when_heap_exhausted(wasm_method: WasmExecutionMethod) {
r#"host code panicked while being called by the runtime: Failed to allocate memory: "Allocator ran out of space""#
);
},
Error::RuntimePanicked(error) if wasm_method == WasmExecutionMethod::Interpreted => {
assert_eq!(error, r#"Failed to allocate memory: "Allocator ran out of space""#);
},
error => panic!("unexpected error: {:?}", error),
}
}
@@ -558,25 +540,6 @@ fn restoration_of_globals(wasm_method: WasmExecutionMethod) {
assert!(res.is_ok());
}
test_wasm_execution!(interpreted_only heap_is_reset_between_calls);
fn heap_is_reset_between_calls(wasm_method: WasmExecutionMethod) {
let runtime = mk_test_runtime(wasm_method, DEFAULT_HEAP_ALLOC_STRATEGY);
let mut instance = runtime.new_instance().unwrap();
let heap_base = instance
.get_global_const("__heap_base")
.expect("`__heap_base` is valid")
.expect("`__heap_base` exists")
.as_i32()
.expect("`__heap_base` is an `i32`");
let params = (heap_base as u32, 512u32 * 64 * 1024).encode();
instance.call_export("check_and_set_in_heap", &params).unwrap();
// Cal it a second time to check that the heap was freed.
instance.call_export("check_and_set_in_heap", &params).unwrap();
}
test_wasm_execution!(parallel_execution);
fn parallel_execution(wasm_method: WasmExecutionMethod) {
let executor = Arc::new(
@@ -787,7 +750,6 @@ fn unreachable_intrinsic(wasm_method: WasmExecutionMethod) {
match call_in_wasm("test_unreachable_intrinsic", &[], wasm_method, &mut ext).unwrap_err() {
Error::AbortedDueToTrap(error) => {
let expected = match wasm_method {
WasmExecutionMethod::Interpreted => "unreachable",
WasmExecutionMethod::Compiled { .. } =>
"wasm trap: wasm `unreachable` instruction executed",
};
@@ -814,9 +776,6 @@ fn return_huge_len(wasm_method: WasmExecutionMethod) {
let mut ext = ext.ext();
match call_in_wasm("test_return_huge_len", &[], wasm_method, &mut ext).unwrap_err() {
Error::Runtime => {
assert_matches!(wasm_method, WasmExecutionMethod::Interpreted);
},
Error::OutputExceedsBounds => {
assert_matches!(wasm_method, WasmExecutionMethod::Compiled { .. });
},
@@ -843,9 +802,6 @@ fn return_max_memory_offset_plus_one(wasm_method: WasmExecutionMethod) {
match call_in_wasm("test_return_max_memory_offset_plus_one", &[], wasm_method, &mut ext)
.unwrap_err()
{
Error::Runtime => {
assert_matches!(wasm_method, WasmExecutionMethod::Interpreted);
},
Error::OutputExceedsBounds => {
assert_matches!(wasm_method, WasmExecutionMethod::Compiled { .. });
},
@@ -859,9 +815,6 @@ fn return_overflow(wasm_method: WasmExecutionMethod) {
let mut ext = ext.ext();
match call_in_wasm("test_return_overflow", &[], wasm_method, &mut ext).unwrap_err() {
Error::Runtime => {
assert_matches!(wasm_method, WasmExecutionMethod::Interpreted);
},
Error::OutputExceedsBounds => {
assert_matches!(wasm_method, WasmExecutionMethod::Compiled { .. });
},
-1
View File
@@ -49,7 +49,6 @@ pub use sp_core::traits::Externalities;
pub use sp_version::{NativeVersion, RuntimeVersion};
#[doc(hidden)]
pub use sp_wasm_interface;
pub use wasmi;
pub use sc_executor_common::{
error,
+4 -19
View File
@@ -43,8 +43,6 @@ use sp_wasm_interface::HostFunctions;
/// Specification of different methods of executing the runtime Wasm code.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub enum WasmExecutionMethod {
/// Uses the Wasmi interpreter.
Interpreted,
/// Uses the Wasmtime compiled runtime.
Compiled {
/// The instantiation strategy to use.
@@ -53,8 +51,10 @@ pub enum WasmExecutionMethod {
}
impl Default for WasmExecutionMethod {
fn default() -> WasmExecutionMethod {
WasmExecutionMethod::Interpreted
fn default() -> Self {
Self::Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite,
}
}
}
@@ -299,21 +299,6 @@ where
H: HostFunctions,
{
match wasm_method {
WasmExecutionMethod::Interpreted => {
// Wasmi doesn't have any need in a cache directory.
//
// We drop the cache_path here to silence warnings that cache_path is not used if
// compiling without the `wasmtime` flag.
let _ = cache_path;
sc_executor_wasmi::create_runtime(
blob,
heap_alloc_strategy,
H::host_functions(),
allow_missing_func_imports,
)
.map(|runtime| -> Box<dyn WasmModule> { Box::new(runtime) })
},
WasmExecutionMethod::Compiled { instantiation_strategy } =>
sc_executor_wasmtime::create_runtime::<H>(
blob,
@@ -1,22 +0,0 @@
[package]
name = "sc-executor-wasmi"
version = "0.10.0-dev"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage = "https://substrate.io"
repository = "https://github.com/paritytech/substrate/"
description = "This crate provides an implementation of `WasmRuntime` that is baked by wasmi."
documentation = "https://docs.rs/sc-executor-wasmi"
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
log = "0.4.17"
wasmi = { version = "0.13.2", features = [ "virtual_memory" ] }
sc-allocator = { version = "4.1.0-dev", path = "../../allocator" }
sc-executor-common = { version = "0.10.0-dev", path = "../common" }
sp-runtime-interface = { version = "7.0.0", path = "../../../primitives/runtime-interface" }
sp-wasm-interface = { version = "7.0.0", path = "../../../primitives/wasm-interface" }
@@ -1,3 +0,0 @@
This crate provides an implementation of `WasmModule` that is baked by wasmi.
License: GPL-3.0-or-later WITH Classpath-exception-2.0
-599
View File
@@ -1,599 +0,0 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// 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 <https://www.gnu.org/licenses/>.
//! This crate provides an implementation of `WasmModule` that is baked by wasmi.
use std::{cell::RefCell, str, sync::Arc};
use log::{error, trace};
use wasmi::{
memory_units::Pages,
FuncInstance, ImportsBuilder, MemoryRef, Module, ModuleInstance, ModuleRef,
RuntimeValue::{self, I32, I64},
TableRef,
};
use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
use sc_executor_common::{
error::{Error, MessageWithBacktrace, WasmError},
runtime_blob::{DataSegmentsSnapshot, RuntimeBlob},
wasm_runtime::{HeapAllocStrategy, InvokeMethod, WasmInstance, WasmModule},
};
use sp_runtime_interface::unpack_ptr_and_len;
use sp_wasm_interface::{Function, FunctionContext, Pointer, Result as WResult, WordSize};
/// Wrapper around [`MemorRef`] that implements [`sc_allocator::Memory`].
struct MemoryWrapper<'a>(&'a MemoryRef);
impl sc_allocator::Memory for MemoryWrapper<'_> {
fn with_access_mut<R>(&mut self, run: impl FnOnce(&mut [u8]) -> R) -> R {
self.0.with_direct_access_mut(run)
}
fn with_access<R>(&self, run: impl FnOnce(&[u8]) -> R) -> R {
self.0.with_direct_access(run)
}
fn pages(&self) -> u32 {
self.0.current_size().0 as _
}
fn max_pages(&self) -> Option<u32> {
self.0.maximum().map(|p| p.0 as _)
}
fn grow(&mut self, additional: u32) -> Result<(), ()> {
self.0
.grow(Pages(additional as _))
.map_err(|e| {
log::error!(
target: "wasm-executor",
"Failed to grow memory by {} pages: {}",
additional,
e,
)
})
.map(drop)
}
}
struct FunctionExecutor {
heap: RefCell<sc_allocator::FreeingBumpHeapAllocator>,
memory: MemoryRef,
host_functions: Arc<Vec<&'static dyn Function>>,
allow_missing_func_imports: bool,
missing_functions: Arc<Vec<String>>,
panic_message: Option<String>,
}
impl FunctionExecutor {
fn new(
m: MemoryRef,
heap_base: u32,
host_functions: Arc<Vec<&'static dyn Function>>,
allow_missing_func_imports: bool,
missing_functions: Arc<Vec<String>>,
) -> Result<Self, Error> {
Ok(FunctionExecutor {
heap: RefCell::new(FreeingBumpHeapAllocator::new(heap_base)),
memory: m,
host_functions,
allow_missing_func_imports,
missing_functions,
panic_message: None,
})
}
}
impl FunctionContext for FunctionExecutor {
fn read_memory_into(&self, address: Pointer<u8>, dest: &mut [u8]) -> WResult<()> {
self.memory.get_into(address.into(), dest).map_err(|e| e.to_string())
}
fn write_memory(&mut self, address: Pointer<u8>, data: &[u8]) -> WResult<()> {
self.memory.set(address.into(), data).map_err(|e| e.to_string())
}
fn allocate_memory(&mut self, size: WordSize) -> WResult<Pointer<u8>> {
self.heap
.borrow_mut()
.allocate(&mut MemoryWrapper(&self.memory), size)
.map_err(|e| e.to_string())
}
fn deallocate_memory(&mut self, ptr: Pointer<u8>) -> WResult<()> {
self.heap
.borrow_mut()
.deallocate(&mut MemoryWrapper(&self.memory), ptr)
.map_err(|e| e.to_string())
}
fn register_panic_error_message(&mut self, message: &str) {
self.panic_message = Some(message.to_owned());
}
}
/// Will be used on initialization of a module to resolve function and memory imports.
struct Resolver<'a> {
/// All the hot functions that we export for the WASM blob.
host_functions: &'a [&'static dyn Function],
/// Should we allow missing function imports?
///
/// If `true`, we return a stub that will return an error when being called.
allow_missing_func_imports: bool,
/// All the names of functions for that we did not provide a host function.
missing_functions: RefCell<Vec<String>>,
}
impl<'a> Resolver<'a> {
fn new(
host_functions: &'a [&'static dyn Function],
allow_missing_func_imports: bool,
) -> Resolver<'a> {
Resolver {
host_functions,
allow_missing_func_imports,
missing_functions: RefCell::new(Vec::new()),
}
}
}
impl<'a> wasmi::ModuleImportResolver for Resolver<'a> {
fn resolve_func(
&self,
name: &str,
signature: &wasmi::Signature,
) -> std::result::Result<wasmi::FuncRef, wasmi::Error> {
let signature = sp_wasm_interface::Signature::from(signature);
for (function_index, function) in self.host_functions.iter().enumerate() {
if name == function.name() {
if signature == function.signature() {
return Ok(wasmi::FuncInstance::alloc_host(signature.into(), function_index))
} else {
return Err(wasmi::Error::Instantiation(format!(
"Invalid signature for function `{}` expected `{:?}`, got `{:?}`",
function.name(),
signature,
function.signature(),
)))
}
}
}
if self.allow_missing_func_imports {
trace!(
target: "wasm-executor",
"Could not find function `{}`, a stub will be provided instead.",
name,
);
let id = self.missing_functions.borrow().len() + self.host_functions.len();
self.missing_functions.borrow_mut().push(name.to_string());
Ok(wasmi::FuncInstance::alloc_host(signature.into(), id))
} else {
Err(wasmi::Error::Instantiation(format!("Export {} not found", name)))
}
}
fn resolve_memory(
&self,
_: &str,
_: &wasmi::MemoryDescriptor,
) -> Result<MemoryRef, wasmi::Error> {
Err(wasmi::Error::Instantiation(
"Internal error, wasmi expects that the wasm blob exports memory.".into(),
))
}
}
impl wasmi::Externals for FunctionExecutor {
fn invoke_index(
&mut self,
index: usize,
args: wasmi::RuntimeArgs,
) -> Result<Option<wasmi::RuntimeValue>, wasmi::Trap> {
let mut args = args.as_ref().iter().copied().map(Into::into);
if let Some(function) = self.host_functions.clone().get(index) {
function
.execute(self, &mut args)
.map_err(|msg| Error::FunctionExecution(function.name().to_string(), msg))
.map_err(wasmi::Trap::from)
.map(|v| v.map(Into::into))
} else if self.allow_missing_func_imports &&
index >= self.host_functions.len() &&
index < self.host_functions.len() + self.missing_functions.len()
{
Err(Error::from(format!(
"Function `{}` is only a stub. Calling a stub is not allowed.",
self.missing_functions[index - self.host_functions.len()],
))
.into())
} else {
Err(Error::from(format!("Could not find host function with index: {}", index)).into())
}
}
}
fn get_mem_instance(module: &ModuleRef) -> Result<MemoryRef, Error> {
Ok(module
.export_by_name("memory")
.ok_or(Error::InvalidMemoryReference)?
.as_memory()
.ok_or(Error::InvalidMemoryReference)?
.clone())
}
/// Find the global named `__heap_base` in the given wasm module instance and
/// tries to get its value.
fn get_heap_base(module: &ModuleRef) -> Result<u32, Error> {
let heap_base_val = module
.export_by_name("__heap_base")
.ok_or(Error::HeapBaseNotFoundOrInvalid)?
.as_global()
.ok_or(Error::HeapBaseNotFoundOrInvalid)?
.get();
match heap_base_val {
wasmi::RuntimeValue::I32(v) => Ok(v as u32),
_ => Err(Error::HeapBaseNotFoundOrInvalid),
}
}
/// Call a given method in the given wasm-module runtime.
fn call_in_wasm_module(
module_instance: &ModuleRef,
memory: &MemoryRef,
method: InvokeMethod,
data: &[u8],
host_functions: Arc<Vec<&'static dyn Function>>,
allow_missing_func_imports: bool,
missing_functions: Arc<Vec<String>>,
allocation_stats: &mut Option<AllocationStats>,
) -> Result<Vec<u8>, Error> {
// Initialize FunctionExecutor.
let table: Option<TableRef> = module_instance
.export_by_name("__indirect_function_table")
.and_then(|e| e.as_table().cloned());
let heap_base = get_heap_base(module_instance)?;
let mut function_executor = FunctionExecutor::new(
memory.clone(),
heap_base,
host_functions,
allow_missing_func_imports,
missing_functions,
)?;
// Write the call data
let offset = function_executor.allocate_memory(data.len() as u32)?;
function_executor.write_memory(offset, data)?;
fn convert_trap(executor: &mut FunctionExecutor, trap: wasmi::Trap) -> Error {
if let Some(message) = executor.panic_message.take() {
Error::AbortedDueToPanic(MessageWithBacktrace { message, backtrace: None })
} else {
Error::AbortedDueToTrap(MessageWithBacktrace {
message: trap.to_string(),
backtrace: None,
})
}
}
let result = match method {
InvokeMethod::Export(method) => module_instance
.invoke_export(
method,
&[I32(u32::from(offset) as i32), I32(data.len() as i32)],
&mut function_executor,
)
.map_err(|error| {
if let wasmi::Error::Trap(trap) = error {
convert_trap(&mut function_executor, trap)
} else {
error.into()
}
}),
InvokeMethod::Table(func_ref) => {
let func = table
.ok_or(Error::NoTable)?
.get(func_ref)?
.ok_or(Error::NoTableEntryWithIndex(func_ref))?;
FuncInstance::invoke(
&func,
&[I32(u32::from(offset) as i32), I32(data.len() as i32)],
&mut function_executor,
)
.map_err(|trap| convert_trap(&mut function_executor, trap))
},
InvokeMethod::TableWithWrapper { dispatcher_ref, func } => {
let dispatcher = table
.ok_or(Error::NoTable)?
.get(dispatcher_ref)?
.ok_or(Error::NoTableEntryWithIndex(dispatcher_ref))?;
FuncInstance::invoke(
&dispatcher,
&[I32(func as _), I32(u32::from(offset) as i32), I32(data.len() as i32)],
&mut function_executor,
)
.map_err(|trap| convert_trap(&mut function_executor, trap))
},
};
*allocation_stats = Some(function_executor.heap.borrow().stats());
match result {
Ok(Some(I64(r))) => {
let (ptr, length) = unpack_ptr_and_len(r as u64);
#[allow(deprecated)]
memory.get(ptr, length as usize).map_err(|_| Error::Runtime)
},
Err(e) => {
trace!(
target: "wasm-executor",
"Failed to execute code with {} pages",
memory.current_size().0,
);
Err(e)
},
_ => Err(Error::InvalidReturn),
}
}
/// Prepare module instance
fn instantiate_module(
module: &Module,
host_functions: &[&'static dyn Function],
allow_missing_func_imports: bool,
) -> Result<(ModuleRef, Vec<String>, MemoryRef), Error> {
let resolver = Resolver::new(host_functions, allow_missing_func_imports);
// start module instantiation. Don't run 'start' function yet.
let intermediate_instance =
ModuleInstance::new(module, &ImportsBuilder::new().with_resolver("env", &resolver))?;
// Verify that the module has the heap base global variable.
let _ = get_heap_base(intermediate_instance.not_started_instance())?;
// The `module` should export the memory with the correct properties (min, max).
//
// This is ensured by modifying the `RuntimeBlob` before initializing the `Module`.
let memory = get_mem_instance(intermediate_instance.not_started_instance())?;
if intermediate_instance.has_start() {
// Runtime is not allowed to have the `start` function.
Err(Error::RuntimeHasStartFn)
} else {
Ok((
intermediate_instance.assert_no_start(),
resolver.missing_functions.into_inner(),
memory,
))
}
}
/// A state snapshot of an instance taken just after instantiation.
///
/// It is used for restoring the state of the module after execution.
#[derive(Clone)]
struct GlobalValsSnapshot {
/// The list of all global mutable variables of the module in their sequential order.
global_mut_values: Vec<RuntimeValue>,
}
impl GlobalValsSnapshot {
// Returns `None` if instance is not valid.
fn take(module_instance: &ModuleRef) -> Self {
// Collect all values of mutable globals.
let global_mut_values = module_instance
.globals()
.iter()
.filter(|g| g.is_mutable())
.map(|g| g.get())
.collect();
Self { global_mut_values }
}
/// Reset the runtime instance to the initial version by restoring
/// the preserved memory and globals.
///
/// Returns `Err` if applying the snapshot is failed.
fn apply(&self, instance: &ModuleRef) -> Result<(), WasmError> {
for (global_ref, global_val) in instance
.globals()
.iter()
.filter(|g| g.is_mutable())
.zip(self.global_mut_values.iter())
{
// the instance should be the same as used for preserving and
// we iterate the same way it as we do it for preserving values that means that the
// types should be the same and all the values are mutable. So no error is expected/
global_ref.set(*global_val).map_err(|_| WasmError::ApplySnapshotFailed)?;
}
Ok(())
}
}
/// A runtime along with initial copy of data segments.
pub struct WasmiRuntime {
/// A wasm module.
module: Module,
/// The host functions registered for this instance.
host_functions: Arc<Vec<&'static dyn Function>>,
/// Enable stub generation for functions that are not available in `host_functions`.
/// These stubs will error when the wasm blob tries to call them.
allow_missing_func_imports: bool,
global_vals_snapshot: GlobalValsSnapshot,
data_segments_snapshot: DataSegmentsSnapshot,
}
impl WasmModule for WasmiRuntime {
fn new_instance(&self) -> Result<Box<dyn WasmInstance>, Error> {
// Instantiate this module.
let (instance, missing_functions, memory) =
instantiate_module(&self.module, &self.host_functions, self.allow_missing_func_imports)
.map_err(|e| WasmError::Instantiation(e.to_string()))?;
Ok(Box::new(WasmiInstance {
instance,
memory,
global_vals_snapshot: self.global_vals_snapshot.clone(),
data_segments_snapshot: self.data_segments_snapshot.clone(),
host_functions: self.host_functions.clone(),
allow_missing_func_imports: self.allow_missing_func_imports,
missing_functions: Arc::new(missing_functions),
memory_zeroed: true,
}))
}
}
/// Create a new `WasmiRuntime` given the code. This function loads the module and
/// stores it in the instance.
pub fn create_runtime(
mut blob: RuntimeBlob,
heap_alloc_strategy: HeapAllocStrategy,
host_functions: Vec<&'static dyn Function>,
allow_missing_func_imports: bool,
) -> Result<WasmiRuntime, WasmError> {
let data_segments_snapshot =
DataSegmentsSnapshot::take(&blob).map_err(|e| WasmError::Other(e.to_string()))?;
// Make sure we only have exported memory to simplify the code of the wasmi executor.
blob.convert_memory_import_into_export()?;
// Ensure that the memory uses the correct heap pages.
blob.setup_memory_according_to_heap_alloc_strategy(heap_alloc_strategy)?;
let module =
Module::from_parity_wasm_module(blob.into_inner()).map_err(|_| WasmError::InvalidModule)?;
let global_vals_snapshot = {
let (instance, _, _) =
instantiate_module(&module, &host_functions, allow_missing_func_imports)
.map_err(|e| WasmError::Instantiation(e.to_string()))?;
GlobalValsSnapshot::take(&instance)
};
Ok(WasmiRuntime {
module,
data_segments_snapshot,
global_vals_snapshot,
host_functions: Arc::new(host_functions),
allow_missing_func_imports,
})
}
/// Wasmi instance wrapper along with the state snapshot.
pub struct WasmiInstance {
/// A wasm module instance.
instance: ModuleRef,
/// The memory instance of used by the wasm module.
memory: MemoryRef,
/// Is the memory zeroed?
memory_zeroed: bool,
/// The snapshot of global variable values just after instantiation.
global_vals_snapshot: GlobalValsSnapshot,
/// The snapshot of data segments.
data_segments_snapshot: DataSegmentsSnapshot,
/// The host functions registered for this instance.
host_functions: Arc<Vec<&'static dyn Function>>,
/// Enable stub generation for functions that are not available in `host_functions`.
/// These stubs will error when the wasm blob trie to call them.
allow_missing_func_imports: bool,
/// List of missing functions detected during function resolution
missing_functions: Arc<Vec<String>>,
}
// This is safe because `WasmiInstance` does not leak any references to `self.memory` and
// `self.instance`
unsafe impl Send for WasmiInstance {}
impl WasmiInstance {
fn call_impl(
&mut self,
method: InvokeMethod,
data: &[u8],
allocation_stats: &mut Option<AllocationStats>,
) -> Result<Vec<u8>, Error> {
// We reuse a single wasm instance for multiple calls and a previous call (if any)
// altered the state. Therefore, we need to restore the instance to original state.
if !self.memory_zeroed {
// First, zero initialize the linear memory.
self.memory.erase().map_err(|e| {
// Snapshot restoration failed. This is pretty unexpected since this can happen
// if some invariant is broken or if the system is under extreme memory pressure
// (so erasing fails).
error!(target: "wasm-executor", "snapshot restoration failed: {}", e);
WasmError::ErasingFailed(e.to_string())
})?;
}
// Second, reapply data segments into the linear memory.
self.data_segments_snapshot
.apply(|offset, contents| self.memory.set(offset, contents))?;
// Third, restore the global variables to their initial values.
self.global_vals_snapshot.apply(&self.instance)?;
let res = call_in_wasm_module(
&self.instance,
&self.memory,
method,
data,
self.host_functions.clone(),
self.allow_missing_func_imports,
self.missing_functions.clone(),
allocation_stats,
);
// If we couldn't unmap it, erase the memory.
self.memory_zeroed = self.memory.erase().is_ok();
res
}
}
impl WasmInstance for WasmiInstance {
fn call_with_allocation_stats(
&mut self,
method: InvokeMethod,
data: &[u8],
) -> (Result<Vec<u8>, Error>, Option<AllocationStats>) {
let mut allocation_stats = None;
let result = self.call_impl(method, data, &mut allocation_stats);
(result, allocation_stats)
}
fn get_global_const(&mut self, name: &str) -> Result<Option<sp_wasm_interface::Value>, Error> {
match self.instance.export_by_name(name) {
Some(global) => Ok(Some(
global
.as_global()
.ok_or_else(|| format!("`{}` is not a global", name))?
.get()
.into(),
)),
None => Ok(None),
}
}
fn linear_memory_base_ptr(&self) -> Option<*const u8> {
Some(self.memory.direct_access().as_ref().as_ptr())
}
}
@@ -109,8 +109,8 @@ fn host_function_not_found() {
.0
.unwrap_err();
assert!(err.contains("Instantiation: Export "));
assert!(err.contains(" not found"));
assert!(err.contains("test_return_data"));
assert!(err.contains(" Failed to create module"));
}
#[test]