use crate::{
error::{Error, Result},
wasm_runtime::{RuntimeCache, WasmExecutionMethod},
RuntimeVersionOf,
};
use std::{
marker::PhantomData,
panic::{AssertUnwindSafe, UnwindSafe},
path::PathBuf,
sync::Arc,
};
use codec::Encode;
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{
AllocationStats, HeapAllocStrategy, WasmInstance, WasmModule, DEFAULT_HEAP_ALLOC_STRATEGY,
},
};
use sp_core::traits::{CallContext, CodeExecutor, Externalities, RuntimeCode};
use sp_version::{GetNativeVersion, NativeVersion, RuntimeVersion};
use sp_wasm_interface::{ExtendedHostFunctions, HostFunctions};
pub fn with_externalities_safe<F, U>(ext: &mut dyn Externalities, f: F) -> Result<U>
where
F: UnwindSafe + FnOnce() -> U,
{
sp_externalities::set_and_run_with_externalities(ext, move || {
let _guard = sp_panic_handler::AbortGuard::force_unwind();
std::panic::catch_unwind(f).map_err(|e| {
if let Some(err) = e.downcast_ref::<String>() {
Error::RuntimePanicked(err.clone())
} else if let Some(err) = e.downcast_ref::<&'static str>() {
Error::RuntimePanicked(err.to_string())
} else {
Error::RuntimePanicked("Unknown panic".into())
}
})
})
}
pub trait NativeExecutionDispatch: Send + Sync {
type ExtendHostFunctions: HostFunctions;
fn dispatch(method: &str, data: &[u8]) -> Option<Vec<u8>>;
fn native_version() -> NativeVersion;
}
fn unwrap_heap_pages(pages: Option<HeapAllocStrategy>) -> HeapAllocStrategy {
pages.unwrap_or_else(|| DEFAULT_HEAP_ALLOC_STRATEGY)
}
pub struct WasmExecutorBuilder<H = sp_io::SubstrateHostFunctions> {
_phantom: PhantomData<H>,
method: WasmExecutionMethod,
onchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
offchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
ignore_onchain_heap_pages: bool,
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
allow_missing_host_functions: bool,
runtime_cache_size: u8,
}
impl<H> WasmExecutorBuilder<H> {
pub fn new() -> Self {
Self {
_phantom: PhantomData,
method: WasmExecutionMethod::default(),
onchain_heap_alloc_strategy: None,
offchain_heap_alloc_strategy: None,
ignore_onchain_heap_pages: false,
max_runtime_instances: 2,
runtime_cache_size: 4,
allow_missing_host_functions: false,
cache_path: None,
}
}
pub fn with_execution_method(mut self, method: WasmExecutionMethod) -> Self {
self.method = method;
self
}
pub fn with_onchain_heap_alloc_strategy(
mut self,
heap_alloc_strategy: HeapAllocStrategy,
) -> Self {
self.onchain_heap_alloc_strategy = Some(heap_alloc_strategy);
self
}
pub fn with_offchain_heap_alloc_strategy(
mut self,
heap_alloc_strategy: HeapAllocStrategy,
) -> Self {
self.offchain_heap_alloc_strategy = Some(heap_alloc_strategy);
self
}
pub fn with_ignore_onchain_heap_pages(mut self, ignore_onchain_heap_pages: bool) -> Self {
self.ignore_onchain_heap_pages = ignore_onchain_heap_pages;
self
}
pub fn with_max_runtime_instances(mut self, instances: usize) -> Self {
self.max_runtime_instances = instances;
self
}
pub fn with_cache_path(mut self, cache_path: impl Into<PathBuf>) -> Self {
self.cache_path = Some(cache_path.into());
self
}
pub fn with_allow_missing_host_functions(mut self, allow: bool) -> Self {
self.allow_missing_host_functions = allow;
self
}
pub fn with_runtime_cache_size(mut self, runtime_cache_size: u8) -> Self {
self.runtime_cache_size = runtime_cache_size;
self
}
pub fn build(self) -> WasmExecutor<H> {
WasmExecutor {
method: self.method,
default_offchain_heap_alloc_strategy: unwrap_heap_pages(
self.offchain_heap_alloc_strategy,
),
default_onchain_heap_alloc_strategy: unwrap_heap_pages(
self.onchain_heap_alloc_strategy,
),
ignore_onchain_heap_pages: self.ignore_onchain_heap_pages,
cache: Arc::new(RuntimeCache::new(
self.max_runtime_instances,
self.cache_path.clone(),
self.runtime_cache_size,
)),
cache_path: self.cache_path,
allow_missing_host_functions: self.allow_missing_host_functions,
phantom: PhantomData,
}
}
}
pub struct WasmExecutor<H = sp_io::SubstrateHostFunctions> {
method: WasmExecutionMethod,
default_onchain_heap_alloc_strategy: HeapAllocStrategy,
default_offchain_heap_alloc_strategy: HeapAllocStrategy,
ignore_onchain_heap_pages: bool,
cache: Arc<RuntimeCache>,
cache_path: Option<PathBuf>,
allow_missing_host_functions: bool,
phantom: PhantomData<H>,
}
impl<H> Clone for WasmExecutor<H> {
fn clone(&self) -> Self {
Self {
method: self.method,
default_onchain_heap_alloc_strategy: self.default_onchain_heap_alloc_strategy,
default_offchain_heap_alloc_strategy: self.default_offchain_heap_alloc_strategy,
ignore_onchain_heap_pages: self.ignore_onchain_heap_pages,
cache: self.cache.clone(),
cache_path: self.cache_path.clone(),
allow_missing_host_functions: self.allow_missing_host_functions,
phantom: self.phantom,
}
}
}
impl Default for WasmExecutor<sp_io::SubstrateHostFunctions> {
fn default() -> Self {
WasmExecutorBuilder::new().build()
}
}
impl<H> WasmExecutor<H> {
#[deprecated(note = "use `Self::builder` method instead of it")]
pub fn new(
method: WasmExecutionMethod,
default_heap_pages: Option<u64>,
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
runtime_cache_size: u8,
) -> Self {
WasmExecutor {
method,
default_onchain_heap_alloc_strategy: unwrap_heap_pages(
default_heap_pages.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ }),
),
default_offchain_heap_alloc_strategy: unwrap_heap_pages(
default_heap_pages.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ }),
),
ignore_onchain_heap_pages: false,
cache: Arc::new(RuntimeCache::new(
max_runtime_instances,
cache_path.clone(),
runtime_cache_size,
)),
cache_path,
allow_missing_host_functions: false,
phantom: PhantomData,
}
}
pub fn builder() -> WasmExecutorBuilder<H> {
WasmExecutorBuilder::new()
}
#[deprecated(note = "use `Self::builder` method instead of it")]
pub fn allow_missing_host_functions(&mut self, allow_missing_host_functions: bool) {
self.allow_missing_host_functions = allow_missing_host_functions
}
}
impl<H> WasmExecutor<H>
where
H: HostFunctions,
{
pub fn with_instance<R, F>(
&self,
runtime_code: &RuntimeCode,
ext: &mut dyn Externalities,
heap_alloc_strategy: HeapAllocStrategy,
f: F,
) -> Result<R>
where
F: FnOnce(
AssertUnwindSafe<&dyn WasmModule>,
AssertUnwindSafe<&mut dyn WasmInstance>,
Option<&RuntimeVersion>,
AssertUnwindSafe<&mut dyn Externalities>,
) -> Result<Result<R>>,
{
match self.cache.with_instance::<H, _, _>(
runtime_code,
ext,
self.method,
heap_alloc_strategy,
self.allow_missing_host_functions,
|module, instance, version, ext| {
let module = AssertUnwindSafe(module);
let instance = AssertUnwindSafe(instance);
let ext = AssertUnwindSafe(ext);
f(module, instance, version, ext)
},
)? {
Ok(r) => r,
Err(e) => Err(e),
}
}
#[doc(hidden)] pub fn uncached_call(
&self,
runtime_blob: RuntimeBlob,
ext: &mut dyn Externalities,
allow_missing_host_functions: bool,
export_name: &str,
call_data: &[u8],
) -> std::result::Result<Vec<u8>, Error> {
self.uncached_call_impl(
runtime_blob,
ext,
allow_missing_host_functions,
export_name,
call_data,
&mut None,
)
}
#[doc(hidden)] pub fn uncached_call_with_allocation_stats(
&self,
runtime_blob: RuntimeBlob,
ext: &mut dyn Externalities,
allow_missing_host_functions: bool,
export_name: &str,
call_data: &[u8],
) -> (std::result::Result<Vec<u8>, Error>, Option<AllocationStats>) {
let mut allocation_stats = None;
let result = self.uncached_call_impl(
runtime_blob,
ext,
allow_missing_host_functions,
export_name,
call_data,
&mut allocation_stats,
);
(result, allocation_stats)
}
fn uncached_call_impl(
&self,
runtime_blob: RuntimeBlob,
ext: &mut dyn Externalities,
allow_missing_host_functions: bool,
export_name: &str,
call_data: &[u8],
allocation_stats_out: &mut Option<AllocationStats>,
) -> std::result::Result<Vec<u8>, Error> {
let module = crate::wasm_runtime::create_wasm_runtime_with_code::<H>(
self.method,
self.default_onchain_heap_alloc_strategy,
runtime_blob,
allow_missing_host_functions,
self.cache_path.as_deref(),
)
.map_err(|e| format!("Failed to create module: {}", e))?;
let instance =
module.new_instance().map_err(|e| format!("Failed to create instance: {}", e))?;
let mut instance = AssertUnwindSafe(instance);
let mut ext = AssertUnwindSafe(ext);
let mut allocation_stats_out = AssertUnwindSafe(allocation_stats_out);
with_externalities_safe(&mut **ext, move || {
let (result, allocation_stats) =
instance.call_with_allocation_stats(export_name.into(), call_data);
**allocation_stats_out = allocation_stats;
result
})
.and_then(|r| r)
}
}
impl<H> sp_core::traits::ReadRuntimeVersion for WasmExecutor<H>
where
H: HostFunctions,
{
fn read_runtime_version(
&self,
wasm_code: &[u8],
ext: &mut dyn Externalities,
) -> std::result::Result<Vec<u8>, String> {
let runtime_blob = RuntimeBlob::uncompress_if_needed(wasm_code)
.map_err(|e| format!("Failed to create runtime blob: {:?}", e))?;
if let Some(version) = crate::wasm_runtime::read_embedded_version(&runtime_blob)
.map_err(|e| format!("Failed to read the static section: {:?}", e))
.map(|v| v.map(|v| v.encode()))?
{
return Ok(version)
}
self.uncached_call(
runtime_blob,
ext,
true,
"Core_version",
&[],
)
.map_err(|e| e.to_string())
}
}
impl<H> CodeExecutor for WasmExecutor<H>
where
H: HostFunctions,
{
type Error = Error;
fn call(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
method: &str,
data: &[u8],
context: CallContext,
) -> (Result<Vec<u8>>, bool) {
tracing::trace!(
target: "executor",
%method,
"Executing function",
);
let on_chain_heap_alloc_strategy = if self.ignore_onchain_heap_pages {
self.default_onchain_heap_alloc_strategy
} else {
runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.default_onchain_heap_alloc_strategy)
};
let heap_alloc_strategy = match context {
CallContext::Offchain => self.default_offchain_heap_alloc_strategy,
CallContext::Onchain => on_chain_heap_alloc_strategy,
};
let result = self.with_instance(
runtime_code,
ext,
heap_alloc_strategy,
|_, mut instance, _on_chain_version, mut ext| {
with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
},
);
(result, false)
}
}
impl<H> RuntimeVersionOf for WasmExecutor<H>
where
H: HostFunctions,
{
fn runtime_version(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
) -> Result<RuntimeVersion> {
let on_chain_heap_pages = if self.ignore_onchain_heap_pages {
self.default_onchain_heap_alloc_strategy
} else {
runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.default_onchain_heap_alloc_strategy)
};
self.with_instance(
runtime_code,
ext,
on_chain_heap_pages,
|_module, _instance, version, _ext| {
Ok(version.cloned().ok_or_else(|| Error::ApiError("Unknown version".into())))
},
)
}
}
#[deprecated(
note = "Native execution will be deprecated, please replace with `WasmExecutor`. Will be removed at end of 2024."
)]
pub struct NativeElseWasmExecutor<D: NativeExecutionDispatch> {
native_version: NativeVersion,
wasm:
WasmExecutor<ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>>,
use_native: bool,
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> NativeElseWasmExecutor<D> {
#[deprecated(note = "use `Self::new_with_wasm_executor` method instead of it")]
pub fn new(
fallback_method: WasmExecutionMethod,
default_heap_pages: Option<u64>,
max_runtime_instances: usize,
runtime_cache_size: u8,
) -> Self {
let heap_pages = default_heap_pages.map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |h| {
HeapAllocStrategy::Static { extra_pages: h as _ }
});
let wasm = WasmExecutor::builder()
.with_execution_method(fallback_method)
.with_onchain_heap_alloc_strategy(heap_pages)
.with_offchain_heap_alloc_strategy(heap_pages)
.with_max_runtime_instances(max_runtime_instances)
.with_runtime_cache_size(runtime_cache_size)
.build();
NativeElseWasmExecutor { native_version: D::native_version(), wasm, use_native: true }
}
pub fn new_with_wasm_executor(
executor: WasmExecutor<
ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>,
>,
) -> Self {
Self { native_version: D::native_version(), wasm: executor, use_native: true }
}
pub fn disable_use_native(&mut self) {
self.use_native = false;
}
#[deprecated(note = "use `Self::new_with_wasm_executor` method instead of it")]
pub fn allow_missing_host_functions(&mut self, allow_missing_host_functions: bool) {
self.wasm.allow_missing_host_functions = allow_missing_host_functions
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> RuntimeVersionOf for NativeElseWasmExecutor<D> {
fn runtime_version(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
) -> Result<RuntimeVersion> {
self.wasm.runtime_version(ext, runtime_code)
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> GetNativeVersion for NativeElseWasmExecutor<D> {
fn native_version(&self) -> &NativeVersion {
&self.native_version
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecutor<D> {
type Error = Error;
fn call(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
method: &str,
data: &[u8],
context: CallContext,
) -> (Result<Vec<u8>>, bool) {
let use_native = self.use_native;
tracing::trace!(
target: "executor",
function = %method,
"Executing function",
);
let on_chain_heap_alloc_strategy = if self.wasm.ignore_onchain_heap_pages {
self.wasm.default_onchain_heap_alloc_strategy
} else {
runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.wasm.default_onchain_heap_alloc_strategy)
};
let heap_alloc_strategy = match context {
CallContext::Offchain => self.wasm.default_offchain_heap_alloc_strategy,
CallContext::Onchain => on_chain_heap_alloc_strategy,
};
let mut used_native = false;
let result = self.wasm.with_instance(
runtime_code,
ext,
heap_alloc_strategy,
|_, mut instance, on_chain_version, mut ext| {
let on_chain_version =
on_chain_version.ok_or_else(|| Error::ApiError("Unknown version".into()))?;
let can_call_with =
on_chain_version.can_call_with(&self.native_version.runtime_version);
if use_native && can_call_with {
tracing::trace!(
target: "executor",
native = %self.native_version.runtime_version,
chain = %on_chain_version,
"Request for native execution succeeded",
);
used_native = true;
Ok(with_externalities_safe(&mut **ext, move || D::dispatch(method, data))?
.ok_or_else(|| Error::MethodNotFound(method.to_owned())))
} else {
if !can_call_with {
tracing::trace!(
target: "executor",
native = %self.native_version.runtime_version,
chain = %on_chain_version,
"Request for native execution failed",
);
}
with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
}
},
);
(result, used_native)
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> Clone for NativeElseWasmExecutor<D> {
fn clone(&self) -> Self {
NativeElseWasmExecutor {
native_version: D::native_version(),
wasm: self.wasm.clone(),
use_native: self.use_native,
}
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> sp_core::traits::ReadRuntimeVersion for NativeElseWasmExecutor<D> {
fn read_runtime_version(
&self,
wasm_code: &[u8],
ext: &mut dyn Externalities,
) -> std::result::Result<Vec<u8>, String> {
self.wasm.read_runtime_version(wasm_code, ext)
}
}
#[cfg(test)]
mod tests {
use super::*;
use sp_runtime_interface::runtime_interface;
#[runtime_interface]
trait MyInterface {
fn say_hello_world(data: &str) {
println!("Hello world from: {}", data);
}
}
pub struct MyExecutorDispatch;
impl NativeExecutionDispatch for MyExecutorDispatch {
type ExtendHostFunctions = (my_interface::HostFunctions, my_interface::HostFunctions);
fn dispatch(method: &str, data: &[u8]) -> Option<Vec<u8>> {
substrate_test_runtime::api::dispatch(method, data)
}
fn native_version() -> NativeVersion {
substrate_test_runtime::native_version()
}
}
#[test]
#[allow(deprecated)]
fn native_executor_registers_custom_interface() {
let executor = NativeElseWasmExecutor::<MyExecutorDispatch>::new_with_wasm_executor(
WasmExecutor::builder().build(),
);
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!(
extract_host_functions(&executor.wasm).iter().filter(|f| f == &function).count(),
2
);
});
my_interface::say_hello_world("hey");
}
}