Statically register host WASM functions (#10394)

* Statically register host WASM functions

* Fix `substrate-test-client` compilation

* Move `ExtendedHostFunctions` to `sp-wasm-interface`

* Fix `sp-runtime-interface` tests' compilation

* Fix `sc-executor-wasmtime` tests' compilation

* Use `runtime_interface` macro in `test-runner`

* Fix `sc-executor` tests' compilation

* Reformatting/`rustfmt`

* Add an extra comment regarding the `H` generic arg in `create_runtime`

* Even more `rustfmt`

* Depend on `wasmtime` without default features in `sp-wasm-interface`

* Bump version of `sp-wasm-interface` to 4.0.1

* Bump `sp-wasm-interface` in `Cargo.lock` too

* Bump all of the `sp-wasm-interface` requirements to 4.0.1

Maybe this will appease cargo-unleash?

* Revert "Bump all of the `sp-wasm-interface` requirements to 4.0.1"

This reverts commit 0f7ccf8e0f371542861121b145ab87af6541ac30.

* Make `cargo-unleash` happy (maybe)

* Use `cargo-unleash` to bump the crates' versions

* Align to review comments
This commit is contained in:
Koute
2021-12-14 17:26:40 +09:00
committed by GitHub
parent 23c5b6755b
commit 7711f5266e
36 changed files with 742 additions and 570 deletions
+2 -2
View File
@@ -24,8 +24,8 @@ sp-panic-handler = { version = "4.0.0-dev", path = "../../primitives/panic-handl
wasmi = "0.9.1"
lazy_static = "1.4.0"
sp-api = { version = "4.0.0-dev", path = "../../primitives/api" }
sp-wasm-interface = { version = "4.0.0", path = "../../primitives/wasm-interface" }
sp-runtime-interface = { version = "4.0.0", path = "../../primitives/runtime-interface" }
sp-wasm-interface = { version = "4.1.0-dev", path = "../../primitives/wasm-interface" }
sp-runtime-interface = { version = "4.1.0-dev", path = "../../primitives/runtime-interface" }
sp-externalities = { version = "0.10.0", path = "../../primitives/externalities" }
sc-executor-common = { version = "0.10.0-dev", path = "common" }
sc-executor-wasmi = { version = "0.10.0-dev", path = "wasmi" }
+1 -1
View File
@@ -20,7 +20,7 @@ codec = { package = "parity-scale-codec", version = "2.0.0" }
wasmi = "0.9.1"
sp-core = { version = "4.1.0-dev", path = "../../../primitives/core" }
sc-allocator = { version = "4.1.0-dev", path = "../../allocator" }
sp-wasm-interface = { version = "4.0.0", path = "../../../primitives/wasm-interface" }
sp-wasm-interface = { version = "4.1.0-dev", path = "../../../primitives/wasm-interface" }
sp-maybe-compressed-blob = { version = "4.1.0-dev", path = "../../../primitives/maybe-compressed-blob" }
sp-serializer = { version = "4.0.0-dev", path = "../../../primitives/serializer" }
thiserror = "1.0.30"
@@ -332,6 +332,14 @@ sp_core::wasm_export_functions! {
fn test_panic_in_spawned() {
sp_tasks::spawn(tasks::panicker, vec![]).join();
}
fn test_return_i8() -> i8 {
-66
}
fn test_take_i8(value: i8) {
assert_eq!(value, -66);
}
}
#[cfg(not(feature = "std"))]
@@ -34,7 +34,6 @@ use sp_core::{
use sp_runtime::traits::BlakeTwo256;
use sp_state_machine::TestExternalities as CoreTestExternalities;
use sp_trie::{trie_types::Layout, TrieConfiguration};
use sp_wasm_interface::HostFunctions as _;
use std::sync::Arc;
use tracing_subscriber::layer::SubscriberExt;
@@ -124,14 +123,8 @@ fn call_in_wasm<E: Externalities>(
execution_method: WasmExecutionMethod,
ext: &mut E,
) -> Result<Vec<u8>, String> {
let executor = crate::WasmExecutor::new(
execution_method,
Some(1024),
HostFunctions::host_functions(),
8,
None,
2,
);
let executor =
crate::WasmExecutor::<HostFunctions>::new(execution_method, Some(1024), 8, None, 2);
executor.uncached_call(
RuntimeBlob::uncompress_if_needed(&wasm_binary_unwrap()[..]).unwrap(),
ext,
@@ -475,10 +468,9 @@ test_wasm_execution!(should_trap_when_heap_exhausted);
fn should_trap_when_heap_exhausted(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let executor = crate::WasmExecutor::new(
let executor = crate::WasmExecutor::<HostFunctions>::new(
wasm_method,
Some(17), // `17` is the initial number of pages compiled into the binary.
HostFunctions::host_functions(),
8,
None,
2,
@@ -501,11 +493,10 @@ fn mk_test_runtime(wasm_method: WasmExecutionMethod, pages: u64) -> Arc<dyn Wasm
let blob = RuntimeBlob::uncompress_if_needed(&wasm_binary_unwrap()[..])
.expect("failed to create a runtime blob out of test runtime");
crate::wasm_runtime::create_wasm_runtime_with_code(
crate::wasm_runtime::create_wasm_runtime_with_code::<HostFunctions>(
wasm_method,
pages,
blob,
HostFunctions::host_functions(),
true,
None,
)
@@ -589,10 +580,9 @@ fn heap_is_reset_between_calls(wasm_method: WasmExecutionMethod) {
test_wasm_execution!(parallel_execution);
fn parallel_execution(wasm_method: WasmExecutionMethod) {
let executor = std::sync::Arc::new(crate::WasmExecutor::new(
let executor = std::sync::Arc::new(crate::WasmExecutor::<HostFunctions>::new(
wasm_method,
Some(1024),
HostFunctions::host_functions(),
8,
None,
2,
@@ -763,11 +753,10 @@ fn memory_is_cleared_between_invocations(wasm_method: WasmExecutionMethod) {
)
)"#).unwrap();
let runtime = crate::wasm_runtime::create_wasm_runtime_with_code(
let runtime = crate::wasm_runtime::create_wasm_runtime_with_code::<HostFunctions>(
wasm_method,
1024,
RuntimeBlob::uncompress_if_needed(&binary[..]).unwrap(),
HostFunctions::host_functions(),
true,
None,
)
@@ -780,3 +769,22 @@ fn memory_is_cleared_between_invocations(wasm_method: WasmExecutionMethod) {
let res = instance.call_export("returns_no_bss_mutable_static", &[0]).unwrap();
assert_eq!(1, u64::decode(&mut &res[..]).unwrap());
}
test_wasm_execution!(return_i8);
fn return_i8(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
call_in_wasm("test_return_i8", &[], wasm_method, &mut ext).unwrap(),
(-66_i8).encode()
);
}
test_wasm_execution!(take_i8);
fn take_i8(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
call_in_wasm("test_take_i8", &(-66_i8).encode(), wasm_method, &mut ext).unwrap();
}
+1 -3
View File
@@ -67,17 +67,15 @@ mod tests {
use sc_executor_common::runtime_blob::RuntimeBlob;
use sc_runtime_test::wasm_binary_unwrap;
use sp_io::TestExternalities;
use sp_wasm_interface::HostFunctions;
#[test]
fn call_in_interpreted_wasm_works() {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
let executor = WasmExecutor::new(
let executor = WasmExecutor::<sp_io::SubstrateHostFunctions>::new(
WasmExecutionMethod::Interpreted,
Some(8),
sp_io::SubstrateHostFunctions::host_functions(),
8,
None,
2,
@@ -24,6 +24,7 @@ use crate::{
use std::{
collections::HashMap,
marker::PhantomData,
panic::{AssertUnwindSafe, UnwindSafe},
path::PathBuf,
result,
@@ -46,7 +47,7 @@ use sp_core::{
use sp_externalities::ExternalitiesExt as _;
use sp_tasks::new_async_externalities;
use sp_version::{GetNativeVersion, NativeVersion, RuntimeVersion};
use sp_wasm_interface::{Function, HostFunctions};
use sp_wasm_interface::{ExtendedHostFunctions, HostFunctions};
/// Default num of pages for the heap
const DEFAULT_HEAP_PAGES: u64 = 2048;
@@ -91,22 +92,36 @@ pub trait NativeExecutionDispatch: Send + Sync {
/// An abstraction over Wasm code executor. Supports selecting execution backend and
/// manages runtime cache.
#[derive(Clone)]
pub struct WasmExecutor {
pub struct WasmExecutor<H> {
/// Method used to execute fallback Wasm code.
method: WasmExecutionMethod,
/// The number of 64KB pages to allocate for Wasm execution.
default_heap_pages: u64,
/// The host functions registered with this instance.
host_functions: Arc<Vec<&'static dyn Function>>,
/// WASM runtime cache.
cache: Arc<RuntimeCache>,
/// The path to a directory which the executor can leverage for a file cache, e.g. put there
/// compiled artifacts.
cache_path: Option<PathBuf>,
phantom: PhantomData<H>,
}
impl WasmExecutor {
impl<H> Clone for WasmExecutor<H> {
fn clone(&self) -> Self {
Self {
method: self.method,
default_heap_pages: self.default_heap_pages,
cache: self.cache.clone(),
cache_path: self.cache_path.clone(),
phantom: self.phantom,
}
}
}
impl<H> WasmExecutor<H>
where
H: HostFunctions,
{
/// Create new instance.
///
/// # Parameters
@@ -127,7 +142,6 @@ impl WasmExecutor {
pub fn new(
method: WasmExecutionMethod,
default_heap_pages: Option<u64>,
host_functions: Vec<&'static dyn Function>,
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
runtime_cache_size: u8,
@@ -135,13 +149,13 @@ impl WasmExecutor {
WasmExecutor {
method,
default_heap_pages: default_heap_pages.unwrap_or(DEFAULT_HEAP_PAGES),
host_functions: Arc::new(host_functions),
cache: Arc::new(RuntimeCache::new(
max_runtime_instances,
cache_path.clone(),
runtime_cache_size,
)),
cache_path,
phantom: PhantomData,
}
}
@@ -173,12 +187,11 @@ impl WasmExecutor {
AssertUnwindSafe<&mut dyn Externalities>,
) -> Result<Result<R>>,
{
match self.cache.with_instance(
match self.cache.with_instance::<H, _, _>(
runtime_code,
ext,
self.method,
self.default_heap_pages,
&*self.host_functions,
allow_missing_host_functions,
|module, instance, version, ext| {
let module = AssertUnwindSafe(module);
@@ -208,11 +221,10 @@ impl WasmExecutor {
export_name: &str,
call_data: &[u8],
) -> std::result::Result<Vec<u8>, String> {
let module = crate::wasm_runtime::create_wasm_runtime_with_code(
let module = crate::wasm_runtime::create_wasm_runtime_with_code::<H>(
self.method,
self.default_heap_pages,
runtime_blob,
self.host_functions.to_vec(),
allow_missing_host_functions,
self.cache_path.as_deref(),
)
@@ -235,7 +247,10 @@ impl WasmExecutor {
}
}
impl sp_core::traits::ReadRuntimeVersion for WasmExecutor {
impl<H> sp_core::traits::ReadRuntimeVersion for WasmExecutor<H>
where
H: HostFunctions,
{
fn read_runtime_version(
&self,
wasm_code: &[u8],
@@ -269,7 +284,10 @@ impl sp_core::traits::ReadRuntimeVersion for WasmExecutor {
}
}
impl CodeExecutor for WasmExecutor {
impl<H> CodeExecutor for WasmExecutor<H>
where
H: HostFunctions,
{
type Error = Error;
fn call<
@@ -299,7 +317,10 @@ impl CodeExecutor for WasmExecutor {
}
}
impl RuntimeVersionOf for WasmExecutor {
impl<H> RuntimeVersionOf for WasmExecutor<H>
where
H: HostFunctions,
{
fn runtime_version(
&self,
ext: &mut dyn Externalities,
@@ -313,13 +334,17 @@ impl RuntimeVersionOf for WasmExecutor {
/// A generic `CodeExecutor` implementation that uses a delegate to determine wasm code equivalence
/// and dispatch to native code when possible, falling back on `WasmExecutor` when not.
pub struct NativeElseWasmExecutor<D> {
pub struct NativeElseWasmExecutor<D>
where
D: NativeExecutionDispatch,
{
/// Dummy field to avoid the compiler complaining about us not using `D`.
_dummy: std::marker::PhantomData<D>,
/// Native runtime version info.
native_version: NativeVersion,
/// Fallback wasm executor.
wasm: WasmExecutor,
wasm:
WasmExecutor<ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>>,
}
impl<D: NativeExecutionDispatch> NativeElseWasmExecutor<D> {
@@ -337,24 +362,9 @@ impl<D: NativeExecutionDispatch> NativeElseWasmExecutor<D> {
max_runtime_instances: usize,
runtime_cache_size: u8,
) -> Self {
let extended = D::ExtendHostFunctions::host_functions();
let mut host_functions = sp_io::SubstrateHostFunctions::host_functions()
.into_iter()
// filter out any host function overrides provided.
.filter(|host_fn| {
extended
.iter()
.find(|ext_host_fn| host_fn.name() == ext_host_fn.name())
.is_none()
})
.collect::<Vec<_>>();
// Add the custom host functions provided by the user.
host_functions.extend(extended);
let wasm_executor = WasmExecutor::new(
fallback_method,
default_heap_pages,
host_functions,
max_runtime_instances,
None,
runtime_cache_size,
@@ -645,8 +655,21 @@ mod tests {
8,
2,
);
fn extract_host_functions<H>(
_: &WasmExecutor<H>,
) -> Vec<&'static dyn sp_wasm_interface::Function>
where
H: HostFunctions,
{
H::host_functions()
}
my_interface::HostFunctions::host_functions().iter().for_each(|function| {
assert_eq!(executor.wasm.host_functions.iter().filter(|f| f == &function).count(), 2);
assert_eq!(
extract_host_functions(&executor.wasm).iter().filter(|f| f == &function).count(),
2
);
});
my_interface::say_hello_world("hey");
+19 -18
View File
@@ -37,7 +37,7 @@ use std::{
sync::Arc,
};
use sp_wasm_interface::Function;
use sp_wasm_interface::HostFunctions;
/// Specification of different methods of executing the runtime Wasm code.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
@@ -199,14 +199,14 @@ impl RuntimeCache {
///
/// `wasm_method` - Type of WASM backend to use.
///
/// `host_functions` - The host functions that should be registered for the Wasm runtime.
///
/// `allow_missing_func_imports` - Ignore missing function imports.
///
/// `max_runtime_instances` - The size of the instances cache.
///
/// `f` - Function to execute.
///
/// `H` - A compile-time list of host functions to expose to the runtime.
///
/// # Returns result of `f` wrapped in an additional result.
/// In case of failure one of two errors can be returned:
///
@@ -214,17 +214,17 @@ impl RuntimeCache {
///
/// `Error::InvalidMemoryReference` is returned if no memory export with the
/// identifier `memory` can be found in the runtime.
pub fn with_instance<'c, R, F>(
pub fn with_instance<'c, H, R, F>(
&self,
runtime_code: &'c RuntimeCode<'c>,
ext: &mut dyn Externalities,
wasm_method: WasmExecutionMethod,
default_heap_pages: u64,
host_functions: &[&'static dyn Function],
allow_missing_func_imports: bool,
f: F,
) -> Result<Result<R, Error>, Error>
where
H: HostFunctions,
F: FnOnce(
&Arc<dyn WasmModule>,
&mut dyn WasmInstance,
@@ -247,12 +247,11 @@ impl RuntimeCache {
let time = std::time::Instant::now();
let result = create_versioned_wasm_runtime(
let result = create_versioned_wasm_runtime::<H>(
&code,
ext,
wasm_method,
heap_pages,
host_functions.into(),
allow_missing_func_imports,
self.max_runtime_instances,
self.cache_path.as_deref(),
@@ -288,14 +287,16 @@ impl RuntimeCache {
}
/// Create a wasm runtime with the given `code`.
pub fn create_wasm_runtime_with_code(
pub fn create_wasm_runtime_with_code<H>(
wasm_method: WasmExecutionMethod,
heap_pages: u64,
blob: RuntimeBlob,
host_functions: Vec<&'static dyn Function>,
allow_missing_func_imports: bool,
cache_path: Option<&Path>,
) -> Result<Arc<dyn WasmModule>, WasmError> {
) -> Result<Arc<dyn WasmModule>, WasmError>
where
H: HostFunctions,
{
match wasm_method {
WasmExecutionMethod::Interpreted => {
// Wasmi doesn't have any need in a cache directory.
@@ -307,13 +308,13 @@ pub fn create_wasm_runtime_with_code(
sc_executor_wasmi::create_runtime(
blob,
heap_pages,
host_functions,
H::host_functions(),
allow_missing_func_imports,
)
.map(|runtime| -> Arc<dyn WasmModule> { Arc::new(runtime) })
},
#[cfg(feature = "wasmtime")]
WasmExecutionMethod::Compiled => sc_executor_wasmtime::create_runtime(
WasmExecutionMethod::Compiled => sc_executor_wasmtime::create_runtime::<H>(
blob,
sc_executor_wasmtime::Config {
heap_pages,
@@ -327,7 +328,6 @@ pub fn create_wasm_runtime_with_code(
parallel_compilation: true,
},
},
host_functions,
)
.map(|runtime| -> Arc<dyn WasmModule> { Arc::new(runtime) }),
}
@@ -392,16 +392,18 @@ pub fn read_embedded_version(blob: &RuntimeBlob) -> Result<Option<RuntimeVersion
}
}
fn create_versioned_wasm_runtime(
fn create_versioned_wasm_runtime<H>(
code: &[u8],
ext: &mut dyn Externalities,
wasm_method: WasmExecutionMethod,
heap_pages: u64,
host_functions: Vec<&'static dyn Function>,
allow_missing_func_imports: bool,
max_instances: usize,
cache_path: Option<&Path>,
) -> Result<VersionedRuntime, WasmError> {
) -> Result<VersionedRuntime, WasmError>
where
H: HostFunctions,
{
// The incoming code may be actually compressed. We decompress it here and then work with
// the uncompressed code from now on.
let blob = sc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(&code)?;
@@ -411,11 +413,10 @@ fn create_versioned_wasm_runtime(
// runtime.
let mut version: Option<_> = read_embedded_version(&blob)?;
let runtime = create_wasm_runtime_with_code(
let runtime = create_wasm_runtime_with_code::<H>(
wasm_method,
heap_pages,
blob,
host_functions,
allow_missing_func_imports,
cache_path,
)?;
+2 -2
View File
@@ -19,7 +19,7 @@ wasmi = "0.9.1"
codec = { package = "parity-scale-codec", version = "2.0.0" }
sc-executor-common = { version = "0.10.0-dev", path = "../common" }
sc-allocator = { version = "4.1.0-dev", path = "../../allocator" }
sp-wasm-interface = { version = "4.0.0", path = "../../../primitives/wasm-interface" }
sp-runtime-interface = { version = "4.0.0", path = "../../../primitives/runtime-interface" }
sp-wasm-interface = { version = "4.1.0-dev", path = "../../../primitives/wasm-interface" }
sp-runtime-interface = { version = "4.1.0-dev", path = "../../../primitives/runtime-interface" }
sp-core = { version = "4.1.0-dev", path = "../../../primitives/core" }
scoped-tls = "1.0"
@@ -19,8 +19,8 @@ log = "0.4.8"
parity-wasm = "0.42.0"
codec = { package = "parity-scale-codec", version = "2.0.0" }
sc-executor-common = { version = "0.10.0-dev", path = "../common" }
sp-wasm-interface = { version = "4.0.0", path = "../../../primitives/wasm-interface" }
sp-runtime-interface = { version = "4.0.0", path = "../../../primitives/runtime-interface" }
sp-wasm-interface = { version = "4.1.0-dev", path = "../../../primitives/wasm-interface", features = ["wasmtime"] }
sp-runtime-interface = { version = "4.1.0-dev", path = "../../../primitives/runtime-interface" }
sp-core = { version = "4.1.0-dev", path = "../../../primitives/core" }
sc-allocator = { version = "4.1.0-dev", path = "../../allocator" }
wasmtime = { version = "0.31.0", default-features = false, features = [
@@ -62,11 +62,11 @@ impl HostState {
/// 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
/// a longer-living `HostState`.
pub(crate) struct HostContext<'a, 'b> {
pub(crate) caller: &'a mut Caller<'b, StoreData>,
pub(crate) struct HostContext<'a> {
pub(crate) caller: Caller<'a, StoreData>,
}
impl<'a, 'b> HostContext<'a, 'b> {
impl<'a> HostContext<'a> {
fn host_state(&self) -> &HostState {
self.caller
.data()
@@ -98,7 +98,7 @@ impl<'a, 'b> HostContext<'a, 'b> {
}
}
impl<'a, 'b> sp_wasm_interface::FunctionContext for HostContext<'a, 'b> {
impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> {
fn read_memory_into(
&self,
address: Pointer<u8>,
@@ -136,7 +136,7 @@ impl<'a, 'b> sp_wasm_interface::FunctionContext for HostContext<'a, 'b> {
}
}
impl<'a, 'b> Sandbox for HostContext<'a, 'b> {
impl<'a> Sandbox for HostContext<'a> {
fn memory_get(
&mut self,
memory_id: MemoryId,
@@ -320,12 +320,12 @@ impl<'a, 'b> Sandbox for HostContext<'a, 'b> {
}
}
struct SandboxContext<'a, 'b, 'c> {
host_context: &'a mut HostContext<'b, 'c>,
struct SandboxContext<'a, 'b> {
host_context: &'a mut HostContext<'b>,
dispatch_thunk: Func,
}
impl<'a, 'b, 'c> sandbox::SandboxContext for SandboxContext<'a, 'b, 'c> {
impl<'a, 'b> sandbox::SandboxContext for SandboxContext<'a, 'b> {
fn invoke(
&mut self,
invoke_args_ptr: Pointer<u8>,
+86 -155
View File
@@ -19,14 +19,11 @@
use crate::{
host::HostContext,
runtime::{Store, StoreData},
util,
};
use sc_executor_common::error::WasmError;
use sp_wasm_interface::{Function, ValueType};
use std::{any::Any, convert::TryInto};
use wasmtime::{
Caller, Extern, ExternType, Func, FuncType, ImportType, Memory, MemoryType, Module, Trap, Val,
};
use sp_wasm_interface::{FunctionContext, HostFunctions};
use std::{collections::HashMap, convert::TryInto};
use wasmtime::{Extern, ExternType, Func, FuncType, ImportType, Memory, MemoryType, Module, Trap};
pub struct Imports {
/// Contains the index into `externs` where the memory import is stored if any. `None` if there
@@ -37,16 +34,19 @@ pub struct Imports {
/// Goes over all imports of a module and prepares a vector of `Extern`s that can be used for
/// instantiation of the module. Returns an error if there are imports that cannot be satisfied.
pub(crate) fn resolve_imports(
pub(crate) fn resolve_imports<H>(
store: &mut Store,
module: &Module,
host_functions: &[&'static dyn Function],
heap_pages: u64,
allow_missing_func_imports: bool,
) -> Result<Imports, WasmError> {
) -> Result<Imports, WasmError>
where
H: HostFunctions,
{
let mut externs = vec![];
let mut memory_import_index = None;
for import_ty in module.imports() {
let mut pending_func_imports = HashMap::new();
for (index, import_ty) in module.imports().enumerate() {
let name = import_name(&import_ty)?;
if import_ty.module() != "env" {
@@ -57,19 +57,89 @@ pub(crate) fn resolve_imports(
)))
}
let resolved = match name {
"memory" => {
memory_import_index = Some(externs.len());
resolve_memory_import(store, &import_ty, heap_pages)?
if name == "memory" {
memory_import_index = Some(index);
externs.push((index, resolve_memory_import(store, &import_ty, heap_pages)?));
continue
}
match import_ty.ty() {
ExternType::Func(func_ty) => {
pending_func_imports.insert(name.to_owned(), (index, import_ty, func_ty));
},
_ =>
resolve_func_import(store, &import_ty, host_functions, allow_missing_func_imports)?,
return Err(WasmError::Other(format!(
"host doesn't provide any non function imports besides 'memory': {}:{}",
import_ty.module(),
name,
))),
};
externs.push(resolved);
}
let mut registry = Registry { store, externs, pending_func_imports };
H::register_static(&mut registry)?;
let mut externs = registry.externs;
if !registry.pending_func_imports.is_empty() {
if allow_missing_func_imports {
for (_, (index, import_ty, func_ty)) in registry.pending_func_imports {
externs.push((
index,
MissingHostFuncHandler::new(&import_ty)?.into_extern(store, &func_ty),
));
}
} else {
let mut names = Vec::new();
for (name, (_, import_ty, _)) in registry.pending_func_imports {
names.push(format!("'{}:{}'", import_ty.module(), name));
}
let names = names.join(", ");
return Err(WasmError::Other(format!(
"runtime requires function imports which are not present on the host: {}",
names
)))
}
}
externs.sort_unstable_by_key(|&(index, _)| index);
let externs = externs.into_iter().map(|(_, ext)| ext).collect();
Ok(Imports { memory_import_index, externs })
}
struct Registry<'a, 'b> {
store: &'a mut Store,
externs: Vec<(usize, Extern)>,
pending_func_imports: HashMap<String, (usize, ImportType<'b>, FuncType)>,
}
impl<'a, 'b> sp_wasm_interface::HostFunctionRegistry for Registry<'a, 'b> {
type State = StoreData;
type Error = WasmError;
type FunctionContext = HostContext<'a>;
fn with_function_context<R>(
caller: wasmtime::Caller<Self::State>,
callback: impl FnOnce(&mut dyn FunctionContext) -> R,
) -> R {
callback(&mut HostContext { caller })
}
fn register_static<Params, Results>(
&mut self,
fn_name: &str,
func: impl wasmtime::IntoFunc<Self::State, Params, Results>,
) -> Result<(), Self::Error> {
if let Some((index, _, _)) = self.pending_func_imports.remove(fn_name) {
let func = Func::wrap(&mut *self.store, func);
self.externs.push((index, Extern::Func(func)));
}
Ok(())
}
}
/// When the module linking proposal is supported the import's name can be `None`.
/// Because we are not using this proposal we could safely unwrap the name.
/// However, we opt for an error in order to avoid panics at all costs.
@@ -139,115 +209,6 @@ fn resolve_memory_import(
Ok(Extern::Memory(memory))
}
fn resolve_func_import(
store: &mut Store,
import_ty: &ImportType,
host_functions: &[&'static dyn Function],
allow_missing_func_imports: bool,
) -> Result<Extern, WasmError> {
let name = import_name(&import_ty)?;
let func_ty = match import_ty.ty() {
ExternType::Func(func_ty) => func_ty,
_ =>
return Err(WasmError::Other(format!(
"host doesn't provide any non function imports besides 'memory': {}:{}",
import_ty.module(),
name,
))),
};
let host_func = match host_functions.iter().find(|host_func| host_func.name() == name) {
Some(host_func) => host_func,
None if allow_missing_func_imports =>
return Ok(MissingHostFuncHandler::new(import_ty)?.into_extern(store, &func_ty)),
None =>
return Err(WasmError::Other(format!(
"host doesn't provide such function: {}:{}",
import_ty.module(),
name,
))),
};
if &func_ty != &wasmtime_func_sig(*host_func) {
return Err(WasmError::Other(format!(
"signature mismatch for: {}:{}",
import_ty.module(),
name,
)))
}
Ok(HostFuncHandler::new(*host_func).into_extern(store))
}
/// This structure implements `Callable` and acts as a bridge between wasmtime and
/// substrate host functions.
struct HostFuncHandler {
host_func: &'static dyn Function,
}
fn call_static<'a>(
static_func: &'static dyn Function,
wasmtime_params: &[Val],
wasmtime_results: &mut [Val],
mut caller: Caller<'a, StoreData>,
) -> Result<(), wasmtime::Trap> {
let unwind_result = {
let mut host_ctx = HostContext { caller: &mut caller };
// `from_wasmtime_val` panics if it encounters a value that doesn't fit into the values
// available in substrate.
//
// This, however, cannot happen since the signature of this function is created from
// a `dyn Function` signature of which cannot have a non substrate value by definition.
let mut params = wasmtime_params.iter().cloned().map(util::from_wasmtime_val);
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
static_func.execute(&mut host_ctx, &mut params)
}))
};
let execution_result = match unwind_result {
Ok(execution_result) => execution_result,
Err(err) => return Err(Trap::new(stringify_panic_payload(err))),
};
match execution_result {
Ok(Some(ret_val)) => {
debug_assert!(
wasmtime_results.len() == 1,
"wasmtime function signature, therefore the number of results, should always \
correspond to the number of results returned by the host function",
);
wasmtime_results[0] = util::into_wasmtime_val(ret_val);
Ok(())
},
Ok(None) => {
debug_assert!(
wasmtime_results.len() == 0,
"wasmtime function signature, therefore the number of results, should always \
correspond to the number of results returned by the host function",
);
Ok(())
},
Err(msg) => Err(Trap::new(msg)),
}
}
impl HostFuncHandler {
fn new(host_func: &'static dyn Function) -> Self {
Self { host_func }
}
fn into_extern(self, store: &mut Store) -> Extern {
let host_func = self.host_func;
let func_ty = wasmtime_func_sig(self.host_func);
let func = Func::new(store, func_ty, move |caller, params, result| {
call_static(host_func, params, result, caller)
});
Extern::Func(func)
}
}
/// A `Callable` handler for missing functions.
struct MissingHostFuncHandler {
module: String,
@@ -270,33 +231,3 @@ impl MissingHostFuncHandler {
Extern::Func(func)
}
}
fn wasmtime_func_sig(func: &dyn Function) -> wasmtime::FuncType {
let signature = func.signature();
let params = signature.args.iter().cloned().map(into_wasmtime_val_type);
let results = signature.return_value.iter().cloned().map(into_wasmtime_val_type);
wasmtime::FuncType::new(params, results)
}
fn into_wasmtime_val_type(val_ty: ValueType) -> wasmtime::ValType {
match val_ty {
ValueType::I32 => wasmtime::ValType::I32,
ValueType::I64 => wasmtime::ValType::I64,
ValueType::F32 => wasmtime::ValType::F32,
ValueType::F64 => wasmtime::ValType::F64,
}
}
/// Attempt to convert a opaque panic payload to a string.
fn stringify_panic_payload(payload: Box<dyn Any + Send + 'static>) -> String {
match payload.downcast::<&'static str>() {
Ok(msg) => msg.to_string(),
Err(payload) => match payload.downcast::<String>() {
Ok(msg) => *msg,
// At least we tried...
Err(_) => "Box<Any>".to_string(),
},
}
}
@@ -24,7 +24,7 @@ use sc_executor_common::{
error::{Error, Result},
wasm_runtime::InvokeMethod,
};
use sp_wasm_interface::{Function, Pointer, Value, WordSize};
use sp_wasm_interface::{HostFunctions, Pointer, Value, WordSize};
use wasmtime::{
AsContext, AsContextMut, Extern, Func, Global, Instance, Memory, Module, Table, Val,
};
@@ -138,13 +138,15 @@ fn extern_func(extern_: &Extern) -> Option<&Func> {
impl InstanceWrapper {
/// Create a new instance wrapper from the given wasm module.
pub fn new(
pub fn new<H>(
module: &Module,
host_functions: &[&'static dyn Function],
heap_pages: u64,
allow_missing_func_imports: bool,
max_memory_size: Option<usize>,
) -> Result<Self> {
) -> Result<Self>
where
H: HostFunctions,
{
let limits = if let Some(max_memory_size) = max_memory_size {
wasmtime::StoreLimitsBuilder::new().memory_size(max_memory_size).build()
} else {
@@ -161,10 +163,9 @@ impl InstanceWrapper {
// Scan all imports, find the matching host functions, and create stubs that adapt arguments
// and results.
let imports = crate::imports::resolve_imports(
let imports = crate::imports::resolve_imports::<H>(
&mut store,
module,
host_functions,
heap_pages,
allow_missing_func_imports,
)?;
@@ -23,6 +23,7 @@ use crate::{
instance_wrapper::{EntryPoint, InstanceWrapper},
util,
};
use core::marker::PhantomData;
use sc_allocator::FreeingBumpHeapAllocator;
use sc_executor_common::{
@@ -33,7 +34,7 @@ use sc_executor_common::{
wasm_runtime::{InvokeMethod, WasmInstance, WasmModule},
};
use sp_runtime_interface::unpack_ptr_and_len;
use sp_wasm_interface::{Function, Pointer, Value, WordSize};
use sp_wasm_interface::{HostFunctions, Pointer, Value, WordSize};
use std::{
path::{Path, PathBuf},
sync::{
@@ -79,29 +80,31 @@ impl StoreData {
pub(crate) type Store = wasmtime::Store<StoreData>;
enum Strategy {
enum Strategy<H> {
FastInstanceReuse {
instance_wrapper: InstanceWrapper,
globals_snapshot: GlobalsSnapshot<wasmtime::Global>,
data_segments_snapshot: Arc<DataSegmentsSnapshot>,
heap_base: u32,
},
RecreateInstance(InstanceCreator),
RecreateInstance(InstanceCreator<H>),
}
struct InstanceCreator {
struct InstanceCreator<H> {
module: Arc<wasmtime::Module>,
host_functions: Vec<&'static dyn Function>,
heap_pages: u64,
allow_missing_func_imports: bool,
max_memory_size: Option<usize>,
phantom: PhantomData<H>,
}
impl InstanceCreator {
impl<H> InstanceCreator<H>
where
H: HostFunctions,
{
fn instantiate(&mut self) -> Result<InstanceWrapper> {
InstanceWrapper::new(
InstanceWrapper::new::<H>(
&*self.module,
&self.host_functions,
self.heap_pages,
self.allow_missing_func_imports,
self.max_memory_size,
@@ -141,19 +144,21 @@ struct InstanceSnapshotData {
/// A `WasmModule` implementation using wasmtime to compile the runtime module to machine code
/// and execute the compiled code.
pub struct WasmtimeRuntime {
pub struct WasmtimeRuntime<H> {
module: Arc<wasmtime::Module>,
snapshot_data: Option<InstanceSnapshotData>,
config: Config,
host_functions: Vec<&'static dyn Function>,
phantom: PhantomData<H>,
}
impl WasmModule for WasmtimeRuntime {
impl<H> WasmModule for WasmtimeRuntime<H>
where
H: HostFunctions,
{
fn new_instance(&self) -> Result<Box<dyn WasmInstance>> {
let strategy = if let Some(ref snapshot_data) = self.snapshot_data {
let mut instance_wrapper = InstanceWrapper::new(
let mut instance_wrapper = InstanceWrapper::new::<H>(
&self.module,
&self.host_functions,
self.config.heap_pages,
self.config.allow_missing_func_imports,
self.config.max_memory_size,
@@ -169,19 +174,19 @@ impl WasmModule for WasmtimeRuntime {
&mut InstanceGlobals { instance: &mut instance_wrapper },
);
Strategy::FastInstanceReuse {
Strategy::<H>::FastInstanceReuse {
instance_wrapper,
globals_snapshot,
data_segments_snapshot: snapshot_data.data_segments_snapshot.clone(),
heap_base,
}
} else {
Strategy::RecreateInstance(InstanceCreator {
Strategy::<H>::RecreateInstance(InstanceCreator {
module: self.module.clone(),
host_functions: self.host_functions.clone(),
heap_pages: self.config.heap_pages,
allow_missing_func_imports: self.config.allow_missing_func_imports,
max_memory_size: self.config.max_memory_size,
phantom: PhantomData,
})
};
@@ -191,11 +196,14 @@ impl WasmModule for WasmtimeRuntime {
/// A `WasmInstance` implementation that reuses compiled module and spawns instances
/// to execute the compiled code.
pub struct WasmtimeInstance {
strategy: Strategy,
pub struct WasmtimeInstance<H> {
strategy: Strategy<H>,
}
impl WasmInstance for WasmtimeInstance {
impl<H> WasmInstance for WasmtimeInstance<H>
where
H: HostFunctions,
{
fn call(&mut self, method: InvokeMethod, data: &[u8]) -> Result<Vec<u8>> {
match &mut self.strategy {
Strategy::FastInstanceReuse {
@@ -483,13 +491,18 @@ enum CodeSupplyMode<'a> {
/// Create a new `WasmtimeRuntime` given the code. This function performs translation from Wasm to
/// machine code, which can be computationally heavy.
pub fn create_runtime(
///
/// The `H` generic parameter is used to statically pass a set of host functions which are exposed
/// to the runtime.
pub fn create_runtime<H>(
blob: RuntimeBlob,
config: Config,
host_functions: Vec<&'static dyn Function>,
) -> std::result::Result<WasmtimeRuntime, WasmError> {
) -> std::result::Result<WasmtimeRuntime<H>, WasmError>
where
H: HostFunctions,
{
// SAFETY: this is safe because it doesn't use `CodeSupplyMode::Artifact`.
unsafe { do_create_runtime(CodeSupplyMode::Verbatim { blob }, config, host_functions) }
unsafe { do_create_runtime::<H>(CodeSupplyMode::Verbatim { blob }, config) }
}
/// The same as [`create_runtime`] but takes a precompiled artifact, which makes this function
@@ -503,23 +516,27 @@ pub fn create_runtime(
///
/// It is ok though if the `compiled_artifact` was created by code of another version or with
/// different configuration flags. In such case the caller will receive an `Err` deterministically.
pub unsafe fn create_runtime_from_artifact(
pub unsafe fn create_runtime_from_artifact<H>(
compiled_artifact: &[u8],
config: Config,
host_functions: Vec<&'static dyn Function>,
) -> std::result::Result<WasmtimeRuntime, WasmError> {
do_create_runtime(CodeSupplyMode::Artifact { compiled_artifact }, config, host_functions)
) -> std::result::Result<WasmtimeRuntime<H>, WasmError>
where
H: HostFunctions,
{
do_create_runtime::<H>(CodeSupplyMode::Artifact { compiled_artifact }, config)
}
/// # Safety
///
/// This is only unsafe if called with [`CodeSupplyMode::Artifact`]. See
/// [`create_runtime_from_artifact`] to get more details.
unsafe fn do_create_runtime(
unsafe fn do_create_runtime<H>(
code_supply_mode: CodeSupplyMode<'_>,
config: Config,
host_functions: Vec<&'static dyn Function>,
) -> std::result::Result<WasmtimeRuntime, WasmError> {
) -> std::result::Result<WasmtimeRuntime<H>, WasmError>
where
H: HostFunctions,
{
// Create the engine, store and finally the module from the given code.
let mut wasmtime_config = common_config(&config.semantics)?;
if let Some(ref cache_path) = config.cache_path {
@@ -566,7 +583,7 @@ unsafe fn do_create_runtime(
},
};
Ok(WasmtimeRuntime { module: Arc::new(module), snapshot_data, config, host_functions })
Ok(WasmtimeRuntime { module: Arc::new(module), snapshot_data, config, phantom: PhantomData })
}
fn instrument(
@@ -78,7 +78,7 @@ impl RuntimeBuilder {
.expect("failed to create a runtime blob out of test runtime")
};
let rt = crate::create_runtime(
let rt = crate::create_runtime::<HostFunctions>(
blob,
crate::Config {
heap_pages: self.heap_pages,
@@ -98,10 +98,6 @@ impl RuntimeBuilder {
parallel_compilation: true,
},
},
{
use sp_wasm_interface::HostFunctions as _;
HostFunctions::host_functions()
},
)
.expect("cannot create runtime");
@@ -316,9 +312,7 @@ fn test_max_memory_pages() {
#[cfg_attr(build_type = "debug", ignore)]
#[test]
fn test_instances_without_reuse_are_not_leaked() {
use sp_wasm_interface::HostFunctions;
let runtime = crate::create_runtime(
let runtime = crate::create_runtime::<HostFunctions>(
RuntimeBlob::uncompress_if_needed(&wasm_binary_unwrap()[..]).unwrap(),
crate::Config {
heap_pages: 2048,
@@ -332,7 +326,6 @@ fn test_instances_without_reuse_are_not_leaked() {
parallel_compilation: true,
},
},
sp_io::SubstrateHostFunctions::host_functions(),
)
.unwrap();