From cc9cd6ab3ffbb1f055e413eec21980e45d6b3218 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 28 Sep 2026 17:00:23 +0300 Subject: [PATCH 01/26] ExperimentalVMHooksBuilder replace allocation with closure --- vm-executor-experimental/src/we_vm_hooks.rs | 15 ++++++++-- .../src/we_vm_hooks_builder.rs | 29 +++++++++++++------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/vm-executor-experimental/src/we_vm_hooks.rs b/vm-executor-experimental/src/we_vm_hooks.rs index 44716471..21339096 100644 --- a/vm-executor-experimental/src/we_vm_hooks.rs +++ b/vm-executor-experimental/src/we_vm_hooks.rs @@ -36,7 +36,16 @@ where store_mut: &mut store_mut, }; - let mut vm_hooks = data.vm_hooks_builder.create_vm_hooks(&mut instance_state); - - f(&mut *vm_hooks) + // The builder lends the hooks through an `FnMut`, while `f` is an `FnOnce`, + // so it gets moved out on the (single) call. + let mut f = Some(f); + let mut result = None; + + data.vm_hooks_builder + .with_vm_hooks(&mut instance_state, &mut |vm_hooks| { + let f = f.take().expect("VM hooks builder called the closure twice"); + result = Some(f(vm_hooks)); + }); + + result.expect("VM hooks builder did not call the closure") } diff --git a/vm-executor-experimental/src/we_vm_hooks_builder.rs b/vm-executor-experimental/src/we_vm_hooks_builder.rs index 39d4d768..8a5f705f 100644 --- a/vm-executor-experimental/src/we_vm_hooks_builder.rs +++ b/vm-executor-experimental/src/we_vm_hooks_builder.rs @@ -2,21 +2,32 @@ use multiversx_chain_vm_executor::{VMHooks, VMHooksDefault}; use crate::ExperimentalInstanceState; +/// Creates the VM hooks used to service one host function call. +/// +/// The hooks borrow the instance state, which is only reachable for the duration of the +/// call, so they cannot outlive it and cannot be returned to the caller. Rather than +/// heap-allocating a `Box` on every single hook invocation, implementations +/// build the hooks on their own stack frame and lend them to the given closure. pub trait ExperimentalVMHooksBuilder { - fn create_vm_hooks<'b, 'h>( - &'b self, - instance_state_ref: &'h mut ExperimentalInstanceState, - ) -> Box; + /// Builds the VM hooks around the given instance state and passes them to `f`. + /// + /// Implementations must call `f` exactly once. + fn with_vm_hooks( + &self, + instance_state_ref: &mut ExperimentalInstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ); } #[derive(Debug)] pub struct ExperimentalVMHooksBuilderDefault; impl ExperimentalVMHooksBuilder for ExperimentalVMHooksBuilderDefault { - fn create_vm_hooks<'b, 'h>( - &'b self, - _instance_state_ref: &'h mut ExperimentalInstanceState, - ) -> Box { - Box::new(VMHooksDefault) + fn with_vm_hooks( + &self, + _instance_state_ref: &mut ExperimentalInstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ) { + f(&mut VMHooksDefault); } } From b0ef314f9da6bd42bfd4afddb4390147d6421f3f Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 29 Sep 2026 12:07:04 +0300 Subject: [PATCH 02/26] BreakpointValue values match middleware values --- .../src/new_traits/breakpoint_value_new.rs | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/vm-executor/src/new_traits/breakpoint_value_new.rs b/vm-executor/src/new_traits/breakpoint_value_new.rs index 1ca4cbde..798ec0e7 100644 --- a/vm-executor/src/new_traits/breakpoint_value_new.rs +++ b/vm-executor/src/new_traits/breakpoint_value_new.rs @@ -1,15 +1,24 @@ use crate::BreakpointValueLegacy; +/// Represents the reason the executor itself interrupted execution. +/// +/// Unlike [`BreakpointValueLegacy`], this only covers the interruptions raised by the +/// executor's own middlewares. Everything the high-level VM signals travels as a +/// [`crate::VMHooksEarlyExit`] instead. +/// +/// The discriminants are the values the metering and opcode control middlewares write into +/// the instance's breakpoint global, so that [`BreakpointValue::as_u64`] and +/// [`TryFrom`] are inverses of each other. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BreakpointValue { /// Lack of a breakpoint None = 0, /// Stopping due to gas being exhausted. - OutOfGas = 1, + OutOfGas = 4, /// Stopping due to over-allocation of WASM memory. - MemoryLimit = 2, + MemoryLimit = 5, } impl BreakpointValue { @@ -40,3 +49,30 @@ impl TryFrom for BreakpointValue { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn breakpoint_value_round_trip() { + for value in [ + BreakpointValue::None, + BreakpointValue::OutOfGas, + BreakpointValue::MemoryLimit, + ] { + assert_eq!(BreakpointValue::try_from(value.as_u64()).ok(), Some(value)); + } + } + + #[test] + fn breakpoint_value_agrees_with_legacy() { + for value in [ + BreakpointValue::None, + BreakpointValue::OutOfGas, + BreakpointValue::MemoryLimit, + ] { + assert_eq!(value.as_u64(), value.to_legacy().as_u64()); + } + } +} From 2642673aec5fc6e231ff2c6c39fc142b7f58db3a Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 29 Sep 2026 12:13:08 +0300 Subject: [PATCH 03/26] ffi hardening: instance memory, panic-free new traits - Instance::get_imported_function_names, needed to validate contract imports - InstanceState gains memory_length/memory_grow; load & store are strict, with memory_load_clamped preserving the Go VM's truncating load - bounds rules shared via checked_load_range/checked_store_range + MemoryError, one implementation for all backends; wasmer 2.2 was not checking at all - panics on Result-returning paths turned into errors, so nothing aborts across the FFI boundary --- vm-executor-experimental/Cargo.toml | 2 +- vm-executor-experimental/src/we_error.rs | 9 + vm-executor-experimental/src/we_executor.rs | 25 +-- vm-executor-experimental/src/we_instance.rs | 21 +- .../src/we_instance_state.rs | 52 ++++- vm-executor-wasmer/Cargo.toml | 1 + .../src/new_traits/wasmer_error.rs | 3 + .../src/new_traits/wasmer_prod_executor.rs | 16 +- .../src/new_traits/wasmer_prod_instance.rs | 4 + .../new_traits/wasmer_prod_instance_state.rs | 25 ++- vm-executor-wasmer/src/wasmer_instance.rs | 38 ++-- vm-executor/src/new_traits.rs | 2 + vm-executor/src/new_traits/instance_new.rs | 7 + vm-executor/src/new_traits/instance_state.rs | 65 +++++- vm-executor/src/new_traits/memory.rs | 212 ++++++++++++++++++ 15 files changed, 414 insertions(+), 68 deletions(-) create mode 100644 vm-executor/src/new_traits/memory.rs diff --git a/vm-executor-experimental/Cargo.toml b/vm-executor-experimental/Cargo.toml index ead272c9..3fd99474 100644 --- a/vm-executor-experimental/Cargo.toml +++ b/vm-executor-experimental/Cargo.toml @@ -29,5 +29,5 @@ wasmer-types = "6.1" log = "0.4" loupe = "0.1.3" -rc-new-cyclic-fallible = "1.0.0" +rc-new-cyclic-fallible = "1.1" thiserror = "2.0" diff --git a/vm-executor-experimental/src/we_error.rs b/vm-executor-experimental/src/we_error.rs index a8782083..35cc33a5 100644 --- a/vm-executor-experimental/src/we_error.rs +++ b/vm-executor-experimental/src/we_error.rs @@ -19,4 +19,13 @@ pub enum ExperimentalError { #[error("instance call error: {0}")] InstanceCall(wasmer::RuntimeError), + + #[error("instance memory access error: {0}")] + MemoryAccess(String), + + #[error("creating an instance from cache is not supported")] + InstanceFromCacheUnsupported, + + #[error("resetting an instance is not supported")] + ResetUnsupported, } diff --git a/vm-executor-experimental/src/we_executor.rs b/vm-executor-experimental/src/we_executor.rs index dbec4db5..a81c3289 100644 --- a/vm-executor-experimental/src/we_executor.rs +++ b/vm-executor-experimental/src/we_executor.rs @@ -1,4 +1,4 @@ -use crate::{ExperimentalInstance, ExperimentalVMHooksBuilder}; +use crate::{ExperimentalError, ExperimentalInstance, ExperimentalVMHooksBuilder}; use multiversx_chain_vm_executor::{ CompilationOptions, Executor, ExecutorError, Instance, OpcodeConfig, check_missing_wasm, }; @@ -30,16 +30,15 @@ impl ExperimentalExecutor { &self, wasm_bytes: &[u8], compilation_options: &CompilationOptions, - ) -> Box { - Box::new( - ExperimentalInstance::try_new_instance( - self.runtime_ref.vm_hooks_builder(), - self.runtime_ref.opcode_config(), - wasm_bytes, - compilation_options, - ) - .expect("instance init failed"), - ) + ) -> Result, ExecutorError> { + let instance = ExperimentalInstance::try_new_instance( + self.runtime_ref.vm_hooks_builder(), + self.runtime_ref.opcode_config(), + wasm_bytes, + compilation_options, + )?; + + Ok(Box::new(instance)) } } @@ -51,7 +50,7 @@ impl Executor for ExperimentalExecutor { ) -> Result, ExecutorError> { check_missing_wasm(wasm_bytes)?; - Ok(self.new_instance_from_bytes(wasm_bytes, compilation_options)) + self.new_instance_from_bytes(wasm_bytes, compilation_options) } fn new_instance_from_cache( @@ -59,6 +58,6 @@ impl Executor for ExperimentalExecutor { _cache_bytes: &[u8], _compilation_options: &CompilationOptions, ) -> Result, ExecutorError> { - panic!("WasmerProdExecutor new_instance_from_cache not supported") + Err(ExperimentalError::InstanceFromCacheUnsupported.into()) } } diff --git a/vm-executor-experimental/src/we_instance.rs b/vm-executor-experimental/src/we_instance.rs index 00a4b38c..4fe35430 100644 --- a/vm-executor-experimental/src/we_instance.rs +++ b/vm-executor-experimental/src/we_instance.rs @@ -12,7 +12,7 @@ use multiversx_chain_vm_executor::{ use rc_new_cyclic_fallible::rc_new_cyclic_fallible; use std::{rc::Rc, rc::Weak, sync::Arc}; use wasmer::sys::{CompilerConfig, Singlepass}; -use wasmer::{Extern, Module, Pages, Store}; +use wasmer::{Extern, ExternType, Module, Pages, Store}; const MAX_MEMORY_PAGES_ALLOWED: Pages = Pages(20); @@ -221,9 +221,10 @@ impl Instance for ExperimentalInstance { match runtime_error.downcast::() { Ok(vm_hooks_error) => InstanceCallResult::VMHooksEarlyExit(vm_hooks_error), Err(other_error) => { - let breakpoint = self - .get_breakpoint_value() - .expect("error retrieving instance breakpoint value"); + let breakpoint = match self.get_breakpoint_value() { + Ok(breakpoint) => breakpoint, + Err(err) => return InstanceCallResult::RuntimeError(err), + }; if breakpoint != BreakpointValue::None { InstanceCallResult::Breakpoint(breakpoint) } else { @@ -273,12 +274,22 @@ impl Instance for ExperimentalInstance { .collect() } + fn get_imported_function_names(&self) -> Vec { + self.inner + .wasmer_instance + .module() + .imports() + .filter(|import| matches!(import.ty(), ExternType::Function(_))) + .map(|import| import.name().to_string()) + .collect() + } + fn get_points_used(&mut self) -> Result { get_points_used(&self.inner.wasmer_instance, &mut self.wasmer_store) } fn reset(&self) -> Result<(), ExecutorError> { - panic!("ExperimentalInstance reset not supported") + Err(ExperimentalError::ResetUnsupported.into()) } fn cache(&self) -> Result, ExecutorError> { diff --git a/vm-executor-experimental/src/we_instance_state.rs b/vm-executor-experimental/src/we_instance_state.rs index 40dab7e8..3adc27be 100644 --- a/vm-executor-experimental/src/we_instance_state.rs +++ b/vm-executor-experimental/src/we_instance_state.rs @@ -1,11 +1,11 @@ use crate::middlewares::{get_points_used, set_points_used}; use crate::{ExperimentalError, ExperimentalInstanceInner}; use multiversx_chain_vm_executor::{ExecutorError, InstanceState}; -use multiversx_chain_vm_executor::{MemLength, MemPtr}; +use multiversx_chain_vm_executor::{MemLength, MemPtr, checked_load_range, checked_store_range}; use std::rc::Rc; use std::rc::Weak; -use wasmer::{MemoryView, StoreMut}; +use wasmer::{MemoryView, Pages, StoreMut}; pub struct ExperimentalInstanceState<'s> { pub wasmer_inner: Weak, @@ -13,9 +13,20 @@ pub struct ExperimentalInstanceState<'s> { } impl ExperimentalInstanceState<'_> { - fn get_memory_view(&self) -> MemoryView<'_> { - let wasmer_inner = self.wasmer_inner.upgrade().unwrap(); - wasmer_inner.get_memory_ref().unwrap().view(&self.store_mut) + fn get_memory_view(&self) -> Result, ExecutorError> { + let wasmer_inner = self.get_wasmer_inner()?; + let memory = wasmer_inner + .get_memory_ref() + .map_err(ExperimentalError::MemoryAccess)?; + + // The view borrows the store, not `wasmer_inner`, which is why it outlives the `Rc` + // obtained above. The memory itself is owned by the instance, kept alive by the + // caller for the duration of the host call. + Ok(memory.view(&self.store_mut)) + } + + fn memory_size(&self) -> Result { + Ok(self.get_memory_view()?.data_size()) } fn get_wasmer_inner(&self) -> Result, ExecutorError> { @@ -38,9 +49,24 @@ impl InstanceState for &'_ mut ExperimentalInstanceState<'_> { Ok(()) } + fn memory_length(&self) -> Result { + self.memory_size() + } + + fn memory_grow(&mut self, by_num_pages: u32) -> Result { + let wasmer_inner = self.get_wasmer_inner()?; + let memory = wasmer_inner + .get_memory_ref() + .map_err(ExperimentalError::MemoryAccess)?; + let pages = memory.grow(&mut self.store_mut, Pages(by_num_pages))?; + Ok(pages.0) + } + fn memory_load_to_slice(&self, mem_ptr: MemPtr, dest: &mut [u8]) -> Result<(), ExecutorError> { - let memory_view = self.get_memory_view(); - memory_view.read(mem_ptr as u64, dest)?; + let memory_view = self.get_memory_view()?; + let (start, _) = + checked_load_range(mem_ptr, dest.len() as MemLength, memory_view.data_size())?; + memory_view.read(start as u64, dest)?; Ok(()) } @@ -50,10 +76,11 @@ impl InstanceState for &'_ mut ExperimentalInstanceState<'_> { mem_ptr: MemPtr, mem_length: MemLength, ) -> Result, ExecutorError> { - let memory_view = self.get_memory_view(); - let len = mem_length as usize; + let memory_view = self.get_memory_view()?; + let (start, end) = checked_load_range(mem_ptr, mem_length, memory_view.data_size())?; + let len = end - start; let mut result = Vec::with_capacity(len); - memory_view.read_uninit(mem_ptr as u64, result.spare_capacity_mut())?; + memory_view.read_uninit(start as u64, result.spare_capacity_mut())?; unsafe { result.set_len(len); } @@ -61,8 +88,9 @@ impl InstanceState for &'_ mut ExperimentalInstanceState<'_> { } fn memory_store(&self, offset: MemPtr, data: &[u8]) -> Result<(), ExecutorError> { - let memory_view = self.get_memory_view(); - memory_view.write(offset as u64, data)?; + let memory_view = self.get_memory_view()?; + let (start, _) = checked_store_range(offset, data.len(), memory_view.data_size())?; + memory_view.write(start as u64, data)?; Ok(()) } } diff --git a/vm-executor-wasmer/Cargo.toml b/vm-executor-wasmer/Cargo.toml index e54502ba..d823b867 100644 --- a/vm-executor-wasmer/Cargo.toml +++ b/vm-executor-wasmer/Cargo.toml @@ -35,4 +35,5 @@ wasmer-types = { git = "https://github.com/multiversx/wasmer", rev = "73f2111abf log = { version = "0.4", features = ["std"] } loupe = "0.1.3" +rc-new-cyclic-fallible = "1.1" thiserror = "2.0" diff --git a/vm-executor-wasmer/src/new_traits/wasmer_error.rs b/vm-executor-wasmer/src/new_traits/wasmer_error.rs index 464266fb..2da3143c 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_error.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_error.rs @@ -19,4 +19,7 @@ pub enum WasmerExecutorError { #[error("instance call error: {0}")] WrappedInstance(String), + + #[error("creating an instance from cache is not supported")] + InstanceFromCacheUnsupported, } diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs index 052fef04..2160c891 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs @@ -4,13 +4,14 @@ use crate::WasmerInstance; use crate::executor_interface::{ CompilationOptions, Executor, ExecutorError, Instance, VMHooksLegacy, check_missing_wasm, }; +use rc_new_cyclic_fallible::rc_new_cyclic_fallible; use std::{ fmt, rc::Rc, sync::{Arc, Mutex}, }; -use super::{WasmerProdInstance, WasmerProdInstanceState}; +use super::{WasmerExecutorError, WasmerProdInstance, WasmerProdInstanceState}; pub trait WasmerProdRuntimeRef: Send + Sync { fn vm_hooks(&self, instance_state: WasmerProdInstanceState) -> Box; @@ -38,8 +39,8 @@ impl WasmerProdExecutor { &self, wasm_bytes: &[u8], compilation_options: &CompilationOptions, - ) -> Box { - let inner_instance_ref = Rc::new_cyclic(|weak| { + ) -> Result, ExecutorError> { + let inner_instance_ref = rc_new_cyclic_fallible(|weak| { let instance_state = WasmerProdInstanceState::new(weak.clone()); let vm_hooks = self.runtime_ref.vm_hooks(instance_state); @@ -49,12 +50,11 @@ impl WasmerProdExecutor { wasm_bytes, &compilation_options.to_legacy(), ) - .expect("instance init failed") - }); + })?; let wasmer_instance_ref = WasmerProdInstance::new(inner_instance_ref); - Box::new(wasmer_instance_ref) + Ok(Box::new(wasmer_instance_ref)) } } @@ -66,7 +66,7 @@ impl Executor for WasmerProdExecutor { ) -> Result, ExecutorError> { check_missing_wasm(wasm_bytes)?; - Ok(self.new_instance_from_bytes(wasm_bytes, compilation_options)) + self.new_instance_from_bytes(wasm_bytes, compilation_options) } fn new_instance_from_cache( @@ -74,6 +74,6 @@ impl Executor for WasmerProdExecutor { _cache_bytes: &[u8], _compilation_options: &CompilationOptions, ) -> Result, ExecutorError> { - panic!("WasmerProdExecutor new_instance_from_cache not supported") + Err(WasmerExecutorError::InstanceFromCacheUnsupported.into()) } } diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs index e8526f1a..0ab4397a 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs @@ -76,6 +76,10 @@ impl Instance for WasmerProdInstance { self.inner_instance_ref.get_exported_function_names() } + fn get_imported_function_names(&self) -> Vec { + self.inner_instance_ref.get_imported_function_names() + } + fn get_points_used(&mut self) -> Result { self.inner_instance_ref .get_points_used() diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs index 50a3d922..412a9d65 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs @@ -24,15 +24,18 @@ impl WasmerProdInstanceState { .map_or_else(|| Err(WasmerExecutorError::BadInstancePointer.into()), Ok) } - pub fn set_breakpoint_value_legacy(&self, value: BreakpointValueLegacy) { - self.instance_rc() - .unwrap() + pub fn set_breakpoint_value_legacy( + &self, + value: BreakpointValueLegacy, + ) -> Result<(), ExecutorError> { + self.instance_rc()? .set_breakpoint_value(value) - .expect("set_breakpoint_value_legacy globals error"); + .map_err(|err| WasmerExecutorError::WrappedInstance(err).into()) } - pub fn set_early_exit(&self, early_exit: VMHooksEarlyExit) { - self.instance_rc().unwrap().set_early_exit(early_exit); + pub fn set_early_exit(&self, early_exit: VMHooksEarlyExit) -> Result<(), ExecutorError> { + self.instance_rc()?.set_early_exit(early_exit); + Ok(()) } } @@ -49,6 +52,16 @@ impl InstanceState for WasmerProdInstanceState { .map_err(|err| WasmerExecutorError::SetPointsUsed(err).into()) } + fn memory_length(&self) -> Result { + self.instance_rc()? + .memory_length() + .map_err(|err| WasmerExecutorError::WrappedInstance(err).into()) + } + + fn memory_grow(&mut self, by_num_pages: u32) -> Result { + self.instance_rc()?.memory_grow(by_num_pages) + } + fn memory_load_to_slice(&self, mem_ptr: MemPtr, dest: &mut [u8]) -> Result<(), ExecutorError> { let instance_rc = self.instance_rc()?; let slice = instance_rc.memory_load(mem_ptr, dest.len() as MemLength)?; diff --git a/vm-executor-wasmer/src/wasmer_instance.rs b/vm-executor-wasmer/src/wasmer_instance.rs index 6ea20330..3eb0b54a 100644 --- a/vm-executor-wasmer/src/wasmer_instance.rs +++ b/vm-executor-wasmer/src/wasmer_instance.rs @@ -1,6 +1,7 @@ use crate::executor_interface::{ BreakpointValueLegacy, CompilationOptionsLegacy, ExecutorError, InstanceLegacy, MemLength, - MemPtr, OpcodeConfig, ServiceError, VMHooksEarlyExit, VMHooksLegacy, + MemPtr, OpcodeConfig, ServiceError, VMHooksEarlyExit, VMHooksLegacy, checked_load_range, + checked_store_range, }; use crate::wasmer_opcode_trace::OpcodeTracer; use crate::wasmer_protected_globals::ProtectedGlobals; @@ -136,6 +137,15 @@ impl WasmerInstance { pub fn take_early_exit(&self) -> Option { self.early_exit_cell.take() } + + pub fn get_imported_function_names(&self) -> Vec { + self.wasmer_instance + .module() + .imports() + .filter(|import| matches!(import.ty(), ExternType::Function(_))) + .map(|import| import.name().to_string()) + .collect() + } } fn get_memories(wasmer_instance: &wasmer::Instance) -> Vec<(&String, &wasmer::Memory)> { @@ -317,28 +327,18 @@ impl InstanceLegacy for WasmerInstance { } fn memory_load(&self, mem_ptr: MemPtr, mem_length: MemLength) -> Result<&[u8], ExecutorError> { - let result = self.get_memory_ref(); - match result { - Ok(memory) => unsafe { - let mem_data = memory.data_unchecked(); - let start = mem_ptr as usize; - let end = (mem_ptr + mem_length) as usize; - Ok(&mem_data[start..end]) - }, - Err(err) => Err(err.into()), - } + let memory = self.get_memory_ref()?; + let (start, end) = checked_load_range(mem_ptr, mem_length, memory.data_size())?; + unsafe { Ok(&memory.data_unchecked()[start..end]) } } fn memory_store(&self, mem_ptr: MemPtr, data: &[u8]) -> Result<(), ExecutorError> { - let result = self.get_memory_ref(); - match result { - Ok(memory) => unsafe { - let mem_data = memory.data_unchecked_mut(); - mem_data[mem_ptr as usize..mem_ptr as usize + data.len()].copy_from_slice(data); - Ok(()) - }, - Err(err) => Err(err.into()), + let memory = self.get_memory_ref()?; + let (start, end) = checked_store_range(mem_ptr, data.len(), memory.data_size())?; + unsafe { + memory.data_unchecked_mut()[start..end].copy_from_slice(data); } + Ok(()) } fn memory_grow(&self, by_num_pages: u32) -> Result { diff --git a/vm-executor/src/new_traits.rs b/vm-executor/src/new_traits.rs index 484ed0b0..911ca391 100644 --- a/vm-executor/src/new_traits.rs +++ b/vm-executor/src/new_traits.rs @@ -3,6 +3,7 @@ mod compilation_options_new; mod executor_new; mod instance_new; mod instance_state; +mod memory; mod vm_hooks_early_exit; mod vm_hooks_legacy_adapter; mod vm_hooks_new; @@ -12,6 +13,7 @@ pub use compilation_options_new::CompilationOptions; pub use executor_new::Executor; pub use instance_new::{Instance, InstanceCallResult}; pub use instance_state::InstanceState; +pub use memory::{MemoryError, checked_load_range, checked_store_range}; pub use vm_hooks_early_exit::VMHooksEarlyExit; pub use vm_hooks_legacy_adapter::{VMHooksLegacyAdapter, VMHooksSetEarlyExit}; pub use vm_hooks_new::{VMHooks, VMHooksDefault}; diff --git a/vm-executor/src/new_traits/instance_new.rs b/vm-executor/src/new_traits/instance_new.rs index db4dd6aa..00f2193e 100644 --- a/vm-executor/src/new_traits/instance_new.rs +++ b/vm-executor/src/new_traits/instance_new.rs @@ -22,6 +22,13 @@ pub trait Instance { /// Required to be able to extract all SC endpoint names. fn get_exported_function_names(&self) -> Vec; + /// Returns the names of all the host functions (VM hooks) the contract imports. + /// + /// Needed to validate a contract against the set of hooks it is allowed to use. All the + /// names are returned at once, rather than offering a per-name query, because the + /// callers check the module against whole sets of names. + fn get_imported_function_names(&self) -> Vec; + /// Returns the number of points(gas) used by the given instance. fn get_points_used(&mut self) -> Result; diff --git a/vm-executor/src/new_traits/instance_state.rs b/vm-executor/src/new_traits/instance_state.rs index acb15dcb..1b120252 100644 --- a/vm-executor/src/new_traits/instance_state.rs +++ b/vm-executor/src/new_traits/instance_state.rs @@ -1,6 +1,12 @@ -use crate::{ExecutorError, MemLength, MemPtr}; +use crate::{ExecutorError, MemLength, MemPtr, MemoryError}; /// The interface through which VM hooks update the instance state. +/// +/// All accesses are validated by the executor, which is the only side that knows the real +/// extent of the instance memory. The load and store methods are **strict**: a range that +/// does not fit entirely inside the memory is rejected with a [`MemoryError`]. The one +/// exception is [`InstanceState::memory_load_clamped`], which reproduces the Go VM's +/// historical truncating behaviour; see its documentation. pub trait InstanceState { /// Returns the number of points(gas) used by the given instance. fn get_points_used(&mut self) -> Result; @@ -8,16 +14,67 @@ pub trait InstanceState { /// Sets the number of points(gas) for the given instance. fn set_points_used(&mut self, points: u64) -> Result<(), ExecutorError>; - /// Copies data to new owned buffer. + /// Gets the size in bytes of the instance memory. + fn memory_length(&self) -> Result; + + /// Grows the instance memory by the given number of pages (of 64Kb each). + /// + /// Returns the number of pages the memory had before growing. + fn memory_grow(&mut self, by_num_pages: u32) -> Result; + + /// Loads data from instance memory into the given slice. + /// + /// The requested range must fit entirely inside the memory. fn memory_load_to_slice(&self, mem_ptr: MemPtr, dest: &mut [u8]) -> Result<(), ExecutorError>; - /// Copies data to new owned buffer. + /// Copies data from instance memory to a new owned buffer. + /// + /// The requested range must fit entirely inside the memory. fn memory_load_owned( &self, mem_ptr: MemPtr, mem_length: MemLength, ) -> Result, ExecutorError>; - /// Loads data to given slice. In certain cases + /// Writes to instance memory. + /// + /// The requested range must fit entirely inside the memory. fn memory_store(&self, mem_ptr: MemPtr, data: &[u8]) -> Result<(), ExecutorError>; + + /// Copies data from instance memory to a new owned buffer, truncating the range at the + /// end of the memory instead of rejecting it. + /// + /// This reproduces, deliberately, the behaviour the Go VM has always had: a load whose + /// offset is valid but whose end lies past the end of the memory yields the bytes that + /// are available, and no error. Contracts have observed this, so the executor has to + /// keep offering it, distinct from the strict [`InstanceState::memory_load_owned`]. + /// + /// An offset outside the memory, or a negative length, is still an error. + fn memory_load_clamped( + &self, + mem_ptr: MemPtr, + mem_length: MemLength, + ) -> Result, ExecutorError> { + if mem_length == 0 { + return Ok(Vec::new()); + } + + let memory_length = self.memory_length()?; + + // Checked in this order to match the Go VM: an out-of-range offset takes + // precedence over a negative length. + if mem_ptr < 0 || mem_ptr as u64 > memory_length { + return Err(MemoryError::BadBounds.into()); + } + if mem_length < 0 { + return Err(MemoryError::NegativeLength.into()); + } + + // Computed as a minimum rather than as `mem_ptr + mem_length`, which can overflow + // for a length a contract is free to choose. + let available = memory_length - mem_ptr as u64; + let to_load = std::cmp::min(mem_length as u64, available); + + self.memory_load_owned(mem_ptr, to_load as MemLength) + } } diff --git a/vm-executor/src/new_traits/memory.rs b/vm-executor/src/new_traits/memory.rs new file mode 100644 index 00000000..29ac0c1a --- /dev/null +++ b/vm-executor/src/new_traits/memory.rs @@ -0,0 +1,212 @@ +use std::{error::Error, fmt}; + +use crate::{MemLength, MemPtr}; + +/// The distinct ways an access to instance memory can fail. +/// +/// The variants are the error cases the high-level VMs tell apart, so that a host can map +/// them onto its own error values by matching, rather than by parsing a message. The +/// messages match the ones the Go VM produces for the same conditions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemoryError { + /// The requested offset lies outside the instance memory. + BadBounds, + + /// The requested offset lies before the start of the instance memory. + BadBoundsLower, + + /// The requested range ends past the end of the instance memory. + BadBoundsUpper, + + /// The requested length is negative. + NegativeLength, +} + +impl MemoryError { + pub fn message(self) -> &'static str { + match self { + MemoryError::BadBounds => "bad bounds", + MemoryError::BadBoundsLower => "bad bounds (lower)", + MemoryError::BadBoundsUpper => "bad bounds (upper)", + MemoryError::NegativeLength => "negative length", + } + } +} + +impl fmt::Display for MemoryError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.message()) + } +} + +impl Error for MemoryError {} + +/// Validates a range for a **strict** load, returning it as a `start..end` byte range. +/// +/// The offset and length checks, and the errors they produce, mirror the Go VM's +/// `MemLoadFromMemory`. Only the end-of-range check differs: where the Go VM truncates the +/// range, this rejects it. Loads that need the truncating behaviour go through +/// [`crate::InstanceState::memory_load_clamped`], which is built on top of this. +/// +/// A zero length yields an empty range, without validating the offset, because that is what +/// the Go VM does. +/// +/// Shared by all the executor backends on purpose: the bounds rules are observable by +/// contracts, so there is one implementation of them and not one per backend. +pub fn checked_load_range( + mem_ptr: MemPtr, + mem_length: MemLength, + memory_size: u64, +) -> Result<(usize, usize), MemoryError> { + if mem_length == 0 { + return Ok((0, 0)); + } + if mem_ptr < 0 || mem_ptr as u64 > memory_size { + return Err(MemoryError::BadBounds); + } + if mem_length < 0 { + return Err(MemoryError::NegativeLength); + } + + let end = (mem_ptr as u64) + .checked_add(mem_length as u64) + .ok_or(MemoryError::BadBoundsUpper)?; + if end > memory_size { + return Err(MemoryError::BadBoundsUpper); + } + + Ok((mem_ptr as usize, end as usize)) +} + +/// Validates a range for a store, returning it as a `start..end` byte range. +/// +/// Mirrors the Go VM's `MemStoreToMemory`, including the fact that an empty write is +/// accepted without validating the offset. +/// +/// Shared by all the executor backends on purpose: the bounds rules are observable by +/// contracts, so there is one implementation of them and not one per backend. +pub fn checked_store_range( + mem_ptr: MemPtr, + data_len: usize, + memory_size: u64, +) -> Result<(usize, usize), MemoryError> { + if data_len == 0 { + return Ok((0, 0)); + } + if mem_ptr < 0 { + return Err(MemoryError::BadBoundsLower); + } + + let end = (mem_ptr as u64) + .checked_add(data_len as u64) + .ok_or(MemoryError::BadBoundsUpper)?; + if end > memory_size { + return Err(MemoryError::BadBoundsUpper); + } + + Ok((mem_ptr as usize, end as usize)) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SIZE: u64 = 100; + + #[test] + fn load_range_ok() { + assert_eq!(checked_load_range(0, 100, SIZE), Ok((0, 100))); + assert_eq!(checked_load_range(90, 10, SIZE), Ok((90, 100))); + assert_eq!(checked_load_range(100, 0, SIZE), Ok((0, 0))); + } + + #[test] + fn load_range_zero_length_ignores_a_bad_offset() { + // Matches the Go VM, which returns an empty slice before looking at the offset. + assert_eq!(checked_load_range(-1, 0, SIZE), Ok((0, 0))); + assert_eq!(checked_load_range(1_000, 0, SIZE), Ok((0, 0))); + } + + #[test] + fn load_range_bad_offset() { + assert_eq!( + checked_load_range(-1, 10, SIZE), + Err(MemoryError::BadBounds) + ); + assert_eq!( + checked_load_range(101, 10, SIZE), + Err(MemoryError::BadBounds) + ); + } + + #[test] + fn load_range_negative_length() { + assert_eq!( + checked_load_range(0, -1, SIZE), + Err(MemoryError::NegativeLength) + ); + // An out-of-range offset takes precedence over a negative length, as in the Go VM. + assert_eq!( + checked_load_range(-1, -1, SIZE), + Err(MemoryError::BadBounds) + ); + } + + #[test] + fn load_range_end_past_memory() { + assert_eq!( + checked_load_range(95, 10, SIZE), + Err(MemoryError::BadBoundsUpper) + ); + // An offset exactly at the end is a valid offset, but nothing can be read from it. + assert_eq!( + checked_load_range(100, 1, SIZE), + Err(MemoryError::BadBoundsUpper) + ); + } + + #[test] + fn load_range_does_not_overflow() { + assert_eq!( + checked_load_range(1, MemLength::MAX, SIZE), + Err(MemoryError::BadBoundsUpper) + ); + } + + #[test] + fn store_range_ok() { + assert_eq!(checked_store_range(0, 100, SIZE), Ok((0, 100))); + assert_eq!(checked_store_range(90, 10, SIZE), Ok((90, 100))); + } + + #[test] + fn store_range_empty_data_ignores_a_bad_offset() { + // Matches the Go VM, which returns before looking at the offset. + assert_eq!(checked_store_range(-1, 0, SIZE), Ok((0, 0))); + } + + #[test] + fn store_range_bad_bounds() { + assert_eq!( + checked_store_range(-1, 10, SIZE), + Err(MemoryError::BadBoundsLower) + ); + assert_eq!( + checked_store_range(95, 10, SIZE), + Err(MemoryError::BadBoundsUpper) + ); + // Unlike a load, a store past the end reports the upper bound, not `BadBounds`. + assert_eq!( + checked_store_range(1_000, 10, SIZE), + Err(MemoryError::BadBoundsUpper) + ); + } + + #[test] + fn store_range_does_not_overflow() { + assert_eq!( + checked_store_range(1, usize::MAX, SIZE), + Err(MemoryError::BadBoundsUpper) + ); + } +} From 98fe7d5e4a736b98d981f23b498f0e93a8d7496f Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 01:19:49 +0300 Subject: [PATCH 04/26] new FFI plan --- PLAN-go-ffi.md | 322 ++++++++++++ PLAN-go-hooks-error-migration.md | 877 +++++++++++++++++++++++++++++++ 2 files changed, 1199 insertions(+) create mode 100644 PLAN-go-ffi.md create mode 100644 PLAN-go-hooks-error-migration.md diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md new file mode 100644 index 00000000..0b51f9c2 --- /dev/null +++ b/PLAN-go-ffi.md @@ -0,0 +1,322 @@ +# Integrating the new executor traits into the Go VM + +Working plan for exposing `vm-executor/src/new_traits` over a C FFI and consuming it from +the Go VM. Status as of 2026-10-01: Phase 1 items 1-5 and Phase 2 are implemented. + +## Why + +The repo was built around Wasmer 2.2, hidden behind the legacy interfaces in +`vm-executor`. Those interfaces cannot express Wasmer 6's ownership model for VM hooks, so a +second set of traits was added in `vm-executor/src/new_traits`, currently driven only by the +Rust VM (`mx-sdk-rs/chain/vm`). The Go VM still speaks the legacy C API. This plan covers +the new FFI crate, the generator work, and the Go-side changes. + +Design principles: configure the executor directly and all at once; prefer pure functions; +one config object rather than a sequence of setters. + +## Where things live + +| Piece | Path | +| --- | --- | +| Interfaces, legacy + new | `vm-executor/src/` and `vm-executor/src/new_traits/` | +| Wasmer 2.2 backend (legacy native, new traits via a shim) | `vm-executor-wasmer/` | +| Wasmer 6 backend (new traits native) | `vm-executor-experimental/` | +| Current C API (legacy only) | `c-api/` | +| New C API (new traits, Wasmer 6) | `c-api-new/` | +| Code generator (drives **both** repos) | `mx-chain-vm-go/vmhost/vmhooks/generate/` | +| Go VM | `mx-chain-vm-go/`, cgo bridge in `wasmer2/` | +| Rust VM, Wasmer 6 consumer | `mx-sdk-rs/chain/vm/src/executor_impl/we_executor.rs` | +| Rust VM, Wasmer 2.2 consumer | `mx-sdk-rs/chain/wasmer-prod/src/wasmer_prod.rs` | + +## How the two interfaces differ + +| | Legacy | New | +| --- | --- | --- | +| `Executor` | 4 methods, 2 of them post-construction setters | 2 instantiation methods; everything else at construction | +| `CompilationOptions` | 7 fields | 4; `gas_limit` moved to `call`, metering and breakpoints always on | +| Instance | one 17-method trait | `Instance` (7, outside view) + `InstanceState` (inside view, for hooks) | +| Call result | `Result<(), String>` plus breakpoint globals to poll | `InstanceCallResult` enum | +| Breakpoints | 6 values | 3; `ExecutionFailed`/`SignalError`/`AsyncCall` became `VMHooksEarlyExit` | +| VM hooks | `&self`, bare returns, carries a context pointer | `&mut self`, `Result<_, VMHooksEarlyExit>`, no context pointer | + +The backends disagree in one way that dictates the whole FFI design. Wasmer 2.2 can hand out +an owned, instance-lifetime state (a `Weak`). Wasmer 6 cannot: memory and +globals are only reachable through the `StoreMut` inside `FunctionEnvMut`, so the state is a +borrow that lives exactly as long as one host call. **The FFI must target the Wasmer 6 +model** — anything designed against 2.2's model cannot be implemented by Wasmer 6. + +## Settled decisions + +### Reference bundling, and what C takes away + +Copy the Rust VM's shape: `VMHooksDispatcher` → `VMHooksHandler` → +`TxVMHooksContext`, which bundles the VM's `TxContextRef` with the executor's +`InstanceState`. `S` is generic over ownership, so the same context type serves an owned +state (2.2) and a `&mut` borrow (Wasmer 6). + +Four guarantees the borrow checker gives for free need explicit replacements over C: + +1. **Lifetime** — a `u64` call token in the context, bumped on hook entry and invalidated on + exit, plus nulling the state pointer. Every state function validates it. A stale handle + becomes a clean error instead of a use-after-free. +2. **Exclusivity** — the context must be **stack-allocated per hook invocation**, never a + per-executor or thread-local slot. Then nesting (`executeOnDestContext` running a + contract from inside a hook) is safe by construction. +3. **Thread affinity** — the Wasmer 6 state borrows the host-call frame. cgo keeps the + goroutine on its OS thread for the call, but a goroutine spawned inside a hook must not + touch the handle. State it as an invariant in the header. +4. **No panics** — a panic across `extern "C"` aborts the node. Every entry point needs the + `capi_safe_unwind` treatment. + +### The hook ABI: a struct return, verified + +A cgo `//export` function **can** return a C struct by value. Verified on darwin/arm64 with +Go 1.23 by building a `c-archive` and calling it from C: 8-, 12- and 16-byte layouts all +return correct values. One 16-byte struct covers every hook shape and is returned in +registers under both SysV AMD64 (RAX:RDX) and AAPCS64 (X0:X1): + +```c +typedef struct { int64_t value; uint64_t early_exit; } vm_exec_hook_result_t; +#define VM_EXEC_NO_EARLY_EXIT 0xFFFFFFFFFFFFFFFFULL +``` + +The sentinel is needed because `early_exit == 0` is a real code — the Rust VM uses +`ASYNC_CALL_EARLY_EXIT_CODE = 0`, and the Go VM has the same async-call case. Verified that +code `0` and "no early exit" stay distinguishable. + +### The early-exit message does not cross the boundary + +A pointer cannot ride along in the struct: `cmd/cgo/doc.go` (go1.23, line 389) says *"A Go +function called by C code may return a Go pointer to pinned memory (which implies that it +may not return a string, slice, channel, and so forth)."* Returning an unpinned Go pointer +appears to work and is not caught even under `GOEXPERIMENT=cgocheck2`, which is exactly why +it must not be relied on. + +A buffer is not the answer either: messages are unbounded. `SignalError` +(`vmhost/vmhooks/baseOps.go:236`) loads `messageLength` bytes straight from wasm memory, +metered per byte, capped only by the memory size. + +But the Go VM does not need the text to travel. `SignalUserError` already records it via +`Output().SetReturnMessage(message)` **inside the hook**, and what comes back out of the +call is only a sentinel — `vmhost/hostCore/breakpoints.go:26-44` maps a breakpoint to +`ErrSignalError` / `ErrNotEnoughGas` / `ErrExecutionFailed` / `ErrMemoryLimit`. So the early +exit needs to carry the **code only**. + +This is a real asymmetry with the Rust VM rather than an oversight: there the message must +travel, because `TxResult::from_error(code, message)` is the only place the text exists — +and it costs nothing, since the hook returns `Err(VMHooksEarlyExit)` in-process with a +`Cow<'static, str>`. + +If the text is ever wanted on the Rust side, it does **not** belong on `InstanceState`: that +trait is call-scoped under Wasmer 6, while an early-exit message must outlive the call, and +an early exit is a property of the call rather than of the instance. It belongs in the FFI +context. Preferred shape is **pull**: one extra hook pointer, +`int32_t get_early_exit_message(void* ctx, uint8_t* buf, int32_t buf_len)` returning the +required length, called by Rust only when a code is set. That keeps the happy path clear and +the obligation out of the hook contract, so it can stay unimplemented until something wants +it. + +### Rust does the memory work and the validation + +Bounds checking and the exact error taxonomy live in Rust, not Go. Go's +`MemLoadFromMemory`/`MemStoreToMemory` helpers are bypassed for the new path. + +### The Go VM's relationship with the hooks changes + +Today there is exactly one `VMHooksImpl` for the whole host +(`vmhost/hostCore/host.go:210`), holding only `host`, with "which instance" answered +dynamically by the instance-tracker stack. That is the mirror image of the Rust design and +is what flips. + +## Open decision gates + +| # | Decision | Why it blocks | +| --- | --- | --- | +| **G1** | Do the hooks hold the state, or take it as a parameter? | The C ABI passes the context as the first argument of every hook — that *is* the parameter form. If the Rust trait keeps the state bundled, the generator must emit and reconcile two shapes. Deciding after the ABI is frozen turns a refactor into a two-repo migration. The Wasmer 6 builder currently returns `Box`, so a per-hook allocation also rides on this. **Phase 2 found this already answered by the committed Wasmer 6 code**: `ExperimentalVMHooksBuilder::with_vm_hooks` hands the builder the state and lends the hooks back through a closure, so the state is *bundled* in Rust and *passed as a parameter* in C — the two are compatible, and the generator emits one shape, not two. The allocation question is also already resolved: the builder lends `&mut dyn VMHooks` on its own stack frame. | +| **G2** | Does the new executor keep the Go VM's truncating load? | **Settled in Phase 2: yes, it clamps.** `vm_exec2_state_mem_load` writes into the caller's buffer and returns `min(requested, available)`, so the truncation is visible in the return value. Chosen to preserve behaviour contracts have already observed; the strict load stays available on the trait for a caller that wants it. The rule itself moved into `clamped_load_len` in `vm-executor/src/new_traits/memory.rs`, so the FFI and `InstanceState::memory_load_clamped` cannot drift apart. | +| **G4** | Which `OpcodeVersion` does each host declare, and does the config stay mutable? | Decides whether `OpcodeConfig` is `Arc>` (2.2 today) or plain (Wasmer 6 today), which decides the shape of the single config struct. **Phase 2 took the immutable branch for the new C API**: the version and the cost table arrive in `vm_exec2_executor_config_t` at `vm_exec2_new_executor` and are never mutated, so there is no `Arc>` on this path and no `set_opcode_config`. The gate stays open for Phase 1.6, which has to reconcile the 2.2 backend's `Arc>`. | + +## Phase 1 — Close the trait gaps + +| # | Item | Status | +| --- | --- | --- | +| 1 | `Instance::get_imported_function_names()`, replacing per-name `has_imported_function`; needed by the Go contract validator (`vmhost/contexts/runtime.go:741,750,759`), which is consensus-relevant | **done** | +| 2 | `InstanceState`: `memory_length`, `memory_grow`, strict load/store, plus `memory_load_clamped` for the Go VM's truncating load; shared `MemoryError` + `checked_load_range`/`checked_store_range` in `vm-executor/src/new_traits/memory.rs` | **done** | +| 3 | Bounds-check `WasmerInstance::memory_load`/`memory_store`, which sliced with an unchecked, overflow-prone `mem_ptr + mem_length` | **done** | +| 4 | Remove every panic from `Result`-returning paths: instance init in both executors (needs `rc_new_cyclic_fallible` for the 2.2 backend), `new_instance_from_cache`, `reset`, the breakpoint read in `call`, the prod state setters, `get_memory_view` | **done** | +| 5 | `BreakpointValue` discriminants aligned to the wire values 4/5 so `as_u64` and `TryFrom` are inverses | **done** (separate commit) | +| 6 | Converge the two `RuntimeRef` traits into one config plus one hooks-provider trait, dropping the `Mutex` | gated on **G1**, **G4** | +| 7 | Apply G1: if the state becomes a hook parameter, the builder trait disappears and `VMHooksWrapper` holds one `Box` per instance | gated on **G1** | + +The bounds rules are deliberately shared by all backends — they are observable by contracts, +so there is one implementation, not one per backend. Ten tests pin the edge cases, including +two asymmetries inherited from Go: a zero length returns empty *before* the offset is +validated, and a store past the end reports `BadBoundsUpper` where a load reports +`BadBounds`. + +## Phase 2 — New FFI crate (`c-api-new`, alongside the existing one) — **done** + +Built fresh so the wasmer2 path keeps shipping. 16 files in `c-api-new/`, 34 tests. + +**Exported surface** (18 functions, all in `c-api-new/libvmexeccapi_new.h`): + +- *Lifecycle*: `vm_exec2_new_executor`, `vm_exec2_new_instance`, the two `_destroy`s. One + config struct per level, passed once; no post-construction setters. +- *Outer instance ops*: `vm_exec2_instance_call` filling a `vm_exec2_call_result_t` + out-struct, `_get_points_used`, `_check_signatures`, `_has_function`, and the two-step + exported- and imported-names pairs. +- *State ops*, all taking a hook context: `_mem_load`, `_mem_store`, `_mem_length`, + `_mem_grow`, `_get_points_used`, `_set_points_used`. + +### What was decided while building it + +**Symbols are prefixed `vm_exec2_`.** The names in the original sketch — +`vm_exec_new_executor`, `vm_exec_new_instance` — are already exported by the legacy `c-api`, +so a binary linking both libraries would have had duplicate symbols. Verified that the two +`.dylib`s now share no `vm_*` symbol, which is what lets the Go VM hold both executors at +once for the differential run in Phase 5. Types keep their generated names +(`vm_exec_opcode_cost_t`), since only `#[no_mangle]` functions collide; the two headers must +not be included in the same translation unit, which Phase 4's separate `wasmer3/` package +satisfies. + +**Errors travel through a caller-provided buffer.** `capi_status` holds the status enum — +`VM_EXEC2_OK = 0` and every failure negative, so the counting functions share the code space +— plus a truncating, NUL-terminating writer that keeps the buffer valid UTF-8. The +`thread_local!` last-error singleton did not come along, as planned. A null pointer argument +returns `VM_EXEC2_NULL_POINTER` rather than a message, because the null pointer may be the +error buffer itself. + +**The executor does not wrap a `dyn Executor`, and that is a finding for Phase 1.6.** The new +`Executor::new_instance` takes only the wasm bytes and the compilation options, with the hooks +coming from the executor's own runtime reference. But this API needs one Go hooks object *per +instance* — that is where the Go side keeps the per-instance early-exit slot that Phase 3's +shim reads — and there is nowhere in that signature to put it. So `vm_exec2_new_instance` +takes `go_vm_hooks` and builds the instance directly against the Wasmer 6 backend. **Phase +1.6's converged hooks-provider trait should take a per-instance host handle**, otherwise the +`Executor` trait cannot serve the C API on either backend. + +**The hook context is pooled, not stack-allocated.** The original sketch called for a +stack-allocated context per invocation. That is unsafe as specified: a Go side holding the +pointer after the hook returns would be reading a reused stack frame, so even *reading the +token in order to reject it* would be undefined behaviour. Contexts are therefore boxed +individually and kept in a per-instance stack for the instance's lifetime — one allocation per +nesting level, reused across calls — which keeps the exclusivity property the stack gave and +makes the rejection path well-defined. The token check is `ctx.token == pool.active_token`, +so a context belonging to an outer call is rejected while a nested call is running and valid +again afterwards. + +### What is deliberately not here + +**The hook dispatch.** The typed function-pointer table and the 277-method `VMHooks` +implementation that calls into it are generated code, and belong to Phase 3 as originally +scoped. Everything they need is in place: the result and context ABI, the state operations, +and `CapiVMHooksBuilder::with_hook_context_raw` as the seam. Until then the builder yields +`VMHooksDefault` behind a single marked `TODO(Phase 3)`, and the table is copied as opaque +pointer-sized words (`CapiHookTable`) so the ownership story does not change when the type +does. The copy-rather-than-borrow decision is already made, matching the legacy API. + +**CI and release wiring.** `make capi2`, `capi2-linux-amd64`, `capi2-linux-arm`, +`capi2-osx-amd64` and `capi2-osx-arm` produce `libvmexeccapi2*` artifacts with the right +soname / install name. They are not in `ci-local` or the release workflows, so the published +assets do not grow a library nothing links against yet; wire them up when Phase 4 starts +consuming them. + +### Testing + +The planned "mock hook-pointer table" test became something stronger: a mock hooks *provider* +that drives the real state FFI from inside a real host call, which is the only place a context +is live. `c-api-new/tests/ffi_boundary.rs` covers the lifecycle and the outer ops through the +exported functions, and from inside a hook asserts the memory and gas operations, including +the G2 edge cases (clamped over-read, offset at and past the end, negative offset, negative +length, and the fact that a store is never truncated). It also delivers two of Phase 5's +checks early: a context used after its call is refused with `VM_EXEC2_STALE_CONTEXT`, and so +is an outer context while a nested call is running. + +## Phase 3 — Generator + +Lands before the Go adapter, since it drives both repos. New writers, modelled on the +existing `eiGenWriteRustCapiVMHooks*.go` and `eiGenWriteCgo.go`: + +- the new hook-pointer struct (context first arg, `vm_exec_hook_result_t` return); +- the new Rust capi `VMHooks` impl (call the pointer, compare against the sentinel, return + `Err`); +- the new Go `//export` shims; +- `eiGenMain.go`: register the writers and the copy targets. + +**The 282 Go hook signatures in `executor/vmHooks.go` do not change.** The shim reads an +`earlyExit` field off the hooks object after the call and packs it into the result struct. +That works only if there is one hooks object per live instance, so it depends on the Phase 4 +provider change rather than on touching the interface. + +## Phase 4 — Go VM (new `wasmer3/` package, parallel to `wasmer2/`) + +Both executors coexist behind `ExecutorAbstractFactory`, which is what makes differential +testing possible. + +1. `executor.InstanceState` interface — `MemLoad`, `MemStore`, `MemLength`, `MemGrow`, + `GetPointsUsed`, `SetPointsUsed` — implemented by a struct wrapping the `ctx` pointer. +2. `executor.VMHooks` becomes a provider: `CreateVMHooks(state InstanceState) VMHooks`, + mirroring the Rust builder. Replaces the single host-wide `VMHooksImpl`. +3. `VMHooksImpl` gains `state` and `earlyExit`; its `MemLoad`/`MemStore` stop going through + `host.Runtime().GetInstance()`. This is the change that decouples the hooks from the + instance tracker. +4. `FailExecution` / `SignalUserError` record an early exit instead of setting a breakpoint, + and keep burning gas as they do now. +5. `handleBreakpointIfAny` maps `InstanceCallResult` instead of a wasmer global, producing + the same sentinels. +6. `MemLoadFromMemory` / `MemStoreToMemory` are bypassed for wasmer3; kept for wasmer2. +7. The validator switches to imported-function *names* rather than per-name queries. +8. The executor factory passes one config at creation; `SetOpcodeCosts` as a + post-construction mutator goes away. Note Go is already stale here — `wasmer2/bridge2.go` + calls `vm_exec_set_opcode_costs` against a header that now exports + `vm_exec_set_opcode_config`. + +Only the memory and gas paths move to the state handle. `IsFunctionImported`, +`ValidateFunctionArities`, `Reset` and `Cache` stay on the outer `Instance`, and keeping that +split crisp is what makes the Go side match the Rust trait split rather than just wrap it. + +## Phase 5 — Bring-up and validation + +- Unit tests per Phase 1 item, plus the FFI boundary test from Phase 2. +- Targeted Go tests for the G2 memory edge cases: the clamped over-read, negative length, low + and high store bounds. The three current implementations disagree, so these come before + anything else is trusted. The Rust side of these is already covered by + `c-api-new/tests/ffi_boundary.rs`; what remains is asserting the Go wrapper agrees. +- **Differential run**: the scenario suite under both factories, asserting identical + `VMOutput` (return code, message, logs, gas used). This is the acceptance criterion. +- Re-entrancy: a contract calling `executeOnDestContext` from inside a hook, confirming + nested contexts do not clobber each other. **Covered at the Rust level in Phase 2**; what + remains is driving it from a real contract through Go. +- Stale-handle test: keep a `ctx` past the hook return and confirm the token check rejects it + rather than faulting. **Done in Phase 2** (`state_ops`). + +## Phase 6 — Deferred + +- `reset()` on the Wasmer 6 instance — prerequisite for Go's warm-instance tracker. +- `new_instance_from_cache` and the `cache` path. +- Only then wire warm instances into wasmer3. + +## Cross-repo note + +`mx-sdk-rs` pins `multiversx-chain-vm-executor* = "=0.5.1"` from crates.io, so it picks up +none of this until a version bump. At that bump, three accumulated breaks need resolving +together: + +- `ExperimentalExecutorRuntimeRef::opcode_cost` → `opcode_config` (carries the G4 decision); +- `VMHooksDispatcher` is missing five newer hooks (`activate_unsafe_mode`, + `deactivate_unsafe_mode`, `managed_get_num_errors`, `managed_get_error_with_index`, + `managed_get_last_error`) — the `VMHooks` trait has 282 required methods and no defaults, + so every addition is a hard break; +- `WasmerProdInstanceState::set_breakpoint_value_legacy` / `set_early_exit` now return + `Result`, so `InstanceStateSetEarlyExit` in `chain/wasmer-prod` must handle them. + +## Critical path + +Originally **G1 → Phase 1.6/1.7 → Phase 2 → Phase 3 → Phase 4**. In the event Phase 2 ran +ahead of Phase 1.6/1.7: G1 turned out to be already answered by the committed Wasmer 6 code, +and the C ABI is parameter-form regardless, so the trait convergence was not a prerequisite +after all — it is now better informed by Phase 2, which found the per-instance hooks argument +the converged trait needs. + +Remaining path: **Phase 3 → Phase 4 → Phase 5**, with Phase 1.6/1.7 landing whenever G4 is +settled across both backends. Phase 3 is now the only thing between here and a Go consumer. diff --git a/PLAN-go-hooks-error-migration.md b/PLAN-go-hooks-error-migration.md new file mode 100644 index 00000000..f64893e4 --- /dev/null +++ b/PLAN-go-hooks-error-migration.md @@ -0,0 +1,877 @@ +# Migrating the Go VM hooks to return `error` + +**Repo:** `mx-chain-vm-go` (branch `wasmer6`) +**Related:** `PLAN-go-ffi.md` in `mx-vm-executor-rs` — this document is the Go-side prerequisite for its Phase 3. + +--- + +## 1. Goal + +Today every VM hook that fails performs the failure side effects itself — it calls +`FailExecution` (888 call sites) or sets a runtime breakpoint (6 sites) — and then returns a +value. After this migration every hook **returns** its failure as an `error` and performs no +failure side effects. The generated hook wrapper applies the effects. + +The motivation is the new executor interface (`vm-executor/src/new_traits`), where a hook +reports early exit through its return value instead of through a breakpoint global. That +transport change comes later; this migration only moves the *decision* out of the hook body, +so that the later change has a single place to hook into. + +### Invariant for this migration + +**This migration must be observably a no-op.** Same return codes, same return messages, same +gas accounting, same error traces, same breakpoints, for every existing test. Any behaviour +change is a bug in the migration, not an improvement. Several tempting cleanups are called +out in §9 as *deliberately deferred*. + +### Scope + +**In scope** + +- All 277 `@autogenerate(VMHooks)` methods in `vmhost/vmhooks/` (see §2 for the census). +- The unexported and `…WithHost` / `…WithTypedArgs` helpers those hooks call, where they + currently perform failure side effects. +- New hand-written support code: `EarlyExit`, `resolveEarlyExit`, `applyEarlyExit`, + `HandleHookError`. +- The code generator in `vmhost/vmhooks/generate/` and its **Go** outputs. +- Go call sites of hooks and helpers in tests and `mock/contracts/`. + +**Out of scope — do not touch** + +- The C ABI. The `// extern …` declarations in `wasmer2/wasmer2ImportsCgo.go` keep their + current signatures (`void` / `int32_t` / `long long`). The error is consumed on the Go side. +- Everything in `mx-vm-executor-rs` and `mx-sdk-rs`. All Rust generator writers + (`eiGenWriteRust*.go`) and all opcode-cost writers must keep producing **byte-identical** + output. This is a verification step, see §8. +- `libvmexeccapi.h`, `libvmexeccapi.*` binaries. +- `vmhost/contexts/runtime.go`: `FailExecution` and `SignalUserError` stay exactly as they are. + +--- + +## 2. Census — what you are changing + +`@autogenerate(VMHooks)` appears 278 times but there are **277 hooks**: `UpgradeContract` +(`baseOps.go:1516-1517`) has the annotation twice. Remove the duplicate line as part of the +migration. + +| source file | void | int32 | int64 | total | `FailExecution` sites | +|---|---:|---:|---:|---:|---:| +| `baseOps.go` | 21 | 46 | 20 | 87 | 234 | +| `managedei.go` | 26 | 15 | 0 | 41 | 119 | +| `bigIntOps.go` | 30 | 11 | 1 | 42 | 126 | +| `manBufOps.go` | 3 | 22 | 2 | 27 | 88 | +| `cryptoei.go` | 2 | 39 | 0 | 41 | 162 | +| `bigFloatOps.go` | 16 | 6 | 0 | 22 | 73 | +| `smallIntOps.go` | 3 | 3 | 6 | 12 | 22 | +| `manMapOps.go` | 0 | 5 | 0 | 5 | 9 | +| **total** | **101** | **147** | **29** | **277** | **833** | + +Plus 4 sites in `vmHooksImpl.go` (the definition and the method wrapper), 1 direct +`runtime.FailExecution(` inside `vmhooks`, and 6 `SetRuntimeBreakpointValue(BreakpointOutOfGas)` +sites (`baseOps.go:1694,1715,1783,1801,1859`, `managedei.go:679`). + +Call-site forms in use: `context.FailExecution(` (571), `FailExecution(` package-level (265), +`runtime.FailExecution(` (1). + +Only **two** types implement `executor.VMHooks`: +`vmhost/vmhooks.VMHooksImpl` and the generated `executor/wrapper.WrapperVMHooks`. There are +~115 hook/helper call sites in `*_test.go` and `mock/contracts/`. + +--- + +## 3. Target shape + +### 3.1 Hook signatures + +| today | after | +|---|---| +| `func (context *VMHooksImpl) X(args…)` | `func (context *VMHooksImpl) X(args…) error` | +| `func (context *VMHooksImpl) X(args…) int32` | `func (context *VMHooksImpl) X(args…) (int32, error)` | +| `func (context *VMHooksImpl) X(args…) int64` | `func (context *VMHooksImpl) X(args…) (int64, error)` | + +The declared error type is the plain `error` interface, **not** `*vmhost.EarlyExit`. This is +deliberate: + +- The ~833 failure sites keep returning the existing package-level sentinels + (`vmhost.ErrBadBounds`, `vmhost.ErrNoManagedBufferUnderThisHandle`, …) with no wrapping and + no allocation. +- `errors.Is` / `errors.As` / `fmt.Errorf("%w")` keep working end to end. +- The many helpers that already return `error` compose unchanged. +- It avoids the typed-nil trap (`var e *EarlyExit = nil; var err error = e; err != nil`). + +`*vmhost.EarlyExit` is just *one* implementation of `error`, used only by the handful of hooks +that need non-default failure behaviour, and recovered with `errors.As`. + +### 3.2 New hand-written code + +Three new pieces. Create them first, before touching anything else. + +**(a) `vmhost/earlyExit.go`** — package `vmhost`, new file. + +```go +package vmhost + +import vmcommon "github.com/multiversx/mx-chain-vm-common-go" + +// EarlyExit is an error that describes how execution must be terminated from +// inside a VM hook. Hooks only need it for the cases that deviate from the +// default failure behaviour; for everything else a plain error is enough and +// resolveEarlyExit supplies the default. +type EarlyExit struct { + // Code selects which termination routine is applied. Only + // vmcommon.ExecutionFailed, vmcommon.UserError and vmcommon.OutOfGas are + // meaningful today. + Code vmcommon.ReturnCode + + // Message is the VMOutput return message. For a user error this is the + // contract's own text, which is not the same as Cause.Error(). + Message string + + // ConsumeAllGas burns all remaining gas before the termination routine runs. + ConsumeAllGas bool + + // Cause is the underlying error, used for the error trace and errors.Is. + Cause error +} + +// Error implements the error interface. +func (e *EarlyExit) Error() string { return e.Message } + +// Unwrap keeps errors.Is/errors.As working through an EarlyExit. +func (e *EarlyExit) Unwrap() error { return e.Cause } + +// ExitUserError terminates execution with a user error carrying the contract's +// own message. It does not consume gas, matching runtime.SignalUserError. +func ExitUserError(message string) *EarlyExit { + return &EarlyExit{ + Code: vmcommon.UserError, + Message: message, + ConsumeAllGas: false, + Cause: ErrSignalError, + } +} + +// ExitOutOfGas terminates execution with an out-of-gas breakpoint only, without +// touching the output state and without consuming the remaining gas. This is the +// legacy async-call behaviour. +func ExitOutOfGas() *EarlyExit { + return &EarlyExit{ + Code: vmcommon.OutOfGas, + Message: ErrNotEnoughGas.Error(), + ConsumeAllGas: false, + Cause: ErrNotEnoughGas, + } +} +``` + +> `ExitUserError` sets `Cause: ErrSignalError` so that the trace entry added by +> `runtime.SignalUserError` is not duplicated — see §5.4 for why the `Cause` of a user error +> is never passed to `AddError` by `applyEarlyExit`. + +**(b) `executor/vmHooksErrorHandler.go`** — package `executor`, new file. + +`executor/vmHooks.go` is generated and marked DO NOT EDIT, so this interface lives in its own +hand-written file. The generated `VMHooks` interface will embed it (§4.3). + +```go +package executor + +// VMHooksErrorHandler applies the side effects of an error returned by a VM hook. +// It is implemented by the VM, and called only by generated hook wrappers. +type VMHooksErrorHandler interface { + // HandleHookError applies the failure side effects for err. It is a no-op + // when err is nil. + HandleHookError(err error) +} +``` + +**(c) `vmhost/vmhooks/earlyExit.go`** — package `vmhooks`, new file. + +```go +package vmhooks + +import ( + "errors" + + vmcommon "github.com/multiversx/mx-chain-vm-common-go" + "github.com/multiversx/mx-chain-vm-go/vmhost" +) + +// HandleHookError applies the side effects of an error returned by a VM hook. +// Called only from generated hook wrappers. +func (context *VMHooksImpl) HandleHookError(err error) { + if err == nil { + return + } + applyEarlyExit(context.host, resolveEarlyExit(err)) +} + +// resolveEarlyExit classifies a hook error into the termination it requires. +// See the contract in the migration document: exactly one call site, never nil in, never nil out. +func resolveEarlyExit(err error) *vmhost.EarlyExit { + var earlyExit *vmhost.EarlyExit + if errors.As(err, &earlyExit) { + return earlyExit + } + + // The default: fail execution and burn the remaining gas. This is what the + // 800+ plain-error failure sites used to do via FailExecution. + return &vmhost.EarlyExit{ + Code: vmcommon.ExecutionFailed, + Message: err.Error(), + ConsumeAllGas: true, + Cause: err, + } +} + +// applyEarlyExit performs the termination. Each branch is exactly one of the three +// effect routines that exist today; no new behaviour is introduced here. +func applyEarlyExit(host vmhost.VMHost, earlyExit *vmhost.EarlyExit) { + if earlyExit.ConsumeAllGas { + metering := host.Metering() + _ = metering.UseGasBounded(metering.GasLeft()) + } + + runtime := host.Runtime() + switch earlyExit.Code { + case vmcommon.UserError: + runtime.SignalUserError(earlyExit.Message) + case vmcommon.OutOfGas: + runtime.SetRuntimeBreakpointValue(vmhost.BreakpointOutOfGas) + default: + runtime.FailExecution(earlyExit.Cause) + } +} +``` + +Check the equivalence before you go further — it is the whole justification for the migration +being a no-op: + +| today | after | +|---|---| +| `FailExecution(host, err)` = burn all gas, then `runtime.FailExecution(err)` | `resolveEarlyExit` → `{ExecutionFailed, burn}` → burn, then `runtime.FailExecution(cause)` | +| `runtime.SignalUserError(msg)`, no burn | `ExitUserError(msg)` → `{UserError, no burn}` → `runtime.SignalUserError(msg)` | +| `runtime.SetRuntimeBreakpointValue(BreakpointOutOfGas)`, no burn, no output change | `ExitOutOfGas()` → `{OutOfGas, no burn}` → `runtime.SetRuntimeBreakpointValue(BreakpointOutOfGas)` | + +--- + +## 4. How `resolveEarlyExit` must be used + +This is the part most likely to be got wrong. Read it before migrating any hook. + +### 4.1 One call site, and it is generated-code-only + +`resolveEarlyExit` is called from **exactly one place**: `(*VMHooksImpl).HandleHookError`. +`HandleHookError` is called from **exactly one kind of place**: a generated hook wrapper. + +**Never** call `resolveEarlyExit` — or `applyEarlyExit`, or `HandleHookError` — from: + +- a hook body, +- a helper called by a hook, +- a `…WithHost` or `…WithTypedArgs` function, +- a context (`vmhost/contexts/*.go`), +- `vmhost/hostCore/*.go`. + +It is unexported specifically to make the second and fourth cases impossible from outside the +package, and you must not export it. If you find yourself wanting to call it from a hook body, +the hook should be returning an error instead. + +### 4.2 It never receives nil and never returns nil + +The `err == nil` check belongs in the **caller** (`HandleHookError`), not in the resolver. This +differs from the old `FailExecution`, which started with `if err == nil { return }`. + +Two consequences: + +- `resolveEarlyExit` may dereference/inspect `err` unconditionally. +- Generated wrappers must guard with `if err != nil` before calling `HandleHookError`. The + wrapper's guard and `HandleHookError`'s guard are both present on purpose — the wrapper's + guard keeps the hot path free of a call, `HandleHookError`'s guard makes the exported + method safe for the hand-written callers in §7. + +### 4.3 It is pass-through for errors that already carry an `EarlyExit` + +The `errors.As` branch returns the existing `*EarlyExit` **unchanged**: no re-classification, +no re-wrapping, no message rewriting, no merging of flags. This is what allows a hook (or a +helper several frames down) to declare its own flavour once and have that survive arbitrary +`fmt.Errorf("…: %w", err)` wrapping on the way out. + +Corollary: if a hook returns `fmt.Errorf("context: %w", vmhost.ExitUserError("boom"))`, the +resolved message is `"boom"`, not `"context: boom"`. If the added context matters, construct +the `EarlyExit` with the full message instead of wrapping it. + +### 4.4 The default branch is the only place the default lives + +The fallback `{ExecutionFailed, err.Error(), ConsumeAllGas: true, Cause: err}` governs all +~833 migrated failure sites. `ConsumeAllGas: true` is the default precisely because "consume +all gas on failure" is what almost every hook does today; the exceptions opt out by returning +an `EarlyExit`. Do not change this default as part of the migration. + +### 4.5 It must not duplicate what `runtime.FailExecution` already does + +`runtime.FailExecution` (`vmhost/contexts/runtime.go:595-612`) already: + +- sets `ReturnCode = vmcommon.ExecutionFailed`, +- sets the return message from `err.Error()`, +- calls `AddError(err)`, +- and **upgrades the breakpoint to `BreakpointOutOfGas` when `errors.Is(err, vmhost.ErrNotEnoughGas)`**, + while leaving `ReturnCode` at `ExecutionFailed`. + +Do **not** replicate that last rule in `resolveEarlyExit`. It is tempting to write +`if errors.Is(err, vmhost.ErrNotEnoughGas) { Code = vmcommon.OutOfGas }`, but that would route +the error to the `OutOfGas` branch of `applyEarlyExit`, which sets only the breakpoint and +skips `SetReturnCode` / `SetReturnMessage` / `AddError`. That is a behaviour change. Leave the +upgrade where it is. + +Likewise, `applyEarlyExit`'s `UserError` branch must not call `AddError(earlyExit.Cause)`: +`runtime.SignalUserError` already adds `errors.New(message)` itself, and adding the cause too +would put an extra entry in the error trace. + +### 4.6 Summary table + +| question | answer | +|---|---| +| who calls it | `(*VMHooksImpl).HandleHookError`, nothing else | +| who calls that | generated wrappers, plus the hand-written shims in §7 | +| nil error | rejected by the caller; never reaches the resolver | +| nil result | impossible; always returns a non-nil `*EarlyExit` | +| already an `EarlyExit` | returned verbatim | +| default flavour | `ExecutionFailed`, message `err.Error()`, consume all gas | +| may a hook call it | no | +| may it be exported | no | + +--- + +## 5. Generator changes + +All in `vmhost/vmhooks/generate/`. Entry point is `cmd/eiGenMain.go`, run with +`go generate ./vmhost/vmhooks/...` or `cd vmhost/vmhooks && go run generate/cmd/eiGenMain.go`. + +### 5.1 `eiGenModel.go` — add one field + +```go + type EIFunction struct { + Name string + Arguments []*EIFunctionArg + Result *EIFunctionResult ++ ++ // ReturnsError is true when the hook has a trailing `error` result. ++ // During the migration both shapes must be supported; see the parser. ++ ReturnsError bool + } +``` + +`Result` keeps its current meaning: the *value* result, `nil` for a void hook. A trailing +`error` is not a value result and must never be represented as one — it does not cross the +C ABI. + +### 5.2 `eiGenParse.go` — accept a trailing `error` + +Replace `extractEIFunctionResult` (currently lines 53-71, which rejects more than one result) +with a version that strips a trailing `error` first: + +```go +func isErrorType(ty ast.Expr) bool { + ident, ok := ty.(*ast.Ident) + return ok && ident.Name == "error" +} + +func extractEIFunctionResults(decl *ast.FuncDecl) (*EIFunctionResult, bool, error) { + if decl.Type.Results == nil { + return nil, false, nil + } + + list := decl.Type.Results.List + returnsError := false + if n := len(list); n > 0 && isErrorType(list[n-1].Type) { + returnsError = true + list = list[:n-1] + } + + switch len(list) { + case 0: + return nil, returnsError, nil + case 1: + eiType, err := processType(list[0].Type) + if err != nil { + return nil, false, err + } + return &EIFunctionResult{Type: eiType}, returnsError, nil + default: + return nil, false, fmt.Errorf( + "too many results in function %s, at most one value result plus a trailing error accepted", + decl.Name.Name) + } +} +``` + +Wire it into `extractEIFunction` and set `ReturnsError` on the `EIFunction`. + +**Tolerant during the migration, mandatory at the end.** The parser must accept hooks that do +*not* return an error while you are migrating file by file — that is what keeps the tree +compiling at every step. Only once all 277 are migrated do you add the enforcement, as the +last step of the migration (§7 step 8): + +```go + if !returnsError { + return nil, fmt.Errorf("VM hook %s must return a trailing error", decl.Name.Name) + } +``` + +Do not add it earlier. Do not skip adding it at the end — it is what stops a future hook from +silently reintroducing in-body failure handling. + +### 5.3 A shared result-signature helper + +Add to `eiGenWriteCgo.go` (next to the other type helpers), used by §5.4 and §5.5: + +```go +// goResultSignature renders the Go result list of a hook, including the leading space. +func goResultSignature(funcMetadata *EIFunction, typeName func(EIType) string) string { + switch { + case funcMetadata.Result == nil && !funcMetadata.ReturnsError: + return "" + case funcMetadata.Result == nil: + return " error" + case !funcMetadata.ReturnsError: + return " " + typeName(funcMetadata.Result.Type) + default: + return fmt.Sprintf(" (%s, error)", typeName(funcMetadata.Result.Type)) + } +} +``` + +### 5.4 `eiGenWriteGoVMHooks.go` — the `VMHooks` interface + +Two changes to `WriteEIInterface`: + +1. Embed the hand-written error handler in the top-level interface, before the groups: + +```go + out.WriteString(` + // VMHooks contains all VM functions that can be called by the executor during SC execution. + type VMHooks interface {`) ++ out.WriteString("\n\tVMHooksErrorHandler") + + for _, group := range eiMetadata.Groups { +``` + +2. Replace the per-method result rendering (lines 34-38) with + `goResultSignature(funcMetadata, vmHooksType)`. + +Leave `externResult` alone — it renders the **C** result and must keep ignoring `ReturnsError`. + +### 5.5 `eiGenWriteVMHooksWrapper.go` — `WrapperVMHooks` + +Change the method rendering to pass both results through, and add a `HandleHookError` +passthrough so `WrapperVMHooks` still satisfies `executor.VMHooks`. + +Target output per hook: + +```go +// StorageLoad VM hook wrapper +func (w *WrapperVMHooks) StorageLoad(keyOffset executor.MemPtr, keyLength executor.MemLength, dataOffset executor.MemPtr) (int32, error) { + callInfo := fmt.Sprintf("StorageLoad(%d, %d, %d)", keyOffset, keyLength, dataOffset) + w.logger.LogVMHookCallBefore(callInfo) + result, err := w.wrappedVMHooks.StorageLoad(keyOffset, keyLength, dataOffset) + w.logger.LogVMHookCallAfter(callInfo) + return result, err +} +``` + +and for a void hook: + +```go + err := w.wrappedVMHooks.SignalError(messageOffset, messageLength) + w.logger.LogVMHookCallAfter(callInfo) + return err +``` + +Note the logger call stays *between* the inner call and the return: `LogVMHookCallAfter` must +still run on the failure path, as it does today. Do not add early returns. + +Append once, after the loop: + +```go + out.WriteString(` +// HandleHookError passthrough +func (w *WrapperVMHooks) HandleHookError(err error) { + w.wrappedVMHooks.HandleHookError(err) +} +`) +``` + +### 5.6 `eiGenWriteCgo.go` — the cgo exports + +`writeCgoFunctions` (the `// extern …` block) and the `//export` **signatures** are +**unchanged**. Only the bodies of the exported functions change, in `writeGoExports`. + +Target output, void hook: + +```go +//export w2_signalError +func w2_signalError(context unsafe.Pointer, messageOffset int32, messageLength int32) { + vmHooks := getVMHooksFromContextRawPtr(context) + err := vmHooks.SignalError(executor.MemPtr(messageOffset), messageLength) + if err != nil { + vmHooks.HandleHookError(err) + } +} +``` + +Target output, valued hook: + +```go +//export w2_storageLoad +func w2_storageLoad(context unsafe.Pointer, keyOffset int32, keyLength int32, dataOffset int32) int32 { + vmHooks := getVMHooksFromContextRawPtr(context) + result, err := vmHooks.StorageLoad(executor.MemPtr(keyOffset), keyLength, executor.MemPtr(dataOffset)) + if err != nil { + vmHooks.HandleHookError(err) + } + return result +} +``` + +Three rules for this writer: + +- **Return the hook's own value on the failure path.** `result` is returned as-is, whatever the + hook produced alongside the error. Do not substitute `0` or `-1`. Today's hooks return a + specific value next to each `FailExecution` call (often `-1`, sometimes `0`, sometimes the + partially-computed value) and that value is written to the wasm stack before the breakpoint + stops execution. Preserving it is what makes the migration a no-op. +- **No Go pointer may be returned.** Unchanged from today, but worth restating: the return type + stays a scalar. The error never crosses into C. +- **Keep supporting non-erroring hooks** while the migration is in progress: if + `!funcMetadata.ReturnsError`, emit exactly today's body. + +`WriteWasmer1Cgo` (`eiGenWriteCgo.go:95`) is **dead code** — nothing calls it, and there is no +`wasmer1` package left in the repo. Delete it, along with `writePopulateImports` if that +becomes unreferenced too. Do not spend effort updating it. + +### 5.7 Generator files that must NOT change + +Verify by diff after regenerating (§8). These writers all consume `Result` for the C/Rust ABI +and must ignore `ReturnsError`: + +`eiGenWriteRustVMHooksNames.go`, `eiGenWriteRustVMHooksTrait.go`, +`eiGenWriteRustVMHooksTraitLegacy.go`, `eiGenWriteRustVMHooksTraitAdapter.go`, +`eiGenWriteRustVMHooksSignatures.go`, `eiGenWriteRustCapiVMHooks.go`, +`eiGenWriteRustCapiVMHooksPointers.go`, `eiGenWriteRustWasmerProdImports.go`, +`eiGenWriteRustWasmerExperimentalImports.go`, `eiGenWriteRustVHDispatcherLegacy.go`, +`eiGenWriteRustUtil.go`, `eiGenWriteHeaders.go`, `eiGenWriteGoNames.go`, and every +`*OpcodeCost*` / `*Opcode*` writer. + +--- + +## 6. Migrating the hook bodies + +### 6.1 The mechanical rules + +Apply these per hook. `⟨v⟩` is whatever value the site returns today. + +| # | today | after | +|---|---|---| +| R1 | `context.FailExecution(err)`
`return` | `return err` | +| R2 | `context.FailExecution(err)`
`return ⟨v⟩` | `return ⟨v⟩, err` | +| R3 | `FailExecution(host, err)`
`return` / `return ⟨v⟩` | same as R1 / R2 | +| R4 | `context.FailExecution(vmhost.ErrXyz)`
`return ⟨v⟩` | `return ⟨v⟩, vmhost.ErrXyz` | +| R5 | `if err != nil && runtime.UseGasBoundedShouldFailExecution() {`
`context.FailExecution(err)`
`return ⟨v⟩`
`}` | `if err != nil && runtime.UseGasBoundedShouldFailExecution() {`
`return ⟨v⟩, err`
`}` | +| R6 | `runtime.SetRuntimeBreakpointValue(vmhost.BreakpointOutOfGas)`
`return ⟨v⟩` | `return ⟨v⟩, vmhost.ExitOutOfGas()` | +| R7 | `runtime.SignalUserError(msg)` (tail of hook) | `return vmhost.ExitUserError(msg)` | +| R8 | `return ⟨v⟩` (success) | `return ⟨v⟩, nil` | +| R9 | bare `return` (success, void hook) | `return nil` | +| R10 | void hook falling off the end of the body | add explicit `return nil` | + +R5 deserves attention: the **condition is unchanged**. When +`UseGasBoundedShouldFailExecution()` is false the error is still dropped and execution +continues — that is today's behaviour and must be preserved. Do not "fix" it into +`return ⟨v⟩, err` unconditionally. + +R6 applies to exactly 6 sites (`baseOps.go:1694,1715,1783,1801,1859`, `managedei.go:679`). +These are the "exception" cases: the legacy async call reports out-of-gas without burning the +remaining gas and without touching the output state. After migration, no hook in +`vmhost/vmhooks/` calls `SetRuntimeBreakpointValue` at all. + +R7 applies to exactly 2 sites: `SignalError` (`baseOps.go:272`) and `ManagedSignalError` +(`managedei.go:192`). After migration, no hook in `vmhost/vmhooks/` calls `SignalUserError`. + +There is 1 direct `runtime.FailExecution(` inside `vmhost/vmhooks/`. Locate it, read the +surrounding code, and migrate it by hand — it is not covered by a rule. + +### 6.2 Worked example — `SignalError` + +Before (`baseOps.go:252-273`): + +```go +func (context *VMHooksImpl) SignalError(messageOffset executor.MemPtr, messageLength executor.MemLength) { + runtime := context.GetRuntimeContext() + metering := context.GetMeteringContext() + metering.StartGasTracing(signalErrorName) + + gasToUse := math.MulUint64(metering.GasSchedule().BaseOperationCost.PersistPerByte, uint64(messageLength)) + gasToUse = math.AddUint64(gasToUse, metering.GasSchedule().BaseOpsAPICost.SignalError) + + err := metering.UseGasBounded(gasToUse) + if err != nil && runtime.UseGasBoundedShouldFailExecution() { + context.FailExecution(err) + return + } + + message, err := context.MemLoad(messageOffset, messageLength) + if err != nil { + context.FailExecution(err) + return + } + + runtime.SignalUserError(string(message)) +} +``` + +After: + +```go +func (context *VMHooksImpl) SignalError(messageOffset executor.MemPtr, messageLength executor.MemLength) error { + runtime := context.GetRuntimeContext() + metering := context.GetMeteringContext() + metering.StartGasTracing(signalErrorName) + + gasToUse := math.MulUint64(metering.GasSchedule().BaseOperationCost.PersistPerByte, uint64(messageLength)) + gasToUse = math.AddUint64(gasToUse, metering.GasSchedule().BaseOpsAPICost.SignalError) + + err := metering.UseGasBounded(gasToUse) + if err != nil && runtime.UseGasBoundedShouldFailExecution() { + return err + } + + message, err := context.MemLoad(messageOffset, messageLength) + if err != nil { + return err + } + + return vmhost.ExitUserError(string(message)) +} +``` + +Note `runtime` is still used (for `UseGasBoundedShouldFailExecution`), so no unused-variable +fallout here. In other hooks, migrating away the last use of `runtime` / `metering` / `host` +**will** produce unused-variable errors — delete the now-dead local. + +### 6.3 Worked example — `AsyncCall` (the R6 case) + +The tail of `AsyncCall` (`baseOps.go:1856-1866`): + +```go + err = async.RegisterLegacyAsyncCall(calledSCAddress, data, value) + if errors.Is(err, vmhost.ErrNotEnoughGas) { + runtime.SetRuntimeBreakpointValue(vmhost.BreakpointOutOfGas) + return + } + if err != nil { + context.FailExecution(err) + return + } +} +``` + +becomes: + +```go + err = async.RegisterLegacyAsyncCall(calledSCAddress, data, value) + if errors.Is(err, vmhost.ErrNotEnoughGas) { + return vmhost.ExitOutOfGas() + } + if err != nil { + return err + } + return nil +} +``` + +### 6.4 Helpers + +Three categories, handle them differently. + +**Already error-returning — leave alone.** `vmhost/vmhooks/managedConversions.go` contains no +`FailExecution` calls (the grep hits are all `UseGasBoundedShouldFailExecution`); it already +returns `(T, error)`. `vmhost/vmhooks/eei_helpers.go` has no failure handling at all. + +**`…WithHost` / `…WithTypedArgs` helpers that call `FailExecution`.** There are ~40 of these +(`grep -n 'WithHost(\|WithTypedArgs(' vmhost/vmhooks/*.go`). Migrate each to return +`(⟨v⟩, error)` / `error` using the same rules, and have the calling hook propagate: + +```go + result, err := context.StorageLoadWithHost(host, keyOffset, keyLength, dataOffset) + return result, err +``` + +or just `return context.StorageLoadWithHost(...)` when the shapes line up. + +**External callers of those helpers** (`mock/contracts/`, `*_test.go`, ~115 sites) then get a +second return value. At those sites the caller is *not* a hook, so it must apply the effects +itself — call the exported `FailExecution(host, err)` explicitly: + +```go + result, err := vmhooks.ManagedBufferCopyByteSliceWithHost(host, …) + if err != nil { + vmhooks.FailExecution(host, err) + return + } +``` + +This is already the idiom at several of those sites (e.g. `vmhost/hosttest/managedops_test.go:33,37`). + +### 6.5 `FailExecution` stays + +Keep `vmhooks.FailExecution(host, err)` (`vmHooksImpl.go:106-115`) exported and **unchanged**. +It is still used by 17 call sites in mocks and tests, and by the non-hook callers in §6.4. + +Keep `(*VMHooksImpl).FailExecution(err)` (`vmHooksImpl.go:96-98`) too, but expect it to become +unused inside the package. Check at the end whether anything still calls it; if not, decide +then whether to delete it (it is not part of the `executor.VMHooks` interface, so removing it +is safe). + +`applyEarlyExit` deliberately does **not** call `FailExecution`; it calls +`runtime.FailExecution` directly, because the gas burn is already done by the `ConsumeAllGas` +branch. Routing it through `FailExecution` would burn twice — harmless +(`UseGasBounded(GasLeft())` is 0 the second time) but misleading. Do not "simplify" it. + +--- + +## 7. Order of work + +Each numbered step ends with a compiling tree and its own commit. The trick that makes this +possible is `ReturnsError` being *per function* plus the parser staying tolerant until step 8. + +1. **Support code.** Add `vmhost/earlyExit.go`, `executor/vmHooksErrorHandler.go`, + `vmhost/vmhooks/earlyExit.go` (§3.2). Nothing references the new types yet. + `go build ./...` passes. + +2. **Generator.** Apply §5.1-§5.6. Do not add the mandatory-error validation. Delete + `WriteWasmer1Cgo`. `go build ./...` passes. + +3. **Regenerate, no hooks migrated yet.** Run the generator. Expected Go diff: the + `VMHooksErrorHandler` line in `executor/vmHooks.go` and the `HandleHookError` passthrough in + `executor/wrapper/wrapperVMHooks.go`. Everything else byte-identical, including all Rust + outputs. `go build ./...` and `make test-short` pass. **If the Rust outputs moved, stop and + fix the generator before continuing.** + +4. **Migrate the hook files, smallest first.** For each file: apply §6.1, run the generator, + `go build ./...`, fix the test/mock call sites for that group, run `make test-short`. + Order: `manMapOps.go` (5) → `smallIntOps.go` (12) → `bigFloatOps.go` (22) → + `manBufOps.go` (27) → `cryptoei.go` (41) → `managedei.go` (41) → `bigIntOps.go` (42) → + `baseOps.go` (87). + `manMapOps.go` is the calibration run: 5 hooks, 9 failure sites, all int32, no helpers. Get + the full loop working there before touching anything bigger. + `baseOps.go` is last because it holds the async/out-of-gas cases (R6), most of the + `…WithHost` helpers, and `SignalError`. + +5. **`…WithHost` / `…WithTypedArgs` helpers** not already done as part of step 4, plus their + external callers (§6.4). + +6. **Remove the duplicate `@autogenerate(VMHooks)`** on `UpgradeContract` + (`baseOps.go:1516-1517`). Regenerate; the hook count printed by the generator should go + from 278 to 277 and the generated output should not otherwise change. + +7. **Sweep.** Run the §8 greps. Everything must be clean before step 8. + +8. **Make the error result mandatory.** Add the validation from §5.2, regenerate, build, test. + This is the step that locks the invariant in. + +9. **Optional cleanup**, separate commit: delete `(*VMHooksImpl).FailExecution` if unused. + +--- + +## 8. Verification + +**Build and test** + +```sh +cd /Users/andreim/multiversx/chain/mx-chain-vm-go +go build ./... +go vet ./... +make test-short # fast signal during step 4 +VMEXECUTOR="wasmer2" go test ./... # full run, before declaring done +``` + +Also run `golangci-lint run` if the repo's config is set up — an `errcheck`-style linter will +flag any error you accidentally drop, which is exactly the class of mistake this migration can +introduce. + +**Greps that must come back empty** (run from `mx-chain-vm-go`): + +```sh +# no hook sets a breakpoint any more +grep -rn "SetRuntimeBreakpointValue" vmhost/vmhooks/ + +# no hook signals a user error directly any more +grep -rn "SignalUserError" vmhost/vmhooks/ + +# no failure side effects inside hook bodies; the only remaining hits +# must be the definition in vmHooksImpl.go and UseGasBoundedShouldFailExecution +grep -rn "FailExecution(" vmhost/vmhooks/*.go | grep -v ShouldFailExecution + +# resolveEarlyExit / applyEarlyExit have exactly one caller each +grep -rn "resolveEarlyExit\|applyEarlyExit" . +``` + +**The Rust outputs must not move.** The generator copies files into `mx-vm-executor-rs` when +`vmhost/vmhooks/generate/cmd/path-to-vm-executor-rs.txt` exists — and it does. After every +regeneration: + +```sh +cd /Users/andreim/multiversx/chain/mx-vm-executor-rs && git status --short +``` + +must show no change to `vm-executor/src/`, `c-api/src/`, `vm-executor-wasmer/src/` or +`vm-executor-experimental/src/`. Same for the `mx-sdk-rs` copies if +`path-to-mx-sdk-rs.txt` is present. + +**Spot-check the diff of the generated cgo file.** `wasmer2/wasmer2ImportsCgo.go` should show +changes only inside `//export` function bodies. If any `// extern` line or any `//export` +signature changed, the C ABI moved and something is wrong. + +**Behaviour equivalence.** The scenario/JSON tests under `vmhost/hosttest/`, +`vmhost/vmhookstest/` and the scenario runner are the real check. They assert return codes, +return messages and exact gas figures, so a misplaced gas burn or a lost `AddError` will show +up as a diff. Do not accept a "fixed" expectation file without understanding why it moved. + +--- + +## 9. Deliberately deferred + +Do not do these now, even though the migration makes them tempting. + +- **Return the early-exit code over the FFI.** That is `PLAN-go-ffi.md` Phase 3. Until then + `HandleHookError` returns nothing and the breakpoint globals still carry the outcome. +- **Fold the `ErrNotEnoughGas` breakpoint upgrade into `resolveEarlyExit`** (see §4.5). +- **Make `ConsumeAllGas` derivable from `Code`.** It is carried explicitly because the two are + independent in general: a `UserError` keeps its gas while an `ExecutionFailed` burns it, and + the FFI phase needs the flag separately from the code. +- **Normalise the values returned alongside errors** (the mix of `-1`, `0` and partial values). + Preserving them is what keeps this migration a no-op; changing them is a separate, testable + decision. +- **Unify `UseGasBoundedShouldFailExecution` handling.** The flag-gated drop-the-error pattern + at ~300 sites is odd but load-bearing; leave it. +- **Delete `vmhooks.FailExecution`.** Non-hook callers still need it (§6.5). + +--- + +## 10. Quick reference + +| thing | where | +|---|---| +| hook sources | `vmhost/vmhooks/{baseOps,managedei,bigFloatOps,bigIntOps,manBufOps,manMapOps,smallIntOps,cryptoei}.go` | +| generator | `vmhost/vmhooks/generate/`, entry `generate/cmd/eiGenMain.go` | +| run generator | `cd vmhost/vmhooks && go run generate/cmd/eiGenMain.go` | +| generated `VMHooks` interface | `executor/vmHooks.go` | +| generated logging wrapper | `executor/wrapper/wrapperVMHooks.go` | +| generated cgo exports | `wasmer2/wasmer2ImportsCgo.go` | +| cgo context helper | `wasmer2/wasmer2ImportsCgoHelper.go:9` | +| `FailExecution` (hooks) | `vmhost/vmhooks/vmHooksImpl.go:106` | +| `runtime.FailExecution` | `vmhost/contexts/runtime.go:595` | +| `runtime.SignalUserError` | `vmhost/contexts/runtime.go:616` | +| breakpoint → error mapping | `vmhost/hostCore/breakpoints.go:26` | +| new: `EarlyExit` | `vmhost/earlyExit.go` | +| new: `VMHooksErrorHandler` | `executor/vmHooksErrorHandler.go` | +| new: `resolveEarlyExit` | `vmhost/vmhooks/earlyExit.go` | From 3982a9c40990b458505465f53feb5b81e3fd7721 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 01:20:47 +0300 Subject: [PATCH 05/26] new FFI initial implementation (phase 2) --- Cargo.toml | 1 + Makefile | 35 ++ c-api-new/Cargo.toml | 20 + c-api-new/build.rs | 39 ++ c-api-new/cbindgen.toml | 5 + c-api-new/libvmexeccapi_new.h | 539 ++++++++++++++++ c-api-new/src/capi_config.rs | 74 +++ c-api-new/src/capi_executor.rs | 94 +++ c-api-new/src/capi_hook_context.rs | 376 +++++++++++ c-api-new/src/capi_hook_table.rs | 96 +++ c-api-new/src/capi_instance.rs | 393 ++++++++++++ c-api-new/src/capi_instance_state.rs | 238 +++++++ c-api-new/src/capi_logger.rs | 20 + c-api-new/src/capi_opcode_cost.rs | 288 +++++++++ c-api-new/src/capi_status.rs | 162 +++++ c-api-new/src/capi_strings.rs | 97 +++ c-api-new/src/lib.rs | 32 + c-api-new/tests/ffi_boundary.rs | 620 +++++++++++++++++++ vm-executor/src/new_traits.rs | 2 +- vm-executor/src/new_traits/instance_state.rs | 20 +- vm-executor/src/new_traits/memory.rs | 61 ++ 21 files changed, 3194 insertions(+), 18 deletions(-) create mode 100644 c-api-new/Cargo.toml create mode 100644 c-api-new/build.rs create mode 100644 c-api-new/cbindgen.toml create mode 100644 c-api-new/libvmexeccapi_new.h create mode 100644 c-api-new/src/capi_config.rs create mode 100644 c-api-new/src/capi_executor.rs create mode 100644 c-api-new/src/capi_hook_context.rs create mode 100644 c-api-new/src/capi_hook_table.rs create mode 100644 c-api-new/src/capi_instance.rs create mode 100644 c-api-new/src/capi_instance_state.rs create mode 100644 c-api-new/src/capi_logger.rs create mode 100644 c-api-new/src/capi_opcode_cost.rs create mode 100644 c-api-new/src/capi_status.rs create mode 100644 c-api-new/src/capi_strings.rs create mode 100644 c-api-new/src/lib.rs create mode 100644 c-api-new/tests/ffi_boundary.rs diff --git a/Cargo.toml b/Cargo.toml index d6646e8a..7a2f9704 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "meta", "c-api", + "c-api-new", "vm-executor", "vm-executor-experimental", "vm-executor-wasmer", diff --git a/Makefile b/Makefile index b061c6e6..f88fea44 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ .PHONY: capi capi-linux-amd64 capi-linux-arm capi-osx-amd64 capi-osx-arm \ capi-osx-amd64-cross capi-linux-amd64-docker capi-linux-arm64-docker \ + capi2 capi2-linux-amd64 capi2-linux-arm capi2-osx-amd64 capi2-osx-arm \ ci-local clean check-macos # cargo and docker already parallelise internally, and a parallel make would @@ -38,6 +39,35 @@ capi-osx-arm: check-macos capi mv target/release/libmultiversx_chain_vm_executor_c_api.dylib target/release/libvmexeccapi_arm.dylib install_name_tool -id @rpath/libvmexeccapi_arm.dylib target/release/libvmexeccapi_arm.dylib +# --- The new C API ------------------------------------------------------------ +# Built into separately named artifacts, so a consumer can link one, the other, or +# both: the two libraries share no exported symbols (`vm_exec_*` against +# `vm_exec2_*`), which is what makes running them side by side for differential +# testing possible. +# +# Not wired into `ci-local` or the release workflows yet. That happens when the Go +# VM starts consuming it, so the release assets do not grow a library nothing +# links against. + +capi2: + cargo build -p multiversx-chain-vm-executor-c-api-new --release + +capi2-linux-amd64: capi2 + mv target/release/libmultiversx_chain_vm_executor_c_api_new.so target/release/libvmexeccapi2.so + patchelf --set-soname libvmexeccapi2.so target/release/libvmexeccapi2.so + +capi2-linux-arm: capi2 + mv target/release/libmultiversx_chain_vm_executor_c_api_new.so target/release/libvmexeccapi2_arm.so + patchelf --set-soname libvmexeccapi2_arm.so target/release/libvmexeccapi2_arm.so + +capi2-osx-amd64: check-macos capi2 + mv target/release/libmultiversx_chain_vm_executor_c_api_new.dylib target/release/libvmexeccapi2.dylib + install_name_tool -id @rpath/libvmexeccapi2.dylib target/release/libvmexeccapi2.dylib + +capi2-osx-arm: check-macos capi2 + mv target/release/libmultiversx_chain_vm_executor_c_api_new.dylib target/release/libvmexeccapi2_arm.dylib + install_name_tool -id @rpath/libvmexeccapi2_arm.dylib target/release/libvmexeccapi2_arm.dylib + # --- Local reproduction of the CI build matrix ------------------------------- # The four targets above are what CI runs natively, one per runner. The targets # below drive the same builds from a single host: the two Linux libraries build @@ -97,5 +127,10 @@ clean: rm -f target/release/libvmexeccapi.dylib rm -f target/release/libvmexeccapi_arm.dylib rm -f c-api/libvmexeccapi.h + rm -f target/release/libvmexeccapi2.so + rm -f target/release/libvmexeccapi2_arm.so + rm -f target/release/libvmexeccapi2.dylib + rm -f target/release/libvmexeccapi2_arm.dylib + rm -f c-api-new/libvmexeccapi_new.h rm -f target/x86_64-apple-darwin/release/libvmexeccapi.dylib rm -rf $(CI_LOCAL_DIR) diff --git a/c-api-new/Cargo.toml b/c-api-new/Cargo.toml new file mode 100644 index 00000000..5c017ead --- /dev/null +++ b/c-api-new/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "multiversx-chain-vm-executor-c-api-new" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +# `cdylib` is what the Go VM links against; `rlib` is what the integration tests under +# `tests/` need in order to call the exported functions from Rust. +crate-type = ["cdylib", "rlib"] + +[dependencies] +meta = { path = "../meta" } +multiversx-chain-vm-executor = { path = "../vm-executor" } +multiversx-chain-vm-executor-wasmer-experimental = { path = "../vm-executor-experimental" } + +libc = { version = "^0.2", default-features = false } + +[build-dependencies] +cbindgen = "0.29" diff --git a/c-api-new/build.rs b/c-api-new/build.rs new file mode 100644 index 00000000..522eba98 --- /dev/null +++ b/c-api-new/build.rs @@ -0,0 +1,39 @@ +extern crate cbindgen; + +use cbindgen::{Builder, Config, Language, Style}; +use std::{env, fs, path::PathBuf}; + +const HEADER_FILE_NAME: &str = "libvmexeccapi_new"; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut crate_header_file = PathBuf::from(&crate_dir); + crate_header_file.push(HEADER_FILE_NAME); + + let out_dir = env::var("OUT_DIR").unwrap(); + let mut out_header_file = PathBuf::from(&out_dir); + out_header_file.push(HEADER_FILE_NAME); + + // Generate the C bindings in the `OUT_DIR`. + out_header_file.set_extension("h"); + // Reads `cbindgen.toml`, which force-exports the types no exported function mentions. + let config = Config::from_root_or_default(&crate_dir); + + Builder::new() + .with_config(config) + .with_crate(&crate_dir) + .with_language(Language::C) + // Same formatting choices as the legacy `c-api` header, so the two are easy to + // diff against each other. + .with_line_length(usize::MAX) + .with_style(Style::Both) + .generate() + .expect("Unable to generate C bindings") + .write_to_file(out_header_file.as_path()); + + // Copy the generated C bindings from `OUT_DIR` to `CARGO_MANIFEST_DIR`. + crate_header_file.set_extension("h"); + out_header_file.set_extension("h"); + fs::copy(out_header_file.as_path(), crate_header_file.as_path()) + .expect("Unable to copy the generated C bindings"); +} diff --git a/c-api-new/cbindgen.toml b/c-api-new/cbindgen.toml new file mode 100644 index 00000000..7a40ddd7 --- /dev/null +++ b/c-api-new/cbindgen.toml @@ -0,0 +1,5 @@ +# `vm_exec2_hook_result_t` is the return type of every generated VM hook pointer, so the Go +# side and the Phase 3 generator both need its typedef — but no function exported from this +# crate mentions it yet, so cbindgen would not emit it on reachability alone. +[export] +include = ["vm_exec2_hook_result_t"] diff --git a/c-api-new/libvmexeccapi_new.h b/c-api-new/libvmexeccapi_new.h new file mode 100644 index 00000000..015ff359 --- /dev/null +++ b/c-api-new/libvmexeccapi_new.h @@ -0,0 +1,539 @@ +#include +#include +#include +#include + +/** + * The value of `vm_exec2_hook_result_t::early_exit` that means "the hook returned normally". + * + * A dedicated sentinel is needed because `0` is a real early-exit code: the Rust VM uses + * `ASYNC_CALL_EARLY_EXIT_CODE = 0` and the Go VM has the same async-call case, so "code 0" + * and "no code" have to stay distinguishable. + */ +#define VM_EXEC2_NO_EARLY_EXIT UINT64_MAX + +/** + * Result of a C API call. + * + * `VM_EXEC2_OK` is zero and every failure is negative, so the functions that return a + * count (the memory loads) can share the same code space: a non-negative result is a + * count, a negative one is one of these. + */ +typedef enum vm_exec2_status_t { + /** + * The call succeeded. + */ + VM_EXEC2_OK = 0, + /** + * The call failed; the error message was written to the provided error buffer. + */ + VM_EXEC2_ERROR = -1, + /** + * A pointer argument that must not be null was null. No message is produced, because + * a null pointer may well be the error buffer itself. + */ + VM_EXEC2_NULL_POINTER = -2, + /** + * A hook context was used outside the host call it belongs to. See + * [`crate::capi_hook_context`]. + */ + VM_EXEC2_STALE_CONTEXT = -3, + /** + * A panic was caught at the boundary. Rust panics must never unwind into C, so every + * entry point traps them and reports this instead. + */ + VM_EXEC2_PANIC = -4, +} vm_exec2_status_t; + +/** + * How a call ended. + * + * This replaces the legacy arrangement, where a call returned a bare ok/error and the VM + * then polled a wasm global to find out what had actually happened. + */ +typedef enum vm_exec2_call_result_kind_t { + /** + * The function returned normally. + */ + VM_EXEC2_CALL_OK = 0, + /** + * No exported function by that name. + */ + VM_EXEC2_CALL_FUNCTION_NOT_FOUND = 1, + /** + * The instance trapped for a reason that is neither a VM hook's doing nor a + * middleware breakpoint. The message is in the error buffer. + */ + VM_EXEC2_CALL_RUNTIME_ERROR = 2, + /** + * A VM hook asked for execution to stop. `early_exit_code` says which; the message, if + * any, is in the error buffer, and is informational — the VM has already recorded its + * own. + */ + VM_EXEC2_CALL_EARLY_EXIT = 3, + /** + * A middleware stopped execution. `breakpoint` says which. + */ + VM_EXEC2_CALL_BREAKPOINT = 4, +} vm_exec2_call_result_kind_t; + +/** + * Opaque handle to an executor. + */ +typedef struct vm_exec2_executor_t { + +} vm_exec2_executor_t; + +/** + * FFI-safe mirror of `OpcodeCost`. + * + * This is a distinct type rather than a cast of `OpcodeCost` itself, so that + * the general-purpose struct is free to change shape without touching the C + * ABI, and vice versa. + */ +typedef struct vm_exec_opcode_cost_t { + uint32_t opcode_block; + uint32_t opcode_br; + uint32_t opcode_brif; + uint32_t opcode_brtable; + uint32_t opcode_call; + uint32_t opcode_callindirect; + uint32_t opcode_catch; + uint32_t opcode_catchall; + uint32_t opcode_delegate; + uint32_t opcode_drop; + uint32_t opcode_else; + uint32_t opcode_end; + uint32_t opcode_globalget; + uint32_t opcode_globalset; + uint32_t opcode_i32add; + uint32_t opcode_i32and; + uint32_t opcode_i32clz; + uint32_t opcode_i32const; + uint32_t opcode_i32ctz; + uint32_t opcode_i32divs; + uint32_t opcode_i32divu; + uint32_t opcode_i32eq; + uint32_t opcode_i32eqz; + uint32_t opcode_i32extend16s; + uint32_t opcode_i32extend8s; + uint32_t opcode_i32ges; + uint32_t opcode_i32geu; + uint32_t opcode_i32gts; + uint32_t opcode_i32gtu; + uint32_t opcode_i32les; + uint32_t opcode_i32leu; + uint32_t opcode_i32load; + uint32_t opcode_i32load16s; + uint32_t opcode_i32load16u; + uint32_t opcode_i32load8s; + uint32_t opcode_i32load8u; + uint32_t opcode_i32lts; + uint32_t opcode_i32ltu; + uint32_t opcode_i32mul; + uint32_t opcode_i32ne; + uint32_t opcode_i32or; + uint32_t opcode_i32popcnt; + uint32_t opcode_i32rems; + uint32_t opcode_i32remu; + uint32_t opcode_i32rotl; + uint32_t opcode_i32rotr; + uint32_t opcode_i32shl; + uint32_t opcode_i32shrs; + uint32_t opcode_i32shru; + uint32_t opcode_i32store; + uint32_t opcode_i32store16; + uint32_t opcode_i32store8; + uint32_t opcode_i32sub; + uint32_t opcode_i32wrapi64; + uint32_t opcode_i32xor; + uint32_t opcode_i64add; + uint32_t opcode_i64and; + uint32_t opcode_i64clz; + uint32_t opcode_i64const; + uint32_t opcode_i64ctz; + uint32_t opcode_i64divs; + uint32_t opcode_i64divu; + uint32_t opcode_i64eq; + uint32_t opcode_i64eqz; + uint32_t opcode_i64extend16s; + uint32_t opcode_i64extend32s; + uint32_t opcode_i64extend8s; + uint32_t opcode_i64extendi32s; + uint32_t opcode_i64extendi32u; + uint32_t opcode_i64ges; + uint32_t opcode_i64geu; + uint32_t opcode_i64gts; + uint32_t opcode_i64gtu; + uint32_t opcode_i64les; + uint32_t opcode_i64leu; + uint32_t opcode_i64load; + uint32_t opcode_i64load16s; + uint32_t opcode_i64load16u; + uint32_t opcode_i64load32s; + uint32_t opcode_i64load32u; + uint32_t opcode_i64load8s; + uint32_t opcode_i64load8u; + uint32_t opcode_i64lts; + uint32_t opcode_i64ltu; + uint32_t opcode_i64mul; + uint32_t opcode_i64ne; + uint32_t opcode_i64or; + uint32_t opcode_i64popcnt; + uint32_t opcode_i64rems; + uint32_t opcode_i64remu; + uint32_t opcode_i64rotl; + uint32_t opcode_i64rotr; + uint32_t opcode_i64shl; + uint32_t opcode_i64shrs; + uint32_t opcode_i64shru; + uint32_t opcode_i64store; + uint32_t opcode_i64store16; + uint32_t opcode_i64store32; + uint32_t opcode_i64store8; + uint32_t opcode_i64sub; + uint32_t opcode_i64xor; + uint32_t opcode_if; + uint32_t opcode_localget; + uint32_t opcode_localset; + uint32_t opcode_localtee; + uint32_t opcode_loop; + uint32_t opcode_memorygrow; + uint32_t opcode_memorysize; + uint32_t opcode_nop; + uint32_t opcode_reffunc; + uint32_t opcode_refisnull; + uint32_t opcode_refnull; + uint32_t opcode_rethrow; + uint32_t opcode_return; + uint32_t opcode_returncall; + uint32_t opcode_returncallindirect; + uint32_t opcode_select; + uint32_t opcode_tableget; + uint32_t opcode_tablegrow; + uint32_t opcode_tableinit; + uint32_t opcode_tableset; + uint32_t opcode_tablesize; + uint32_t opcode_throw; + uint32_t opcode_try; + uint32_t opcode_typedselect; + uint32_t opcode_unreachable; + uint32_t opcode_unwind; + uint32_t opcode_memorycopy; + uint32_t opcode_memoryfill; + uint32_t opcode_memorycopyperbyte; + uint32_t opcode_memoryfillperbyte; + uint32_t opcode_localallocate; +} vm_exec_opcode_cost_t; + +/** + * Everything an executor is configured with. + * + * The opcode cost table is referenced rather than inlined, because it is large and the + * caller already holds one. It is read during the call and not retained. + */ +typedef struct vm_exec2_executor_config_t { + /** + * Which opcode set and whitelist to apply; see `OpcodeVersion` on the Rust side. + */ + int32_t opcode_version; + /** + * The per-opcode gas costs. Must not be null. + */ + const struct vm_exec_opcode_cost_t *opcode_cost; +} vm_exec2_executor_config_t; + +/** + * Opaque handle to an instance. + */ +typedef struct vm_exec2_instance_t { + +} vm_exec2_instance_t; + +/** + * FFI-safe mirror of the new `CompilationOptions`. + * + * Four fields, against the legacy seven: `gas_limit` moved to the call, and metering and + * runtime breakpoints are no longer optional. Fields that are `usize` on the Rust side are + * fixed to `u64` here, since `usize` has no guaranteed width across host platforms. + */ +typedef struct vm_exec2_compilation_options_t { + uint64_t unmetered_locals; + uint64_t max_memory_grow; + uint64_t max_memory_grow_delta; + bool opcode_trace; +} vm_exec2_compilation_options_t; + +/** + * The out-struct of `vm_exec2_instance_call`. + */ +typedef struct vm_exec2_call_result_t { + enum vm_exec2_call_result_kind_t kind; + /** + * Meaningful only for `VM_EXEC2_CALL_EARLY_EXIT`. + */ + uint64_t early_exit_code; + /** + * Meaningful only for `VM_EXEC2_CALL_BREAKPOINT`. Values match `BreakpointValue`: + * 4 = out of gas, 5 = memory limit. + */ + uint64_t breakpoint; +} vm_exec2_call_result_t; + +/** + * What a VM hook is handed as its first argument. + * + * Only `go_vm_hooks` is part of the layout the Go side mirrors; the rest is private to + * Rust, and the whole pointer is what travels back into the state functions. + */ +typedef struct vm_exec2_hook_context_t { + /** + * The Go VM hooks object for this instance. Read by the Go side, never by Rust. + */ + void *go_vm_hooks; + /** + * Erased `&mut ExperimentalInstanceState<'_>`; null while no call is active. + */ + void *state; + /** + * Erased `&RefCell`, for the token check. + */ + const void *pool; + /** + * This activation's call token; `0` while no call is active. + */ + uint64_t token; +} vm_exec2_hook_context_t; + +/** + * What every VM hook returns. + * + * One 16-byte struct covers every hook shape — `void`, `i32`, `i64` — and is returned in + * registers under both SysV AMD64 (RAX:RDX) and AAPCS64 (X0:X1), so the early-exit channel + * costs no memory traffic. A cgo `//export` function may return a C struct by value, which + * is what makes this shape available to the Go side. + * + * The early-exit *message* does not travel in this struct. A pointer cannot: cgo forbids + * returning an unpinned Go pointer, and a fixed buffer cannot work because the messages are + * unbounded user text. The Go VM does not need it to travel, because the hook has already + * recorded the message in its own output state by the time it returns. + */ +typedef struct vm_exec2_hook_result_t { + /** + * The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for + * any hook that reports an early exit. + */ + int64_t value; + /** + * The early-exit code, or `VM_EXEC2_NO_EARLY_EXIT`. + */ + uint64_t early_exit; +} vm_exec2_hook_result_t; + +/** + * A buffer of this size holds every message the executor currently produces. + * + * Exported so the Go side has one place to take the number from rather than picking its + * own. + */ +extern const int VM_EXEC2_ERROR_BUF_RECOMMENDED_LEN; + +/** + * Creates an executor from one configuration struct and one hook table. + * + * `hook_table` is copied, so the caller is free to release it as soon as this returns. + * `hook_table_len` is the number of pointer-sized entries in it. + * + * On success writes the executor to `executor_out` and returns `VM_EXEC2_OK`. The caller + * owns the result and must release it with `vm_exec2_executor_destroy`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec2_status_t vm_exec2_new_executor(struct vm_exec2_executor_t **executor_out, const struct vm_exec2_executor_config_t *config, const void *hook_table, uintptr_t hook_table_len, char *err_buf, int err_buf_len); + +/** + * Destroys an executor. Does nothing if `executor` is null. + * + * Instances created from an executor do not borrow it, so the order of destruction between + * the two does not matter. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +void vm_exec2_executor_destroy(struct vm_exec2_executor_t *executor); + +/** + * Creates an instance. + * + * `go_vm_hooks` is the Go VM hooks object for *this* instance; it is handed back to every + * VM hook as field 0 of the hook context. It must stay alive for the instance's lifetime. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec2_status_t vm_exec2_new_instance(struct vm_exec2_executor_t *executor, struct vm_exec2_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const struct vm_exec2_compilation_options_t *options, void *go_vm_hooks, char *err_buf, int err_buf_len); + +/** + * Destroys an instance. Does nothing if `instance` is null. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +void vm_exec2_instance_destroy(struct vm_exec2_instance_t *instance); + +/** + * Calls an exported function, with a gas limit for this call. + * + * Always writes `result_out` when it returns `VM_EXEC2_OK`. A non-`OK` status means the + * call could not be attempted at all, which is distinct from a call that was attempted and + * failed — the latter is reported through `result_out.kind`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec2_status_t vm_exec2_instance_call(struct vm_exec2_instance_t *instance, const char *func_name, uint64_t points_limit, struct vm_exec2_call_result_t *result_out, char *err_buf, int err_buf_len); + +/** + * Returns the number of points (gas) used so far. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec2_status_t vm_exec2_instance_get_points_used(struct vm_exec2_instance_t *instance, uint64_t *points_out, char *err_buf, int err_buf_len); + +/** + * Checks that every exported function takes no arguments and returns nothing. + * + * Returns 1 if so, 0 if not, or a negative status. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec2_instance_check_signatures(const struct vm_exec2_instance_t *instance); + +/** + * Checks whether the contract exports a function with the given name. + * + * Returns 1 if so, 0 if not, or a negative status. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec2_instance_has_function(const struct vm_exec2_instance_t *instance, const char *func_name); + +/** + * Buffer length needed by `vm_exec2_instance_exported_function_names`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec2_instance_exported_function_names_length(const struct vm_exec2_instance_t *instance); + +/** + * Writes the exported function names, pipe-separated, e.g. `"init|endpoint1|endpoint2"`. + * + * No ordering is guaranteed. Returns the number of bytes written including the terminator, + * or `-1` if the buffer is absent or too small. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec2_instance_exported_function_names(const struct vm_exec2_instance_t *instance, char *dest, int dest_len); + +/** + * Buffer length needed by `vm_exec2_instance_imported_function_names`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec2_instance_imported_function_names_length(const struct vm_exec2_instance_t *instance); + +/** + * Writes the names of the VM hooks the contract imports, pipe-separated. + * + * All of them at once, rather than a per-name query, because the Go contract validator + * checks the module against whole sets of allowed names. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec2_instance_imported_function_names(const struct vm_exec2_instance_t *instance, char *dest, int dest_len); + +/** + * Loads from instance memory into a caller-provided buffer, **truncating** at the end of + * the memory. + * + * Returns the number of bytes actually written, which may be fewer than `dest_len`, or a + * negative status. The truncation is where the Go VM's historical load semantics live: a + * load whose offset is valid but whose end lies past the end of the memory yields the bytes + * that are available and no error. Contracts have observed that, so the executor keeps + * offering it. An offset outside the memory, or a negative length, is still an error. + * + * `dest_len` doubles as the requested length, which is what makes the clamping visible to + * the caller: Go allocates `length` bytes and gets back `min(length, available)`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int64_t vm_exec2_state_mem_load(struct vm_exec2_hook_context_t *ctx, int32_t mem_ptr, uint8_t *dest, int32_t dest_len, char *err_buf, int err_buf_len); + +/** + * Writes to instance memory. The range must fit entirely inside the memory. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec2_status_t vm_exec2_state_mem_store(struct vm_exec2_hook_context_t *ctx, int32_t mem_ptr, const uint8_t *src, int32_t src_len, char *err_buf, int err_buf_len); + +/** + * Returns the size in bytes of the instance memory. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec2_status_t vm_exec2_state_mem_length(struct vm_exec2_hook_context_t *ctx, uint64_t *length_out, char *err_buf, int err_buf_len); + +/** + * Grows the instance memory by `by_num_pages` pages of 64Kb each. + * + * Writes the number of pages the memory had *before* growing to `prev_pages_out`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec2_status_t vm_exec2_state_mem_grow(struct vm_exec2_hook_context_t *ctx, uint32_t by_num_pages, uint32_t *prev_pages_out, char *err_buf, int err_buf_len); + +/** + * Returns the number of points (gas) used by the instance so far. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec2_status_t vm_exec2_state_get_points_used(struct vm_exec2_hook_context_t *ctx, uint64_t *points_out, char *err_buf, int err_buf_len); + +/** + * Sets the number of points (gas) used by the instance. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec2_status_t vm_exec2_state_set_points_used(struct vm_exec2_hook_context_t *ctx, uint64_t points, char *err_buf, int err_buf_len); diff --git a/c-api-new/src/capi_config.rs b/c-api-new/src/capi_config.rs new file mode 100644 index 00000000..e093aa30 --- /dev/null +++ b/c-api-new/src/capi_config.rs @@ -0,0 +1,74 @@ +//! FFI-safe mirrors of the configuration structs. +//! +//! Both are passed by pointer, once, at the point the thing they configure is created. +//! There are no post-construction setters in this API: the legacy +//! `vm_exec_executor_set_vm_hooks_ptr` / `vm_exec_set_opcode_config` pair does not come +//! along. + +use multiversx_chain_vm_executor::{CompilationOptions, OpcodeConfig, OpcodeVersion}; + +use crate::capi_opcode_cost::vm_exec_opcode_cost_t; + +/// Everything an executor is configured with. +/// +/// The opcode cost table is referenced rather than inlined, because it is large and the +/// caller already holds one. It is read during the call and not retained. +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct vm_exec2_executor_config_t { + /// Which opcode set and whitelist to apply; see `OpcodeVersion` on the Rust side. + pub opcode_version: i32, + + /// The per-opcode gas costs. Must not be null. + pub opcode_cost: *const vm_exec_opcode_cost_t, +} + +impl vm_exec2_executor_config_t { + /// Converts to the executor's own config type. + /// + /// # Safety + /// + /// `self.opcode_cost` must be a valid, non-null pointer for the duration of the call. + pub(crate) unsafe fn to_opcode_config(&self) -> Result { + let Some(opcode_version) = OpcodeVersion::from_i32(self.opcode_version) else { + return Err(format!( + "invalid opcode version code: {}", + self.opcode_version + )); + }; + + if self.opcode_cost.is_null() { + return Err("opcode cost ptr is null".to_string()); + } + + Ok(OpcodeConfig { + opcode_version, + opcode_cost: unsafe { (*self.opcode_cost).to_opcode_cost() }, + }) + } +} + +/// FFI-safe mirror of the new `CompilationOptions`. +/// +/// Four fields, against the legacy seven: `gas_limit` moved to the call, and metering and +/// runtime breakpoints are no longer optional. Fields that are `usize` on the Rust side are +/// fixed to `u64` here, since `usize` has no guaranteed width across host platforms. +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct vm_exec2_compilation_options_t { + pub unmetered_locals: u64, + pub max_memory_grow: u64, + pub max_memory_grow_delta: u64, + pub opcode_trace: bool, +} + +impl vm_exec2_compilation_options_t { + pub(crate) fn to_compilation_options(&self) -> CompilationOptions { + CompilationOptions { + unmetered_locals: self.unmetered_locals as usize, + max_memory_grow: self.max_memory_grow as usize, + max_memory_grow_delta: self.max_memory_grow_delta as usize, + opcode_trace: self.opcode_trace, + } + } +} diff --git a/c-api-new/src/capi_executor.rs b/c-api-new/src/capi_executor.rs new file mode 100644 index 00000000..3c4826f7 --- /dev/null +++ b/c-api-new/src/capi_executor.rs @@ -0,0 +1,94 @@ +//! Executor lifecycle. +//! +//! An executor here is the shared, immutable configuration: the opcode config and the Go +//! hook table. It is passed once, at creation, and never mutated — the legacy +//! `vm_exec_set_opcode_config` and `vm_exec_executor_set_vm_hooks_ptr` setters have no +//! counterpart. +//! +//! Note that it does not wrap a `dyn Executor`. The new `Executor` trait's `new_instance` +//! takes only the wasm bytes and the compilation options, with the hooks coming from the +//! executor's own runtime reference — but this API needs one Go hooks object *per instance*, +//! because that is where the Go side keeps the per-instance early-exit slot. So instances are +//! built directly against the Wasmer 6 backend, and the per-instance pointer is supplied at +//! `vm_exec2_new_instance`. Converging the two backends' runtime-reference traits (Phase 1.6) +//! should give the hooks provider that per-instance argument, at which point this can go back +//! through the trait and serve both backends. + +use libc::{c_char, c_int}; +use meta::capi_safe_unwind; +use multiversx_chain_vm_executor::OpcodeConfig; +use std::ffi::c_void; +use std::sync::Arc; + +use crate::capi_config::vm_exec2_executor_config_t; +use crate::capi_hook_table::CapiHookTable; +use crate::capi_status::{vm_exec2_status_t, write_error}; + +/// Opaque handle to an executor. +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct vm_exec2_executor_t; + +pub struct CapiExecutor { + pub(crate) opcode_config: Arc, + pub(crate) hook_table: CapiHookTable, +} + +/// Creates an executor from one configuration struct and one hook table. +/// +/// `hook_table` is copied, so the caller is free to release it as soon as this returns. +/// `hook_table_len` is the number of pointer-sized entries in it. +/// +/// On success writes the executor to `executor_out` and returns `VM_EXEC2_OK`. The caller +/// owns the result and must release it with `vm_exec2_executor_destroy`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] +pub unsafe extern "C" fn vm_exec2_new_executor( + executor_out: *mut *mut vm_exec2_executor_t, + config: *const vm_exec2_executor_config_t, + hook_table: *const c_void, + hook_table_len: usize, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + if executor_out.is_null() || config.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + } + + let opcode_config = match unsafe { (*config).to_opcode_config() } { + Ok(opcode_config) => opcode_config, + Err(message) => return write_error(message, err_buf, err_buf_len), + }; + + let capi_executor = CapiExecutor { + opcode_config: Arc::new(opcode_config), + hook_table: unsafe { CapiHookTable::copy_from(hook_table, hook_table_len) }, + }; + + unsafe { + *executor_out = Box::into_raw(Box::new(capi_executor)) as *mut vm_exec2_executor_t; + } + + vm_exec2_status_t::VM_EXEC2_OK +} + +/// Destroys an executor. Does nothing if `executor` is null. +/// +/// Instances created from an executor do not borrow it, so the order of destruction between +/// the two does not matter. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn vm_exec2_executor_destroy(executor: *mut vm_exec2_executor_t) { + if !executor.is_null() { + drop(unsafe { Box::from_raw(executor as *mut CapiExecutor) }); + } +} diff --git a/c-api-new/src/capi_hook_context.rs b/c-api-new/src/capi_hook_context.rs new file mode 100644 index 00000000..d6bd41eb --- /dev/null +++ b/c-api-new/src/capi_hook_context.rs @@ -0,0 +1,376 @@ +//! The hook context: what a VM hook is handed, and what makes it safe to hand over. +//! +//! Under Wasmer 6 the instance state is a borrow of the host-call frame — memory and globals +//! are only reachable through the `StoreMut` inside `FunctionEnvMut`, so the state is valid +//! for exactly one hook invocation and not a moment longer. In Rust the borrow checker +//! enforces that. Over C it has to be enforced dynamically, which is what this module does. +//! +//! The four guarantees that are lost at the boundary, and their replacements: +//! +//! 1. **Lifetime.** Each activation gets a fresh, non-zero call token. The context records +//! it, and the pool records which token is currently innermost. A state function accepts +//! a context only while the two agree, so a context used after its hook returned is +//! rejected with `VM_EXEC2_STALE_CONTEXT` instead of dereferencing a dead borrow. +//! +//! 2. **Exclusivity.** Contexts are activated in a stack, one per nesting level, so a hook +//! that re-enters the VM (`executeOnDestContext` running a contract from inside a hook) +//! cannot clobber its caller's context. Only the innermost is usable, which matches the +//! fact that an outer call's `StoreMut` borrow is not available while an inner call holds +//! one. +//! +//! 3. **Thread affinity.** cgo keeps a goroutine on its OS thread for the duration of a +//! call, so the hook itself is fine. A goroutine *spawned* inside a hook must never touch +//! the context: the pool is not synchronised, and the token check is not a substitute for +//! a lock. This is an invariant on the caller, stated here and in the generated header. +//! +//! 4. **No panics.** Handled by `capi_safe_unwind` on every entry point, not here. +//! +//! The contexts themselves are individually boxed and kept in a pool for the lifetime of the +//! instance, rather than living on the Rust stack. A stack-allocated context would leave the +//! Go side holding a pointer into a frame that has since been reused, so even reading the +//! token to reject it would be undefined behaviour. Pooling costs one allocation per nesting +//! level, reused across calls, and makes the rejection path well-defined. + +use std::cell::RefCell; +use std::ffi::c_void; + +use multiversx_chain_vm_executor::{VMHooks, VMHooksDefault}; +use multiversx_chain_vm_executor_wasmer_experimental::{ + ExperimentalInstanceState, ExperimentalVMHooksBuilder, +}; + +use crate::capi_hook_table::CapiHookTable; + +/// The value of `vm_exec2_hook_result_t::early_exit` that means "the hook returned normally". +/// +/// A dedicated sentinel is needed because `0` is a real early-exit code: the Rust VM uses +/// `ASYNC_CALL_EARLY_EXIT_CODE = 0` and the Go VM has the same async-call case, so "code 0" +/// and "no code" have to stay distinguishable. +pub const VM_EXEC2_NO_EARLY_EXIT: u64 = u64::MAX; + +/// What every VM hook returns. +/// +/// One 16-byte struct covers every hook shape — `void`, `i32`, `i64` — and is returned in +/// registers under both SysV AMD64 (RAX:RDX) and AAPCS64 (X0:X1), so the early-exit channel +/// costs no memory traffic. A cgo `//export` function may return a C struct by value, which +/// is what makes this shape available to the Go side. +/// +/// The early-exit *message* does not travel in this struct. A pointer cannot: cgo forbids +/// returning an unpinned Go pointer, and a fixed buffer cannot work because the messages are +/// unbounded user text. The Go VM does not need it to travel, because the hook has already +/// recorded the message in its own output state by the time it returns. +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct vm_exec2_hook_result_t { + /// The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for + /// any hook that reports an early exit. + pub value: i64, + + /// The early-exit code, or `VM_EXEC2_NO_EARLY_EXIT`. + pub early_exit: u64, +} + +impl vm_exec2_hook_result_t { + /// The hook returned normally, with the given value (`0` for a `void` hook). + pub fn ok(value: i64) -> Self { + vm_exec2_hook_result_t { + value, + early_exit: VM_EXEC2_NO_EARLY_EXIT, + } + } + + /// The hook requested an early exit with the given code. + pub fn early_exit(code: u64) -> Self { + vm_exec2_hook_result_t { + value: 0, + early_exit: code, + } + } + + /// The early-exit code, if there is one. + pub fn early_exit_code(self) -> Option { + if self.early_exit == VM_EXEC2_NO_EARLY_EXIT { + None + } else { + Some(self.early_exit) + } + } +} + +/// What a VM hook is handed as its first argument. +/// +/// Only `go_vm_hooks` is part of the layout the Go side mirrors; the rest is private to +/// Rust, and the whole pointer is what travels back into the state functions. +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct vm_exec2_hook_context_t { + /// The Go VM hooks object for this instance. Read by the Go side, never by Rust. + pub go_vm_hooks: *mut c_void, + + /// Erased `&mut ExperimentalInstanceState<'_>`; null while no call is active. + state: *mut c_void, + + /// Erased `&RefCell`, for the token check. + pool: *const c_void, + + /// This activation's call token; `0` while no call is active. + token: u64, +} + +/// Lifetime-erased form of the instance state pointer kept in a context. +/// +/// The real type is `&mut ExperimentalInstanceState<'s>` for the `'s` of the host-call frame. +/// That lifetime cannot be expressed across C, so it is erased here and enforced at runtime +/// by the call token. +type ErasedInstanceState = ExperimentalInstanceState<'static>; + +impl vm_exec2_hook_context_t { + fn inactive() -> Self { + vm_exec2_hook_context_t { + go_vm_hooks: std::ptr::null_mut(), + state: std::ptr::null_mut(), + pool: std::ptr::null(), + token: 0, + } + } +} + +/// Borrows the instance state out of a context, after checking that the context is live. +/// +/// This is the one place the erased pointer is reconstituted, so it is the one place the +/// token check has to be right. +/// +/// # Safety +/// +/// `ctx` must either be null or point to a `vm_exec2_hook_context_t` that this crate +/// activated. Pooled contexts are never freed before their instance, so a context pointer +/// obtained from a hook always satisfies this, live or not. +pub(crate) unsafe fn with_instance_state( + ctx: *mut vm_exec2_hook_context_t, + f: impl FnOnce(&mut &mut ExperimentalInstanceState<'static>) -> R, +) -> Result { + if ctx.is_null() { + return Err(ContextError::NullContext); + } + + let ctx = unsafe { &*ctx }; + if ctx.token == 0 || ctx.state.is_null() || ctx.pool.is_null() { + return Err(ContextError::Stale); + } + + // Shared borrow only, and released before `f` runs, so a hook is free to re-enter the + // VM and activate a nested context while this one is being read. + let active_token = { + let pool = unsafe { &*(ctx.pool as *const RefCell) }; + pool.borrow().active_token + }; + if ctx.token != active_token { + return Err(ContextError::Stale); + } + + let mut state: &mut ErasedInstanceState = + unsafe { &mut *(ctx.state as *mut ErasedInstanceState) }; + Ok(f(&mut state)) +} + +/// Why a context could not be used. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ContextError { + NullContext, + Stale, +} + +/// The per-instance stack of hook contexts. +#[derive(Default)] +pub struct CapiHookContextPool { + /// One boxed context per nesting level. + /// + /// The `Box` is load-bearing and not the redundancy clippy takes it for: a + /// `Vec` would move its elements when it grows, invalidating + /// context pointers that outer hook invocations are still holding. Boxing each context + /// individually pins its address for the instance's lifetime, which is what lets a + /// stale pointer be read and rejected instead of being undefined behaviour. + #[allow(clippy::vec_box)] + slots: Vec>, + + /// Nesting depth of the currently active call. + depth: usize, + + /// Monotonic token source. Never reused, so a context from a finished call can never + /// coincide with a later one. + next_token: u64, + + /// Token of the innermost active call, or `0` when no call is active. + active_token: u64, +} + +/// Builds the VM hooks for one host call, and owns the context lifecycle. +pub struct CapiVMHooksBuilder { + go_vm_hooks: *mut c_void, + #[allow(dead_code)] // Consumed by the generated hooks implementation; see `with_vm_hooks`. + hook_table: CapiHookTable, + pool: RefCell, +} + +impl CapiVMHooksBuilder { + pub fn new(go_vm_hooks: *mut c_void, hook_table: CapiHookTable) -> Self { + CapiVMHooksBuilder { + go_vm_hooks, + hook_table, + pool: RefCell::new(CapiHookContextPool::default()), + } + } + + /// Activates a context around `instance_state` and runs `f` with it. + /// + /// The context is deactivated before this returns, whether `f` panics or not, so a + /// pointer that escapes is guaranteed to fail the token check rather than reach a dead + /// borrow. + pub fn with_hook_context( + &self, + instance_state: &mut ExperimentalInstanceState<'_>, + f: impl FnOnce(*mut vm_exec2_hook_context_t) -> R, + ) -> R { + let state_ptr = (instance_state as *mut ExperimentalInstanceState<'_>).cast::(); + + // Safety: the pointer is derived from a live `&mut` that outlives this call, and the + // context holding it is deactivated before we return. + unsafe { self.with_hook_context_raw(state_ptr, f) } + } + + /// [`Self::with_hook_context`] over an already-erased state pointer. + /// + /// This is the seam the generated hooks implementation and the boundary tests use: the + /// typed entry point above is the only place that knows the real state type, and + /// everything downstream of here deals in the erased pointer anyway. + /// + /// # Safety + /// + /// `state` must point to a live `ExperimentalInstanceState` for the whole of `f`, unless + /// `f` never reaches the state through the context. + pub unsafe fn with_hook_context_raw( + &self, + state: *mut c_void, + f: impl FnOnce(*mut vm_exec2_hook_context_t) -> R, + ) -> R { + let (ctx_ptr, parent_token) = { + let mut pool = self.pool.borrow_mut(); + + let depth = pool.depth; + if depth == pool.slots.len() { + pool.slots + .push(Box::new(vm_exec2_hook_context_t::inactive())); + } + + pool.next_token += 1; + let token = pool.next_token; + let parent_token = pool.active_token; + pool.active_token = token; + pool.depth = depth + 1; + + let pool_ptr = &self.pool as *const RefCell as *const c_void; + + let slot = &mut pool.slots[depth]; + slot.go_vm_hooks = self.go_vm_hooks; + slot.state = state; + slot.pool = pool_ptr; + slot.token = token; + + (&mut **slot as *mut vm_exec2_hook_context_t, parent_token) + }; + + // The pool borrow is released across `f`, because the hook will call back into the + // state functions, which borrow it again. + let guard = DeactivateOnDrop { + builder: self, + ctx_ptr, + parent_token, + }; + let result = f(ctx_ptr); + drop(guard); + + result + } + + fn deactivate(&self, ctx_ptr: *mut vm_exec2_hook_context_t, parent_token: u64) { + let mut pool = self.pool.borrow_mut(); + pool.depth = pool.depth.saturating_sub(1); + pool.active_token = parent_token; + + // Clearing the slot is belt and braces: the token check alone already rejects it. + // It matters because it makes a stale read fail on the cheap check rather than on + // the pool lookup, and leaves no dangling state pointer behind in the pool. + let ctx = unsafe { &mut *ctx_ptr }; + ctx.state = std::ptr::null_mut(); + ctx.token = 0; + } +} + +/// Runs the deactivation even if the hook body unwinds. +struct DeactivateOnDrop<'a> { + builder: &'a CapiVMHooksBuilder, + ctx_ptr: *mut vm_exec2_hook_context_t, + parent_token: u64, +} + +impl Drop for DeactivateOnDrop<'_> { + fn drop(&mut self) { + self.builder.deactivate(self.ctx_ptr, self.parent_token); + } +} + +impl ExperimentalVMHooksBuilder for CapiVMHooksBuilder { + fn with_vm_hooks( + &self, + instance_state_ref: &mut ExperimentalInstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ) { + self.with_hook_context(instance_state_ref, |_ctx| { + // TODO(Phase 3): replace with the generated `CapiVMHooks` built from `_ctx` and + // `self.hook_table`, which dispatches each of the 277 hooks to its Go function + // pointer and turns a non-sentinel `early_exit` into `Err(VMHooksEarlyExit)`. + // Everything that implementation needs — the context, the table, the token + // lifecycle — is in place; the 277-method `VMHooks` impl is generated code and + // does not belong in a hand-written file. + f(&mut VMHooksDefault); + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hook_result_distinguishes_code_zero_from_no_code() { + assert_eq!(vm_exec2_hook_result_t::ok(7).early_exit_code(), None); + assert_eq!( + vm_exec2_hook_result_t::early_exit(0).early_exit_code(), + Some(0) + ); + assert_eq!( + vm_exec2_hook_result_t::early_exit(4).early_exit_code(), + Some(4) + ); + } + + #[test] + fn hook_result_fits_in_two_registers() { + assert_eq!(std::mem::size_of::(), 16); + assert_eq!(std::mem::align_of::(), 8); + } + + #[test] + fn null_context_is_rejected() { + let result = unsafe { with_instance_state(std::ptr::null_mut(), |_| ()) }; + assert_eq!(result, Err(ContextError::NullContext)); + } + + #[test] + fn inactive_context_is_rejected() { + let mut ctx = vm_exec2_hook_context_t::inactive(); + let result = unsafe { with_instance_state(&mut ctx, |_| ()) }; + assert_eq!(result, Err(ContextError::Stale)); + } +} diff --git a/c-api-new/src/capi_hook_table.rs b/c-api-new/src/capi_hook_table.rs new file mode 100644 index 00000000..7a068a12 --- /dev/null +++ b/c-api-new/src/capi_hook_table.rs @@ -0,0 +1,96 @@ +//! The Go VM hook function-pointer table, owned by Rust. +//! +//! The table is **copied** at executor creation rather than referenced, exactly as the +//! legacy API does. Holding the caller's pointer instead would make the executor's validity +//! depend on the caller keeping a Go-side struct alive and unmoved for the executor's whole +//! lifetime, which is not a contract worth having at this boundary. +//! +//! Phase 2 copies it as opaque pointer-sized words, because the typed struct — one correctly +//! typed `extern "C" fn` per hook — is generated code that arrives with Phase 3. The copy is +//! what the generated implementation will read through, so the ownership and lifetime story +//! does not change when the type does. + +use std::ffi::c_void; +use std::sync::Arc; + +/// An owned copy of the caller's hook table. +/// +/// Cloning is cheap and shares the copy; each instance's hooks builder holds one. +#[derive(Clone)] +pub struct CapiHookTable { + words: Arc<[*const c_void]>, +} + +// The table is a block of plain function pointers with no interior mutability. The +// `ExperimentalExecutorRuntimeRef` it ends up behind is `Send + Sync`, so this has to be +// too; the pointers themselves are only ever read, and only from the thread servicing a +// host call. +unsafe impl Send for CapiHookTable {} +unsafe impl Sync for CapiHookTable {} + +impl CapiHookTable { + /// Copies `len` pointer-sized words out of `table`. + /// + /// # Safety + /// + /// `table` must point to at least `len` readable pointer-sized words. + pub unsafe fn copy_from(table: *const c_void, len: usize) -> Self { + let words: Arc<[*const c_void]> = if table.is_null() || len == 0 { + Arc::from(Vec::new()) + } else { + let src = unsafe { std::slice::from_raw_parts(table as *const *const c_void, len) }; + Arc::from(src.to_vec()) + }; + + CapiHookTable { words } + } + + /// Number of pointer-sized words copied. + pub fn len(&self) -> usize { + self.words.len() + } + + pub fn is_empty(&self) -> bool { + self.words.is_empty() + } + + /// The `index`-th function pointer, or `None` if the table is shorter than that. + /// + /// Only used by the generated hooks implementation, which knows the real layout. + pub fn word(&self, index: usize) -> Option<*const c_void> { + self.words.get(index).copied() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn copies_the_caller_s_words() { + // Real addresses rather than small integers, so the test does not depend on what a + // fabricated pointer value is allowed to be. + let targets = [10u64, 20u64, 30u64]; + let table: [*const c_void; 3] = [ + (&targets[0] as *const u64).cast(), + (&targets[1] as *const u64).cast(), + (&targets[2] as *const u64).cast(), + ]; + let copied = unsafe { CapiHookTable::copy_from(table.as_ptr() as *const c_void, 3) }; + + assert_eq!(copied.len(), 3); + assert_eq!(copied.word(0), Some(table[0])); + assert_eq!(copied.word(2), Some(table[2])); + assert_eq!(copied.word(3), None); + } + + #[test] + fn tolerates_an_absent_table() { + let empty = unsafe { CapiHookTable::copy_from(std::ptr::null(), 0) }; + assert!(empty.is_empty()); + assert_eq!(empty.word(0), None); + + let null_with_len = unsafe { CapiHookTable::copy_from(std::ptr::null(), 10) }; + assert!(null_with_len.is_empty()); + } +} diff --git a/c-api-new/src/capi_instance.rs b/c-api-new/src/capi_instance.rs new file mode 100644 index 00000000..55e14f5f --- /dev/null +++ b/c-api-new/src/capi_instance.rs @@ -0,0 +1,393 @@ +//! Instance lifecycle and the outer instance operations. +//! +//! "Outer" is the side of the trait split that the VM sees from outside a host call: +//! calling, gas accounting, and module introspection. Everything a *hook* needs is in +//! `capi_instance_state`, reached through a hook context rather than through this handle. +//! Keeping the split is what makes the Go side match the Rust trait pair instead of merely +//! wrapping it. + +use libc::{c_char, c_int}; +use meta::capi_safe_unwind; +use multiversx_chain_vm_executor::{BreakpointValue, Instance, InstanceCallResult}; +use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalInstance; +use std::ffi::{CStr, c_void}; +use std::slice; + +use crate::capi_config::vm_exec2_compilation_options_t; +use crate::capi_executor::{CapiExecutor, vm_exec2_executor_t}; +use crate::capi_hook_context::CapiVMHooksBuilder; +use crate::capi_status::{vm_exec2_status_t, write_error}; +use crate::capi_strings::{copy_pipe_joined, pipe_joined_length}; + +/// Opaque handle to an instance. +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct vm_exec2_instance_t; + +pub struct CapiInstance { + pub(crate) content: ExperimentalInstance, +} + +/// How a call ended. +/// +/// This replaces the legacy arrangement, where a call returned a bare ok/error and the VM +/// then polled a wasm global to find out what had actually happened. +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum vm_exec2_call_result_kind_t { + /// The function returned normally. + VM_EXEC2_CALL_OK = 0, + + /// No exported function by that name. + VM_EXEC2_CALL_FUNCTION_NOT_FOUND = 1, + + /// The instance trapped for a reason that is neither a VM hook's doing nor a + /// middleware breakpoint. The message is in the error buffer. + VM_EXEC2_CALL_RUNTIME_ERROR = 2, + + /// A VM hook asked for execution to stop. `early_exit_code` says which; the message, if + /// any, is in the error buffer, and is informational — the VM has already recorded its + /// own. + VM_EXEC2_CALL_EARLY_EXIT = 3, + + /// A middleware stopped execution. `breakpoint` says which. + VM_EXEC2_CALL_BREAKPOINT = 4, +} + +/// The out-struct of `vm_exec2_instance_call`. +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct vm_exec2_call_result_t { + pub kind: vm_exec2_call_result_kind_t, + + /// Meaningful only for `VM_EXEC2_CALL_EARLY_EXIT`. + pub early_exit_code: u64, + + /// Meaningful only for `VM_EXEC2_CALL_BREAKPOINT`. Values match `BreakpointValue`: + /// 4 = out of gas, 5 = memory limit. + pub breakpoint: u64, +} + +impl vm_exec2_call_result_t { + fn of_kind(kind: vm_exec2_call_result_kind_t) -> Self { + vm_exec2_call_result_t { + kind, + early_exit_code: 0, + breakpoint: 0, + } + } +} + +/// Creates an instance. +/// +/// `go_vm_hooks` is the Go VM hooks object for *this* instance; it is handed back to every +/// VM hook as field 0 of the hook context. It must stay alive for the instance's lifetime. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[allow(clippy::too_many_arguments)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] +pub unsafe extern "C" fn vm_exec2_new_instance( + executor: *mut vm_exec2_executor_t, + instance_out: *mut *mut vm_exec2_instance_t, + wasm_bytes: *const u8, + wasm_bytes_len: u32, + options: *const vm_exec2_compilation_options_t, + go_vm_hooks: *mut c_void, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + if executor.is_null() || instance_out.is_null() || wasm_bytes.is_null() || options.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + } + + let capi_executor = unsafe { &*(executor as *const CapiExecutor) }; + let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_len as usize) }; + let compilation_options = unsafe { (*options).to_compilation_options() }; + + if let Err(err) = multiversx_chain_vm_executor::check_missing_wasm(wasm_bytes) { + return write_error(err, err_buf, err_buf_len); + } + + let builder = CapiVMHooksBuilder::new(go_vm_hooks, capi_executor.hook_table.clone()); + + let instance = match ExperimentalInstance::try_new_instance( + Box::new(builder), + capi_executor.opcode_config.clone(), + wasm_bytes, + &compilation_options, + ) { + Ok(instance) => instance, + Err(err) => return write_error(err, err_buf, err_buf_len), + }; + + unsafe { + *instance_out = + Box::into_raw(Box::new(CapiInstance { content: instance })) as *mut vm_exec2_instance_t; + } + + vm_exec2_status_t::VM_EXEC2_OK +} + +/// Destroys an instance. Does nothing if `instance` is null. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn vm_exec2_instance_destroy(instance: *mut vm_exec2_instance_t) { + if !instance.is_null() { + drop(unsafe { Box::from_raw(instance as *mut CapiInstance) }); + } +} + +/// Calls an exported function, with a gas limit for this call. +/// +/// Always writes `result_out` when it returns `VM_EXEC2_OK`. A non-`OK` status means the +/// call could not be attempted at all, which is distinct from a call that was attempted and +/// failed — the latter is reported through `result_out.kind`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] +pub unsafe extern "C" fn vm_exec2_instance_call( + instance: *mut vm_exec2_instance_t, + func_name: *const c_char, + points_limit: u64, + result_out: *mut vm_exec2_call_result_t, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + if instance.is_null() || func_name.is_null() || result_out.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + } + + let capi_instance = unsafe { &mut *(instance as *mut CapiInstance) }; + + let Ok(func_name) = unsafe { CStr::from_ptr(func_name) }.to_str() else { + return write_error("function name is not valid UTF-8", err_buf, err_buf_len); + }; + + let result = match capi_instance.content.call(func_name, points_limit) { + InstanceCallResult::Ok => { + vm_exec2_call_result_t::of_kind(vm_exec2_call_result_kind_t::VM_EXEC2_CALL_OK) + } + InstanceCallResult::FunctionNotFound => vm_exec2_call_result_t::of_kind( + vm_exec2_call_result_kind_t::VM_EXEC2_CALL_FUNCTION_NOT_FOUND, + ), + InstanceCallResult::RuntimeError(err) => { + write_error(err, err_buf, err_buf_len); + vm_exec2_call_result_t::of_kind( + vm_exec2_call_result_kind_t::VM_EXEC2_CALL_RUNTIME_ERROR, + ) + } + InstanceCallResult::VMHooksEarlyExit(early_exit) => { + // Informational: the hook has already recorded its own message on the VM side. + write_error(&early_exit.message, err_buf, err_buf_len); + vm_exec2_call_result_t { + kind: vm_exec2_call_result_kind_t::VM_EXEC2_CALL_EARLY_EXIT, + early_exit_code: early_exit.code, + breakpoint: 0, + } + } + InstanceCallResult::Breakpoint(breakpoint) => vm_exec2_call_result_t { + kind: vm_exec2_call_result_kind_t::VM_EXEC2_CALL_BREAKPOINT, + early_exit_code: 0, + breakpoint: breakpoint_as_u64(breakpoint), + }, + }; + + unsafe { + *result_out = result; + } + + vm_exec2_status_t::VM_EXEC2_OK +} + +fn breakpoint_as_u64(breakpoint: BreakpointValue) -> u64 { + breakpoint.as_u64() +} + +/// Returns the number of points (gas) used so far. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] +pub unsafe extern "C" fn vm_exec2_instance_get_points_used( + instance: *mut vm_exec2_instance_t, + points_out: *mut u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + if instance.is_null() || points_out.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + } + + let capi_instance = unsafe { &mut *(instance as *mut CapiInstance) }; + match capi_instance.content.get_points_used() { + Ok(points) => { + unsafe { + *points_out = points; + } + vm_exec2_status_t::VM_EXEC2_OK + } + Err(err) => write_error(err, err_buf, err_buf_len), + } +} + +/// Checks that every exported function takes no arguments and returns nothing. +/// +/// Returns 1 if so, 0 if not, or a negative status. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec2_instance_check_signatures( + instance: *const vm_exec2_instance_t, +) -> c_int { + if instance.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*(instance as *const CapiInstance) }; + c_int::from(capi_instance.content.check_signatures()) +} + +/// Checks whether the contract exports a function with the given name. +/// +/// Returns 1 if so, 0 if not, or a negative status. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec2_instance_has_function( + instance: *const vm_exec2_instance_t, + func_name: *const c_char, +) -> c_int { + if instance.is_null() || func_name.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*(instance as *const CapiInstance) }; + let Ok(func_name) = unsafe { CStr::from_ptr(func_name) }.to_str() else { + return vm_exec2_status_t::VM_EXEC2_ERROR.as_c_int(); + }; + + c_int::from(capi_instance.content.has_function(func_name)) +} + +/// Buffer length needed by `vm_exec2_instance_exported_function_names`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec2_instance_exported_function_names_length( + instance: *const vm_exec2_instance_t, +) -> c_int { + if instance.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*(instance as *const CapiInstance) }; + pipe_joined_length(&capi_instance.content.get_exported_function_names()) +} + +/// Writes the exported function names, pipe-separated, e.g. `"init|endpoint1|endpoint2"`. +/// +/// No ordering is guaranteed. Returns the number of bytes written including the terminator, +/// or `-1` if the buffer is absent or too small. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec2_instance_exported_function_names( + instance: *const vm_exec2_instance_t, + dest: *mut c_char, + dest_len: c_int, +) -> c_int { + if instance.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*(instance as *const CapiInstance) }; + unsafe { + copy_pipe_joined( + &capi_instance.content.get_exported_function_names(), + dest, + dest_len, + ) + } +} + +/// Buffer length needed by `vm_exec2_instance_imported_function_names`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec2_instance_imported_function_names_length( + instance: *const vm_exec2_instance_t, +) -> c_int { + if instance.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*(instance as *const CapiInstance) }; + pipe_joined_length(&capi_instance.content.get_imported_function_names()) +} + +/// Writes the names of the VM hooks the contract imports, pipe-separated. +/// +/// All of them at once, rather than a per-name query, because the Go contract validator +/// checks the module against whole sets of allowed names. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec2_instance_imported_function_names( + instance: *const vm_exec2_instance_t, + dest: *mut c_char, + dest_len: c_int, +) -> c_int { + if instance.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*(instance as *const CapiInstance) }; + unsafe { + copy_pipe_joined( + &capi_instance.content.get_imported_function_names(), + dest, + dest_len, + ) + } +} diff --git a/c-api-new/src/capi_instance_state.rs b/c-api-new/src/capi_instance_state.rs new file mode 100644 index 00000000..80ddcb66 --- /dev/null +++ b/c-api-new/src/capi_instance_state.rs @@ -0,0 +1,238 @@ +//! The state operations: what a VM hook may do to the instance it is running inside. +//! +//! Every function here takes a hook context and is valid only while the hook that was handed +//! that context is still running. See `capi_hook_context` for how that is enforced; a stale +//! context yields `VM_EXEC2_STALE_CONTEXT` rather than a dereference of a dead borrow. +//! +//! Bounds checking lives on this side of the boundary, not in Go. The executor is the only +//! side that knows the real extent of the instance memory, and the rules are observable by +//! contracts, so there is one implementation of them — shared with the other backends in +//! `vm-executor/src/new_traits/memory.rs` — rather than one per caller. + +use libc::{c_char, c_int}; +use meta::capi_safe_unwind; +use multiversx_chain_vm_executor::{ + ExecutorError, InstanceState, MemLength, MemPtr, ServiceError, clamped_load_len, +}; +use std::slice; + +use crate::capi_hook_context::{ContextError, vm_exec2_hook_context_t, with_instance_state}; +use crate::capi_status::{vm_exec2_status_t, write_error, write_error_count}; + +impl ContextError { + fn as_status(self) -> vm_exec2_status_t { + match self { + ContextError::NullContext => vm_exec2_status_t::VM_EXEC2_NULL_POINTER, + ContextError::Stale => vm_exec2_status_t::VM_EXEC2_STALE_CONTEXT, + } + } +} + +/// Loads from instance memory into a caller-provided buffer, **truncating** at the end of +/// the memory. +/// +/// Returns the number of bytes actually written, which may be fewer than `dest_len`, or a +/// negative status. The truncation is where the Go VM's historical load semantics live: a +/// load whose offset is valid but whose end lies past the end of the memory yields the bytes +/// that are available and no error. Contracts have observed that, so the executor keeps +/// offering it. An offset outside the memory, or a negative length, is still an error. +/// +/// `dest_len` doubles as the requested length, which is what makes the clamping visible to +/// the caller: Go allocates `length` bytes and gets back `min(length, available)`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_i64())] +pub unsafe extern "C" fn vm_exec2_state_mem_load( + ctx: *mut vm_exec2_hook_context_t, + mem_ptr: i32, + dest: *mut u8, + dest_len: i32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> i64 { + let loaded = unsafe { + with_instance_state(ctx, |state| -> Result { + let memory_length = state.memory_length()?; + let to_load = + clamped_load_len(mem_ptr as MemPtr, dest_len as MemLength, memory_length)?; + if to_load == 0 { + return Ok(0); + } + + if dest.is_null() { + return Err(Box::new(ServiceError::new("destination buffer is null"))); + } + + let dest = slice::from_raw_parts_mut(dest, to_load); + state.memory_load_to_slice(mem_ptr as MemPtr, dest)?; + Ok(to_load) + }) + }; + + match loaded { + Err(context_err) => context_err.as_status().as_i64(), + Ok(Err(err)) => write_error_count(err, err_buf, err_buf_len), + Ok(Ok(to_load)) => to_load as i64, + } +} + +/// Writes to instance memory. The range must fit entirely inside the memory. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] +pub unsafe extern "C" fn vm_exec2_state_mem_store( + ctx: *mut vm_exec2_hook_context_t, + mem_ptr: i32, + src: *const u8, + src_len: i32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + if src_len < 0 { + return write_error("negative length", err_buf, err_buf_len); + } + if src.is_null() && src_len > 0 { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + } + + let stored = unsafe { + with_instance_state(ctx, |state| { + let data = if src_len == 0 { + &[][..] + } else { + slice::from_raw_parts(src, src_len as usize) + }; + state.memory_store(mem_ptr as MemPtr, data) + }) + }; + + match stored { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(())) => vm_exec2_status_t::VM_EXEC2_OK, + } +} + +/// Returns the size in bytes of the instance memory. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] +pub unsafe extern "C" fn vm_exec2_state_mem_length( + ctx: *mut vm_exec2_hook_context_t, + length_out: *mut u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + if length_out.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + } + + let length = unsafe { with_instance_state(ctx, |state| state.memory_length()) }; + + match length { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(length)) => { + unsafe { + *length_out = length; + } + vm_exec2_status_t::VM_EXEC2_OK + } + } +} + +/// Grows the instance memory by `by_num_pages` pages of 64Kb each. +/// +/// Writes the number of pages the memory had *before* growing to `prev_pages_out`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] +pub unsafe extern "C" fn vm_exec2_state_mem_grow( + ctx: *mut vm_exec2_hook_context_t, + by_num_pages: u32, + prev_pages_out: *mut u32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + if prev_pages_out.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + } + + let grown = unsafe { with_instance_state(ctx, |state| state.memory_grow(by_num_pages)) }; + + match grown { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(prev_pages)) => { + unsafe { + *prev_pages_out = prev_pages; + } + vm_exec2_status_t::VM_EXEC2_OK + } + } +} + +/// Returns the number of points (gas) used by the instance so far. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] +pub unsafe extern "C" fn vm_exec2_state_get_points_used( + ctx: *mut vm_exec2_hook_context_t, + points_out: *mut u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + if points_out.is_null() { + return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + } + + let points = unsafe { with_instance_state(ctx, |state| state.get_points_used()) }; + + match points { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(points)) => { + unsafe { + *points_out = points; + } + vm_exec2_status_t::VM_EXEC2_OK + } + } +} + +/// Sets the number of points (gas) used by the instance. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] +pub unsafe extern "C" fn vm_exec2_state_set_points_used( + ctx: *mut vm_exec2_hook_context_t, + points: u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + let set = unsafe { with_instance_state(ctx, |state| state.set_points_used(points)) }; + + match set { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(())) => vm_exec2_status_t::VM_EXEC2_OK, + } +} diff --git a/c-api-new/src/capi_logger.rs b/c-api-new/src/capi_logger.rs new file mode 100644 index 00000000..90a238e7 --- /dev/null +++ b/c-api-new/src/capi_logger.rs @@ -0,0 +1,20 @@ +//! Panic suppression at the C boundary. +//! +//! The module is named `capi_logger` because that is the path the shared +//! `meta::capi_safe_unwind` attribute expands to. It deliberately does not carry the legacy +//! crate's logging FFI: log configuration belongs to the process, and the legacy `c-api` +//! already owns those symbols. + +use std::sync::Once; + +static PANIC_HANDLER: Once = Once::new(); + +/// Installs a no-op panic hook, so a caught panic does not also print a backtrace. +/// +/// A panic must never unwind across `extern "C"`; the `capi_safe_unwind` attribute catches +/// it and turns it into `VM_EXEC2_PANIC`. This only silences the default hook's output. +pub fn set_panic_handler() { + PANIC_HANDLER.call_once(|| { + std::panic::set_hook(Box::new(|_| {})); + }); +} diff --git a/c-api-new/src/capi_opcode_cost.rs b/c-api-new/src/capi_opcode_cost.rs new file mode 100644 index 00000000..26abdb2c --- /dev/null +++ b/c-api-new/src/capi_opcode_cost.rs @@ -0,0 +1,288 @@ +// Code generated by vmhooks generator. DO NOT EDIT. + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +use multiversx_chain_vm_executor::OpcodeCost; + +/// FFI-safe mirror of `OpcodeCost`. +/// +/// This is a distinct type rather than a cast of `OpcodeCost` itself, so that +/// the general-purpose struct is free to change shape without touching the C +/// ABI, and vice versa. +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct vm_exec_opcode_cost_t { + pub opcode_block: u32, + pub opcode_br: u32, + pub opcode_brif: u32, + pub opcode_brtable: u32, + pub opcode_call: u32, + pub opcode_callindirect: u32, + pub opcode_catch: u32, + pub opcode_catchall: u32, + pub opcode_delegate: u32, + pub opcode_drop: u32, + pub opcode_else: u32, + pub opcode_end: u32, + pub opcode_globalget: u32, + pub opcode_globalset: u32, + pub opcode_i32add: u32, + pub opcode_i32and: u32, + pub opcode_i32clz: u32, + pub opcode_i32const: u32, + pub opcode_i32ctz: u32, + pub opcode_i32divs: u32, + pub opcode_i32divu: u32, + pub opcode_i32eq: u32, + pub opcode_i32eqz: u32, + pub opcode_i32extend16s: u32, + pub opcode_i32extend8s: u32, + pub opcode_i32ges: u32, + pub opcode_i32geu: u32, + pub opcode_i32gts: u32, + pub opcode_i32gtu: u32, + pub opcode_i32les: u32, + pub opcode_i32leu: u32, + pub opcode_i32load: u32, + pub opcode_i32load16s: u32, + pub opcode_i32load16u: u32, + pub opcode_i32load8s: u32, + pub opcode_i32load8u: u32, + pub opcode_i32lts: u32, + pub opcode_i32ltu: u32, + pub opcode_i32mul: u32, + pub opcode_i32ne: u32, + pub opcode_i32or: u32, + pub opcode_i32popcnt: u32, + pub opcode_i32rems: u32, + pub opcode_i32remu: u32, + pub opcode_i32rotl: u32, + pub opcode_i32rotr: u32, + pub opcode_i32shl: u32, + pub opcode_i32shrs: u32, + pub opcode_i32shru: u32, + pub opcode_i32store: u32, + pub opcode_i32store16: u32, + pub opcode_i32store8: u32, + pub opcode_i32sub: u32, + pub opcode_i32wrapi64: u32, + pub opcode_i32xor: u32, + pub opcode_i64add: u32, + pub opcode_i64and: u32, + pub opcode_i64clz: u32, + pub opcode_i64const: u32, + pub opcode_i64ctz: u32, + pub opcode_i64divs: u32, + pub opcode_i64divu: u32, + pub opcode_i64eq: u32, + pub opcode_i64eqz: u32, + pub opcode_i64extend16s: u32, + pub opcode_i64extend32s: u32, + pub opcode_i64extend8s: u32, + pub opcode_i64extendi32s: u32, + pub opcode_i64extendi32u: u32, + pub opcode_i64ges: u32, + pub opcode_i64geu: u32, + pub opcode_i64gts: u32, + pub opcode_i64gtu: u32, + pub opcode_i64les: u32, + pub opcode_i64leu: u32, + pub opcode_i64load: u32, + pub opcode_i64load16s: u32, + pub opcode_i64load16u: u32, + pub opcode_i64load32s: u32, + pub opcode_i64load32u: u32, + pub opcode_i64load8s: u32, + pub opcode_i64load8u: u32, + pub opcode_i64lts: u32, + pub opcode_i64ltu: u32, + pub opcode_i64mul: u32, + pub opcode_i64ne: u32, + pub opcode_i64or: u32, + pub opcode_i64popcnt: u32, + pub opcode_i64rems: u32, + pub opcode_i64remu: u32, + pub opcode_i64rotl: u32, + pub opcode_i64rotr: u32, + pub opcode_i64shl: u32, + pub opcode_i64shrs: u32, + pub opcode_i64shru: u32, + pub opcode_i64store: u32, + pub opcode_i64store16: u32, + pub opcode_i64store32: u32, + pub opcode_i64store8: u32, + pub opcode_i64sub: u32, + pub opcode_i64xor: u32, + pub opcode_if: u32, + pub opcode_localget: u32, + pub opcode_localset: u32, + pub opcode_localtee: u32, + pub opcode_loop: u32, + pub opcode_memorygrow: u32, + pub opcode_memorysize: u32, + pub opcode_nop: u32, + pub opcode_reffunc: u32, + pub opcode_refisnull: u32, + pub opcode_refnull: u32, + pub opcode_rethrow: u32, + pub opcode_return: u32, + pub opcode_returncall: u32, + pub opcode_returncallindirect: u32, + pub opcode_select: u32, + pub opcode_tableget: u32, + pub opcode_tablegrow: u32, + pub opcode_tableinit: u32, + pub opcode_tableset: u32, + pub opcode_tablesize: u32, + pub opcode_throw: u32, + pub opcode_try: u32, + pub opcode_typedselect: u32, + pub opcode_unreachable: u32, + pub opcode_unwind: u32, + pub opcode_memorycopy: u32, + pub opcode_memoryfill: u32, + pub opcode_memorycopyperbyte: u32, + pub opcode_memoryfillperbyte: u32, + pub opcode_localallocate: u32, +} + +impl vm_exec_opcode_cost_t { + pub(crate) fn to_opcode_cost(&self) -> OpcodeCost { + OpcodeCost { + opcode_block: self.opcode_block, + opcode_br: self.opcode_br, + opcode_brif: self.opcode_brif, + opcode_brtable: self.opcode_brtable, + opcode_call: self.opcode_call, + opcode_callindirect: self.opcode_callindirect, + opcode_catch: self.opcode_catch, + opcode_catchall: self.opcode_catchall, + opcode_delegate: self.opcode_delegate, + opcode_drop: self.opcode_drop, + opcode_else: self.opcode_else, + opcode_end: self.opcode_end, + opcode_globalget: self.opcode_globalget, + opcode_globalset: self.opcode_globalset, + opcode_i32add: self.opcode_i32add, + opcode_i32and: self.opcode_i32and, + opcode_i32clz: self.opcode_i32clz, + opcode_i32const: self.opcode_i32const, + opcode_i32ctz: self.opcode_i32ctz, + opcode_i32divs: self.opcode_i32divs, + opcode_i32divu: self.opcode_i32divu, + opcode_i32eq: self.opcode_i32eq, + opcode_i32eqz: self.opcode_i32eqz, + opcode_i32extend16s: self.opcode_i32extend16s, + opcode_i32extend8s: self.opcode_i32extend8s, + opcode_i32ges: self.opcode_i32ges, + opcode_i32geu: self.opcode_i32geu, + opcode_i32gts: self.opcode_i32gts, + opcode_i32gtu: self.opcode_i32gtu, + opcode_i32les: self.opcode_i32les, + opcode_i32leu: self.opcode_i32leu, + opcode_i32load: self.opcode_i32load, + opcode_i32load16s: self.opcode_i32load16s, + opcode_i32load16u: self.opcode_i32load16u, + opcode_i32load8s: self.opcode_i32load8s, + opcode_i32load8u: self.opcode_i32load8u, + opcode_i32lts: self.opcode_i32lts, + opcode_i32ltu: self.opcode_i32ltu, + opcode_i32mul: self.opcode_i32mul, + opcode_i32ne: self.opcode_i32ne, + opcode_i32or: self.opcode_i32or, + opcode_i32popcnt: self.opcode_i32popcnt, + opcode_i32rems: self.opcode_i32rems, + opcode_i32remu: self.opcode_i32remu, + opcode_i32rotl: self.opcode_i32rotl, + opcode_i32rotr: self.opcode_i32rotr, + opcode_i32shl: self.opcode_i32shl, + opcode_i32shrs: self.opcode_i32shrs, + opcode_i32shru: self.opcode_i32shru, + opcode_i32store: self.opcode_i32store, + opcode_i32store16: self.opcode_i32store16, + opcode_i32store8: self.opcode_i32store8, + opcode_i32sub: self.opcode_i32sub, + opcode_i32wrapi64: self.opcode_i32wrapi64, + opcode_i32xor: self.opcode_i32xor, + opcode_i64add: self.opcode_i64add, + opcode_i64and: self.opcode_i64and, + opcode_i64clz: self.opcode_i64clz, + opcode_i64const: self.opcode_i64const, + opcode_i64ctz: self.opcode_i64ctz, + opcode_i64divs: self.opcode_i64divs, + opcode_i64divu: self.opcode_i64divu, + opcode_i64eq: self.opcode_i64eq, + opcode_i64eqz: self.opcode_i64eqz, + opcode_i64extend16s: self.opcode_i64extend16s, + opcode_i64extend32s: self.opcode_i64extend32s, + opcode_i64extend8s: self.opcode_i64extend8s, + opcode_i64extendi32s: self.opcode_i64extendi32s, + opcode_i64extendi32u: self.opcode_i64extendi32u, + opcode_i64ges: self.opcode_i64ges, + opcode_i64geu: self.opcode_i64geu, + opcode_i64gts: self.opcode_i64gts, + opcode_i64gtu: self.opcode_i64gtu, + opcode_i64les: self.opcode_i64les, + opcode_i64leu: self.opcode_i64leu, + opcode_i64load: self.opcode_i64load, + opcode_i64load16s: self.opcode_i64load16s, + opcode_i64load16u: self.opcode_i64load16u, + opcode_i64load32s: self.opcode_i64load32s, + opcode_i64load32u: self.opcode_i64load32u, + opcode_i64load8s: self.opcode_i64load8s, + opcode_i64load8u: self.opcode_i64load8u, + opcode_i64lts: self.opcode_i64lts, + opcode_i64ltu: self.opcode_i64ltu, + opcode_i64mul: self.opcode_i64mul, + opcode_i64ne: self.opcode_i64ne, + opcode_i64or: self.opcode_i64or, + opcode_i64popcnt: self.opcode_i64popcnt, + opcode_i64rems: self.opcode_i64rems, + opcode_i64remu: self.opcode_i64remu, + opcode_i64rotl: self.opcode_i64rotl, + opcode_i64rotr: self.opcode_i64rotr, + opcode_i64shl: self.opcode_i64shl, + opcode_i64shrs: self.opcode_i64shrs, + opcode_i64shru: self.opcode_i64shru, + opcode_i64store: self.opcode_i64store, + opcode_i64store16: self.opcode_i64store16, + opcode_i64store32: self.opcode_i64store32, + opcode_i64store8: self.opcode_i64store8, + opcode_i64sub: self.opcode_i64sub, + opcode_i64xor: self.opcode_i64xor, + opcode_if: self.opcode_if, + opcode_localget: self.opcode_localget, + opcode_localset: self.opcode_localset, + opcode_localtee: self.opcode_localtee, + opcode_loop: self.opcode_loop, + opcode_memorygrow: self.opcode_memorygrow, + opcode_memorysize: self.opcode_memorysize, + opcode_nop: self.opcode_nop, + opcode_reffunc: self.opcode_reffunc, + opcode_refisnull: self.opcode_refisnull, + opcode_refnull: self.opcode_refnull, + opcode_rethrow: self.opcode_rethrow, + opcode_return: self.opcode_return, + opcode_returncall: self.opcode_returncall, + opcode_returncallindirect: self.opcode_returncallindirect, + opcode_select: self.opcode_select, + opcode_tableget: self.opcode_tableget, + opcode_tablegrow: self.opcode_tablegrow, + opcode_tableinit: self.opcode_tableinit, + opcode_tableset: self.opcode_tableset, + opcode_tablesize: self.opcode_tablesize, + opcode_throw: self.opcode_throw, + opcode_try: self.opcode_try, + opcode_typedselect: self.opcode_typedselect, + opcode_unreachable: self.opcode_unreachable, + opcode_unwind: self.opcode_unwind, + opcode_memorycopy: self.opcode_memorycopy, + opcode_memoryfill: self.opcode_memoryfill, + opcode_memorycopyperbyte: self.opcode_memorycopyperbyte, + opcode_memoryfillperbyte: self.opcode_memoryfillperbyte, + opcode_localallocate: self.opcode_localallocate, + } + } +} diff --git a/c-api-new/src/capi_status.rs b/c-api-new/src/capi_status.rs new file mode 100644 index 00000000..a344af98 --- /dev/null +++ b/c-api-new/src/capi_status.rs @@ -0,0 +1,162 @@ +//! Status codes and the out-param error channel. +//! +//! The legacy C API reports errors through a `thread_local!` last-error singleton that the +//! caller reads back in a separate call. That does not survive the move to the new +//! interface: nothing pins a goroutine to the same OS thread between two cgo calls, so the +//! second call can land on a thread whose singleton holds someone else's error, or nothing +//! at all. Here every fallible entry point takes a caller-provided buffer instead, and +//! writes its message into it before returning. + +use libc::{c_char, c_int}; +use std::fmt::Display; +use std::{ptr, slice}; + +/// Result of a C API call. +/// +/// `VM_EXEC2_OK` is zero and every failure is negative, so the functions that return a +/// count (the memory loads) can share the same code space: a non-negative result is a +/// count, a negative one is one of these. +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum vm_exec2_status_t { + /// The call succeeded. + VM_EXEC2_OK = 0, + + /// The call failed; the error message was written to the provided error buffer. + VM_EXEC2_ERROR = -1, + + /// A pointer argument that must not be null was null. No message is produced, because + /// a null pointer may well be the error buffer itself. + VM_EXEC2_NULL_POINTER = -2, + + /// A hook context was used outside the host call it belongs to. See + /// [`crate::capi_hook_context`]. + VM_EXEC2_STALE_CONTEXT = -3, + + /// A panic was caught at the boundary. Rust panics must never unwind into C, so every + /// entry point traps them and reports this instead. + VM_EXEC2_PANIC = -4, +} + +impl vm_exec2_status_t { + pub fn as_c_int(self) -> c_int { + self as c_int + } + + pub fn as_i64(self) -> i64 { + self as i64 + } +} + +/// Writes an error message into a caller-provided buffer. +/// +/// The message is truncated to fit and always NUL-terminated. Truncation is silent and +/// deliberate: the alternative is a second call to ask for the length, which is the +/// round-trip this API is getting rid of. Callers size the buffer once; see +/// `VM_EXEC2_ERROR_BUF_RECOMMENDED_LEN`. +/// +/// A null buffer, or a buffer with no room for the terminator, is accepted and ignored, so +/// that a caller that does not care about the message can pass `(NULL, 0)`. +pub(crate) fn write_error( + message: impl Display, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec2_status_t { + if !err_buf.is_null() && err_buf_len > 0 { + let message = message.to_string(); + let capacity = err_buf_len as usize - 1; // room for the NUL + + // Truncated on a char boundary, so that the buffer always holds valid UTF-8 and the + // Go side never has to deal with a split code point. + let mut copy_len = message.len().min(capacity); + while copy_len > 0 && !message.is_char_boundary(copy_len) { + copy_len -= 1; + } + + unsafe { + let dest = slice::from_raw_parts_mut(err_buf as *mut u8, err_buf_len as usize); + ptr::copy_nonoverlapping(message.as_ptr(), dest.as_mut_ptr(), copy_len); + dest[copy_len] = 0; + } + } + + vm_exec2_status_t::VM_EXEC2_ERROR +} + +/// Writes a message and returns it as a negative count, for the functions that return one. +pub(crate) fn write_error_count( + message: impl Display, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> i64 { + write_error(message, err_buf, err_buf_len).as_i64() +} + +/// A buffer of this size holds every message the executor currently produces. +/// +/// Exported so the Go side has one place to take the number from rather than picking its +/// own. +#[unsafe(no_mangle)] +pub static VM_EXEC2_ERROR_BUF_RECOMMENDED_LEN: c_int = 1024; + +#[cfg(test)] +mod tests { + use super::*; + + fn write_to_buf(message: &str, len: usize) -> (vm_exec2_status_t, String) { + let mut buf = vec![0i8; len]; + let status = write_error(message, buf.as_mut_ptr() as *mut c_char, len as c_int); + let bytes: Vec = buf + .iter() + .take_while(|b| **b != 0) + .map(|b| *b as u8) + .collect(); + (status, String::from_utf8(bytes).expect("not valid UTF-8")) + } + + #[test] + fn error_message_round_trips() { + let (status, written) = write_to_buf("boom", 64); + assert_eq!(status, vm_exec2_status_t::VM_EXEC2_ERROR); + assert_eq!(written, "boom"); + } + + #[test] + fn error_message_is_truncated_to_fit() { + let (_, written) = write_to_buf("0123456789", 5); + assert_eq!(written, "0123"); + } + + #[test] + fn error_message_truncation_keeps_valid_utf8() { + // "é" is two bytes, so a 4-byte buffer holds "aé" and not a split code point. + let (_, written) = write_to_buf("aéé", 4); + assert_eq!(written, "aé"); + } + + #[test] + fn error_message_tolerates_no_buffer() { + assert_eq!( + write_error("boom", std::ptr::null_mut(), 0), + vm_exec2_status_t::VM_EXEC2_ERROR + ); + assert_eq!( + write_error("boom", std::ptr::null_mut(), 64), + vm_exec2_status_t::VM_EXEC2_ERROR + ); + } + + #[test] + fn status_codes_leave_room_for_counts() { + assert_eq!(vm_exec2_status_t::VM_EXEC2_OK.as_i64(), 0); + for status in [ + vm_exec2_status_t::VM_EXEC2_ERROR, + vm_exec2_status_t::VM_EXEC2_NULL_POINTER, + vm_exec2_status_t::VM_EXEC2_STALE_CONTEXT, + vm_exec2_status_t::VM_EXEC2_PANIC, + ] { + assert!(status.as_i64() < 0, "{status:?} must be negative"); + } + } +} diff --git a/c-api-new/src/capi_strings.rs b/c-api-new/src/capi_strings.rs new file mode 100644 index 00000000..6995283f --- /dev/null +++ b/c-api-new/src/capi_strings.rs @@ -0,0 +1,97 @@ +//! The two-step string-out convention. +//! +//! A list of names is returned as a single pipe-separated string, in two calls: one for the +//! length, one for the bytes. Same convention as the legacy API, so the Go side's existing +//! helper shape carries over. + +use libc::{c_char, c_int}; +use std::{ptr, slice}; + +/// The length, including the NUL terminator, that `copy_pipe_joined` needs for `names`. +pub(crate) fn pipe_joined_length(names: &[String]) -> c_int { + if names.is_empty() { + return 0; + } + + // One separator between each pair, plus the NUL: `len_sum + count` covers both. + let len_sum: usize = names.iter().map(String::len).sum(); + (len_sum + names.len()) as c_int +} + +/// Writes `names` into `dest` as a pipe-separated, NUL-terminated string. +/// +/// Returns the number of bytes written including the terminator, or `-1` if `dest` is null +/// or too small. Callers size `dest` with [`pipe_joined_length`]. +/// +/// # Safety +/// +/// `dest` must be null or writable for `dest_len` bytes. +pub(crate) unsafe fn copy_pipe_joined( + names: &[String], + dest: *mut c_char, + dest_len: c_int, +) -> c_int { + let joined = names.join("|"); + + if dest.is_null() || dest_len <= 0 { + return -1; + } + let dest_len = dest_len as usize; + if joined.len() >= dest_len { + return -1; + } + + unsafe { + let dest = slice::from_raw_parts_mut(dest as *mut u8, dest_len); + ptr::copy_nonoverlapping(joined.as_ptr(), dest.as_mut_ptr(), joined.len()); + dest[joined.len()] = 0; + } + + joined.len() as c_int + 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + fn join_into(names: &[&str], buf_len: usize) -> (c_int, String) { + let names: Vec = names.iter().map(|n| n.to_string()).collect(); + let mut buf = vec![0i8; buf_len]; + let written = + unsafe { copy_pipe_joined(&names, buf.as_mut_ptr() as *mut c_char, buf_len as c_int) }; + let bytes: Vec = buf + .iter() + .take_while(|b| **b != 0) + .map(|b| *b as u8) + .collect(); + (written, String::from_utf8(bytes).unwrap()) + } + + #[test] + fn length_accounts_for_separators_and_terminator() { + let names = vec!["init".to_string(), "upgrade".to_string()]; + // "init|upgrade" is 12 bytes, plus the NUL. + assert_eq!(pipe_joined_length(&names), 13); + assert_eq!(pipe_joined_length(&[]), 0); + } + + #[test] + fn length_is_exactly_what_the_copy_needs() { + let names = vec!["init".to_string(), "upgrade".to_string()]; + let needed = pipe_joined_length(&names) as usize; + + let (written, joined) = join_into(&["init", "upgrade"], needed); + assert_eq!(written, needed as c_int); + assert_eq!(joined, "init|upgrade"); + + // One byte short must be refused rather than truncated. + let (written, _) = join_into(&["init", "upgrade"], needed - 1); + assert_eq!(written, -1); + } + + #[test] + fn refuses_a_missing_buffer() { + let names = vec!["init".to_string()]; + assert_eq!(unsafe { copy_pipe_joined(&names, ptr::null_mut(), 64) }, -1); + } +} diff --git a/c-api-new/src/lib.rs b/c-api-new/src/lib.rs new file mode 100644 index 00000000..761d1de7 --- /dev/null +++ b/c-api-new/src/lib.rs @@ -0,0 +1,32 @@ +//! C API for the new executor interface (`vm-executor/src/new_traits`). +//! +//! Built alongside the legacy `c-api` rather than replacing it, so the wasmer2 path keeps +//! shipping while this one is brought up. The two differ in four ways that matter to a +//! caller: +//! +//! - **Exported symbols are prefixed `vm_exec2_`**, so both libraries can be linked into one +//! binary. The legacy API already owns `vm_exec_new_executor` and friends. +//! - **Errors travel through a caller-provided buffer**, not a thread-local last-error +//! singleton. See `capi_status`. +//! - **Configuration is passed once**, at the point the thing it configures is created. +//! There are no post-construction setters. +//! - **A call reports what happened in its result**, instead of leaving the caller to poll a +//! wasm global afterwards. See `vm_exec2_call_result_t`. +//! +//! The hook dispatch itself — the typed function-pointer table and the 277-method `VMHooks` +//! implementation that calls into it — is generated code and arrives with Phase 3 of +//! `PLAN-go-ffi.md`. Everything it needs is here: the result and context ABI +//! (`capi_hook_context`), the table ownership (`capi_hook_table`) and the state operations +//! (`capi_instance_state`). + +mod capi_logger; + +pub mod capi_config; +pub mod capi_executor; +pub mod capi_hook_context; +pub mod capi_hook_table; +pub mod capi_instance; +pub mod capi_instance_state; +pub mod capi_opcode_cost; +pub mod capi_status; +pub(crate) mod capi_strings; diff --git a/c-api-new/tests/ffi_boundary.rs b/c-api-new/tests/ffi_boundary.rs new file mode 100644 index 00000000..98fc3a90 --- /dev/null +++ b/c-api-new/tests/ffi_boundary.rs @@ -0,0 +1,620 @@ +//! Drives the C API from Rust, so the boundary is testable before any Go exists. +//! +//! Two halves: +//! +//! - [`lifecycle`] goes through the exported functions exactly as the Go side will, using a +//! mock hook table. +//! - [`state_ops`] installs a mock hooks provider that calls the state functions from inside +//! a real host call, which is the only place a hook context is live. It also checks that a +//! context which outlives its call, or which belongs to an outer call while a nested one is +//! running, is rejected rather than followed. + +use std::ffi::{CString, c_char, c_int, c_void}; +use std::sync::{Arc, Mutex}; + +use multiversx_chain_vm_executor::{VMHooks, VMHooksDefault}; +use multiversx_chain_vm_executor_c_api_new::capi_config::{ + vm_exec2_compilation_options_t, vm_exec2_executor_config_t, +}; +use multiversx_chain_vm_executor_c_api_new::capi_executor::{ + vm_exec2_executor_destroy, vm_exec2_executor_t, vm_exec2_new_executor, +}; +use multiversx_chain_vm_executor_c_api_new::capi_hook_context::{ + CapiVMHooksBuilder, vm_exec2_hook_context_t, +}; +use multiversx_chain_vm_executor_c_api_new::capi_hook_table::CapiHookTable; +use multiversx_chain_vm_executor_c_api_new::capi_instance::*; +use multiversx_chain_vm_executor_c_api_new::capi_instance_state::*; +use multiversx_chain_vm_executor_c_api_new::capi_opcode_cost::vm_exec_opcode_cost_t; +use multiversx_chain_vm_executor_c_api_new::capi_status::vm_exec2_status_t; +use multiversx_chain_vm_executor_wasmer_experimental::{ + ExperimentalInstance, ExperimentalInstanceState, ExperimentalVMHooksBuilder, +}; + +/// One memory page, five known bytes at offset 0, one endpoint that calls a VM hook. +const CONTRACT_WAT: &str = r#" +(module + (import "env" "getGasLeft" (func $getGasLeft (result i64))) + (memory (export "memory") 1) + (data (i32.const 0) "hello") + (func (export "doNothing")) + (func (export "callHook") (drop (call $getGasLeft))) +) +"#; + +const ERR_BUF_LEN: c_int = 1024; + +struct ErrBuf(Vec); + +impl ErrBuf { + fn new() -> Self { + ErrBuf(vec![0; ERR_BUF_LEN as usize]) + } + + fn ptr(&mut self) -> *mut c_char { + self.0.as_mut_ptr() + } + + fn message(&self) -> String { + let bytes: Vec = self + .0 + .iter() + .take_while(|b| **b != 0) + .map(|b| *b as u8) + .collect(); + String::from_utf8(bytes).expect("error message is not valid UTF-8") + } +} + +fn opcode_cost() -> vm_exec_opcode_cost_t { + // A `#[repr(C)]` struct of `u32`s, so an all-zero cost table is a valid value. Zero + // costs keep the tests away from the metering limit, which is not what they are about. + unsafe { std::mem::zeroed() } +} + +fn compilation_options() -> vm_exec2_compilation_options_t { + vm_exec2_compilation_options_t { + unmetered_locals: 0, + max_memory_grow: 10, + max_memory_grow_delta: 10, + opcode_trace: false, + } +} + +/// A mock hook table: plausible non-null words, never called in this phase. +fn mock_hook_table() -> Vec<*const c_void> { + (1..=8usize).map(|n| n as *const c_void).collect() +} + +fn new_executor(err: &mut ErrBuf) -> *mut vm_exec2_executor_t { + let cost = opcode_cost(); + let config = vm_exec2_executor_config_t { + opcode_version: 1, + opcode_cost: &cost, + }; + let table = mock_hook_table(); + + let mut executor: *mut vm_exec2_executor_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec2_new_executor( + &mut executor, + &config, + table.as_ptr() as *const c_void, + table.len(), + err.ptr(), + ERR_BUF_LEN, + ) + }; + + assert_eq!( + status, + vm_exec2_status_t::VM_EXEC2_OK, + "new_executor failed: {}", + err.message() + ); + assert!(!executor.is_null()); + executor +} + +fn new_instance(executor: *mut vm_exec2_executor_t, err: &mut ErrBuf) -> *mut vm_exec2_instance_t { + let options = compilation_options(); + let wasm = CONTRACT_WAT.as_bytes(); + let go_vm_hooks = 0xBEEFusize as *mut c_void; + + let mut instance: *mut vm_exec2_instance_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec2_new_instance( + executor, + &mut instance, + wasm.as_ptr(), + wasm.len() as u32, + &options, + go_vm_hooks, + err.ptr(), + ERR_BUF_LEN, + ) + }; + + assert_eq!( + status, + vm_exec2_status_t::VM_EXEC2_OK, + "new_instance failed: {}", + err.message() + ); + assert!(!instance.is_null()); + instance +} + +fn call( + instance: *mut vm_exec2_instance_t, + func_name: &str, + err: &mut ErrBuf, +) -> vm_exec2_call_result_t { + let name = CString::new(func_name).unwrap(); + let mut result = vm_exec2_call_result_t { + kind: vm_exec2_call_result_kind_t::VM_EXEC2_CALL_RUNTIME_ERROR, + early_exit_code: u64::MAX, + breakpoint: u64::MAX, + }; + + let status = unsafe { + vm_exec2_instance_call( + instance, + name.as_ptr(), + 1_000_000, + &mut result, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec2_status_t::VM_EXEC2_OK); + result +} + +fn names( + instance: *mut vm_exec2_instance_t, + length: unsafe extern "C" fn(*const vm_exec2_instance_t) -> c_int, + write: unsafe extern "C" fn(*const vm_exec2_instance_t, *mut c_char, c_int) -> c_int, +) -> Vec { + let needed = unsafe { length(instance) }; + assert!(needed > 0, "expected a non-empty name list"); + + let mut buf = vec![0 as c_char; needed as usize]; + let written = unsafe { write(instance, buf.as_mut_ptr(), needed) }; + assert_eq!(written, needed, "two-step length and write must agree"); + + let bytes: Vec = buf + .iter() + .take_while(|b| **b != 0) + .map(|b| *b as u8) + .collect(); + let mut names: Vec = String::from_utf8(bytes) + .unwrap() + .split('|') + .map(str::to_string) + .collect(); + names.sort(); + names +} + +#[test] +fn lifecycle() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + // A call that does nothing succeeds. + let result = call(instance, "doNothing", &mut err); + assert_eq!(result.kind, vm_exec2_call_result_kind_t::VM_EXEC2_CALL_OK); + + // A call into a VM hook succeeds too; with no generated dispatch yet, the hooks are the + // executor's own placeholder implementation. + let result = call(instance, "callHook", &mut err); + assert_eq!(result.kind, vm_exec2_call_result_kind_t::VM_EXEC2_CALL_OK); + + // A missing endpoint is reported in the result, not as a failure of the call itself. + let result = call(instance, "noSuchEndpoint", &mut err); + assert_eq!( + result.kind, + vm_exec2_call_result_kind_t::VM_EXEC2_CALL_FUNCTION_NOT_FOUND + ); + + // Gas accounting is readable from outside a host call. + let mut points = u64::MAX; + let status = + unsafe { vm_exec2_instance_get_points_used(instance, &mut points, err.ptr(), ERR_BUF_LEN) }; + assert_eq!(status, vm_exec2_status_t::VM_EXEC2_OK); + assert_ne!(points, u64::MAX, "points_used was not written"); + + // Every endpoint takes no arguments and returns nothing. + assert_eq!(unsafe { vm_exec2_instance_check_signatures(instance) }, 1); + + let present = CString::new("doNothing").unwrap(); + let absent = CString::new("noSuchEndpoint").unwrap(); + assert_eq!( + unsafe { vm_exec2_instance_has_function(instance, present.as_ptr()) }, + 1 + ); + assert_eq!( + unsafe { vm_exec2_instance_has_function(instance, absent.as_ptr()) }, + 0 + ); + + assert_eq!( + names( + instance, + vm_exec2_instance_exported_function_names_length, + vm_exec2_instance_exported_function_names, + ), + vec!["callHook".to_string(), "doNothing".to_string()] + ); + + // All of the hooks the module imports, in one call. + assert_eq!( + names( + instance, + vm_exec2_instance_imported_function_names_length, + vm_exec2_instance_imported_function_names, + ), + vec!["getGasLeft".to_string()] + ); + + unsafe { + vm_exec2_instance_destroy(instance); + vm_exec2_executor_destroy(executor); + } +} + +#[test] +fn null_arguments_are_refused_without_a_message() { + let mut err = ErrBuf::new(); + let mut executor: *mut vm_exec2_executor_t = std::ptr::null_mut(); + + // A null config cannot be reported through the error buffer, because the buffer itself + // may be the thing that is null, so these return a dedicated status instead. + let status = unsafe { + vm_exec2_new_executor( + &mut executor, + std::ptr::null(), + std::ptr::null(), + 0, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec2_status_t::VM_EXEC2_NULL_POINTER); + assert!(executor.is_null()); + + assert_eq!( + unsafe { vm_exec2_instance_check_signatures(std::ptr::null()) }, + vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int() + ); +} + +#[test] +fn an_invalid_opcode_version_is_reported_through_the_buffer() { + let mut err = ErrBuf::new(); + let cost = opcode_cost(); + let config = vm_exec2_executor_config_t { + opcode_version: 99, + opcode_cost: &cost, + }; + + let mut executor: *mut vm_exec2_executor_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec2_new_executor( + &mut executor, + &config, + std::ptr::null(), + 0, + err.ptr(), + ERR_BUF_LEN, + ) + }; + + assert_eq!(status, vm_exec2_status_t::VM_EXEC2_ERROR); + assert_eq!(err.message(), "invalid opcode version code: 99"); + assert!(executor.is_null()); +} + +#[test] +fn a_context_is_rejected_when_no_call_is_active() { + let mut err = ErrBuf::new(); + + assert_eq!( + unsafe { + vm_exec2_state_mem_length(std::ptr::null_mut(), &mut 0u64, err.ptr(), ERR_BUF_LEN) + }, + vm_exec2_status_t::VM_EXEC2_NULL_POINTER + ); + + let mut points = 0u64; + assert_eq!( + unsafe { + vm_exec2_state_get_points_used( + std::ptr::null_mut(), + &mut points, + err.ptr(), + ERR_BUF_LEN, + ) + }, + vm_exec2_status_t::VM_EXEC2_NULL_POINTER + ); +} + +/// What `state_ops` records from inside the host call, to be asserted after it returns. +#[derive(Default)] +struct Outcome { + failures: Vec, + hook_calls: usize, + escaped_context: usize, +} + +impl Outcome { + fn check(&mut self, what: &str, condition: bool, detail: String) { + if !condition { + self.failures.push(format!("{what}: {detail}")); + } + } +} + +/// A mock hooks provider that exercises the state FFI from inside a real host call. +struct ProbingHooksBuilder { + inner: CapiVMHooksBuilder, + outcome: Arc>, +} + +impl ExperimentalVMHooksBuilder for ProbingHooksBuilder { + fn with_vm_hooks( + &self, + instance_state_ref: &mut ExperimentalInstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ) { + let state_ptr = (instance_state_ref as *mut ExperimentalInstanceState<'_>).cast::(); + let mut outcome = self.outcome.lock().unwrap(); + outcome.hook_calls += 1; + + unsafe { + self.inner.with_hook_context_raw(state_ptr, |ctx| { + probe_state_ops(ctx, &mut outcome); + probe_nesting(&self.inner, state_ptr, ctx, &mut outcome); + outcome.escaped_context = ctx as usize; + }); + } + + drop(outcome); + f(&mut VMHooksDefault); + } +} + +fn mem_load(ctx: *mut vm_exec2_hook_context_t, mem_ptr: i32, dest: &mut [u8], len: i32) -> i64 { + unsafe { + vm_exec2_state_mem_load( + ctx, + mem_ptr, + dest.as_mut_ptr(), + len, + std::ptr::null_mut(), + 0, + ) + } +} + +fn mem_store(ctx: *mut vm_exec2_hook_context_t, mem_ptr: i32, src: &[u8]) -> vm_exec2_status_t { + unsafe { + vm_exec2_state_mem_store( + ctx, + mem_ptr, + src.as_ptr(), + src.len() as i32, + std::ptr::null_mut(), + 0, + ) + } +} + +fn probe_state_ops(ctx: *mut vm_exec2_hook_context_t, outcome: &mut Outcome) { + const PAGE: u64 = 65536; + + let mut length = 0u64; + let status = unsafe { vm_exec2_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; + outcome.check( + "mem_length", + status == vm_exec2_status_t::VM_EXEC2_OK && length == PAGE, + format!("{status:?}, length {length}"), + ); + + // A load entirely inside the memory returns what the data segment put there. + let mut buf = [0u8; 5]; + let loaded = mem_load(ctx, 0, &mut buf, 5); + outcome.check( + "mem_load reads the data segment", + loaded == 5 && &buf == b"hello", + format!("loaded {loaded}, {:?}", std::str::from_utf8(&buf)), + ); + + // A load that runs past the end is truncated, not refused: the Go VM's semantics. + let mut buf = [0u8; 10]; + let loaded = mem_load(ctx, (PAGE - 2) as i32, &mut buf, 10); + outcome.check( + "mem_load clamps at the end of memory", + loaded == 2, + format!("loaded {loaded}"), + ); + + // An offset exactly at the end is a valid offset that yields nothing. + let loaded = mem_load(ctx, PAGE as i32, &mut buf, 1); + outcome.check( + "mem_load at the end of memory yields nothing", + loaded == 0, + format!("loaded {loaded}"), + ); + + // An offset past the end is an error, even though a too-long range is not. + let loaded = mem_load(ctx, (PAGE + 1) as i32, &mut buf, 1); + outcome.check( + "mem_load rejects an offset past the end", + loaded == vm_exec2_status_t::VM_EXEC2_ERROR.as_i64(), + format!("loaded {loaded}"), + ); + + let loaded = mem_load(ctx, -1, &mut buf, 1); + outcome.check( + "mem_load rejects a negative offset", + loaded == vm_exec2_status_t::VM_EXEC2_ERROR.as_i64(), + format!("loaded {loaded}"), + ); + + let loaded = mem_load(ctx, 0, &mut buf, -1); + outcome.check( + "mem_load rejects a negative length", + loaded == vm_exec2_status_t::VM_EXEC2_ERROR.as_i64(), + format!("loaded {loaded}"), + ); + + // A store, read back through a load. + let status = mem_store(ctx, 16, b"ABC"); + let mut buf = [0u8; 3]; + let loaded = mem_load(ctx, 16, &mut buf, 3); + outcome.check( + "mem_store round-trips", + status == vm_exec2_status_t::VM_EXEC2_OK && loaded == 3 && &buf == b"ABC", + format!( + "{status:?}, loaded {loaded}, {:?}", + std::str::from_utf8(&buf) + ), + ); + + // Unlike a load, a store is never truncated. + let status = mem_store(ctx, (PAGE - 1) as i32, b"ABC"); + outcome.check( + "mem_store rejects a range past the end", + status == vm_exec2_status_t::VM_EXEC2_ERROR, + format!("{status:?}"), + ); + + // Gas, through the state handle rather than the instance handle. + let status = unsafe { vm_exec2_state_set_points_used(ctx, 42, std::ptr::null_mut(), 0) }; + let mut points = 0u64; + let get_status = + unsafe { vm_exec2_state_get_points_used(ctx, &mut points, std::ptr::null_mut(), 0) }; + outcome.check( + "points used round-trip", + status == vm_exec2_status_t::VM_EXEC2_OK + && get_status == vm_exec2_status_t::VM_EXEC2_OK + && points == 42, + format!("{status:?}, {get_status:?}, points {points}"), + ); + + // Growing reports the page count from before the growth. + let mut prev_pages = 0u32; + let status = + unsafe { vm_exec2_state_mem_grow(ctx, 1, &mut prev_pages, std::ptr::null_mut(), 0) }; + let mut length_after = 0u64; + unsafe { vm_exec2_state_mem_length(ctx, &mut length_after, std::ptr::null_mut(), 0) }; + outcome.check( + "mem_grow", + status == vm_exec2_status_t::VM_EXEC2_OK && prev_pages == 1 && length_after == 2 * PAGE, + format!("{status:?}, prev_pages {prev_pages}, length {length_after}"), + ); +} + +/// A nested host call must not let the outer call's context stay usable. +fn probe_nesting( + builder: &CapiVMHooksBuilder, + state_ptr: *mut c_void, + outer_ctx: *mut vm_exec2_hook_context_t, + outcome: &mut Outcome, +) { + unsafe { + builder.with_hook_context_raw(state_ptr, |inner_ctx| { + outcome.check( + "nesting uses a distinct context", + inner_ctx != outer_ctx, + format!("inner {inner_ctx:?} outer {outer_ctx:?}"), + ); + + let mut length = 0u64; + let inner_status = + vm_exec2_state_mem_length(inner_ctx, &mut length, std::ptr::null_mut(), 0); + outcome.check( + "the innermost context works", + inner_status == vm_exec2_status_t::VM_EXEC2_OK, + format!("{inner_status:?}"), + ); + + let outer_status = + vm_exec2_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); + outcome.check( + "an outer context is rejected while a nested call runs", + outer_status == vm_exec2_status_t::VM_EXEC2_STALE_CONTEXT, + format!("{outer_status:?}"), + ); + }); + + // ... and is usable again once the nested call has returned. + let mut length = 0u64; + let status = vm_exec2_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); + outcome.check( + "the outer context works again after the nested call", + status == vm_exec2_status_t::VM_EXEC2_OK, + format!("{status:?}"), + ); + } +} + +#[test] +fn state_ops() { + let outcome = Arc::new(Mutex::new(Outcome::default())); + let builder = ProbingHooksBuilder { + inner: CapiVMHooksBuilder::new(0xBEEFusize as *mut c_void, unsafe { + CapiHookTable::copy_from(std::ptr::null(), 0) + }), + outcome: outcome.clone(), + }; + + let mut instance = ExperimentalInstance::try_new_instance( + Box::new(builder), + Arc::new(Default::default()), + CONTRACT_WAT.as_bytes(), + // The same options as the C path, built directly: the FFI mirror is crate-private. + &multiversx_chain_vm_executor::CompilationOptions { + unmetered_locals: 0, + max_memory_grow: 10, + max_memory_grow_delta: 10, + opcode_trace: false, + }, + ) + .expect("instance creation failed"); + + use multiversx_chain_vm_executor::Instance; + let result = instance.call("callHook", 1_000_000); + assert!( + matches!(result, multiversx_chain_vm_executor::InstanceCallResult::Ok), + "the probing call did not succeed" + ); + + let outcome = outcome.lock().unwrap(); + assert_eq!( + outcome.hook_calls, 1, + "the hook was not called exactly once" + ); + assert!( + outcome.failures.is_empty(), + "state op failures:\n {}", + outcome.failures.join("\n ") + ); + + // The context that escaped the host call must now be refused. Contexts are pooled for + // the instance's lifetime, so this reads live memory and fails the token check, rather + // than being undefined behaviour. + let escaped = outcome.escaped_context as *mut vm_exec2_hook_context_t; + let mut length = 0u64; + let status = + unsafe { vm_exec2_state_mem_length(escaped, &mut length, std::ptr::null_mut(), 0) }; + assert_eq!( + status, + vm_exec2_status_t::VM_EXEC2_STALE_CONTEXT, + "a context used after its call must be rejected" + ); +} diff --git a/vm-executor/src/new_traits.rs b/vm-executor/src/new_traits.rs index 911ca391..531c4859 100644 --- a/vm-executor/src/new_traits.rs +++ b/vm-executor/src/new_traits.rs @@ -13,7 +13,7 @@ pub use compilation_options_new::CompilationOptions; pub use executor_new::Executor; pub use instance_new::{Instance, InstanceCallResult}; pub use instance_state::InstanceState; -pub use memory::{MemoryError, checked_load_range, checked_store_range}; +pub use memory::{MemoryError, checked_load_range, checked_store_range, clamped_load_len}; pub use vm_hooks_early_exit::VMHooksEarlyExit; pub use vm_hooks_legacy_adapter::{VMHooksLegacyAdapter, VMHooksSetEarlyExit}; pub use vm_hooks_new::{VMHooks, VMHooksDefault}; diff --git a/vm-executor/src/new_traits/instance_state.rs b/vm-executor/src/new_traits/instance_state.rs index 1b120252..26a62102 100644 --- a/vm-executor/src/new_traits/instance_state.rs +++ b/vm-executor/src/new_traits/instance_state.rs @@ -1,10 +1,10 @@ -use crate::{ExecutorError, MemLength, MemPtr, MemoryError}; +use crate::{ExecutorError, MemLength, MemPtr, clamped_load_len}; /// The interface through which VM hooks update the instance state. /// /// All accesses are validated by the executor, which is the only side that knows the real /// extent of the instance memory. The load and store methods are **strict**: a range that -/// does not fit entirely inside the memory is rejected with a [`MemoryError`]. The one +/// does not fit entirely inside the memory is rejected with a [`crate::MemoryError`]. The one /// exception is [`InstanceState::memory_load_clamped`], which reproduces the Go VM's /// historical truncating behaviour; see its documentation. pub trait InstanceState { @@ -59,21 +59,7 @@ pub trait InstanceState { return Ok(Vec::new()); } - let memory_length = self.memory_length()?; - - // Checked in this order to match the Go VM: an out-of-range offset takes - // precedence over a negative length. - if mem_ptr < 0 || mem_ptr as u64 > memory_length { - return Err(MemoryError::BadBounds.into()); - } - if mem_length < 0 { - return Err(MemoryError::NegativeLength.into()); - } - - // Computed as a minimum rather than as `mem_ptr + mem_length`, which can overflow - // for a length a contract is free to choose. - let available = memory_length - mem_ptr as u64; - let to_load = std::cmp::min(mem_length as u64, available); + let to_load = clamped_load_len(mem_ptr, mem_length, self.memory_length()?)?; self.memory_load_owned(mem_ptr, to_load as MemLength) } diff --git a/vm-executor/src/new_traits/memory.rs b/vm-executor/src/new_traits/memory.rs index 29ac0c1a..a487ecbe 100644 --- a/vm-executor/src/new_traits/memory.rs +++ b/vm-executor/src/new_traits/memory.rs @@ -78,6 +78,40 @@ pub fn checked_load_range( Ok((mem_ptr as usize, end as usize)) } +/// Computes how many bytes a **clamped** load may read. +/// +/// Where [`checked_load_range`] rejects a range that ends past the end of the memory, this +/// truncates it, which is the Go VM's historical behaviour. The offset and length checks, and +/// the order in which they are applied, are the same as for a strict load. +/// +/// Returning the length rather than a range lets a caller read straight into a +/// caller-provided buffer, which is what the C API needs; +/// [`crate::InstanceState::memory_load_clamped`] is the owned-buffer counterpart. Both go +/// through here, so the truncating rule has a single implementation. +pub fn clamped_load_len( + mem_ptr: MemPtr, + mem_length: MemLength, + memory_size: u64, +) -> Result { + if mem_length == 0 { + return Ok(0); + } + + // Checked in this order to match the Go VM: an out-of-range offset takes precedence + // over a negative length. + if mem_ptr < 0 || mem_ptr as u64 > memory_size { + return Err(MemoryError::BadBounds); + } + if mem_length < 0 { + return Err(MemoryError::NegativeLength); + } + + // Computed as a minimum rather than as `mem_ptr + mem_length`, which can overflow for a + // length a contract is free to choose. + let available = memory_size - mem_ptr as u64; + Ok(std::cmp::min(mem_length as u64, available) as usize) +} + /// Validates a range for a store, returning it as a `start..end` byte range. /// /// Mirrors the Go VM's `MemStoreToMemory`, including the fact that an empty write is @@ -173,6 +207,33 @@ mod tests { ); } + #[test] + fn clamped_load_len_truncates_at_the_end_of_memory() { + assert_eq!(clamped_load_len(0, 100, SIZE), Ok(100)); + assert_eq!(clamped_load_len(95, 10, SIZE), Ok(5)); + assert_eq!(clamped_load_len(1, MemLength::MAX, SIZE), Ok(99)); + // An offset exactly at the end is valid, and yields nothing. + assert_eq!(clamped_load_len(100, 1, SIZE), Ok(0)); + } + + #[test] + fn clamped_load_len_rejects_what_a_strict_load_rejects() { + assert_eq!(clamped_load_len(-1, 10, SIZE), Err(MemoryError::BadBounds)); + assert_eq!(clamped_load_len(101, 10, SIZE), Err(MemoryError::BadBounds)); + assert_eq!( + clamped_load_len(0, -1, SIZE), + Err(MemoryError::NegativeLength) + ); + // Same precedence as a strict load. + assert_eq!(clamped_load_len(-1, -1, SIZE), Err(MemoryError::BadBounds)); + } + + #[test] + fn clamped_load_len_zero_length_ignores_a_bad_offset() { + assert_eq!(clamped_load_len(-1, 0, SIZE), Ok(0)); + assert_eq!(clamped_load_len(1_000, 0, SIZE), Ok(0)); + } + #[test] fn store_range_ok() { assert_eq!(checked_store_range(0, 100, SIZE), Ok((0, 100))); From 90f467a4119f88bfe0ad18468c90da8ab1fd655f Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 11:22:12 +0300 Subject: [PATCH 06/26] new FFI - generated VM hook table (phase 3) --- PLAN-go-breakpoint-removal.md | 365 +++++++ PLAN-go-ffi.md | 105 +- c-api-new/libvmexeccapi_new.h | 385 +++++++- c-api-new/src/capi_executor.rs | 17 +- c-api-new/src/capi_hook_table.rs | 96 -- c-api-new/src/capi_instance.rs | 2 +- c-api-new/src/capi_instance_state.rs | 4 +- c-api-new/src/capi_status.rs | 2 +- ...ook_context.rs => capi_vm_hook_context.rs} | 34 +- c-api-new/src/capi_vm_hook_pointers.rs | 300 ++++++ c-api-new/src/capi_vm_hook_table.rs | 44 + c-api-new/src/capi_vm_hooks.rs | 908 ++++++++++++++++++ c-api-new/src/lib.rs | 16 +- c-api-new/tests/ffi_boundary.rs | 240 ++++- 14 files changed, 2276 insertions(+), 242 deletions(-) create mode 100644 PLAN-go-breakpoint-removal.md delete mode 100644 c-api-new/src/capi_hook_table.rs rename c-api-new/src/{capi_hook_context.rs => capi_vm_hook_context.rs} (91%) create mode 100644 c-api-new/src/capi_vm_hook_pointers.rs create mode 100644 c-api-new/src/capi_vm_hook_table.rs create mode 100644 c-api-new/src/capi_vm_hooks.rs diff --git a/PLAN-go-breakpoint-removal.md b/PLAN-go-breakpoint-removal.md new file mode 100644 index 00000000..b867408d --- /dev/null +++ b/PLAN-go-breakpoint-removal.md @@ -0,0 +1,365 @@ +# Replacing runtime breakpoints with early exits in the Go VM + +**Repo:** `mx-chain-vm-go` (branch `wasmer6`), with supporting changes in `mx-vm-executor-rs` +**Related:** +- `PLAN-go-hooks-error-migration.md` — the prerequisite, **already landed** (see §1). +- `PLAN-go-ffi.md` — Phase 3 (new Go instance interface) is the hard blocker for this work. + +**Status:** analysis only. No code has been written for this change. + +--- + +## 1. Where we are + +The hook error migration is complete on branch `wasmer6`: + +``` +c4989db26 Delete unused (*VMHooksImpl).FailExecution method +e307ad8e2 Make the trailing error result mandatory for all VM hooks +2b267172c Migrate baseOps.go hooks to return error — last of 8 files +... (6 more per-file migration commits) +``` + +All 278 `@autogenerate(VMHooks)` hooks return a trailing `error`. The eight hook files contain +no remaining `FailExecution`, `SignalUserError` or `SetRuntimeBreakpointValue` calls — a grep +for those names in `vmhost/vmhooks/` returns only the `UseGasBoundedShouldFailExecution()` +feature-flag name and two explanatory comments. + +**This is the leverage for everything below.** Every hook failure now funnels through one +function, `(*VMHooksImpl).HandleHookError` in `vmhost/vmhooks/earlyExit.go`. Replacing +breakpoints with early exits is a change to `applyEarlyExit` plus the call-result handler. It +is *not* another pass over 278 hooks. + +--- + +## 2. The central observation + +The breakpoint global is a `u64` wasm global, polled by the metering middleware at every +basic-block boundary. It currently serves **two unrelated purposes**: + +1. **Halt trigger** (mechanical) — nonzero means "trap at the next checkpoint". +2. **Reason channel** (semantic) — `vmhost/hostCore/breakpoints.go:13` reads it back after the + trap to decide what happened, and maps it to a sentinel error. + +Early exits replace **the reason channel only**. The halt trigger is still required in at +least one place (§4.3), so the global does not disappear. + +Separating the two is both possible and already proven — see §3. It is what makes a staged +migration possible: *the Go VM can stop interpreting breakpoints long before it stops writing +one.* + +--- + +## 3. The Rust precedent (copy this design) + +In the Rust VM, `InstanceStateSetEarlyExit::set_early_exit` does **both** things, and says so: + +`mx-sdk-rs-w1/chain/wasmer-prod/src/wasmer_prod.rs:97-107` + +```rust +impl InstanceStateSetEarlyExit for WasmerProdInstanceStateAdapter { + fn set_early_exit(&self, early_exit: VMHooksEarlyExit) { + // This ensures that the execution will halt after the current instruction. + self.0 + .set_breakpoint_value_legacy(early_exit_to_breakpoint_value(&early_exit)) + .expect("set_breakpoint_value_legacy error"); + + // This is passed to the error handler at the end of the executor execution. + self.0 + .set_early_exit(early_exit) + .expect("set_early_exit error"); + } +} +``` + +And on the way out, the early exit **wins**: + +`mx-vm-executor-rs/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs:39-64` + +```rust +Err(err) => { + if let Some(early_exit) = self.inner_instance_ref.take_early_exit() { + return InstanceCallResult::VMHooksEarlyExit(early_exit); + } + // only now read the breakpoint global — middleware-originated stops + ... +} +``` + +So the value written by `early_exit_to_breakpoint_value` is **never read back**. It is a +doorbell, not a message. `take_early_exit` also consumes the slot, so there is no sticky state +to clear between calls. + +### Code numbering, already settled + +`mx-sdk-rs-w1/chain/vm/src/host/vm_hooks/vh_early_exit.rs` + +```rust +pub const ASYNC_CALL_EARLY_EXIT_CODE: u64 = 0; + +pub fn early_exit_out_of_gas() -> VMHooksEarlyExit { ... ReturnCode::OutOfGas.as_u64() ... } +pub fn early_exit_vm_error(message: &'static str) -> VMHooksEarlyExit { ... ReturnCode::ExecutionFailed ... } +pub fn early_exit_async_call() -> VMHooksEarlyExit { ... ASYNC_CALL_EARLY_EXIT_CODE ... } +``` + +The code is `ReturnCode::as_u64()`, and `0` is reused as the async-call sentinel because a hook +never exits with `Ok`. Go has `vmcommon.ReturnCode` with `Ok = 0`, so **the same sentinel works +unchanged**, and `vmhost.EarlyExit.Code` is already a `vmcommon.ReturnCode` — it was built for +this. + +The consumer to mirror is +`mx-sdk-rs-w1/chain/vm/src/host/runtime/runtime_instance_call_default.rs:55-85`. + +--- + +## 4. Census: every production write site + +| # | Site | Value written | Triggered by | Fate | +|---|---|---|---|---| +| 1 | `vmhost/vmhooks/earlyExit.go:50` | `OutOfGas` | hook | → early exit | +| 2 | `vmhost/contexts/runtime.go:614` (`FailExecution`) | `ExecutionFailed` / `OutOfGas` | hook | → early exit | +| 3 | `vmhost/contexts/runtime.go:629` (`SignalUserError`) | `SignalError` | hook | → early exit | +| 4 | `vmhost/contexts/async.go:583` | `AsyncCall` | hook | → early exit, code-0 sentinel | +| 5 | `vmhost/contexts/runtime.go:311` | `None` (reset) | VM bookkeeping | → delete | +| 6 | `vmhost/hostCore/executionAsync.go:10` | `None` (reset) | VM bookkeeping | → delete | +| 7 | `vmhost/hostCore/host.go:455`, `:534` | `ExecutionFailed` | **watchdog goroutine** | → **must stay** | + +Plus the reader: `vmhost/hostCore/breakpoints.go` (`handleBreakpointIfAny` / +`handleBreakpoint`), and the interface declarations at `vmhost/interface.go:140-141`. + +Test/mock surface: **19** sites in `mock/contracts/` + `testcommon/`, **14** in `_test.go` +files, and `mock/context/instanceMock.go`. + +### 4.1 Rows 1-4 — hook-originated + +All four are reachable only from hook bodies, so all four are already covered by the single +`HandleHookError` funnel. + +Row 4 was verified specifically: `AsyncContext.RegisterLegacyAsyncCall` has exactly four +callers, all hooks — `vmhost/vmhooks/baseOps.go:1608`, `:1688`, `:1746` and +`vmhost/vmhooks/managedei.go:647`. + +### 4.2 Rows 5-6 — resets + +Both write `BreakpointNone` to clear sticky state. Under the early-exit model the reason is a +return value rather than instance state, and the Rust side's `take_early_exit()` consumes the +slot, so there is nothing to clear. Row 5 belongs to warm-instance startup and row 6 to +`handleAsyncCallBreakpoint`; both become dead once rows 1-4 are converted. + +### 4.3 Row 7 — the watchdog, which cannot become an early exit + +`vmhost/hostCore/host.go:528-536`: + +```go + case <-ctx.Done(): + // Terminated due to timeout. The VM sets the `ExecutionFailed` breakpoint + // in Wasmer. Also, the VM must wait for Wasmer to reach the end of a WASM + // basic block in order to close the WASM instance cleanly. ... + host.Runtime().FailExecution(vmhost.ErrExecutionFailedWithTimeout) + <-done + err = vmhost.ErrExecutionFailedWithTimeout +``` + +This runs on a **different goroutine** while the executing goroutine is inside +`instance.Call()`. No hook is running, so there is no return value to carry anything. It is a +genuinely asynchronous interruption and it is the reason the halt trigger survives. + +**Gap in the new trait set.** The new `BreakpointValue` +(`mx-vm-executor-rs/vm-executor/src/new_traits/breakpoint_value_new.rs`) has only `None`, +`OutOfGas` and `MemoryLimit`, deliberately — its doc comment says it "only covers the +interruptions raised by the executor's own middlewares." A host-requested abort fits neither +that enum nor `VMHooksEarlyExit`. Options: + +- **(recommended)** add `Instance::request_stop()` and `InstanceCallResult::Interrupted`. This + names the thing honestly and is the only variant that is legitimately callable across + threads, which should be documented on the method. +- add a third `BreakpointValue` variant for host-requested stops — muddies the + "middlewares only" invariant. + +Either way, `SetRuntimeBreakpointValue` should be **removed** from `vmhost.RuntimeContext` +(`vmhost/interface.go:140`) and replaced by a narrow `InterruptExecution()` with exactly one +caller. Leaving a general-purpose breakpoint setter on the interface is what allowed +hook-level failure signalling to grow there in the first place. + +### 4.4 Stragglers that are not hooks + +`vmhost/contexts/blockchain.go:353` — `PopSetActiveState` calls `vmhooks.FailExecution` when +`RevertToSnapshot` fails, during state unwinding, outside any hook. Either plumb an error out +of `PopSetActiveState` or give it the watchdog treatment. One site, but it will not fall out of +the hook model. + +`vmhost/vmhooks/vmHooksImpl.go:100-110` — the package-level `vmhooks.FailExecution(host, err)` +stays exported for mocks, `testcommon`, and `blockchain.go`. 17 external callers. + +--- + +## 5. Breakpoint values after the migration + +The point of the exercise: each value ends up with exactly one producer. + +| Value | Producer after migration | +|---|---| +| `BreakpointNone` (0) | — | +| `BreakpointExecutionFailed` (1) | the watchdog only (§4.3), or gone if `Interrupted` is added | +| `BreakpointAsyncCall` (2) | **nobody — delete** | +| `BreakpointSignalError` (3) | **nobody — delete** | +| `BreakpointOutOfGas` (4) | metering middleware only | +| `BreakpointMemoryLimit` (5) | memory middleware only | + +Rows 2 and 3 going to zero producers is the **completeness check** for this work: grep for +`BreakpointAsyncCall` and `BreakpointSignalError` and expect only the definitions in +`vmhost/common.go` and their `String()` cases. + +Note that `BreakpointOutOfGas` has two producers today (middleware *and* VM-injected, via both +`earlyExit.go:50` and the `errors.Is(err, ErrNotEnoughGas)` upgrade inside +`runtime.FailExecution`). After the migration the VM-side one becomes +`EarlyExit{Code: OutOfGas}`. Both still map to `ErrNotEnoughGas`, so behaviour converges — but +they become distinguishable, which is an improvement for diagnostics. + +--- + +## 6. What it takes + +### 6.1 Hard blocker: a Go-side `InstanceCallResult` + +`vmhost/contexts/runtime.go:813`: + +```go +func (context *runtimeContext) CallSCFunction(functionName string) error { + return context.iTracker.Instance().CallFunction(functionName) +} +``` + +`executor.Instance` (`executor/executorInstance.go:12`) offers only +`CallFunction(functionName string) error`. **There is nowhere for a reason to travel.** Until +the new instance interface lands (`PLAN-go-ffi.md` Phase 3), the breakpoint global is the only +channel and none of this work can start. + +Sketch of the target, mirroring `InstanceCallResult` in Rust: + +```go +// package executor +type InstanceCallResultKind uint8 + +const ( + CallResultOk InstanceCallResultKind = iota + CallResultFunctionNotFound + CallResultRuntimeError + CallResultEarlyExit + CallResultBreakpoint + // CallResultInterrupted — if the request_stop design in §4.3 is adopted +) + +type InstanceCallResult struct { + Kind InstanceCallResultKind + Err error // CallResultRuntimeError + ExitCode uint64 // CallResultEarlyExit — vmcommon.ReturnCode, or 0 = async call + ExitMsg string // CallResultEarlyExit + Breakpoint BreakpointValue // CallResultBreakpoint — OutOfGas | MemoryLimit only +} +``` + +Then `handleBreakpointIfAny(err)` becomes a **pure function** `resolveCallResult(result) error` +with no global reads. The three call sites are already identical in shape and convert together: +`vmhost/hostCore/execution.go:928`, `:1173`, `:1290`. + +The async-call branch is the one that returns `nil`, preserving +`handleAsyncCallBreakpoint`'s "stopped intentionally, continue with the async call" semantics. + +### 6.2 `ConsumeAllGas` does not cross the FFI + +`VMHooksEarlyExit` carries only `code` and `message`. `vmhost.EarlyExit.ConsumeAllGas` is a +Go-side concept, so the burn must stay in `HandleHookError`, executed before the hook returns. +`applyEarlyExit` therefore keeps a gas responsibility even after it loses its breakpoint +responsibility. Do not try to encode the flag in the exit code. + +### 6.3 Mocks are roughly a third of the work + +`mock/context/instanceMock.go:63-76` currently *polls* the breakpoint global and reconstructs +an error message from the VMOutput: + +```go +func (instance *InstanceMock) CallFunction(funcName string) error { + err := instance.DefaultErrors[funcName] + method := instance.Methods[funcName] + newInstance := method() + if vmhost.BreakpointValue(instance.GetBreakpointValue()) != vmhost.BreakpointNone { + ... + } + return err +} +``` + +It is emulating the middleware. It needs its own early-exit slot so that mock contracts can +return exits the way real hooks now do. Mechanical, but it touches many files — and these tests +are the only proof the migration is behaviour-preserving. + +--- + +## 7. Recommended staging + +Split along the semantic/mechanical seam. + +### Step A — stop *interpreting* breakpoints (no wasmer 6 dependency) + +1. Land the Go `InstanceCallResult` and the new instance interface. +2. Convert the three `handleBreakpointIfAny` sites to `resolveCallResult`. +3. Have `applyEarlyExit` record the exit on the instance **in addition to** setting the + breakpoint — exactly mirroring `wasmer_prod.rs:97-107`. +4. Delete the reset sites (rows 5-6). + +Breakpoint writes still happen; nothing reads their *value* except the middleware fallback. +`BreakpointAsyncCall` and `BreakpointSignalError` become dead (§5). Behaviour-preserving and +fully testable on wasmer 2 today. This is the larger change. + +### Step B — stop *writing* them (needs the new FFI) + +1. On the new FFI the exit travels in the hook's return value and the Rust side halts, so + `applyEarlyExit` loses its breakpoint branch entirely. +2. Delete `SetRuntimeBreakpointValue` from `vmhost.RuntimeContext`; add the narrow + `InterruptExecution()` for the watchdog (§4.3). +3. Resolve `blockchain.go:353` (§4.4). + +Small, and it depends on the new FFI crate. + +--- + +## 8. Risks + +**Halt timing changes in Step B.** Today a hook that fails keeps executing wasm until the +middleware next polls the global; with a real trap the halt is immediate. This is observable in +points-used, and therefore in nested-execution gas accounting. It is a *correctness +improvement*, but it is a behaviour change and needs differential testing against the wasmer 2 +path, not just unit tests. + +**The sentinel collision is load-bearing.** `ASYNC_CALL_EARLY_EXIT_CODE = 0` works only because +no hook ever exits with `Ok`. If a future hook wants to exit with `Ok`, the numbering breaks +silently on both sides of the FFI. Worth an assertion at the point where an `EarlyExit` is +constructed. + +**`BreakpointExecutionFailed` must not quietly acquire new producers.** After Step B its only +producer is the watchdog. A code review rule or a lint on `InterruptExecution` callers is +cheaper than rediscovering this in a year. + +--- + +## 9. Verification greps + +```bash +cd mx-chain-vm-go + +# must reach zero producers (definitions in vmhost/common.go excepted) +grep -rn "BreakpointAsyncCall\|BreakpointSignalError" --include="*.go" . + +# the breakpoint setter: 7 production sites before, 1 narrow interrupt after +grep -rn "SetRuntimeBreakpointValue(" --include="*.go" vmhost/ executor/ wasmer2/ | grep -v _test + +# the hook funnel must stay the only place failure effects are applied +grep -rn "FailExecution\|SignalUserError\|SetRuntimeBreakpointValue" \ + --include="*.go" vmhost/vmhooks/ | grep -v _test | grep -v earlyExit.go +# expect: only UseGasBoundedShouldFailExecution() flag hits, 2 comments, +# and the package-level vmhooks.FailExecution in vmHooksImpl.go + +# hook count, for sanity +grep -rn "@autogenerate(VMHooks)" --include="*.go" vmhost/vmhooks/ | wc -l # 278 +``` diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 0b51f9c2..a7b15fcc 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -1,7 +1,7 @@ # Integrating the new executor traits into the Go VM Working plan for exposing `vm-executor/src/new_traits` over a C FFI and consuming it from -the Go VM. Status as of 2026-10-01: Phase 1 items 1-5 and Phase 2 are implemented. +the Go VM. Status as of 2026-10-02: Phase 1 items 1-5, Phase 2 and Phase 3 are implemented. ## Why @@ -232,21 +232,95 @@ length, and the fact that a store is never truncated). It also delivers two of P checks early: a context used after its call is refused with `VM_EXEC2_STALE_CONTEXT`, and so is an outer context while a nested call is running. -## Phase 3 — Generator +## Phase 3 — Generator — **done** -Lands before the Go adapter, since it drives both repos. New writers, modelled on the -existing `eiGenWriteRustCapiVMHooks*.go` and `eiGenWriteCgo.go`: +Three new writers in `mx-chain-vm-go/vmhost/vmhooks/generate`, registered in `eiGenMain.go` +with their copy targets. All 277 hooks, generated into both repos. -- the new hook-pointer struct (context first arg, `vm_exec_hook_result_t` return); -- the new Rust capi `VMHooks` impl (call the pointer, compare against the sentinel, return - `Err`); -- the new Go `//export` shims; -- `eiGenMain.go`: register the writers and the copy targets. +| Writer | Output | +| --- | --- | +| `WriteRustCapi2VMHooksPointers` | `c-api-new/src/capi_vm_hook_pointers.rs` | +| `WriteRustCapi2VMHooks` | `c-api-new/src/capi_vm_hooks.rs` | +| `WriteWasmer3Cgo` | `mx-chain-vm-go/wasmer3/wasmer3ImportsCgo.go` | + +`capi_opcode_cost.rs` also gained a second copy target, so `c-api-new` no longer carries a +hand-copied duplicate of it. + +### The `earlyExit` field is not needed after all + +The original sketch had the shim reading an `earlyExit` field off the hooks object after the +call, and noted that this "depends on the Phase 4 provider change". That was written before +the Go hook migration landed. Every hook now **returns** its error, so the shim converts it +inline: + +```go +//export w3_getGasLeft +func w3_getGasLeft(ctx *C.vm_exec2_hook_context_t) C.vm_exec2_hook_result_t { + vmHooks := getVMHooksFromHookContext(ctx) + result, err := vmHooks.GetGasLeft() + if err != nil { + return hookEarlyExit(vmHooks, err) + } + return hookOk(int64(result)) +} +``` + +No field, no ordering hazard, and no dependency on Phase 4. The 277 Go hook signatures in +`executor/vmHooks.go` still do not change. + +### Converting the error to a code + +Two small hand-written pieces, deliberately split so neither has to know the other's job: + +- `executor/earlyExit.go` — `EarlyExitError` (an error that knows its own code) and + `EarlyExitCodeOf`, a pure function that falls back to `vmcommon.ExecutionFailed`. It finds + the code through `errors.As`, so it survives the `fmt.Errorf("%w")` wrapping the hook + helpers do. +- `vmhost.EarlyExit.EarlyExitCode()` — three lines, returning the `vmcommon.ReturnCode` it + already carries. + +The wire codes are `vmcommon.ReturnCode` values, matching the Rust VM's `ReturnCode::as_u64`. +Nothing in the failure path produces `vmcommon.Ok`, which is what keeps code 0 free to mean +"legacy async call", as `ASYNC_CALL_EARLY_EXIT_CODE` does on the Rust side. + +`hookEarlyExit` calls `HandleHookError` for the side effects and `EarlyExitCodeOf` for the +code. Under this executor the breakpoint `HandleHookError` still sets is redundant — the early +exit already stops the instance — and it goes away with `PLAN-go-breakpoint-removal.md`. + +### The generated shims check their own ABI + +cgo types the hook table's fields as `*[0]byte`, so the Go assignment in +`populateCgoFunctionPointers` would accept a function of any shape, and a mismatch would only +show up as a corrupted stack at the first call. The generated file therefore emits a C +initializer assigning every shim to its field — the one place both types are visible at once: + +```c +// static const vm_exec2_vm_hook_c_func_pointers w3_signature_check __attribute__((unused)) = { +// .get_gas_left_func_ptr = w3_getGasLeft, +// ... +``` + +Verified by deliberately widening one shim's argument and confirming the build fails on +exactly that field. + +### Rust side wired up + +`CapiHookTable` now holds a copy of the generated table rather than opaque words, and +`vm_exec2_new_executor` takes it typed, so the Phase 2 placeholder parameter pair +`(hook_table, hook_table_len)` is gone. `CapiVMHooksBuilder` builds a `CapiVMHooks` around the +activated context instead of yielding `VMHooksDefault`; the Phase 2 `TODO(Phase 3)` is closed. + +### Testing + +`hook_dispatch` in `c-api-new/tests/ffi_boundary.rs` runs a contract that calls two hooks +through a mock table, covering the whole round trip in one test: wasm into Rust, out through a +C function pointer, back into Rust through the state functions to read the contract's memory, +and — for the hook that asks to stop — out again as `VM_EXEC2_CALL_EARLY_EXIT` carrying the +code. It also pins the thing the whole ABI rests on: the sentinel keeps "returned normally" +distinct from early-exit code 0. -**The 282 Go hook signatures in `executor/vmHooks.go` do not change.** The shim reads an -`earlyExit` field off the hooks object after the call and packs it into the result struct. -That works only if there is one hooks object per live instance, so it depends on the Phase 4 -provider change rather than on touching the interface. +On the Go side, the package compiling *is* the check that a cgo `//export` function can return +a C struct by value — now across all 277 shims rather than the original three-layout spike. ## Phase 4 — Go VM (new `wasmer3/` package, parallel to `wasmer2/`) @@ -318,5 +392,6 @@ and the C ABI is parameter-form regardless, so the trait convergence was not a p after all — it is now better informed by Phase 2, which found the per-instance hooks argument the converged trait needs. -Remaining path: **Phase 3 → Phase 4 → Phase 5**, with Phase 1.6/1.7 landing whenever G4 is -settled across both backends. Phase 3 is now the only thing between here and a Go consumer. +Remaining path: **Phase 4 → Phase 5**, with Phase 1.6/1.7 landing whenever G4 is settled +across both backends. Both sides of the boundary now exist and are tested in isolation; what +is left is the Go VM actually linking the library and driving it. diff --git a/c-api-new/libvmexeccapi_new.h b/c-api-new/libvmexeccapi_new.h index 015ff359..17ba18f4 100644 --- a/c-api-new/libvmexeccapi_new.h +++ b/c-api-new/libvmexeccapi_new.h @@ -35,7 +35,7 @@ typedef enum vm_exec2_status_t { VM_EXEC2_NULL_POINTER = -2, /** * A hook context was used outside the host call it belongs to. See - * [`crate::capi_hook_context`]. + * [`crate::capi_vm_hook_context`]. */ VM_EXEC2_STALE_CONTEXT = -3, /** @@ -244,41 +244,29 @@ typedef struct vm_exec2_executor_config_t { } vm_exec2_executor_config_t; /** - * Opaque handle to an instance. - */ -typedef struct vm_exec2_instance_t { - -} vm_exec2_instance_t; - -/** - * FFI-safe mirror of the new `CompilationOptions`. + * What every VM hook returns. * - * Four fields, against the legacy seven: `gas_limit` moved to the call, and metering and - * runtime breakpoints are no longer optional. Fields that are `usize` on the Rust side are - * fixed to `u64` here, since `usize` has no guaranteed width across host platforms. - */ -typedef struct vm_exec2_compilation_options_t { - uint64_t unmetered_locals; - uint64_t max_memory_grow; - uint64_t max_memory_grow_delta; - bool opcode_trace; -} vm_exec2_compilation_options_t; - -/** - * The out-struct of `vm_exec2_instance_call`. + * One 16-byte struct covers every hook shape — `void`, `i32`, `i64` — and is returned in + * registers under both SysV AMD64 (RAX:RDX) and AAPCS64 (X0:X1), so the early-exit channel + * costs no memory traffic. A cgo `//export` function may return a C struct by value, which + * is what makes this shape available to the Go side. + * + * The early-exit *message* does not travel in this struct. A pointer cannot: cgo forbids + * returning an unpinned Go pointer, and a fixed buffer cannot work because the messages are + * unbounded user text. The Go VM does not need it to travel, because the hook has already + * recorded the message in its own output state by the time it returns. */ -typedef struct vm_exec2_call_result_t { - enum vm_exec2_call_result_kind_t kind; +typedef struct vm_exec2_hook_result_t { /** - * Meaningful only for `VM_EXEC2_CALL_EARLY_EXIT`. + * The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for + * any hook that reports an early exit. */ - uint64_t early_exit_code; + int64_t value; /** - * Meaningful only for `VM_EXEC2_CALL_BREAKPOINT`. Values match `BreakpointValue`: - * 4 = out of gas, 5 = memory limit. + * The early-exit code, or `VM_EXEC2_NO_EARLY_EXIT`. */ - uint64_t breakpoint; -} vm_exec2_call_result_t; + uint64_t early_exit; +} vm_exec2_hook_result_t; /** * What a VM hook is handed as its first argument. @@ -296,7 +284,7 @@ typedef struct vm_exec2_hook_context_t { */ void *state; /** - * Erased `&RefCell`, for the token check. + * Erased `&RefCell`, for the token check. */ const void *pool; /** @@ -306,29 +294,326 @@ typedef struct vm_exec2_hook_context_t { } vm_exec2_hook_context_t; /** - * What every VM hook returns. + * The Go VM's hook implementations, as a table of C function pointers. * - * One 16-byte struct covers every hook shape — `void`, `i32`, `i64` — and is returned in - * registers under both SysV AMD64 (RAX:RDX) and AAPCS64 (X0:X1), so the early-exit channel - * costs no memory traffic. A cgo `//export` function may return a C struct by value, which - * is what makes this shape available to the Go side. + * Copied into Rust at executor creation; see CapiVMHookTable. + */ +typedef struct vm_exec2_vm_hook_c_func_pointers { + struct vm_exec2_hook_result_t (*get_gas_left_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_sc_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); + struct vm_exec2_hook_result_t (*get_owner_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); + struct vm_exec2_hook_result_t (*get_shard_of_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset); + struct vm_exec2_hook_result_t (*is_smart_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset); + struct vm_exec2_hook_result_t (*signal_error_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t message_offset, int32_t message_length); + struct vm_exec2_hook_result_t (*get_external_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t result_offset); + struct vm_exec2_hook_result_t (*get_block_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t nonce, int32_t result_offset); + struct vm_exec2_hook_result_t (*get_esdt_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_offset); + struct vm_exec2_hook_result_t (*get_esdt_nft_name_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); + struct vm_exec2_hook_result_t (*get_esdt_nft_attribute_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); + struct vm_exec2_hook_result_t (*get_esdt_nft_uri_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); + struct vm_exec2_hook_result_t (*get_esdt_token_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t value_handle, int32_t properties_offset, int32_t hash_offset, int32_t name_offset, int32_t attributes_offset, int32_t creator_offset, int32_t royalties_handle, int32_t uris_offset); + struct vm_exec2_hook_result_t (*get_esdt_local_roles_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec2_hook_result_t (*validate_token_identifier_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec2_hook_result_t (*transfer_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); + struct vm_exec2_hook_result_t (*transfer_value_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*transfer_esdt_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*transfer_esdt_nft_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t nonce, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t num_token_transfers, int32_t token_transfers_args_length_offset, int32_t token_transfer_data_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*create_async_call_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t data_length, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback); + struct vm_exec2_hook_result_t (*set_async_context_callback_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t callback, int32_t callback_length, int32_t data, int32_t data_length, int64_t gas); + struct vm_exec2_hook_result_t (*upgrade_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*upgrade_from_source_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*delete_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*async_call_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); + struct vm_exec2_hook_result_t (*get_argument_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id); + struct vm_exec2_hook_result_t (*get_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id, int32_t arg_offset); + struct vm_exec2_hook_result_t (*get_function_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t function_offset); + struct vm_exec2_hook_result_t (*get_num_arguments_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*storage_store_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset, int32_t data_length); + struct vm_exec2_hook_result_t (*storage_load_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec2_hook_result_t (*storage_load_from_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t key_offset, int32_t key_length, int32_t data_offset); + struct vm_exec2_hook_result_t (*storage_load_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset); + struct vm_exec2_hook_result_t (*set_storage_lock_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t lock_timestamp); + struct vm_exec2_hook_result_t (*get_storage_lock_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec2_hook_result_t (*is_storage_locked_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec2_hook_result_t (*clear_storage_lock_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec2_hook_result_t (*get_caller_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); + struct vm_exec2_hook_result_t (*check_no_payment_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); + struct vm_exec2_hook_result_t (*get_esdt_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); + struct vm_exec2_hook_result_t (*get_esdt_value_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset, int32_t index); + struct vm_exec2_hook_result_t (*get_esdt_token_name_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); + struct vm_exec2_hook_result_t (*get_esdt_token_name_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset, int32_t index); + struct vm_exec2_hook_result_t (*get_esdt_token_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_esdt_token_nonce_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t index); + struct vm_exec2_hook_result_t (*get_current_esdt_nft_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len); + struct vm_exec2_hook_result_t (*get_esdt_token_type_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_esdt_token_type_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t index); + struct vm_exec2_hook_result_t (*get_num_esdt_transfers_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_call_value_token_name_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset); + struct vm_exec2_hook_result_t (*get_call_value_token_name_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset, int32_t index); + struct vm_exec2_hook_result_t (*is_reserved_function_name_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t name_handle); + struct vm_exec2_hook_result_t (*write_log_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_pointer, int32_t data_length, int32_t topic_ptr, int32_t num_topics); + struct vm_exec2_hook_result_t (*write_event_log_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t num_topics, int32_t topic_lengths_offset, int32_t topic_offset, int32_t data_offset, int32_t data_length); + struct vm_exec2_hook_result_t (*get_block_timestamp_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_block_timestamp_ms_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_block_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_block_round_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_block_epoch_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_block_random_seed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t pointer); + struct vm_exec2_hook_result_t (*get_state_root_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t pointer); + struct vm_exec2_hook_result_t (*get_prev_block_timestamp_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_prev_block_timestamp_ms_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_prev_block_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_prev_block_round_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_prev_block_epoch_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_prev_block_random_seed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t pointer); + struct vm_exec2_hook_result_t (*get_block_round_time_ms_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*epoch_start_block_timestamp_ms_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*epoch_start_block_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*epoch_start_block_round_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*finish_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t pointer, int32_t length); + struct vm_exec2_hook_result_t (*execute_on_same_context_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*execute_on_dest_context_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*execute_read_only_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*create_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t result_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*deploy_from_source_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t result_address_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec2_hook_result_t (*get_num_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*get_return_data_size_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_id); + struct vm_exec2_hook_result_t (*get_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_id, int32_t data_offset); + struct vm_exec2_hook_result_t (*clean_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*delete_from_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_id); + struct vm_exec2_hook_result_t (*get_original_tx_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset); + struct vm_exec2_hook_result_t (*get_current_tx_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset); + struct vm_exec2_hook_result_t (*get_prev_tx_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset); + struct vm_exec2_hook_result_t (*managed_sc_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*managed_owner_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*managed_caller_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*managed_get_original_caller_addr_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*managed_get_relayer_addr_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*managed_signal_error_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t err_handle); + struct vm_exec2_hook_result_t (*managed_write_log_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t topics_handle, int32_t data_handle); + struct vm_exec2_hook_result_t (*managed_get_original_tx_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_get_state_root_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_get_block_random_seed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_get_prev_block_random_seed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_get_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_id, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_get_multi_esdt_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t multi_call_value_handle); + struct vm_exec2_hook_result_t (*managed_get_all_transfers_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t transfer_call_values_list_handle); + struct vm_exec2_hook_result_t (*managed_get_back_transfers_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t esdt_transfers_value_handle, int32_t egld_value_handle); + struct vm_exec2_hook_result_t (*managed_get_esdt_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle); + struct vm_exec2_hook_result_t (*managed_get_esdt_token_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle, int32_t properties_handle, int32_t hash_handle, int32_t name_handle, int32_t attributes_handle, int32_t creator_handle, int32_t royalties_handle, int32_t uris_handle); + struct vm_exec2_hook_result_t (*managed_get_esdt_token_type_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t type_handle); + struct vm_exec2_hook_result_t (*managed_async_call_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle); + struct vm_exec2_hook_result_t (*managed_create_async_call_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback, int32_t callback_closure_handle); + struct vm_exec2_hook_result_t (*managed_get_callback_closure_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t callback_closure_handle); + struct vm_exec2_hook_result_t (*managed_upgrade_from_source_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_upgrade_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_delete_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int64_t gas_limit, int32_t arguments_handle); + struct vm_exec2_hook_result_t (*managed_deploy_from_source_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_create_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_execute_read_only_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_execute_on_same_context_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_execute_on_dest_context_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_execute_on_dest_context_with_error_return_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*managed_multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec2_hook_result_t (*managed_multi_transfer_esdt_nft_execute_with_return_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec2_hook_result_t (*managed_multi_transfer_esdt_nft_execute_by_user_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t user_handle, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec2_hook_result_t (*managed_transfer_value_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dst_handle, int32_t value_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec2_hook_result_t (*managed_is_esdt_frozen_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce); + struct vm_exec2_hook_result_t (*managed_is_esdt_limited_transfer_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec2_hook_result_t (*managed_is_esdt_paused_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec2_hook_result_t (*managed_buffer_to_hex_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t source_handle, int32_t dest_handle); + struct vm_exec2_hook_result_t (*managed_get_code_metadata_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t response_handle); + struct vm_exec2_hook_result_t (*managed_get_code_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t code_hash_handle); + struct vm_exec2_hook_result_t (*managed_is_builtin_function_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t function_name_handle); + struct vm_exec2_hook_result_t (*big_float_new_from_parts_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t integral_part, int32_t fractional_part, int32_t exponent); + struct vm_exec2_hook_result_t (*big_float_new_from_frac_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t numerator, int64_t denominator); + struct vm_exec2_hook_result_t (*big_float_new_from_sci_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t significand, int64_t exponent); + struct vm_exec2_hook_result_t (*big_float_add_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_float_sub_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_float_mul_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_float_div_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_float_neg_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_float_clone_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_float_cmp_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_float_abs_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_float_sign_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_float_sqrt_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_float_pow_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t exponent); + struct vm_exec2_hook_result_t (*big_float_floor_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_float_ceil_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_float_truncate_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_float_set_int64_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int64_t value); + struct vm_exec2_hook_result_t (*big_float_is_int_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_float_set_big_int_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t big_int_handle); + struct vm_exec2_hook_result_t (*big_float_get_const_pi_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*big_float_get_const_e_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*big_int_get_unsigned_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id, int32_t destination_handle); + struct vm_exec2_hook_result_t (*big_int_get_signed_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id, int32_t destination_handle); + struct vm_exec2_hook_result_t (*big_int_storage_store_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t source_handle); + struct vm_exec2_hook_result_t (*big_int_storage_load_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t destination_handle); + struct vm_exec2_hook_result_t (*big_int_get_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*big_int_get_esdt_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination); + struct vm_exec2_hook_result_t (*big_int_get_esdt_call_value_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t index); + struct vm_exec2_hook_result_t (*big_int_get_external_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t result); + struct vm_exec2_hook_result_t (*big_int_get_esdt_external_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_handle); + struct vm_exec2_hook_result_t (*big_int_new_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t small_value); + struct vm_exec2_hook_result_t (*big_int_unsigned_byte_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec2_hook_result_t (*big_int_signed_byte_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec2_hook_result_t (*big_int_get_unsigned_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); + struct vm_exec2_hook_result_t (*big_int_get_signed_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); + struct vm_exec2_hook_result_t (*big_int_set_unsigned_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); + struct vm_exec2_hook_result_t (*big_int_set_signed_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); + struct vm_exec2_hook_result_t (*big_int_is_int64_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*big_int_get_int64_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec2_hook_result_t (*big_int_set_int64_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int64_t value); + struct vm_exec2_hook_result_t (*big_int_add_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_sub_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_mul_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_tdiv_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_tmod_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_ediv_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_emod_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_sqrt_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_int_pow_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_log2_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op1_handle); + struct vm_exec2_hook_result_t (*big_int_abs_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_int_neg_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_int_sign_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_int_cmp_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_not_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec2_hook_result_t (*big_int_and_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_or_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_xor_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec2_hook_result_t (*big_int_shr_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); + struct vm_exec2_hook_result_t (*big_int_shl_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); + struct vm_exec2_hook_result_t (*big_int_finish_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec2_hook_result_t (*big_int_finish_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec2_hook_result_t (*big_int_to_string_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t big_int_handle, int32_t destination_handle); + struct vm_exec2_hook_result_t (*mbuffer_new_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*mbuffer_new_from_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t data_length); + struct vm_exec2_hook_result_t (*mbuffer_get_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle); + struct vm_exec2_hook_result_t (*mbuffer_get_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t result_offset); + struct vm_exec2_hook_result_t (*mbuffer_get_byte_slice_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t result_offset); + struct vm_exec2_hook_result_t (*mbuffer_copy_byte_slice_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t destination_handle); + struct vm_exec2_hook_result_t (*mbuffer_eq_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle1, int32_t m_buffer_handle2); + struct vm_exec2_hook_result_t (*mbuffer_set_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t data_offset, int32_t data_length); + struct vm_exec2_hook_result_t (*mbuffer_set_byte_slice_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t starting_position, int32_t data_length, int32_t data_offset); + struct vm_exec2_hook_result_t (*mbuffer_append_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_handle); + struct vm_exec2_hook_result_t (*mbuffer_append_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_offset, int32_t data_length); + struct vm_exec2_hook_result_t (*mbuffer_to_big_int_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec2_hook_result_t (*mbuffer_to_big_int_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec2_hook_result_t (*mbuffer_from_big_int_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec2_hook_result_t (*mbuffer_from_big_int_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec2_hook_result_t (*mbuffer_to_small_int_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle); + struct vm_exec2_hook_result_t (*mbuffer_to_small_int_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle); + struct vm_exec2_hook_result_t (*mbuffer_from_small_int_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); + struct vm_exec2_hook_result_t (*mbuffer_from_small_int_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); + struct vm_exec2_hook_result_t (*mbuffer_to_big_float_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); + struct vm_exec2_hook_result_t (*mbuffer_from_big_float_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); + struct vm_exec2_hook_result_t (*mbuffer_storage_store_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t source_handle); + struct vm_exec2_hook_result_t (*mbuffer_storage_load_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t destination_handle); + struct vm_exec2_hook_result_t (*mbuffer_storage_load_from_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t key_handle, int32_t destination_handle); + struct vm_exec2_hook_result_t (*mbuffer_get_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id, int32_t destination_handle); + struct vm_exec2_hook_result_t (*mbuffer_finish_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t source_handle); + struct vm_exec2_hook_result_t (*mbuffer_set_random_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t length); + struct vm_exec2_hook_result_t (*managed_map_new_func_ptr)(struct vm_exec2_hook_context_t *ctx); + struct vm_exec2_hook_result_t (*managed_map_put_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t value_handle); + struct vm_exec2_hook_result_t (*managed_map_get_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); + struct vm_exec2_hook_result_t (*managed_map_remove_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); + struct vm_exec2_hook_result_t (*managed_map_contains_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle); + struct vm_exec2_hook_result_t (*small_int_get_unsigned_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id); + struct vm_exec2_hook_result_t (*small_int_get_signed_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id); + struct vm_exec2_hook_result_t (*small_int_finish_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t value); + struct vm_exec2_hook_result_t (*small_int_finish_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t value); + struct vm_exec2_hook_result_t (*small_int_storage_store_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); + struct vm_exec2_hook_result_t (*small_int_storage_store_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); + struct vm_exec2_hook_result_t (*small_int_storage_load_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec2_hook_result_t (*small_int_storage_load_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec2_hook_result_t (*int64get_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id); + struct vm_exec2_hook_result_t (*int64finish_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t value); + struct vm_exec2_hook_result_t (*int64storage_store_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); + struct vm_exec2_hook_result_t (*int64storage_load_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec2_hook_result_t (*sha256_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); + struct vm_exec2_hook_result_t (*managed_sha256_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); + struct vm_exec2_hook_result_t (*keccak256_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); + struct vm_exec2_hook_result_t (*managed_keccak256_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); + struct vm_exec2_hook_result_t (*ripemd160_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); + struct vm_exec2_hook_result_t (*managed_ripemd160_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); + struct vm_exec2_hook_result_t (*verify_bls_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); + struct vm_exec2_hook_result_t (*managed_verify_bls_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec2_hook_result_t (*verify_ed25519_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); + struct vm_exec2_hook_result_t (*managed_verify_ed25519_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec2_hook_result_t (*verify_custom_secp256k1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset, int32_t hash_type); + struct vm_exec2_hook_result_t (*managed_verify_custom_secp256k1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle, int32_t hash_type); + struct vm_exec2_hook_result_t (*verify_secp256k1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset); + struct vm_exec2_hook_result_t (*managed_verify_secp256k1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec2_hook_result_t (*encode_secp256k1_der_signature_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t r_offset, int32_t r_length, int32_t s_offset, int32_t s_length, int32_t sig_offset); + struct vm_exec2_hook_result_t (*managed_encode_secp256k1_der_signature_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t r_handle, int32_t s_handle, int32_t sig_handle); + struct vm_exec2_hook_result_t (*add_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t fst_point_xhandle, int32_t fst_point_yhandle, int32_t snd_point_xhandle, int32_t snd_point_yhandle); + struct vm_exec2_hook_result_t (*double_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); + struct vm_exec2_hook_result_t (*is_on_curve_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); + struct vm_exec2_hook_result_t (*scalar_base_mult_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); + struct vm_exec2_hook_result_t (*managed_scalar_base_mult_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); + struct vm_exec2_hook_result_t (*scalar_mult_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_offset, int32_t length); + struct vm_exec2_hook_result_t (*managed_scalar_mult_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_handle); + struct vm_exec2_hook_result_t (*marshal_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); + struct vm_exec2_hook_result_t (*managed_marshal_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*marshal_compressed_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); + struct vm_exec2_hook_result_t (*managed_marshal_compressed_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*unmarshal_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); + struct vm_exec2_hook_result_t (*managed_unmarshal_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); + struct vm_exec2_hook_result_t (*unmarshal_compressed_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); + struct vm_exec2_hook_result_t (*managed_unmarshal_compressed_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); + struct vm_exec2_hook_result_t (*generate_key_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_offset); + struct vm_exec2_hook_result_t (*managed_generate_key_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_handle); + struct vm_exec2_hook_result_t (*create_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t data_length); + struct vm_exec2_hook_result_t (*managed_create_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_handle); + struct vm_exec2_hook_result_t (*get_curve_length_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t ec_handle); + struct vm_exec2_hook_result_t (*get_priv_key_byte_length_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t ec_handle); + struct vm_exec2_hook_result_t (*elliptic_curve_get_values_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t ec_handle, int32_t field_order_handle, int32_t base_point_order_handle, int32_t eq_constant_handle, int32_t x_base_point_handle, int32_t y_base_point_handle); + struct vm_exec2_hook_result_t (*managed_verify_secp256r1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec2_hook_result_t (*managed_verify_blssignature_share_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec2_hook_result_t (*managed_verify_blsaggregated_signature_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); +} vm_exec2_vm_hook_c_func_pointers; + +/** + * Opaque handle to an instance. + */ +typedef struct vm_exec2_instance_t { + +} vm_exec2_instance_t; + +/** + * FFI-safe mirror of the new `CompilationOptions`. * - * The early-exit *message* does not travel in this struct. A pointer cannot: cgo forbids - * returning an unpinned Go pointer, and a fixed buffer cannot work because the messages are - * unbounded user text. The Go VM does not need it to travel, because the hook has already - * recorded the message in its own output state by the time it returns. + * Four fields, against the legacy seven: `gas_limit` moved to the call, and metering and + * runtime breakpoints are no longer optional. Fields that are `usize` on the Rust side are + * fixed to `u64` here, since `usize` has no guaranteed width across host platforms. */ -typedef struct vm_exec2_hook_result_t { +typedef struct vm_exec2_compilation_options_t { + uint64_t unmetered_locals; + uint64_t max_memory_grow; + uint64_t max_memory_grow_delta; + bool opcode_trace; +} vm_exec2_compilation_options_t; + +/** + * The out-struct of `vm_exec2_instance_call`. + */ +typedef struct vm_exec2_call_result_t { + enum vm_exec2_call_result_kind_t kind; /** - * The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for - * any hook that reports an early exit. + * Meaningful only for `VM_EXEC2_CALL_EARLY_EXIT`. */ - int64_t value; + uint64_t early_exit_code; /** - * The early-exit code, or `VM_EXEC2_NO_EARLY_EXIT`. + * Meaningful only for `VM_EXEC2_CALL_BREAKPOINT`. Values match `BreakpointValue`: + * 4 = out of gas, 5 = memory limit. */ - uint64_t early_exit; -} vm_exec2_hook_result_t; + uint64_t breakpoint; +} vm_exec2_call_result_t; /** * A buffer of this size holds every message the executor currently produces. @@ -341,8 +626,8 @@ extern const int VM_EXEC2_ERROR_BUF_RECOMMENDED_LEN; /** * Creates an executor from one configuration struct and one hook table. * - * `hook_table` is copied, so the caller is free to release it as soon as this returns. - * `hook_table_len` is the number of pointer-sized entries in it. + * `hook_table` is copied, so the caller is free to release it as soon as this returns. Every + * entry must be populated; see `CapiVMHookTable::copy_from`. * * On success writes the executor to `executor_out` and returns `VM_EXEC2_OK`. The caller * owns the result and must release it with `vm_exec2_executor_destroy`. @@ -351,7 +636,7 @@ extern const int VM_EXEC2_ERROR_BUF_RECOMMENDED_LEN; * * C API function, works with raw object pointers. */ -enum vm_exec2_status_t vm_exec2_new_executor(struct vm_exec2_executor_t **executor_out, const struct vm_exec2_executor_config_t *config, const void *hook_table, uintptr_t hook_table_len, char *err_buf, int err_buf_len); +enum vm_exec2_status_t vm_exec2_new_executor(struct vm_exec2_executor_t **executor_out, const struct vm_exec2_executor_config_t *config, const struct vm_exec2_vm_hook_c_func_pointers *hook_table, char *err_buf, int err_buf_len); /** * Destroys an executor. Does nothing if `executor` is null. diff --git a/c-api-new/src/capi_executor.rs b/c-api-new/src/capi_executor.rs index 3c4826f7..7ff24e3f 100644 --- a/c-api-new/src/capi_executor.rs +++ b/c-api-new/src/capi_executor.rs @@ -17,12 +17,12 @@ use libc::{c_char, c_int}; use meta::capi_safe_unwind; use multiversx_chain_vm_executor::OpcodeConfig; -use std::ffi::c_void; use std::sync::Arc; use crate::capi_config::vm_exec2_executor_config_t; -use crate::capi_hook_table::CapiHookTable; use crate::capi_status::{vm_exec2_status_t, write_error}; +use crate::capi_vm_hook_pointers::vm_exec2_vm_hook_c_func_pointers; +use crate::capi_vm_hook_table::CapiVMHookTable; /// Opaque handle to an executor. #[allow(non_camel_case_types)] @@ -31,13 +31,13 @@ pub struct vm_exec2_executor_t; pub struct CapiExecutor { pub(crate) opcode_config: Arc, - pub(crate) hook_table: CapiHookTable, + pub(crate) hook_table: CapiVMHookTable, } /// Creates an executor from one configuration struct and one hook table. /// -/// `hook_table` is copied, so the caller is free to release it as soon as this returns. -/// `hook_table_len` is the number of pointer-sized entries in it. +/// `hook_table` is copied, so the caller is free to release it as soon as this returns. Every +/// entry must be populated; see `CapiVMHookTable::copy_from`. /// /// On success writes the executor to `executor_out` and returns `VM_EXEC2_OK`. The caller /// owns the result and must release it with `vm_exec2_executor_destroy`. @@ -51,12 +51,11 @@ pub struct CapiExecutor { pub unsafe extern "C" fn vm_exec2_new_executor( executor_out: *mut *mut vm_exec2_executor_t, config: *const vm_exec2_executor_config_t, - hook_table: *const c_void, - hook_table_len: usize, + hook_table: *const vm_exec2_vm_hook_c_func_pointers, err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec2_status_t { - if executor_out.is_null() || config.is_null() { + if executor_out.is_null() || config.is_null() || hook_table.is_null() { return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; } @@ -67,7 +66,7 @@ pub unsafe extern "C" fn vm_exec2_new_executor( let capi_executor = CapiExecutor { opcode_config: Arc::new(opcode_config), - hook_table: unsafe { CapiHookTable::copy_from(hook_table, hook_table_len) }, + hook_table: unsafe { CapiVMHookTable::copy_from(hook_table) }, }; unsafe { diff --git a/c-api-new/src/capi_hook_table.rs b/c-api-new/src/capi_hook_table.rs deleted file mode 100644 index 7a068a12..00000000 --- a/c-api-new/src/capi_hook_table.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! The Go VM hook function-pointer table, owned by Rust. -//! -//! The table is **copied** at executor creation rather than referenced, exactly as the -//! legacy API does. Holding the caller's pointer instead would make the executor's validity -//! depend on the caller keeping a Go-side struct alive and unmoved for the executor's whole -//! lifetime, which is not a contract worth having at this boundary. -//! -//! Phase 2 copies it as opaque pointer-sized words, because the typed struct — one correctly -//! typed `extern "C" fn` per hook — is generated code that arrives with Phase 3. The copy is -//! what the generated implementation will read through, so the ownership and lifetime story -//! does not change when the type does. - -use std::ffi::c_void; -use std::sync::Arc; - -/// An owned copy of the caller's hook table. -/// -/// Cloning is cheap and shares the copy; each instance's hooks builder holds one. -#[derive(Clone)] -pub struct CapiHookTable { - words: Arc<[*const c_void]>, -} - -// The table is a block of plain function pointers with no interior mutability. The -// `ExperimentalExecutorRuntimeRef` it ends up behind is `Send + Sync`, so this has to be -// too; the pointers themselves are only ever read, and only from the thread servicing a -// host call. -unsafe impl Send for CapiHookTable {} -unsafe impl Sync for CapiHookTable {} - -impl CapiHookTable { - /// Copies `len` pointer-sized words out of `table`. - /// - /// # Safety - /// - /// `table` must point to at least `len` readable pointer-sized words. - pub unsafe fn copy_from(table: *const c_void, len: usize) -> Self { - let words: Arc<[*const c_void]> = if table.is_null() || len == 0 { - Arc::from(Vec::new()) - } else { - let src = unsafe { std::slice::from_raw_parts(table as *const *const c_void, len) }; - Arc::from(src.to_vec()) - }; - - CapiHookTable { words } - } - - /// Number of pointer-sized words copied. - pub fn len(&self) -> usize { - self.words.len() - } - - pub fn is_empty(&self) -> bool { - self.words.is_empty() - } - - /// The `index`-th function pointer, or `None` if the table is shorter than that. - /// - /// Only used by the generated hooks implementation, which knows the real layout. - pub fn word(&self, index: usize) -> Option<*const c_void> { - self.words.get(index).copied() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn copies_the_caller_s_words() { - // Real addresses rather than small integers, so the test does not depend on what a - // fabricated pointer value is allowed to be. - let targets = [10u64, 20u64, 30u64]; - let table: [*const c_void; 3] = [ - (&targets[0] as *const u64).cast(), - (&targets[1] as *const u64).cast(), - (&targets[2] as *const u64).cast(), - ]; - let copied = unsafe { CapiHookTable::copy_from(table.as_ptr() as *const c_void, 3) }; - - assert_eq!(copied.len(), 3); - assert_eq!(copied.word(0), Some(table[0])); - assert_eq!(copied.word(2), Some(table[2])); - assert_eq!(copied.word(3), None); - } - - #[test] - fn tolerates_an_absent_table() { - let empty = unsafe { CapiHookTable::copy_from(std::ptr::null(), 0) }; - assert!(empty.is_empty()); - assert_eq!(empty.word(0), None); - - let null_with_len = unsafe { CapiHookTable::copy_from(std::ptr::null(), 10) }; - assert!(null_with_len.is_empty()); - } -} diff --git a/c-api-new/src/capi_instance.rs b/c-api-new/src/capi_instance.rs index 55e14f5f..8b6aca12 100644 --- a/c-api-new/src/capi_instance.rs +++ b/c-api-new/src/capi_instance.rs @@ -15,9 +15,9 @@ use std::slice; use crate::capi_config::vm_exec2_compilation_options_t; use crate::capi_executor::{CapiExecutor, vm_exec2_executor_t}; -use crate::capi_hook_context::CapiVMHooksBuilder; use crate::capi_status::{vm_exec2_status_t, write_error}; use crate::capi_strings::{copy_pipe_joined, pipe_joined_length}; +use crate::capi_vm_hook_context::CapiVMHooksBuilder; /// Opaque handle to an instance. #[allow(non_camel_case_types)] diff --git a/c-api-new/src/capi_instance_state.rs b/c-api-new/src/capi_instance_state.rs index 80ddcb66..64406909 100644 --- a/c-api-new/src/capi_instance_state.rs +++ b/c-api-new/src/capi_instance_state.rs @@ -1,7 +1,7 @@ //! The state operations: what a VM hook may do to the instance it is running inside. //! //! Every function here takes a hook context and is valid only while the hook that was handed -//! that context is still running. See `capi_hook_context` for how that is enforced; a stale +//! that context is still running. See `capi_vm_hook_context` for how that is enforced; a stale //! context yields `VM_EXEC2_STALE_CONTEXT` rather than a dereference of a dead borrow. //! //! Bounds checking lives on this side of the boundary, not in Go. The executor is the only @@ -16,8 +16,8 @@ use multiversx_chain_vm_executor::{ }; use std::slice; -use crate::capi_hook_context::{ContextError, vm_exec2_hook_context_t, with_instance_state}; use crate::capi_status::{vm_exec2_status_t, write_error, write_error_count}; +use crate::capi_vm_hook_context::{ContextError, vm_exec2_hook_context_t, with_instance_state}; impl ContextError { fn as_status(self) -> vm_exec2_status_t { diff --git a/c-api-new/src/capi_status.rs b/c-api-new/src/capi_status.rs index a344af98..fe5db97b 100644 --- a/c-api-new/src/capi_status.rs +++ b/c-api-new/src/capi_status.rs @@ -31,7 +31,7 @@ pub enum vm_exec2_status_t { VM_EXEC2_NULL_POINTER = -2, /// A hook context was used outside the host call it belongs to. See - /// [`crate::capi_hook_context`]. + /// [`crate::capi_vm_hook_context`]. VM_EXEC2_STALE_CONTEXT = -3, /// A panic was caught at the boundary. Rust panics must never unwind into C, so every diff --git a/c-api-new/src/capi_hook_context.rs b/c-api-new/src/capi_vm_hook_context.rs similarity index 91% rename from c-api-new/src/capi_hook_context.rs rename to c-api-new/src/capi_vm_hook_context.rs index d6bd41eb..7d10305d 100644 --- a/c-api-new/src/capi_hook_context.rs +++ b/c-api-new/src/capi_vm_hook_context.rs @@ -34,12 +34,13 @@ use std::cell::RefCell; use std::ffi::c_void; -use multiversx_chain_vm_executor::{VMHooks, VMHooksDefault}; +use multiversx_chain_vm_executor::VMHooks; use multiversx_chain_vm_executor_wasmer_experimental::{ ExperimentalInstanceState, ExperimentalVMHooksBuilder, }; -use crate::capi_hook_table::CapiHookTable; +use crate::capi_vm_hook_table::CapiVMHookTable; +use crate::capi_vm_hooks::CapiVMHooks; /// The value of `vm_exec2_hook_result_t::early_exit` that means "the hook returned normally". /// @@ -111,7 +112,7 @@ pub struct vm_exec2_hook_context_t { /// Erased `&mut ExperimentalInstanceState<'_>`; null while no call is active. state: *mut c_void, - /// Erased `&RefCell`, for the token check. + /// Erased `&RefCell`, for the token check. pool: *const c_void, /// This activation's call token; `0` while no call is active. @@ -162,7 +163,7 @@ pub(crate) unsafe fn with_instance_state( // Shared borrow only, and released before `f` runs, so a hook is free to re-enter the // VM and activate a nested context while this one is being read. let active_token = { - let pool = unsafe { &*(ctx.pool as *const RefCell) }; + let pool = unsafe { &*(ctx.pool as *const RefCell) }; pool.borrow().active_token }; if ctx.token != active_token { @@ -183,7 +184,7 @@ pub(crate) enum ContextError { /// The per-instance stack of hook contexts. #[derive(Default)] -pub struct CapiHookContextPool { +pub struct CapiVMHookContextPool { /// One boxed context per nesting level. /// /// The `Box` is load-bearing and not the redundancy clippy takes it for: a @@ -208,17 +209,16 @@ pub struct CapiHookContextPool { /// Builds the VM hooks for one host call, and owns the context lifecycle. pub struct CapiVMHooksBuilder { go_vm_hooks: *mut c_void, - #[allow(dead_code)] // Consumed by the generated hooks implementation; see `with_vm_hooks`. - hook_table: CapiHookTable, - pool: RefCell, + hook_table: CapiVMHookTable, + pool: RefCell, } impl CapiVMHooksBuilder { - pub fn new(go_vm_hooks: *mut c_void, hook_table: CapiHookTable) -> Self { + pub fn new(go_vm_hooks: *mut c_void, hook_table: CapiVMHookTable) -> Self { CapiVMHooksBuilder { go_vm_hooks, hook_table, - pool: RefCell::new(CapiHookContextPool::default()), + pool: RefCell::new(CapiVMHookContextPool::default()), } } @@ -269,7 +269,7 @@ impl CapiVMHooksBuilder { pool.active_token = token; pool.depth = depth + 1; - let pool_ptr = &self.pool as *const RefCell as *const c_void; + let pool_ptr = &self.pool as *const RefCell as *const c_void; let slot = &mut pool.slots[depth]; slot.go_vm_hooks = self.go_vm_hooks; @@ -326,14 +326,10 @@ impl ExperimentalVMHooksBuilder for CapiVMHooksBuilder { instance_state_ref: &mut ExperimentalInstanceState, f: &mut dyn FnMut(&mut dyn VMHooks), ) { - self.with_hook_context(instance_state_ref, |_ctx| { - // TODO(Phase 3): replace with the generated `CapiVMHooks` built from `_ctx` and - // `self.hook_table`, which dispatches each of the 277 hooks to its Go function - // pointer and turns a non-sentinel `early_exit` into `Err(VMHooksEarlyExit)`. - // Everything that implementation needs — the context, the table, the token - // lifecycle — is in place; the 277-method `VMHooks` impl is generated code and - // does not belong in a hand-written file. - f(&mut VMHooksDefault); + self.with_hook_context(instance_state_ref, |ctx| { + // The hooks borrow the context, so they live exactly as long as this activation + // — which is also exactly as long as the instance state they reach through it. + f(&mut CapiVMHooks::new(ctx, self.hook_table.func_pointers())); }); } } diff --git a/c-api-new/src/capi_vm_hook_pointers.rs b/c-api-new/src/capi_vm_hook_pointers.rs new file mode 100644 index 00000000..f32df131 --- /dev/null +++ b/c-api-new/src/capi_vm_hook_pointers.rs @@ -0,0 +1,300 @@ +// Code generated by vmhooks generator. DO NOT EDIT. + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +use crate::capi_vm_hook_context::{vm_exec2_hook_context_t, vm_exec2_hook_result_t}; + +/// The Go VM's hook implementations, as a table of C function pointers. +/// +/// Copied into Rust at executor creation; see CapiVMHookTable. +#[repr(C)] +#[derive(Clone)] +#[allow(non_camel_case_types)] +#[rustfmt::skip] +pub struct vm_exec2_vm_hook_c_func_pointers { + pub get_gas_left_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, + pub get_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, + pub get_shard_of_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32) -> vm_exec2_hook_result_t, + pub is_smart_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32) -> vm_exec2_hook_result_t, + pub signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, message_offset: i32, message_length: i32) -> vm_exec2_hook_result_t, + pub get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, result_offset: i32) -> vm_exec2_hook_result_t, + pub get_block_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, nonce: i64, result_offset: i32) -> vm_exec2_hook_result_t, + pub get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_offset: i32) -> vm_exec2_hook_result_t, + pub get_esdt_nft_name_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec2_hook_result_t, + pub get_esdt_nft_attribute_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec2_hook_result_t, + pub get_esdt_nft_uri_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec2_hook_result_t, + pub get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, value_handle: i32, properties_offset: i32, hash_offset: i32, name_offset: i32, attributes_offset: i32, creator_offset: i32, royalties_handle: i32, uris_offset: i32) -> vm_exec2_hook_result_t, + pub get_esdt_local_roles_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, token_id_handle: i32) -> vm_exec2_hook_result_t, + pub validate_token_identifier_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, token_id_handle: i32) -> vm_exec2_hook_result_t, + pub transfer_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, + pub transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub transfer_esdt_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, nonce: i64, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, num_token_transfers: i32, token_transfers_args_length_offset: i32, token_transfer_data_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, data_length: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64) -> vm_exec2_hook_result_t, + pub set_async_context_callback_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, callback: i32, callback_length: i32, data: i32, data_length: i32, gas: i64) -> vm_exec2_hook_result_t, + pub upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, gas_limit: i64, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, + pub get_argument_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32) -> vm_exec2_hook_result_t, + pub get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32, arg_offset: i32) -> vm_exec2_hook_result_t, + pub get_function_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, function_offset: i32) -> vm_exec2_hook_result_t, + pub get_num_arguments_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, + pub storage_load_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, + pub storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub set_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, lock_timestamp: i64) -> vm_exec2_hook_result_t, + pub get_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, + pub is_storage_locked_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, + pub clear_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, + pub get_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, + pub check_no_payment_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, + pub get_esdt_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, + pub get_esdt_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32, index: i32) -> vm_exec2_hook_result_t, + pub get_esdt_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, + pub get_esdt_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32, index: i32) -> vm_exec2_hook_result_t, + pub get_esdt_token_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_esdt_token_nonce_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, index: i32) -> vm_exec2_hook_result_t, + pub get_current_esdt_nft_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32) -> vm_exec2_hook_result_t, + pub get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_esdt_token_type_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, index: i32) -> vm_exec2_hook_result_t, + pub get_num_esdt_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_call_value_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, call_value_offset: i32, token_name_offset: i32) -> vm_exec2_hook_result_t, + pub get_call_value_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, call_value_offset: i32, token_name_offset: i32, index: i32) -> vm_exec2_hook_result_t, + pub is_reserved_function_name_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, name_handle: i32) -> vm_exec2_hook_result_t, + pub write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_pointer: i32, data_length: i32, topic_ptr: i32, num_topics: i32) -> vm_exec2_hook_result_t, + pub write_event_log_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, num_topics: i32, topic_lengths_offset: i32, topic_offset: i32, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, + pub get_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, pointer: i32) -> vm_exec2_hook_result_t, + pub get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, pointer: i32) -> vm_exec2_hook_result_t, + pub get_prev_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_prev_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_prev_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_prev_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_prev_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, pointer: i32) -> vm_exec2_hook_result_t, + pub get_block_round_time_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub epoch_start_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub epoch_start_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub epoch_start_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub finish_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, pointer: i32, length: i32) -> vm_exec2_hook_result_t, + pub execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, address_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, result_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, result_address_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub get_num_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub get_return_data_size_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_id: i32) -> vm_exec2_hook_result_t, + pub get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_id: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub clean_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub delete_from_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_id: i32) -> vm_exec2_hook_result_t, + pub get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32) -> vm_exec2_hook_result_t, + pub get_current_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32) -> vm_exec2_hook_result_t, + pub get_prev_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32) -> vm_exec2_hook_result_t, + pub managed_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub managed_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub managed_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_original_caller_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_relayer_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub managed_signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, err_handle: i32) -> vm_exec2_hook_result_t, + pub managed_write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, topics_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_id: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_multi_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, multi_call_value_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_all_transfers_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, transfer_call_values_list_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_back_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, esdt_transfers_value_handle: i32, egld_value_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32, properties_handle: i32, hash_handle: i32, name_handle: i32, attributes_handle: i32, creator_handle: i32, royalties_handle: i32, uris_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, type_handle: i32) -> vm_exec2_hook_result_t, + pub managed_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, + pub managed_create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64, callback_closure_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_callback_closure_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, callback_closure_handle: i32) -> vm_exec2_hook_result_t, + pub managed_upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, gas_limit: i64, arguments_handle: i32) -> vm_exec2_hook_result_t, + pub managed_deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, address_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_execute_on_dest_context_with_error_return_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub managed_multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, + pub managed_multi_transfer_esdt_nft_execute_with_return_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, + pub managed_multi_transfer_esdt_nft_execute_by_user_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, user_handle: i32, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, + pub managed_transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dst_handle: i32, value_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, + pub managed_is_esdt_frozen_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64) -> vm_exec2_hook_result_t, + pub managed_is_esdt_limited_transfer_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, token_id_handle: i32) -> vm_exec2_hook_result_t, + pub managed_is_esdt_paused_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, token_id_handle: i32) -> vm_exec2_hook_result_t, + pub managed_buffer_to_hex_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, source_handle: i32, dest_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_code_metadata_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, response_handle: i32) -> vm_exec2_hook_result_t, + pub managed_get_code_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, code_hash_handle: i32) -> vm_exec2_hook_result_t, + pub managed_is_builtin_function_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, function_name_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_new_from_parts_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, integral_part: i32, fractional_part: i32, exponent: i32) -> vm_exec2_hook_result_t, + pub big_float_new_from_frac_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, numerator: i64, denominator: i64) -> vm_exec2_hook_result_t, + pub big_float_new_from_sci_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, significand: i64, exponent: i64) -> vm_exec2_hook_result_t, + pub big_float_add_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_div_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_clone_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32, exponent: i32) -> vm_exec2_hook_result_t, + pub big_float_floor_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_ceil_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_truncate_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, value: i64) -> vm_exec2_hook_result_t, + pub big_float_is_int_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_set_big_int_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_get_const_pi_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub big_float_get_const_e_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32, destination_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32, destination_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, source_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, destination_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_get_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination: i32) -> vm_exec2_hook_result_t, + pub big_int_get_esdt_call_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, index: i32) -> vm_exec2_hook_result_t, + pub big_int_get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, result: i32) -> vm_exec2_hook_result_t, + pub big_int_get_esdt_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_new_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, small_value: i64) -> vm_exec2_hook_result_t, + pub big_int_unsigned_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_signed_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_get_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec2_hook_result_t, + pub big_int_get_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec2_hook_result_t, + pub big_int_set_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec2_hook_result_t, + pub big_int_set_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec2_hook_result_t, + pub big_int_is_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_get_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, value: i64) -> vm_exec2_hook_result_t, + pub big_int_add_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_tdiv_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_tmod_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_ediv_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_emod_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_log2_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op1_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_not_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_and_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_or_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_xor_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_shr_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec2_hook_result_t, + pub big_int_shl_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec2_hook_result_t, + pub big_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32) -> vm_exec2_hook_result_t, + pub big_int_to_string_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, big_int_handle: i32, destination_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_new_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub mbuffer_new_from_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, + pub mbuffer_get_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_get_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, result_offset: i32) -> vm_exec2_hook_result_t, + pub mbuffer_get_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, result_offset: i32) -> vm_exec2_hook_result_t, + pub mbuffer_copy_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, destination_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_eq_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle1: i32, m_buffer_handle2: i32) -> vm_exec2_hook_result_t, + pub mbuffer_set_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, + pub mbuffer_set_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, starting_position: i32, data_length: i32, data_offset: i32) -> vm_exec2_hook_result_t, + pub mbuffer_append_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, accumulator_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_append_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, accumulator_handle: i32, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, + pub mbuffer_to_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_to_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_from_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_from_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_to_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_to_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_from_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec2_hook_result_t, + pub mbuffer_from_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec2_hook_result_t, + pub mbuffer_to_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_from_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, source_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, destination_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, key_handle: i32, destination_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32, destination_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_finish_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, source_handle: i32) -> vm_exec2_hook_result_t, + pub mbuffer_set_random_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, length: i32) -> vm_exec2_hook_result_t, + pub managed_map_new_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, + pub managed_map_put_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_map_handle: i32, key_handle: i32, value_handle: i32) -> vm_exec2_hook_result_t, + pub managed_map_get_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec2_hook_result_t, + pub managed_map_remove_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec2_hook_result_t, + pub managed_map_contains_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_map_handle: i32, key_handle: i32) -> vm_exec2_hook_result_t, + pub small_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32) -> vm_exec2_hook_result_t, + pub small_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32) -> vm_exec2_hook_result_t, + pub small_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, value: i64) -> vm_exec2_hook_result_t, + pub small_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, value: i64) -> vm_exec2_hook_result_t, + pub small_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec2_hook_result_t, + pub small_int_storage_store_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec2_hook_result_t, + pub small_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, + pub small_int_storage_load_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, + pub int64get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32) -> vm_exec2_hook_result_t, + pub int64finish_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, value: i64) -> vm_exec2_hook_result_t, + pub int64storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec2_hook_result_t, + pub int64storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, + pub sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec2_hook_result_t, + pub managed_sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec2_hook_result_t, + pub keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec2_hook_result_t, + pub managed_keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec2_hook_result_t, + pub ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec2_hook_result_t, + pub managed_ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec2_hook_result_t, + pub verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec2_hook_result_t, + pub managed_verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, + pub verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec2_hook_result_t, + pub managed_verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, + pub verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32, hash_type: i32) -> vm_exec2_hook_result_t, + pub managed_verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32, hash_type: i32) -> vm_exec2_hook_result_t, + pub verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec2_hook_result_t, + pub managed_verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, + pub encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, r_offset: i32, r_length: i32, s_offset: i32, s_length: i32, sig_offset: i32) -> vm_exec2_hook_result_t, + pub managed_encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, r_handle: i32, s_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, + pub add_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, fst_point_xhandle: i32, fst_point_yhandle: i32, snd_point_xhandle: i32, snd_point_yhandle: i32) -> vm_exec2_hook_result_t, + pub double_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec2_hook_result_t, + pub is_on_curve_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec2_hook_result_t, + pub scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, + pub managed_scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, + pub scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, + pub managed_scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_handle: i32) -> vm_exec2_hook_result_t, + pub marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec2_hook_result_t, + pub managed_marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec2_hook_result_t, + pub managed_marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, + pub managed_unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, + pub unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, + pub managed_unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, + pub generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec2_hook_result_t, + pub managed_generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, + pub create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, + pub managed_create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_handle: i32) -> vm_exec2_hook_result_t, + pub get_curve_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, ec_handle: i32) -> vm_exec2_hook_result_t, + pub get_priv_key_byte_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, ec_handle: i32) -> vm_exec2_hook_result_t, + pub elliptic_curve_get_values_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, ec_handle: i32, field_order_handle: i32, base_point_order_handle: i32, eq_constant_handle: i32, x_base_point_handle: i32, y_base_point_handle: i32) -> vm_exec2_hook_result_t, + pub managed_verify_secp256r1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, + pub managed_verify_blssignature_share_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, + pub managed_verify_blsaggregated_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, +} + +impl std::fmt::Debug for vm_exec2_vm_hook_c_func_pointers { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "vm_exec2_vm_hook_c_func_pointers") + } +} diff --git a/c-api-new/src/capi_vm_hook_table.rs b/c-api-new/src/capi_vm_hook_table.rs new file mode 100644 index 00000000..1cb32f43 --- /dev/null +++ b/c-api-new/src/capi_vm_hook_table.rs @@ -0,0 +1,44 @@ +//! The Go VM hook function-pointer table, owned by Rust. +//! +//! The table is **copied** at executor creation rather than referenced, as the legacy API +//! also does. Holding the caller's pointer instead would make the executor's validity depend +//! on the caller keeping a Go-side struct alive and unmoved for the executor's whole +//! lifetime, which is not a contract worth having at this boundary. + +use std::sync::Arc; + +use crate::capi_vm_hook_pointers::vm_exec2_vm_hook_c_func_pointers; + +/// An owned copy of the caller's hook table. +/// +/// Cloning is cheap and shares the copy; each instance's hooks builder holds one. +#[derive(Clone, Debug)] +pub struct CapiVMHookTable { + func_pointers: Arc, +} + +// The table is a block of plain function pointers with no interior mutability, and is only +// ever read. The `ExperimentalExecutorRuntimeRef` it ends up behind is `Send + Sync`, so this +// has to be too. +unsafe impl Send for CapiVMHookTable {} +unsafe impl Sync for CapiVMHookTable {} + +impl CapiVMHookTable { + /// Copies the table the caller provided. + /// + /// # Safety + /// + /// `func_pointers` must be a valid, non-null pointer to a fully populated table. Every + /// entry is called without a further null check, because a partially filled table is not + /// something this API can detect: a null function pointer and a wrong one are + /// indistinguishable here. + pub unsafe fn copy_from(func_pointers: *const vm_exec2_vm_hook_c_func_pointers) -> Self { + CapiVMHookTable { + func_pointers: Arc::new(unsafe { (*func_pointers).clone() }), + } + } + + pub fn func_pointers(&self) -> &vm_exec2_vm_hook_c_func_pointers { + &self.func_pointers + } +} diff --git a/c-api-new/src/capi_vm_hooks.rs b/c-api-new/src/capi_vm_hooks.rs new file mode 100644 index 00000000..15fecd23 --- /dev/null +++ b/c-api-new/src/capi_vm_hooks.rs @@ -0,0 +1,908 @@ +// Code generated by vmhooks generator. DO NOT EDIT. + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +use multiversx_chain_vm_executor::{MemLength, MemPtr, VMHooks, VMHooksEarlyExit}; + +use crate::capi_vm_hook_context::{vm_exec2_hook_context_t, vm_exec2_hook_result_t}; +use crate::capi_vm_hook_pointers::vm_exec2_vm_hook_c_func_pointers; + +/// Services one host call by dispatching each VM hook to the Go side. +/// +/// Built on the stack for the duration of a single host call, around the hook context that +/// the Go side hands back to the state functions. It borrows the table rather than owning a +/// copy, because the executor already owns one for the lifetime of the instance. +pub struct CapiVMHooks<'a> { + ctx: *mut vm_exec2_hook_context_t, + func_pointers: &'a vm_exec2_vm_hook_c_func_pointers, +} + +impl<'a> CapiVMHooks<'a> { + pub fn new( + ctx: *mut vm_exec2_hook_context_t, + func_pointers: &'a vm_exec2_vm_hook_c_func_pointers, + ) -> Self { + CapiVMHooks { ctx, func_pointers } + } + + fn convert_mem_ptr(&self, mem_ptr: MemPtr) -> i32 { + mem_ptr as i32 + } + + fn convert_mem_length(&self, mem_length: MemLength) -> i32 { + mem_length as i32 + } +} + +impl std::fmt::Debug for CapiVMHooks<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "CapiVMHooks") + } +} + +/// An early-exit code travels back without its message. +/// +/// The message stays on the Go side: the hook has already recorded it in the VM's own output +/// state before returning, and it could not cross anyway — cgo forbids returning an unpinned +/// Go pointer, and the messages are unbounded user text. +fn early_exit(code: u64) -> VMHooksEarlyExit { + VMHooksEarlyExit::new(code) +} + +fn unpack_void(result: vm_exec2_hook_result_t) -> Result<(), VMHooksEarlyExit> { + match result.early_exit_code() { + Some(code) => Err(early_exit(code)), + None => Ok(()), + } +} + +fn unpack_i32(result: vm_exec2_hook_result_t) -> Result { + match result.early_exit_code() { + Some(code) => Err(early_exit(code)), + None => Ok(result.value as i32), + } +} + +fn unpack_i64(result: vm_exec2_hook_result_t) -> Result { + match result.early_exit_code() { + Some(code) => Err(early_exit(code)), + None => Ok(result.value), + } +} + +#[rustfmt::skip] +impl VMHooks for CapiVMHooks<'_> { + fn get_gas_left(&mut self) -> Result { + unpack_i64((self.func_pointers.get_gas_left_func_ptr)(self.ctx)) + } + fn get_sc_address(&mut self, result_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_sc_address_func_ptr)(self.ctx, self.convert_mem_ptr(result_offset))) + } + fn get_owner_address(&mut self, result_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_owner_address_func_ptr)(self.ctx, self.convert_mem_ptr(result_offset))) + } + fn get_shard_of_address(&mut self, address_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_shard_of_address_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset))) + } + fn is_smart_contract(&mut self, address_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.is_smart_contract_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset))) + } + fn signal_error(&mut self, message_offset: MemPtr, message_length: MemLength) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.signal_error_func_ptr)(self.ctx, self.convert_mem_ptr(message_offset), self.convert_mem_length(message_length))) + } + fn get_external_balance(&mut self, address_offset: MemPtr, result_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_external_balance_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(result_offset))) + } + fn get_block_hash(&mut self, nonce: i64, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_block_hash_func_ptr)(self.ctx, nonce, self.convert_mem_ptr(result_offset))) + } + fn get_esdt_balance(&mut self, address_offset: MemPtr, token_id_offset: MemPtr, token_id_len: MemLength, nonce: i64, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_esdt_balance_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(token_id_offset), self.convert_mem_length(token_id_len), nonce, self.convert_mem_ptr(result_offset))) + } + fn get_esdt_nft_name_length(&mut self, address_offset: MemPtr, token_id_offset: MemPtr, token_id_len: MemLength, nonce: i64) -> Result { + unpack_i32((self.func_pointers.get_esdt_nft_name_length_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(token_id_offset), self.convert_mem_length(token_id_len), nonce)) + } + fn get_esdt_nft_attribute_length(&mut self, address_offset: MemPtr, token_id_offset: MemPtr, token_id_len: MemLength, nonce: i64) -> Result { + unpack_i32((self.func_pointers.get_esdt_nft_attribute_length_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(token_id_offset), self.convert_mem_length(token_id_len), nonce)) + } + fn get_esdt_nft_uri_length(&mut self, address_offset: MemPtr, token_id_offset: MemPtr, token_id_len: MemLength, nonce: i64) -> Result { + unpack_i32((self.func_pointers.get_esdt_nft_uri_length_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(token_id_offset), self.convert_mem_length(token_id_len), nonce)) + } + fn get_esdt_token_data(&mut self, address_offset: MemPtr, token_id_offset: MemPtr, token_id_len: MemLength, nonce: i64, value_handle: i32, properties_offset: MemPtr, hash_offset: MemPtr, name_offset: MemPtr, attributes_offset: MemPtr, creator_offset: MemPtr, royalties_handle: i32, uris_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_esdt_token_data_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(token_id_offset), self.convert_mem_length(token_id_len), nonce, value_handle, self.convert_mem_ptr(properties_offset), self.convert_mem_ptr(hash_offset), self.convert_mem_ptr(name_offset), self.convert_mem_ptr(attributes_offset), self.convert_mem_ptr(creator_offset), royalties_handle, self.convert_mem_ptr(uris_offset))) + } + fn get_esdt_local_roles(&mut self, token_id_handle: i32) -> Result { + unpack_i64((self.func_pointers.get_esdt_local_roles_func_ptr)(self.ctx, token_id_handle)) + } + fn validate_token_identifier(&mut self, token_id_handle: i32) -> Result { + unpack_i32((self.func_pointers.validate_token_identifier_func_ptr)(self.ctx, token_id_handle)) + } + fn transfer_value(&mut self, dest_offset: MemPtr, value_offset: MemPtr, data_offset: MemPtr, length: MemLength) -> Result { + unpack_i32((self.func_pointers.transfer_value_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), self.convert_mem_ptr(value_offset), self.convert_mem_ptr(data_offset), self.convert_mem_length(length))) + } + fn transfer_value_execute(&mut self, dest_offset: MemPtr, value_offset: MemPtr, gas_limit: i64, function_offset: MemPtr, function_length: MemLength, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.transfer_value_execute_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), self.convert_mem_ptr(value_offset), gas_limit, self.convert_mem_ptr(function_offset), self.convert_mem_length(function_length), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn transfer_esdt_execute(&mut self, dest_offset: MemPtr, token_id_offset: MemPtr, token_id_len: MemLength, value_offset: MemPtr, gas_limit: i64, function_offset: MemPtr, function_length: MemLength, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.transfer_esdt_execute_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), self.convert_mem_ptr(token_id_offset), self.convert_mem_length(token_id_len), self.convert_mem_ptr(value_offset), gas_limit, self.convert_mem_ptr(function_offset), self.convert_mem_length(function_length), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn transfer_esdt_nft_execute(&mut self, dest_offset: MemPtr, token_id_offset: MemPtr, token_id_len: MemLength, value_offset: MemPtr, nonce: i64, gas_limit: i64, function_offset: MemPtr, function_length: MemLength, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.transfer_esdt_nft_execute_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), self.convert_mem_ptr(token_id_offset), self.convert_mem_length(token_id_len), self.convert_mem_ptr(value_offset), nonce, gas_limit, self.convert_mem_ptr(function_offset), self.convert_mem_length(function_length), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn multi_transfer_esdt_nft_execute(&mut self, dest_offset: MemPtr, num_token_transfers: i32, token_transfers_args_length_offset: MemPtr, token_transfer_data_offset: MemPtr, gas_limit: i64, function_offset: MemPtr, function_length: MemLength, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.multi_transfer_esdt_nft_execute_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), num_token_transfers, self.convert_mem_ptr(token_transfers_args_length_offset), self.convert_mem_ptr(token_transfer_data_offset), gas_limit, self.convert_mem_ptr(function_offset), self.convert_mem_length(function_length), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn create_async_call(&mut self, dest_offset: MemPtr, value_offset: MemPtr, data_offset: MemPtr, data_length: MemLength, success_offset: MemPtr, success_length: MemLength, error_offset: MemPtr, error_length: MemLength, gas: i64, extra_gas_for_callback: i64) -> Result { + unpack_i32((self.func_pointers.create_async_call_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), self.convert_mem_ptr(value_offset), self.convert_mem_ptr(data_offset), self.convert_mem_length(data_length), self.convert_mem_ptr(success_offset), self.convert_mem_length(success_length), self.convert_mem_ptr(error_offset), self.convert_mem_length(error_length), gas, extra_gas_for_callback)) + } + fn set_async_context_callback(&mut self, callback: MemPtr, callback_length: MemLength, data: MemPtr, data_length: MemLength, gas: i64) -> Result { + unpack_i32((self.func_pointers.set_async_context_callback_func_ptr)(self.ctx, self.convert_mem_ptr(callback), self.convert_mem_length(callback_length), self.convert_mem_ptr(data), self.convert_mem_length(data_length), gas)) + } + fn upgrade_contract(&mut self, dest_offset: MemPtr, gas_limit: i64, value_offset: MemPtr, code_offset: MemPtr, code_metadata_offset: MemPtr, length: MemLength, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.upgrade_contract_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), gas_limit, self.convert_mem_ptr(value_offset), self.convert_mem_ptr(code_offset), self.convert_mem_ptr(code_metadata_offset), self.convert_mem_length(length), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn upgrade_from_source_contract(&mut self, dest_offset: MemPtr, gas_limit: i64, value_offset: MemPtr, source_contract_address_offset: MemPtr, code_metadata_offset: MemPtr, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.upgrade_from_source_contract_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), gas_limit, self.convert_mem_ptr(value_offset), self.convert_mem_ptr(source_contract_address_offset), self.convert_mem_ptr(code_metadata_offset), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn delete_contract(&mut self, dest_offset: MemPtr, gas_limit: i64, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.delete_contract_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), gas_limit, num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn async_call(&mut self, dest_offset: MemPtr, value_offset: MemPtr, data_offset: MemPtr, length: MemLength) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.async_call_func_ptr)(self.ctx, self.convert_mem_ptr(dest_offset), self.convert_mem_ptr(value_offset), self.convert_mem_ptr(data_offset), self.convert_mem_length(length))) + } + fn get_argument_length(&mut self, id: i32) -> Result { + unpack_i32((self.func_pointers.get_argument_length_func_ptr)(self.ctx, id)) + } + fn get_argument(&mut self, id: i32, arg_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_argument_func_ptr)(self.ctx, id, self.convert_mem_ptr(arg_offset))) + } + fn get_function(&mut self, function_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_function_func_ptr)(self.ctx, self.convert_mem_ptr(function_offset))) + } + fn get_num_arguments(&mut self) -> Result { + unpack_i32((self.func_pointers.get_num_arguments_func_ptr)(self.ctx)) + } + fn storage_store(&mut self, key_offset: MemPtr, key_length: MemLength, data_offset: MemPtr, data_length: MemLength) -> Result { + unpack_i32((self.func_pointers.storage_store_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), self.convert_mem_ptr(data_offset), self.convert_mem_length(data_length))) + } + fn storage_load_length(&mut self, key_offset: MemPtr, key_length: MemLength) -> Result { + unpack_i32((self.func_pointers.storage_load_length_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length))) + } + fn storage_load_from_address(&mut self, address_offset: MemPtr, key_offset: MemPtr, key_length: MemLength, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.storage_load_from_address_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), self.convert_mem_ptr(data_offset))) + } + fn storage_load(&mut self, key_offset: MemPtr, key_length: MemLength, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.storage_load_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), self.convert_mem_ptr(data_offset))) + } + fn set_storage_lock(&mut self, key_offset: MemPtr, key_length: MemLength, lock_timestamp: i64) -> Result { + unpack_i32((self.func_pointers.set_storage_lock_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), lock_timestamp)) + } + fn get_storage_lock(&mut self, key_offset: MemPtr, key_length: MemLength) -> Result { + unpack_i64((self.func_pointers.get_storage_lock_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length))) + } + fn is_storage_locked(&mut self, key_offset: MemPtr, key_length: MemLength) -> Result { + unpack_i32((self.func_pointers.is_storage_locked_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length))) + } + fn clear_storage_lock(&mut self, key_offset: MemPtr, key_length: MemLength) -> Result { + unpack_i32((self.func_pointers.clear_storage_lock_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length))) + } + fn get_caller(&mut self, result_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_caller_func_ptr)(self.ctx, self.convert_mem_ptr(result_offset))) + } + fn check_no_payment(&mut self) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.check_no_payment_func_ptr)(self.ctx)) + } + fn get_call_value(&mut self, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_call_value_func_ptr)(self.ctx, self.convert_mem_ptr(result_offset))) + } + fn get_esdt_value(&mut self, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_esdt_value_func_ptr)(self.ctx, self.convert_mem_ptr(result_offset))) + } + fn get_esdt_value_by_index(&mut self, result_offset: MemPtr, index: i32) -> Result { + unpack_i32((self.func_pointers.get_esdt_value_by_index_func_ptr)(self.ctx, self.convert_mem_ptr(result_offset), index)) + } + fn get_esdt_token_name(&mut self, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_esdt_token_name_func_ptr)(self.ctx, self.convert_mem_ptr(result_offset))) + } + fn get_esdt_token_name_by_index(&mut self, result_offset: MemPtr, index: i32) -> Result { + unpack_i32((self.func_pointers.get_esdt_token_name_by_index_func_ptr)(self.ctx, self.convert_mem_ptr(result_offset), index)) + } + fn get_esdt_token_nonce(&mut self) -> Result { + unpack_i64((self.func_pointers.get_esdt_token_nonce_func_ptr)(self.ctx)) + } + fn get_esdt_token_nonce_by_index(&mut self, index: i32) -> Result { + unpack_i64((self.func_pointers.get_esdt_token_nonce_by_index_func_ptr)(self.ctx, index)) + } + fn get_current_esdt_nft_nonce(&mut self, address_offset: MemPtr, token_id_offset: MemPtr, token_id_len: MemLength) -> Result { + unpack_i64((self.func_pointers.get_current_esdt_nft_nonce_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(token_id_offset), self.convert_mem_length(token_id_len))) + } + fn get_esdt_token_type(&mut self) -> Result { + unpack_i32((self.func_pointers.get_esdt_token_type_func_ptr)(self.ctx)) + } + fn get_esdt_token_type_by_index(&mut self, index: i32) -> Result { + unpack_i32((self.func_pointers.get_esdt_token_type_by_index_func_ptr)(self.ctx, index)) + } + fn get_num_esdt_transfers(&mut self) -> Result { + unpack_i32((self.func_pointers.get_num_esdt_transfers_func_ptr)(self.ctx)) + } + fn get_call_value_token_name(&mut self, call_value_offset: MemPtr, token_name_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_call_value_token_name_func_ptr)(self.ctx, self.convert_mem_ptr(call_value_offset), self.convert_mem_ptr(token_name_offset))) + } + fn get_call_value_token_name_by_index(&mut self, call_value_offset: MemPtr, token_name_offset: MemPtr, index: i32) -> Result { + unpack_i32((self.func_pointers.get_call_value_token_name_by_index_func_ptr)(self.ctx, self.convert_mem_ptr(call_value_offset), self.convert_mem_ptr(token_name_offset), index)) + } + fn is_reserved_function_name(&mut self, name_handle: i32) -> Result { + unpack_i32((self.func_pointers.is_reserved_function_name_func_ptr)(self.ctx, name_handle)) + } + fn write_log(&mut self, data_pointer: MemPtr, data_length: MemLength, topic_ptr: MemPtr, num_topics: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.write_log_func_ptr)(self.ctx, self.convert_mem_ptr(data_pointer), self.convert_mem_length(data_length), self.convert_mem_ptr(topic_ptr), num_topics)) + } + fn write_event_log(&mut self, num_topics: i32, topic_lengths_offset: MemPtr, topic_offset: MemPtr, data_offset: MemPtr, data_length: MemLength) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.write_event_log_func_ptr)(self.ctx, num_topics, self.convert_mem_ptr(topic_lengths_offset), self.convert_mem_ptr(topic_offset), self.convert_mem_ptr(data_offset), self.convert_mem_length(data_length))) + } + fn get_block_timestamp(&mut self) -> Result { + unpack_i64((self.func_pointers.get_block_timestamp_func_ptr)(self.ctx)) + } + fn get_block_timestamp_ms(&mut self) -> Result { + unpack_i64((self.func_pointers.get_block_timestamp_ms_func_ptr)(self.ctx)) + } + fn get_block_nonce(&mut self) -> Result { + unpack_i64((self.func_pointers.get_block_nonce_func_ptr)(self.ctx)) + } + fn get_block_round(&mut self) -> Result { + unpack_i64((self.func_pointers.get_block_round_func_ptr)(self.ctx)) + } + fn get_block_epoch(&mut self) -> Result { + unpack_i64((self.func_pointers.get_block_epoch_func_ptr)(self.ctx)) + } + fn get_block_random_seed(&mut self, pointer: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_block_random_seed_func_ptr)(self.ctx, self.convert_mem_ptr(pointer))) + } + fn get_state_root_hash(&mut self, pointer: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_state_root_hash_func_ptr)(self.ctx, self.convert_mem_ptr(pointer))) + } + fn get_prev_block_timestamp(&mut self) -> Result { + unpack_i64((self.func_pointers.get_prev_block_timestamp_func_ptr)(self.ctx)) + } + fn get_prev_block_timestamp_ms(&mut self) -> Result { + unpack_i64((self.func_pointers.get_prev_block_timestamp_ms_func_ptr)(self.ctx)) + } + fn get_prev_block_nonce(&mut self) -> Result { + unpack_i64((self.func_pointers.get_prev_block_nonce_func_ptr)(self.ctx)) + } + fn get_prev_block_round(&mut self) -> Result { + unpack_i64((self.func_pointers.get_prev_block_round_func_ptr)(self.ctx)) + } + fn get_prev_block_epoch(&mut self) -> Result { + unpack_i64((self.func_pointers.get_prev_block_epoch_func_ptr)(self.ctx)) + } + fn get_prev_block_random_seed(&mut self, pointer: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_prev_block_random_seed_func_ptr)(self.ctx, self.convert_mem_ptr(pointer))) + } + fn get_block_round_time_ms(&mut self) -> Result { + unpack_i64((self.func_pointers.get_block_round_time_ms_func_ptr)(self.ctx)) + } + fn epoch_start_block_timestamp_ms(&mut self) -> Result { + unpack_i64((self.func_pointers.epoch_start_block_timestamp_ms_func_ptr)(self.ctx)) + } + fn epoch_start_block_nonce(&mut self) -> Result { + unpack_i64((self.func_pointers.epoch_start_block_nonce_func_ptr)(self.ctx)) + } + fn epoch_start_block_round(&mut self) -> Result { + unpack_i64((self.func_pointers.epoch_start_block_round_func_ptr)(self.ctx)) + } + fn finish(&mut self, pointer: MemPtr, length: MemLength) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.finish_func_ptr)(self.ctx, self.convert_mem_ptr(pointer), self.convert_mem_length(length))) + } + fn execute_on_same_context(&mut self, gas_limit: i64, address_offset: MemPtr, value_offset: MemPtr, function_offset: MemPtr, function_length: MemLength, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.execute_on_same_context_func_ptr)(self.ctx, gas_limit, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(value_offset), self.convert_mem_ptr(function_offset), self.convert_mem_length(function_length), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn execute_on_dest_context(&mut self, gas_limit: i64, address_offset: MemPtr, value_offset: MemPtr, function_offset: MemPtr, function_length: MemLength, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.execute_on_dest_context_func_ptr)(self.ctx, gas_limit, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(value_offset), self.convert_mem_ptr(function_offset), self.convert_mem_length(function_length), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn execute_read_only(&mut self, gas_limit: i64, address_offset: MemPtr, function_offset: MemPtr, function_length: MemLength, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.execute_read_only_func_ptr)(self.ctx, gas_limit, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(function_offset), self.convert_mem_length(function_length), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn create_contract(&mut self, gas_limit: i64, value_offset: MemPtr, code_offset: MemPtr, code_metadata_offset: MemPtr, length: MemLength, result_offset: MemPtr, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.create_contract_func_ptr)(self.ctx, gas_limit, self.convert_mem_ptr(value_offset), self.convert_mem_ptr(code_offset), self.convert_mem_ptr(code_metadata_offset), self.convert_mem_length(length), self.convert_mem_ptr(result_offset), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn deploy_from_source_contract(&mut self, gas_limit: i64, value_offset: MemPtr, source_contract_address_offset: MemPtr, code_metadata_offset: MemPtr, result_address_offset: MemPtr, num_arguments: i32, arguments_length_offset: MemPtr, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.deploy_from_source_contract_func_ptr)(self.ctx, gas_limit, self.convert_mem_ptr(value_offset), self.convert_mem_ptr(source_contract_address_offset), self.convert_mem_ptr(code_metadata_offset), self.convert_mem_ptr(result_address_offset), num_arguments, self.convert_mem_ptr(arguments_length_offset), self.convert_mem_ptr(data_offset))) + } + fn get_num_return_data(&mut self) -> Result { + unpack_i32((self.func_pointers.get_num_return_data_func_ptr)(self.ctx)) + } + fn get_return_data_size(&mut self, result_id: i32) -> Result { + unpack_i32((self.func_pointers.get_return_data_size_func_ptr)(self.ctx, result_id)) + } + fn get_return_data(&mut self, result_id: i32, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.get_return_data_func_ptr)(self.ctx, result_id, self.convert_mem_ptr(data_offset))) + } + fn clean_return_data(&mut self) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.clean_return_data_func_ptr)(self.ctx)) + } + fn delete_from_return_data(&mut self, result_id: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.delete_from_return_data_func_ptr)(self.ctx, result_id)) + } + fn get_original_tx_hash(&mut self, data_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_original_tx_hash_func_ptr)(self.ctx, self.convert_mem_ptr(data_offset))) + } + fn get_current_tx_hash(&mut self, data_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_current_tx_hash_func_ptr)(self.ctx, self.convert_mem_ptr(data_offset))) + } + fn get_prev_tx_hash(&mut self, data_offset: MemPtr) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.get_prev_tx_hash_func_ptr)(self.ctx, self.convert_mem_ptr(data_offset))) + } + fn managed_sc_address(&mut self, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_sc_address_func_ptr)(self.ctx, destination_handle)) + } + fn managed_owner_address(&mut self, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_owner_address_func_ptr)(self.ctx, destination_handle)) + } + fn managed_caller(&mut self, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_caller_func_ptr)(self.ctx, destination_handle)) + } + fn managed_get_original_caller_addr(&mut self, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_original_caller_addr_func_ptr)(self.ctx, destination_handle)) + } + fn managed_get_relayer_addr(&mut self, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_relayer_addr_func_ptr)(self.ctx, destination_handle)) + } + fn managed_signal_error(&mut self, err_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_signal_error_func_ptr)(self.ctx, err_handle)) + } + fn managed_write_log(&mut self, topics_handle: i32, data_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_write_log_func_ptr)(self.ctx, topics_handle, data_handle)) + } + fn managed_get_original_tx_hash(&mut self, result_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_original_tx_hash_func_ptr)(self.ctx, result_handle)) + } + fn managed_get_state_root_hash(&mut self, result_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_state_root_hash_func_ptr)(self.ctx, result_handle)) + } + fn managed_get_block_random_seed(&mut self, result_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_block_random_seed_func_ptr)(self.ctx, result_handle)) + } + fn managed_get_prev_block_random_seed(&mut self, result_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_prev_block_random_seed_func_ptr)(self.ctx, result_handle)) + } + fn managed_get_return_data(&mut self, result_id: i32, result_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_return_data_func_ptr)(self.ctx, result_id, result_handle)) + } + fn managed_get_multi_esdt_call_value(&mut self, multi_call_value_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_multi_esdt_call_value_func_ptr)(self.ctx, multi_call_value_handle)) + } + fn managed_get_all_transfers_call_value(&mut self, transfer_call_values_list_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_all_transfers_call_value_func_ptr)(self.ctx, transfer_call_values_list_handle)) + } + fn managed_get_back_transfers(&mut self, esdt_transfers_value_handle: i32, egld_value_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_back_transfers_func_ptr)(self.ctx, esdt_transfers_value_handle, egld_value_handle)) + } + fn managed_get_esdt_balance(&mut self, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_esdt_balance_func_ptr)(self.ctx, address_handle, token_id_handle, nonce, value_handle)) + } + fn managed_get_esdt_token_data(&mut self, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32, properties_handle: i32, hash_handle: i32, name_handle: i32, attributes_handle: i32, creator_handle: i32, royalties_handle: i32, uris_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_esdt_token_data_func_ptr)(self.ctx, address_handle, token_id_handle, nonce, value_handle, properties_handle, hash_handle, name_handle, attributes_handle, creator_handle, royalties_handle, uris_handle)) + } + fn managed_get_esdt_token_type(&mut self, address_handle: i32, token_id_handle: i32, nonce: i64, type_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_esdt_token_type_func_ptr)(self.ctx, address_handle, token_id_handle, nonce, type_handle)) + } + fn managed_async_call(&mut self, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_async_call_func_ptr)(self.ctx, dest_handle, value_handle, function_handle, arguments_handle)) + } + fn managed_create_async_call(&mut self, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, success_offset: MemPtr, success_length: MemLength, error_offset: MemPtr, error_length: MemLength, gas: i64, extra_gas_for_callback: i64, callback_closure_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_create_async_call_func_ptr)(self.ctx, dest_handle, value_handle, function_handle, arguments_handle, self.convert_mem_ptr(success_offset), self.convert_mem_length(success_length), self.convert_mem_ptr(error_offset), self.convert_mem_length(error_length), gas, extra_gas_for_callback, callback_closure_handle)) + } + fn managed_get_callback_closure(&mut self, callback_closure_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_callback_closure_func_ptr)(self.ctx, callback_closure_handle)) + } + fn managed_upgrade_from_source_contract(&mut self, dest_handle: i32, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_upgrade_from_source_contract_func_ptr)(self.ctx, dest_handle, gas, value_handle, address_handle, code_metadata_handle, arguments_handle, result_handle)) + } + fn managed_upgrade_contract(&mut self, dest_handle: i32, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_upgrade_contract_func_ptr)(self.ctx, dest_handle, gas, value_handle, code_handle, code_metadata_handle, arguments_handle, result_handle)) + } + fn managed_delete_contract(&mut self, dest_handle: i32, gas_limit: i64, arguments_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_delete_contract_func_ptr)(self.ctx, dest_handle, gas_limit, arguments_handle)) + } + fn managed_deploy_from_source_contract(&mut self, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_deploy_from_source_contract_func_ptr)(self.ctx, gas, value_handle, address_handle, code_metadata_handle, arguments_handle, result_address_handle, result_handle)) + } + fn managed_create_contract(&mut self, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_create_contract_func_ptr)(self.ctx, gas, value_handle, code_handle, code_metadata_handle, arguments_handle, result_address_handle, result_handle)) + } + fn managed_execute_read_only(&mut self, gas: i64, address_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_execute_read_only_func_ptr)(self.ctx, gas, address_handle, function_handle, arguments_handle, result_handle)) + } + fn managed_execute_on_same_context(&mut self, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_execute_on_same_context_func_ptr)(self.ctx, gas, address_handle, value_handle, function_handle, arguments_handle, result_handle)) + } + fn managed_execute_on_dest_context(&mut self, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_execute_on_dest_context_func_ptr)(self.ctx, gas, address_handle, value_handle, function_handle, arguments_handle, result_handle)) + } + fn managed_execute_on_dest_context_with_error_return(&mut self, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_execute_on_dest_context_with_error_return_func_ptr)(self.ctx, gas, address_handle, value_handle, function_handle, arguments_handle, result_handle)) + } + fn managed_multi_transfer_esdt_nft_execute(&mut self, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_multi_transfer_esdt_nft_execute_func_ptr)(self.ctx, dst_handle, token_transfers_handle, gas_limit, function_handle, arguments_handle)) + } + fn managed_multi_transfer_esdt_nft_execute_with_return(&mut self, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_multi_transfer_esdt_nft_execute_with_return_func_ptr)(self.ctx, dst_handle, token_transfers_handle, gas_limit, function_handle, arguments_handle)) + } + fn managed_multi_transfer_esdt_nft_execute_by_user(&mut self, user_handle: i32, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_multi_transfer_esdt_nft_execute_by_user_func_ptr)(self.ctx, user_handle, dst_handle, token_transfers_handle, gas_limit, function_handle, arguments_handle)) + } + fn managed_transfer_value_execute(&mut self, dst_handle: i32, value_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_transfer_value_execute_func_ptr)(self.ctx, dst_handle, value_handle, gas_limit, function_handle, arguments_handle)) + } + fn managed_is_esdt_frozen(&mut self, address_handle: i32, token_id_handle: i32, nonce: i64) -> Result { + unpack_i32((self.func_pointers.managed_is_esdt_frozen_func_ptr)(self.ctx, address_handle, token_id_handle, nonce)) + } + fn managed_is_esdt_limited_transfer(&mut self, token_id_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_is_esdt_limited_transfer_func_ptr)(self.ctx, token_id_handle)) + } + fn managed_is_esdt_paused(&mut self, token_id_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_is_esdt_paused_func_ptr)(self.ctx, token_id_handle)) + } + fn managed_buffer_to_hex(&mut self, source_handle: i32, dest_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_buffer_to_hex_func_ptr)(self.ctx, source_handle, dest_handle)) + } + fn managed_get_code_metadata(&mut self, address_handle: i32, response_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_code_metadata_func_ptr)(self.ctx, address_handle, response_handle)) + } + fn managed_get_code_hash(&mut self, address_handle: i32, code_hash_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.managed_get_code_hash_func_ptr)(self.ctx, address_handle, code_hash_handle)) + } + fn managed_is_builtin_function(&mut self, function_name_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_is_builtin_function_func_ptr)(self.ctx, function_name_handle)) + } + fn big_float_new_from_parts(&mut self, integral_part: i32, fractional_part: i32, exponent: i32) -> Result { + unpack_i32((self.func_pointers.big_float_new_from_parts_func_ptr)(self.ctx, integral_part, fractional_part, exponent)) + } + fn big_float_new_from_frac(&mut self, numerator: i64, denominator: i64) -> Result { + unpack_i32((self.func_pointers.big_float_new_from_frac_func_ptr)(self.ctx, numerator, denominator)) + } + fn big_float_new_from_sci(&mut self, significand: i64, exponent: i64) -> Result { + unpack_i32((self.func_pointers.big_float_new_from_sci_func_ptr)(self.ctx, significand, exponent)) + } + fn big_float_add(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_add_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_float_sub(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_sub_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_float_mul(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_mul_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_float_div(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_div_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_float_neg(&mut self, destination_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_neg_func_ptr)(self.ctx, destination_handle, op_handle)) + } + fn big_float_clone(&mut self, destination_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_clone_func_ptr)(self.ctx, destination_handle, op_handle)) + } + fn big_float_cmp(&mut self, op1_handle: i32, op2_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_float_cmp_func_ptr)(self.ctx, op1_handle, op2_handle)) + } + fn big_float_abs(&mut self, destination_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_abs_func_ptr)(self.ctx, destination_handle, op_handle)) + } + fn big_float_sign(&mut self, op_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_float_sign_func_ptr)(self.ctx, op_handle)) + } + fn big_float_sqrt(&mut self, destination_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_sqrt_func_ptr)(self.ctx, destination_handle, op_handle)) + } + fn big_float_pow(&mut self, destination_handle: i32, op_handle: i32, exponent: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_pow_func_ptr)(self.ctx, destination_handle, op_handle, exponent)) + } + fn big_float_floor(&mut self, dest_big_int_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_floor_func_ptr)(self.ctx, dest_big_int_handle, op_handle)) + } + fn big_float_ceil(&mut self, dest_big_int_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_ceil_func_ptr)(self.ctx, dest_big_int_handle, op_handle)) + } + fn big_float_truncate(&mut self, dest_big_int_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_truncate_func_ptr)(self.ctx, dest_big_int_handle, op_handle)) + } + fn big_float_set_int64(&mut self, destination_handle: i32, value: i64) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_set_int64_func_ptr)(self.ctx, destination_handle, value)) + } + fn big_float_is_int(&mut self, op_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_float_is_int_func_ptr)(self.ctx, op_handle)) + } + fn big_float_set_big_int(&mut self, destination_handle: i32, big_int_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_set_big_int_func_ptr)(self.ctx, destination_handle, big_int_handle)) + } + fn big_float_get_const_pi(&mut self, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_get_const_pi_func_ptr)(self.ctx, destination_handle)) + } + fn big_float_get_const_e(&mut self, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_float_get_const_e_func_ptr)(self.ctx, destination_handle)) + } + fn big_int_get_unsigned_argument(&mut self, id: i32, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_get_unsigned_argument_func_ptr)(self.ctx, id, destination_handle)) + } + fn big_int_get_signed_argument(&mut self, id: i32, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_get_signed_argument_func_ptr)(self.ctx, id, destination_handle)) + } + fn big_int_storage_store_unsigned(&mut self, key_offset: MemPtr, key_length: MemLength, source_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_int_storage_store_unsigned_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), source_handle)) + } + fn big_int_storage_load_unsigned(&mut self, key_offset: MemPtr, key_length: MemLength, destination_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_int_storage_load_unsigned_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), destination_handle)) + } + fn big_int_get_call_value(&mut self, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_get_call_value_func_ptr)(self.ctx, destination_handle)) + } + fn big_int_get_esdt_call_value(&mut self, destination: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_get_esdt_call_value_func_ptr)(self.ctx, destination)) + } + fn big_int_get_esdt_call_value_by_index(&mut self, destination_handle: i32, index: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_get_esdt_call_value_by_index_func_ptr)(self.ctx, destination_handle, index)) + } + fn big_int_get_external_balance(&mut self, address_offset: MemPtr, result: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_get_external_balance_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), result)) + } + fn big_int_get_esdt_external_balance(&mut self, address_offset: MemPtr, token_id_offset: MemPtr, token_id_len: MemLength, nonce: i64, result_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_get_esdt_external_balance_func_ptr)(self.ctx, self.convert_mem_ptr(address_offset), self.convert_mem_ptr(token_id_offset), self.convert_mem_length(token_id_len), nonce, result_handle)) + } + fn big_int_new(&mut self, small_value: i64) -> Result { + unpack_i32((self.func_pointers.big_int_new_func_ptr)(self.ctx, small_value)) + } + fn big_int_unsigned_byte_length(&mut self, reference_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_int_unsigned_byte_length_func_ptr)(self.ctx, reference_handle)) + } + fn big_int_signed_byte_length(&mut self, reference_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_int_signed_byte_length_func_ptr)(self.ctx, reference_handle)) + } + fn big_int_get_unsigned_bytes(&mut self, reference_handle: i32, byte_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.big_int_get_unsigned_bytes_func_ptr)(self.ctx, reference_handle, self.convert_mem_ptr(byte_offset))) + } + fn big_int_get_signed_bytes(&mut self, reference_handle: i32, byte_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.big_int_get_signed_bytes_func_ptr)(self.ctx, reference_handle, self.convert_mem_ptr(byte_offset))) + } + fn big_int_set_unsigned_bytes(&mut self, destination_handle: i32, byte_offset: MemPtr, byte_length: MemLength) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_set_unsigned_bytes_func_ptr)(self.ctx, destination_handle, self.convert_mem_ptr(byte_offset), self.convert_mem_length(byte_length))) + } + fn big_int_set_signed_bytes(&mut self, destination_handle: i32, byte_offset: MemPtr, byte_length: MemLength) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_set_signed_bytes_func_ptr)(self.ctx, destination_handle, self.convert_mem_ptr(byte_offset), self.convert_mem_length(byte_length))) + } + fn big_int_is_int64(&mut self, destination_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_int_is_int64_func_ptr)(self.ctx, destination_handle)) + } + fn big_int_get_int64(&mut self, destination_handle: i32) -> Result { + unpack_i64((self.func_pointers.big_int_get_int64_func_ptr)(self.ctx, destination_handle)) + } + fn big_int_set_int64(&mut self, destination_handle: i32, value: i64) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_set_int64_func_ptr)(self.ctx, destination_handle, value)) + } + fn big_int_add(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_add_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_sub(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_sub_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_mul(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_mul_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_tdiv(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_tdiv_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_tmod(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_tmod_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_ediv(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_ediv_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_emod(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_emod_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_sqrt(&mut self, destination_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_sqrt_func_ptr)(self.ctx, destination_handle, op_handle)) + } + fn big_int_pow(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_pow_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_log2(&mut self, op1_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_int_log2_func_ptr)(self.ctx, op1_handle)) + } + fn big_int_abs(&mut self, destination_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_abs_func_ptr)(self.ctx, destination_handle, op_handle)) + } + fn big_int_neg(&mut self, destination_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_neg_func_ptr)(self.ctx, destination_handle, op_handle)) + } + fn big_int_sign(&mut self, op_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_int_sign_func_ptr)(self.ctx, op_handle)) + } + fn big_int_cmp(&mut self, op1_handle: i32, op2_handle: i32) -> Result { + unpack_i32((self.func_pointers.big_int_cmp_func_ptr)(self.ctx, op1_handle, op2_handle)) + } + fn big_int_not(&mut self, destination_handle: i32, op_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_not_func_ptr)(self.ctx, destination_handle, op_handle)) + } + fn big_int_and(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_and_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_or(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_or_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_xor(&mut self, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_xor_func_ptr)(self.ctx, destination_handle, op1_handle, op2_handle)) + } + fn big_int_shr(&mut self, destination_handle: i32, op_handle: i32, bits: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_shr_func_ptr)(self.ctx, destination_handle, op_handle, bits)) + } + fn big_int_shl(&mut self, destination_handle: i32, op_handle: i32, bits: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_shl_func_ptr)(self.ctx, destination_handle, op_handle, bits)) + } + fn big_int_finish_unsigned(&mut self, reference_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_finish_unsigned_func_ptr)(self.ctx, reference_handle)) + } + fn big_int_finish_signed(&mut self, reference_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_finish_signed_func_ptr)(self.ctx, reference_handle)) + } + fn big_int_to_string(&mut self, big_int_handle: i32, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.big_int_to_string_func_ptr)(self.ctx, big_int_handle, destination_handle)) + } + fn mbuffer_new(&mut self) -> Result { + unpack_i32((self.func_pointers.mbuffer_new_func_ptr)(self.ctx)) + } + fn mbuffer_new_from_bytes(&mut self, data_offset: MemPtr, data_length: MemLength) -> Result { + unpack_i32((self.func_pointers.mbuffer_new_from_bytes_func_ptr)(self.ctx, self.convert_mem_ptr(data_offset), self.convert_mem_length(data_length))) + } + fn mbuffer_get_length(&mut self, m_buffer_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_get_length_func_ptr)(self.ctx, m_buffer_handle)) + } + fn mbuffer_get_bytes(&mut self, m_buffer_handle: i32, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.mbuffer_get_bytes_func_ptr)(self.ctx, m_buffer_handle, self.convert_mem_ptr(result_offset))) + } + fn mbuffer_get_byte_slice(&mut self, source_handle: i32, starting_position: i32, slice_length: i32, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.mbuffer_get_byte_slice_func_ptr)(self.ctx, source_handle, starting_position, slice_length, self.convert_mem_ptr(result_offset))) + } + fn mbuffer_copy_byte_slice(&mut self, source_handle: i32, starting_position: i32, slice_length: i32, destination_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_copy_byte_slice_func_ptr)(self.ctx, source_handle, starting_position, slice_length, destination_handle)) + } + fn mbuffer_eq(&mut self, m_buffer_handle1: i32, m_buffer_handle2: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_eq_func_ptr)(self.ctx, m_buffer_handle1, m_buffer_handle2)) + } + fn mbuffer_set_bytes(&mut self, m_buffer_handle: i32, data_offset: MemPtr, data_length: MemLength) -> Result { + unpack_i32((self.func_pointers.mbuffer_set_bytes_func_ptr)(self.ctx, m_buffer_handle, self.convert_mem_ptr(data_offset), self.convert_mem_length(data_length))) + } + fn mbuffer_set_byte_slice(&mut self, m_buffer_handle: i32, starting_position: i32, data_length: MemLength, data_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.mbuffer_set_byte_slice_func_ptr)(self.ctx, m_buffer_handle, starting_position, self.convert_mem_length(data_length), self.convert_mem_ptr(data_offset))) + } + fn mbuffer_append(&mut self, accumulator_handle: i32, data_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_append_func_ptr)(self.ctx, accumulator_handle, data_handle)) + } + fn mbuffer_append_bytes(&mut self, accumulator_handle: i32, data_offset: MemPtr, data_length: MemLength) -> Result { + unpack_i32((self.func_pointers.mbuffer_append_bytes_func_ptr)(self.ctx, accumulator_handle, self.convert_mem_ptr(data_offset), self.convert_mem_length(data_length))) + } + fn mbuffer_to_big_int_unsigned(&mut self, m_buffer_handle: i32, big_int_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_to_big_int_unsigned_func_ptr)(self.ctx, m_buffer_handle, big_int_handle)) + } + fn mbuffer_to_big_int_signed(&mut self, m_buffer_handle: i32, big_int_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_to_big_int_signed_func_ptr)(self.ctx, m_buffer_handle, big_int_handle)) + } + fn mbuffer_from_big_int_unsigned(&mut self, m_buffer_handle: i32, big_int_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_from_big_int_unsigned_func_ptr)(self.ctx, m_buffer_handle, big_int_handle)) + } + fn mbuffer_from_big_int_signed(&mut self, m_buffer_handle: i32, big_int_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_from_big_int_signed_func_ptr)(self.ctx, m_buffer_handle, big_int_handle)) + } + fn mbuffer_to_small_int_unsigned(&mut self, m_buffer_handle: i32) -> Result { + unpack_i64((self.func_pointers.mbuffer_to_small_int_unsigned_func_ptr)(self.ctx, m_buffer_handle)) + } + fn mbuffer_to_small_int_signed(&mut self, m_buffer_handle: i32) -> Result { + unpack_i64((self.func_pointers.mbuffer_to_small_int_signed_func_ptr)(self.ctx, m_buffer_handle)) + } + fn mbuffer_from_small_int_unsigned(&mut self, m_buffer_handle: i32, value: i64) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.mbuffer_from_small_int_unsigned_func_ptr)(self.ctx, m_buffer_handle, value)) + } + fn mbuffer_from_small_int_signed(&mut self, m_buffer_handle: i32, value: i64) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.mbuffer_from_small_int_signed_func_ptr)(self.ctx, m_buffer_handle, value)) + } + fn mbuffer_to_big_float(&mut self, m_buffer_handle: i32, big_float_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_to_big_float_func_ptr)(self.ctx, m_buffer_handle, big_float_handle)) + } + fn mbuffer_from_big_float(&mut self, m_buffer_handle: i32, big_float_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_from_big_float_func_ptr)(self.ctx, m_buffer_handle, big_float_handle)) + } + fn mbuffer_storage_store(&mut self, key_handle: i32, source_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_storage_store_func_ptr)(self.ctx, key_handle, source_handle)) + } + fn mbuffer_storage_load(&mut self, key_handle: i32, destination_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_storage_load_func_ptr)(self.ctx, key_handle, destination_handle)) + } + fn mbuffer_storage_load_from_address(&mut self, address_handle: i32, key_handle: i32, destination_handle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.mbuffer_storage_load_from_address_func_ptr)(self.ctx, address_handle, key_handle, destination_handle)) + } + fn mbuffer_get_argument(&mut self, id: i32, destination_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_get_argument_func_ptr)(self.ctx, id, destination_handle)) + } + fn mbuffer_finish(&mut self, source_handle: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_finish_func_ptr)(self.ctx, source_handle)) + } + fn mbuffer_set_random(&mut self, destination_handle: i32, length: i32) -> Result { + unpack_i32((self.func_pointers.mbuffer_set_random_func_ptr)(self.ctx, destination_handle, length)) + } + fn managed_map_new(&mut self) -> Result { + unpack_i32((self.func_pointers.managed_map_new_func_ptr)(self.ctx)) + } + fn managed_map_put(&mut self, m_map_handle: i32, key_handle: i32, value_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_map_put_func_ptr)(self.ctx, m_map_handle, key_handle, value_handle)) + } + fn managed_map_get(&mut self, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_map_get_func_ptr)(self.ctx, m_map_handle, key_handle, out_value_handle)) + } + fn managed_map_remove(&mut self, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_map_remove_func_ptr)(self.ctx, m_map_handle, key_handle, out_value_handle)) + } + fn managed_map_contains(&mut self, m_map_handle: i32, key_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_map_contains_func_ptr)(self.ctx, m_map_handle, key_handle)) + } + fn small_int_get_unsigned_argument(&mut self, id: i32) -> Result { + unpack_i64((self.func_pointers.small_int_get_unsigned_argument_func_ptr)(self.ctx, id)) + } + fn small_int_get_signed_argument(&mut self, id: i32) -> Result { + unpack_i64((self.func_pointers.small_int_get_signed_argument_func_ptr)(self.ctx, id)) + } + fn small_int_finish_unsigned(&mut self, value: i64) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.small_int_finish_unsigned_func_ptr)(self.ctx, value)) + } + fn small_int_finish_signed(&mut self, value: i64) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.small_int_finish_signed_func_ptr)(self.ctx, value)) + } + fn small_int_storage_store_unsigned(&mut self, key_offset: MemPtr, key_length: MemLength, value: i64) -> Result { + unpack_i32((self.func_pointers.small_int_storage_store_unsigned_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), value)) + } + fn small_int_storage_store_signed(&mut self, key_offset: MemPtr, key_length: MemLength, value: i64) -> Result { + unpack_i32((self.func_pointers.small_int_storage_store_signed_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), value)) + } + fn small_int_storage_load_unsigned(&mut self, key_offset: MemPtr, key_length: MemLength) -> Result { + unpack_i64((self.func_pointers.small_int_storage_load_unsigned_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length))) + } + fn small_int_storage_load_signed(&mut self, key_offset: MemPtr, key_length: MemLength) -> Result { + unpack_i64((self.func_pointers.small_int_storage_load_signed_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length))) + } + fn int64get_argument(&mut self, id: i32) -> Result { + unpack_i64((self.func_pointers.int64get_argument_func_ptr)(self.ctx, id)) + } + fn int64finish(&mut self, value: i64) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.int64finish_func_ptr)(self.ctx, value)) + } + fn int64storage_store(&mut self, key_offset: MemPtr, key_length: MemLength, value: i64) -> Result { + unpack_i32((self.func_pointers.int64storage_store_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), value)) + } + fn int64storage_load(&mut self, key_offset: MemPtr, key_length: MemLength) -> Result { + unpack_i64((self.func_pointers.int64storage_load_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length))) + } + fn sha256(&mut self, data_offset: MemPtr, length: MemLength, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.sha256_func_ptr)(self.ctx, self.convert_mem_ptr(data_offset), self.convert_mem_length(length), self.convert_mem_ptr(result_offset))) + } + fn managed_sha256(&mut self, input_handle: i32, output_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_sha256_func_ptr)(self.ctx, input_handle, output_handle)) + } + fn keccak256(&mut self, data_offset: MemPtr, length: MemLength, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.keccak256_func_ptr)(self.ctx, self.convert_mem_ptr(data_offset), self.convert_mem_length(length), self.convert_mem_ptr(result_offset))) + } + fn managed_keccak256(&mut self, input_handle: i32, output_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_keccak256_func_ptr)(self.ctx, input_handle, output_handle)) + } + fn ripemd160(&mut self, data_offset: MemPtr, length: MemLength, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.ripemd160_func_ptr)(self.ctx, self.convert_mem_ptr(data_offset), self.convert_mem_length(length), self.convert_mem_ptr(result_offset))) + } + fn managed_ripemd160(&mut self, input_handle: i32, output_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_ripemd160_func_ptr)(self.ctx, input_handle, output_handle)) + } + fn verify_bls(&mut self, key_offset: MemPtr, message_offset: MemPtr, message_length: MemLength, sig_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.verify_bls_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_ptr(message_offset), self.convert_mem_length(message_length), self.convert_mem_ptr(sig_offset))) + } + fn managed_verify_bls(&mut self, key_handle: i32, message_handle: i32, sig_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_verify_bls_func_ptr)(self.ctx, key_handle, message_handle, sig_handle)) + } + fn verify_ed25519(&mut self, key_offset: MemPtr, message_offset: MemPtr, message_length: MemLength, sig_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.verify_ed25519_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_ptr(message_offset), self.convert_mem_length(message_length), self.convert_mem_ptr(sig_offset))) + } + fn managed_verify_ed25519(&mut self, key_handle: i32, message_handle: i32, sig_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_verify_ed25519_func_ptr)(self.ctx, key_handle, message_handle, sig_handle)) + } + fn verify_custom_secp256k1(&mut self, key_offset: MemPtr, key_length: MemLength, message_offset: MemPtr, message_length: MemLength, sig_offset: MemPtr, hash_type: i32) -> Result { + unpack_i32((self.func_pointers.verify_custom_secp256k1_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), self.convert_mem_ptr(message_offset), self.convert_mem_length(message_length), self.convert_mem_ptr(sig_offset), hash_type)) + } + fn managed_verify_custom_secp256k1(&mut self, key_handle: i32, message_handle: i32, sig_handle: i32, hash_type: i32) -> Result { + unpack_i32((self.func_pointers.managed_verify_custom_secp256k1_func_ptr)(self.ctx, key_handle, message_handle, sig_handle, hash_type)) + } + fn verify_secp256k1(&mut self, key_offset: MemPtr, key_length: MemLength, message_offset: MemPtr, message_length: MemLength, sig_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.verify_secp256k1_func_ptr)(self.ctx, self.convert_mem_ptr(key_offset), self.convert_mem_length(key_length), self.convert_mem_ptr(message_offset), self.convert_mem_length(message_length), self.convert_mem_ptr(sig_offset))) + } + fn managed_verify_secp256k1(&mut self, key_handle: i32, message_handle: i32, sig_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_verify_secp256k1_func_ptr)(self.ctx, key_handle, message_handle, sig_handle)) + } + fn encode_secp256k1_der_signature(&mut self, r_offset: MemPtr, r_length: MemLength, s_offset: MemPtr, s_length: MemLength, sig_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.encode_secp256k1_der_signature_func_ptr)(self.ctx, self.convert_mem_ptr(r_offset), self.convert_mem_length(r_length), self.convert_mem_ptr(s_offset), self.convert_mem_length(s_length), self.convert_mem_ptr(sig_offset))) + } + fn managed_encode_secp256k1_der_signature(&mut self, r_handle: i32, s_handle: i32, sig_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_encode_secp256k1_der_signature_func_ptr)(self.ctx, r_handle, s_handle, sig_handle)) + } + fn add_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, fst_point_xhandle: i32, fst_point_yhandle: i32, snd_point_xhandle: i32, snd_point_yhandle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.add_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, fst_point_xhandle, fst_point_yhandle, snd_point_xhandle, snd_point_yhandle)) + } + fn double_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> Result<(), VMHooksEarlyExit> { + unpack_void((self.func_pointers.double_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, point_xhandle, point_yhandle)) + } + fn is_on_curve_ec(&mut self, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> Result { + unpack_i32((self.func_pointers.is_on_curve_ec_func_ptr)(self.ctx, ec_handle, point_xhandle, point_yhandle)) + } + fn scalar_base_mult_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: MemPtr, length: MemLength) -> Result { + unpack_i32((self.func_pointers.scalar_base_mult_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, self.convert_mem_ptr(data_offset), self.convert_mem_length(length))) + } + fn managed_scalar_base_mult_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_scalar_base_mult_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, data_handle)) + } + fn scalar_mult_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_offset: MemPtr, length: MemLength) -> Result { + unpack_i32((self.func_pointers.scalar_mult_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, point_xhandle, point_yhandle, self.convert_mem_ptr(data_offset), self.convert_mem_length(length))) + } + fn managed_scalar_mult_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_scalar_mult_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, point_xhandle, point_yhandle, data_handle)) + } + fn marshal_ec(&mut self, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.marshal_ec_func_ptr)(self.ctx, x_pair_handle, y_pair_handle, ec_handle, self.convert_mem_ptr(result_offset))) + } + fn managed_marshal_ec(&mut self, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_marshal_ec_func_ptr)(self.ctx, x_pair_handle, y_pair_handle, ec_handle, result_handle)) + } + fn marshal_compressed_ec(&mut self, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.marshal_compressed_ec_func_ptr)(self.ctx, x_pair_handle, y_pair_handle, ec_handle, self.convert_mem_ptr(result_offset))) + } + fn managed_marshal_compressed_ec(&mut self, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_marshal_compressed_ec_func_ptr)(self.ctx, x_pair_handle, y_pair_handle, ec_handle, result_handle)) + } + fn unmarshal_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: MemPtr, length: MemLength) -> Result { + unpack_i32((self.func_pointers.unmarshal_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, self.convert_mem_ptr(data_offset), self.convert_mem_length(length))) + } + fn managed_unmarshal_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_unmarshal_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, data_handle)) + } + fn unmarshal_compressed_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: MemPtr, length: MemLength) -> Result { + unpack_i32((self.func_pointers.unmarshal_compressed_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, self.convert_mem_ptr(data_offset), self.convert_mem_length(length))) + } + fn managed_unmarshal_compressed_ec(&mut self, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_unmarshal_compressed_ec_func_ptr)(self.ctx, x_result_handle, y_result_handle, ec_handle, data_handle)) + } + fn generate_key_ec(&mut self, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_offset: MemPtr) -> Result { + unpack_i32((self.func_pointers.generate_key_ec_func_ptr)(self.ctx, x_pub_key_handle, y_pub_key_handle, ec_handle, self.convert_mem_ptr(result_offset))) + } + fn managed_generate_key_ec(&mut self, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_generate_key_ec_func_ptr)(self.ctx, x_pub_key_handle, y_pub_key_handle, ec_handle, result_handle)) + } + fn create_ec(&mut self, data_offset: MemPtr, data_length: MemLength) -> Result { + unpack_i32((self.func_pointers.create_ec_func_ptr)(self.ctx, self.convert_mem_ptr(data_offset), self.convert_mem_length(data_length))) + } + fn managed_create_ec(&mut self, data_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_create_ec_func_ptr)(self.ctx, data_handle)) + } + fn get_curve_length_ec(&mut self, ec_handle: i32) -> Result { + unpack_i32((self.func_pointers.get_curve_length_ec_func_ptr)(self.ctx, ec_handle)) + } + fn get_priv_key_byte_length_ec(&mut self, ec_handle: i32) -> Result { + unpack_i32((self.func_pointers.get_priv_key_byte_length_ec_func_ptr)(self.ctx, ec_handle)) + } + fn elliptic_curve_get_values(&mut self, ec_handle: i32, field_order_handle: i32, base_point_order_handle: i32, eq_constant_handle: i32, x_base_point_handle: i32, y_base_point_handle: i32) -> Result { + unpack_i32((self.func_pointers.elliptic_curve_get_values_func_ptr)(self.ctx, ec_handle, field_order_handle, base_point_order_handle, eq_constant_handle, x_base_point_handle, y_base_point_handle)) + } + fn managed_verify_secp256r1(&mut self, key_handle: i32, message_handle: i32, sig_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_verify_secp256r1_func_ptr)(self.ctx, key_handle, message_handle, sig_handle)) + } + fn managed_verify_blssignature_share(&mut self, key_handle: i32, message_handle: i32, sig_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_verify_blssignature_share_func_ptr)(self.ctx, key_handle, message_handle, sig_handle)) + } + fn managed_verify_blsaggregated_signature(&mut self, key_handle: i32, message_handle: i32, sig_handle: i32) -> Result { + unpack_i32((self.func_pointers.managed_verify_blsaggregated_signature_func_ptr)(self.ctx, key_handle, message_handle, sig_handle)) + } +} diff --git a/c-api-new/src/lib.rs b/c-api-new/src/lib.rs index 761d1de7..b88863ae 100644 --- a/c-api-new/src/lib.rs +++ b/c-api-new/src/lib.rs @@ -13,20 +13,22 @@ //! - **A call reports what happened in its result**, instead of leaving the caller to poll a //! wasm global afterwards. See `vm_exec2_call_result_t`. //! -//! The hook dispatch itself — the typed function-pointer table and the 277-method `VMHooks` -//! implementation that calls into it — is generated code and arrives with Phase 3 of -//! `PLAN-go-ffi.md`. Everything it needs is here: the result and context ABI -//! (`capi_hook_context`), the table ownership (`capi_hook_table`) and the state operations -//! (`capi_instance_state`). +//! The hook dispatch — the typed function-pointer table (`capi_vm_hook_pointers`) and the +//! `VMHooks` implementation that calls into it (`capi_vm_hooks`) — is generated from the Go +//! VM's hook definitions by the generator in `mx-chain-vm-go/vmhost/vmhooks/generate`. It +//! builds on the result and context ABI in `capi_vm_hook_context`, the table ownership in +//! `capi_vm_hook_table`, and the state operations in `capi_instance_state`. mod capi_logger; pub mod capi_config; pub mod capi_executor; -pub mod capi_hook_context; -pub mod capi_hook_table; pub mod capi_instance; pub mod capi_instance_state; pub mod capi_opcode_cost; pub mod capi_status; pub(crate) mod capi_strings; +pub mod capi_vm_hook_context; +pub mod capi_vm_hook_pointers; +pub mod capi_vm_hook_table; +pub mod capi_vm_hooks; diff --git a/c-api-new/tests/ffi_boundary.rs b/c-api-new/tests/ffi_boundary.rs index 98fc3a90..46b9bb8a 100644 --- a/c-api-new/tests/ffi_boundary.rs +++ b/c-api-new/tests/ffi_boundary.rs @@ -1,16 +1,18 @@ //! Drives the C API from Rust, so the boundary is testable before any Go exists. //! -//! Two halves: +//! Three halves: //! -//! - [`lifecycle`] goes through the exported functions exactly as the Go side will, using a -//! mock hook table. -//! - [`state_ops`] installs a mock hooks provider that calls the state functions from inside -//! a real host call, which is the only place a hook context is live. It also checks that a -//! context which outlives its call, or which belongs to an outer call while a nested one is -//! running, is rejected rather than followed. +//! - [`lifecycle`] goes through the exported functions exactly as the Go side will. +//! - [`hook_dispatch`] runs a contract that calls VM hooks, with a mock hook table standing +//! in for the generated Go shims. This is the full round trip: wasm into Rust, out through +//! a C function pointer, back into Rust through the state functions, and — for an early +//! exit — out again as an `InstanceCallResult`. +//! - [`state_ops`] installs a mock hooks *provider* instead, to reach the context lifecycle +//! directly: a context that outlives its call, and an outer context while a nested call is +//! running, must both be refused. use std::ffi::{CString, c_char, c_int, c_void}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use multiversx_chain_vm_executor::{VMHooks, VMHooksDefault}; use multiversx_chain_vm_executor_c_api_new::capi_config::{ @@ -19,31 +21,40 @@ use multiversx_chain_vm_executor_c_api_new::capi_config::{ use multiversx_chain_vm_executor_c_api_new::capi_executor::{ vm_exec2_executor_destroy, vm_exec2_executor_t, vm_exec2_new_executor, }; -use multiversx_chain_vm_executor_c_api_new::capi_hook_context::{ - CapiVMHooksBuilder, vm_exec2_hook_context_t, -}; -use multiversx_chain_vm_executor_c_api_new::capi_hook_table::CapiHookTable; use multiversx_chain_vm_executor_c_api_new::capi_instance::*; use multiversx_chain_vm_executor_c_api_new::capi_instance_state::*; use multiversx_chain_vm_executor_c_api_new::capi_opcode_cost::vm_exec_opcode_cost_t; use multiversx_chain_vm_executor_c_api_new::capi_status::vm_exec2_status_t; +use multiversx_chain_vm_executor_c_api_new::capi_vm_hook_context::{ + CapiVMHooksBuilder, vm_exec2_hook_context_t, vm_exec2_hook_result_t, +}; +use multiversx_chain_vm_executor_c_api_new::capi_vm_hook_pointers::vm_exec2_vm_hook_c_func_pointers; +use multiversx_chain_vm_executor_c_api_new::capi_vm_hook_table::CapiVMHookTable; use multiversx_chain_vm_executor_wasmer_experimental::{ ExperimentalInstance, ExperimentalInstanceState, ExperimentalVMHooksBuilder, }; -/// One memory page, five known bytes at offset 0, one endpoint that calls a VM hook. +/// One memory page, five known bytes at offset 0, and endpoints that call VM hooks. const CONTRACT_WAT: &str = r#" (module (import "env" "getGasLeft" (func $getGasLeft (result i64))) + (import "env" "signalError" (func $signalError (param i32 i32))) (memory (export "memory") 1) (data (i32.const 0) "hello") (func (export "doNothing")) - (func (export "callHook") (drop (call $getGasLeft))) + (func (export "callGetGasLeft") (drop (call $getGasLeft))) + (func (export "callSignalError") (call $signalError (i32.const 0) (i32.const 5))) ) "#; const ERR_BUF_LEN: c_int = 1024; +/// The code the mock `signalError` reports; `vmcommon.UserError` on the Go side. +const USER_ERROR_CODE: u64 = 4; + +/// Reported by a hook the test contract was not supposed to call. +const UNEXPECTED_HOOK_CODE: u64 = 9999; + struct ErrBuf(Vec); impl ErrBuf { @@ -67,8 +78,8 @@ impl ErrBuf { } fn opcode_cost() -> vm_exec_opcode_cost_t { - // A `#[repr(C)]` struct of `u32`s, so an all-zero cost table is a valid value. Zero - // costs keep the tests away from the metering limit, which is not what they are about. + // A `#[repr(C)]` struct of `u32`s, so an all-zero cost table is a valid value. Zero costs + // keep the tests away from the metering limit, which is not what they are about. unsafe { std::mem::zeroed() } } @@ -81,26 +92,132 @@ fn compilation_options() -> vm_exec2_compilation_options_t { } } -/// A mock hook table: plausible non-null words, never called in this phase. -fn mock_hook_table() -> Vec<*const c_void> { - (1..=8usize).map(|n| n as *const c_void).collect() +// --- The mock hook table ---------------------------------------------------------------- +// +// Only the two hooks the test contract imports get a correctly typed implementation. The rest +// of the table is filled with a stub of the same shape, so that copying the table reads valid +// function pointers throughout — a `mem::zeroed` table would be immediately invalid, since +// `extern "C" fn` is a non-nullable type. Calling one of the stubs would be a bug in the test +// contract, and it reports a code the assertions would catch. + +extern "C" fn stub_hook(_ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t { + vm_exec2_hook_result_t::early_exit(UNEXPECTED_HOOK_CODE) +} + +/// What the mock hooks record, for assertions after the call has returned. +/// +/// They cannot assert directly: a panic must not unwind out of an `extern "C"` function. +#[derive(Default)] +struct HookLog { + get_gas_left_calls: usize, + signal_error_calls: usize, + /// What `getGasLeft` read out of the contract's memory through its context. + memory_seen: Option>, + /// The message `signalError` was pointed at. + signalled_message: Option>, + /// Non-empty if a state operation misbehaved. + failures: Vec, +} + +fn hook_log() -> &'static Mutex { + static LOG: OnceLock> = OnceLock::new(); + LOG.get_or_init(|| Mutex::new(HookLog::default())) +} + +/// Loads from the contract's memory through the hook context it was handed. +fn load_through_context( + ctx: *mut vm_exec2_hook_context_t, + mem_ptr: i32, + len: i32, +) -> Result, i64> { + let mut buf = vec![0u8; len.max(0) as usize]; + let loaded = unsafe { + vm_exec2_state_mem_load(ctx, mem_ptr, buf.as_mut_ptr(), len, std::ptr::null_mut(), 0) + }; + if loaded < 0 { + return Err(loaded); + } + buf.truncate(loaded as usize); + Ok(buf) +} + +extern "C" fn mock_get_gas_left(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t { + let mut log = hook_log().lock().unwrap(); + log.get_gas_left_calls += 1; + + // The point of the exercise: a hook reaching back into the instance it is running inside, + // through the context the executor handed it. + match load_through_context(ctx, 0, 5) { + Ok(bytes) => log.memory_seen = Some(bytes), + Err(status) => log + .failures + .push(format!("mem_load in getGasLeft: {status}")), + } + + let mut length = 0u64; + let status = unsafe { vm_exec2_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; + if status != vm_exec2_status_t::VM_EXEC2_OK || length != 65536 { + log.failures + .push(format!("mem_length in getGasLeft: {status:?}, {length}")); + } + + vm_exec2_hook_result_t::ok(4242) +} + +extern "C" fn mock_signal_error( + ctx: *mut vm_exec2_hook_context_t, + message_offset: i32, + message_length: i32, +) -> vm_exec2_hook_result_t { + let mut log = hook_log().lock().unwrap(); + log.signal_error_calls += 1; + + match load_through_context(ctx, message_offset, message_length) { + Ok(bytes) => log.signalled_message = Some(bytes), + Err(status) => log + .failures + .push(format!("mem_load in signalError: {status}")), + } + + // A hook that wants execution to stop says so in its result. Nothing is written to a + // breakpoint global on the way out. + vm_exec2_hook_result_t::early_exit(USER_ERROR_CODE) } +/// Builds a table whose every entry is a valid function pointer. +fn mock_hook_table() -> &'static vm_exec2_vm_hook_c_func_pointers { + const WORD: usize = std::mem::size_of::(); + let words = std::mem::size_of::() / WORD; + assert_eq!( + std::mem::size_of::() % WORD, + 0, + "the hook table should be a flat block of function pointers" + ); + + let filled = vec![stub_hook as *const () as usize; words].into_boxed_slice(); + let table = + unsafe { &mut *(Box::leak(filled).as_mut_ptr() as *mut vm_exec2_vm_hook_c_func_pointers) }; + + table.get_gas_left_func_ptr = mock_get_gas_left; + table.signal_error_func_ptr = mock_signal_error; + table +} + +// --- Driving the exported functions ------------------------------------------------------- + fn new_executor(err: &mut ErrBuf) -> *mut vm_exec2_executor_t { let cost = opcode_cost(); let config = vm_exec2_executor_config_t { opcode_version: 1, opcode_cost: &cost, }; - let table = mock_hook_table(); let mut executor: *mut vm_exec2_executor_t = std::ptr::null_mut(); let status = unsafe { vm_exec2_new_executor( &mut executor, &config, - table.as_ptr() as *const c_void, - table.len(), + mock_hook_table(), err.ptr(), ERR_BUF_LEN, ) @@ -203,15 +320,9 @@ fn lifecycle() { let executor = new_executor(&mut err); let instance = new_instance(executor, &mut err); - // A call that does nothing succeeds. let result = call(instance, "doNothing", &mut err); assert_eq!(result.kind, vm_exec2_call_result_kind_t::VM_EXEC2_CALL_OK); - // A call into a VM hook succeeds too; with no generated dispatch yet, the hooks are the - // executor's own placeholder implementation. - let result = call(instance, "callHook", &mut err); - assert_eq!(result.kind, vm_exec2_call_result_kind_t::VM_EXEC2_CALL_OK); - // A missing endpoint is reported in the result, not as a failure of the call itself. let result = call(instance, "noSuchEndpoint", &mut err); assert_eq!( @@ -219,14 +330,12 @@ fn lifecycle() { vm_exec2_call_result_kind_t::VM_EXEC2_CALL_FUNCTION_NOT_FOUND ); - // Gas accounting is readable from outside a host call. let mut points = u64::MAX; let status = unsafe { vm_exec2_instance_get_points_used(instance, &mut points, err.ptr(), ERR_BUF_LEN) }; assert_eq!(status, vm_exec2_status_t::VM_EXEC2_OK); assert_ne!(points, u64::MAX, "points_used was not written"); - // Every endpoint takes no arguments and returns nothing. assert_eq!(unsafe { vm_exec2_instance_check_signatures(instance) }, 1); let present = CString::new("doNothing").unwrap(); @@ -246,7 +355,11 @@ fn lifecycle() { vm_exec2_instance_exported_function_names_length, vm_exec2_instance_exported_function_names, ), - vec!["callHook".to_string(), "doNothing".to_string()] + vec![ + "callGetGasLeft".to_string(), + "callSignalError".to_string(), + "doNothing".to_string() + ] ); // All of the hooks the module imports, in one call. @@ -256,7 +369,7 @@ fn lifecycle() { vm_exec2_instance_imported_function_names_length, vm_exec2_instance_imported_function_names, ), - vec!["getGasLeft".to_string()] + vec!["getGasLeft".to_string(), "signalError".to_string()] ); unsafe { @@ -265,19 +378,61 @@ fn lifecycle() { } } +#[test] +fn hook_dispatch() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + // A hook that returns normally: execution continues, and the contract gets the value. + let result = call(instance, "callGetGasLeft", &mut err); + assert_eq!( + result.kind, + vm_exec2_call_result_kind_t::VM_EXEC2_CALL_OK, + "calling a hook that returns normally should not stop execution" + ); + + // A hook that asks to stop: the code travels out in the call result, and the sentinel + // keeps it distinct from "no early exit". + let result = call(instance, "callSignalError", &mut err); + assert_eq!( + result.kind, + vm_exec2_call_result_kind_t::VM_EXEC2_CALL_EARLY_EXIT + ); + assert_eq!(result.early_exit_code, USER_ERROR_CODE); + + let log = hook_log().lock().unwrap(); + assert!( + log.failures.is_empty(), + "state op failures inside hooks:\n {}", + log.failures.join("\n ") + ); + assert_eq!(log.get_gas_left_calls, 1); + assert_eq!(log.signal_error_calls, 1); + + // Both hooks reached the contract's memory through the context they were handed. + assert_eq!(log.memory_seen.as_deref(), Some(&b"hello"[..])); + assert_eq!(log.signalled_message.as_deref(), Some(&b"hello"[..])); + + drop(log); + unsafe { + vm_exec2_instance_destroy(instance); + vm_exec2_executor_destroy(executor); + } +} + #[test] fn null_arguments_are_refused_without_a_message() { let mut err = ErrBuf::new(); let mut executor: *mut vm_exec2_executor_t = std::ptr::null_mut(); - // A null config cannot be reported through the error buffer, because the buffer itself - // may be the thing that is null, so these return a dedicated status instead. + // A null config cannot be reported through the error buffer, because the buffer itself may + // be the thing that is null, so these return a dedicated status instead. let status = unsafe { vm_exec2_new_executor( &mut executor, std::ptr::null(), std::ptr::null(), - 0, err.ptr(), ERR_BUF_LEN, ) @@ -305,8 +460,7 @@ fn an_invalid_opcode_version_is_reported_through_the_buffer() { vm_exec2_new_executor( &mut executor, &config, - std::ptr::null(), - 0, + mock_hook_table(), err.ptr(), ERR_BUF_LEN, ) @@ -342,6 +496,8 @@ fn a_context_is_rejected_when_no_call_is_active() { ); } +// --- The context lifecycle, reached through a mock hooks provider -------------------------- + /// What `state_ops` records from inside the host call, to be asserted after it returns. #[derive(Default)] struct Outcome { @@ -568,7 +724,7 @@ fn state_ops() { let outcome = Arc::new(Mutex::new(Outcome::default())); let builder = ProbingHooksBuilder { inner: CapiVMHooksBuilder::new(0xBEEFusize as *mut c_void, unsafe { - CapiHookTable::copy_from(std::ptr::null(), 0) + CapiVMHookTable::copy_from(mock_hook_table()) }), outcome: outcome.clone(), }; @@ -588,7 +744,7 @@ fn state_ops() { .expect("instance creation failed"); use multiversx_chain_vm_executor::Instance; - let result = instance.call("callHook", 1_000_000); + let result = instance.call("callGetGasLeft", 1_000_000); assert!( matches!(result, multiversx_chain_vm_executor::InstanceCallResult::Ok), "the probing call did not succeed" @@ -605,9 +761,9 @@ fn state_ops() { outcome.failures.join("\n ") ); - // The context that escaped the host call must now be refused. Contexts are pooled for - // the instance's lifetime, so this reads live memory and fails the token check, rather - // than being undefined behaviour. + // The context that escaped the host call must now be refused. Contexts are pooled for the + // instance's lifetime, so this reads live memory and fails the token check, rather than + // being undefined behaviour. let escaped = outcome.escaped_context as *mut vm_exec2_hook_context_t; let mut length = 0u64; let status = From c5363be925640a640b341743acd03212c50bbc7c Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 11:22:22 +0300 Subject: [PATCH 07/26] new FFI plan update --- PLAN-go-breakpoint-removal.md | 4 ++-- PLAN-go-ffi.md | 28 +++++++++++++++++++--------- PLAN-go-hooks-error-migration.md | 4 ++++ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/PLAN-go-breakpoint-removal.md b/PLAN-go-breakpoint-removal.md index b867408d..18b559c7 100644 --- a/PLAN-go-breakpoint-removal.md +++ b/PLAN-go-breakpoint-removal.md @@ -104,7 +104,7 @@ pub fn early_exit_async_call() -> VMHooksEarlyExit { ... ASYNC_CALL_EARLY_EXIT_C The code is `ReturnCode::as_u64()`, and `0` is reused as the async-call sentinel because a hook never exits with `Ok`. Go has `vmcommon.ReturnCode` with `Ok = 0`, so **the same sentinel works -unchanged**, and `vmhost.EarlyExit.Code` is already a `vmcommon.ReturnCode` — it was built for +unchanged**, and `executor.EarlyExit.Code` is already a `vmcommon.ReturnCode` — it was built for this. The consumer to mirror is @@ -268,7 +268,7 @@ The async-call branch is the one that returns `nil`, preserving ### 6.2 `ConsumeAllGas` does not cross the FFI -`VMHooksEarlyExit` carries only `code` and `message`. `vmhost.EarlyExit.ConsumeAllGas` is a +`VMHooksEarlyExit` carries only `code` and `message`. `executor.EarlyExit.ConsumeAllGas` is a Go-side concept, so the burn must stay in `HandleHookError`, executed before the hook returns. `applyEarlyExit` therefore keeps a gas responsibility even after it loses its breakpoint responsibility. Do not try to encode the flag in the exit code. diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index a7b15fcc..88ae8b60 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -212,7 +212,7 @@ implementation that calls into it are generated code, and belong to Phase 3 as o scoped. Everything they need is in place: the result and context ABI, the state operations, and `CapiVMHooksBuilder::with_hook_context_raw` as the seam. Until then the builder yields `VMHooksDefault` behind a single marked `TODO(Phase 3)`, and the table is copied as opaque -pointer-sized words (`CapiHookTable`) so the ownership story does not change when the type +pointer-sized words (`CapiVMHookTable`) so the ownership story does not change when the type does. The copy-rather-than-borrow decision is already made, matching the legacy API. **CI and release wiring.** `make capi2`, `capi2-linux-amd64`, `capi2-linux-arm`, @@ -270,14 +270,24 @@ No field, no ordering hazard, and no dependency on Phase 4. The 277 Go hook sign ### Converting the error to a code -Two small hand-written pieces, deliberately split so neither has to know the other's job: +`executor/earlyExit.go` owns the `EarlyExit` struct — it is what crosses the boundary, so it +belongs in the package the VM and the backends share rather than in the VM. Alongside it: -- `executor/earlyExit.go` — `EarlyExitError` (an error that knows its own code) and - `EarlyExitCodeOf`, a pure function that falls back to `vmcommon.ExecutionFailed`. It finds - the code through `errors.As`, so it survives the `fmt.Errorf("%w")` wrapping the hook - helpers do. -- `vmhost.EarlyExit.EarlyExitCode()` — three lines, returning the `vmcommon.ReturnCode` it - already carries. +- `ExitExecutionFailed`, the one definition of what a plain error means (execution failed, + burn the remaining gas). Both the VM, which applies the side effects, and the executor, + which only reports a code, go through it. +- `EarlyExitCodeOf`, a pure function finding the code with `errors.As`, so it survives the + `fmt.Errorf("%w")` wrapping the hook helpers do. + +`vmhost` keeps the two constructors that deviate from that default, `ExitUserError` and +`ExitOutOfGas`, because they carry VM sentinel errors. `vmhooks.resolveEarlyExit` keeps its +single call site and delegates its default branch to `ExitExecutionFailed`. + +An earlier iteration had `EarlyExit` stay in `vmhost` with an `EarlyExitError` interface in +`executor` to reach it. That was dropped: the interface had exactly one implementor and no +prospect of a second, and it is not what kept `wasmer3` from importing `vmhost` — a struct in +`executor` preserves that property just as well, while letting the default live once instead +of twice. The wire codes are `vmcommon.ReturnCode` values, matching the Rust VM's `ReturnCode::as_u64`. Nothing in the failure path produces `vmcommon.Ok`, which is what keeps code 0 free to mean @@ -305,7 +315,7 @@ exactly that field. ### Rust side wired up -`CapiHookTable` now holds a copy of the generated table rather than opaque words, and +`CapiVMHookTable` now holds a copy of the generated table rather than opaque words, and `vm_exec2_new_executor` takes it typed, so the Phase 2 placeholder parameter pair `(hook_table, hook_table_len)` is gone. `CapiVMHooksBuilder` builds a `CapiVMHooks` around the activated context instead of yielding `VMHooksDefault`; the Phase 2 `TODO(Phase 3)` is closed. diff --git a/PLAN-go-hooks-error-migration.md b/PLAN-go-hooks-error-migration.md index f64893e4..f032a67a 100644 --- a/PLAN-go-hooks-error-migration.md +++ b/PLAN-go-hooks-error-migration.md @@ -2,6 +2,10 @@ **Repo:** `mx-chain-vm-go` (branch `wasmer6`) **Related:** `PLAN-go-ffi.md` in `mx-vm-executor-rs` — this document is the Go-side prerequisite for its Phase 3. +**Status:** done, and left as the work order it was. One thing has since moved: the `EarlyExit` +type now lives in `executor`, not `vmhost`, so that an executor reporting early exits through +its own call result can read the code off it. Everything below still describes the shape +accurately apart from that package. See Phase 3 of `PLAN-go-ffi.md`. --- From 85883a36b8367bc2f81e60567a0d3d819fa5edcd Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 12:00:23 +0300 Subject: [PATCH 08/26] new FFI plan update --- PLAN-go-ffi.md | 188 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 156 insertions(+), 32 deletions(-) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 88ae8b60..77fdda0a 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -34,7 +34,7 @@ one config object rather than a sequence of setters. | --- | --- | --- | | `Executor` | 4 methods, 2 of them post-construction setters | 2 instantiation methods; everything else at construction | | `CompilationOptions` | 7 fields | 4; `gas_limit` moved to `call`, metering and breakpoints always on | -| Instance | one 17-method trait | `Instance` (7, outside view) + `InstanceState` (inside view, for hooks) | +| Instance | one 17-method trait | `Instance` (8, outside view) + `InstanceState` (8, inside view, for hooks) | | Call result | `Result<(), String>` plus breakpoint globals to poll | `InstanceCallResult` enum | | Breakpoints | 6 values | 3; `ExecutionFailed`/`SignalError`/`AsyncCall` became `VMHooksEarlyExit` | | VM hooks | `&self`, bare returns, carries a context pointer | `&mut self`, `Result<_, VMHooksEarlyExit>`, no context pointer | @@ -135,6 +135,8 @@ is what flips. | **G1** | Do the hooks hold the state, or take it as a parameter? | The C ABI passes the context as the first argument of every hook — that *is* the parameter form. If the Rust trait keeps the state bundled, the generator must emit and reconcile two shapes. Deciding after the ABI is frozen turns a refactor into a two-repo migration. The Wasmer 6 builder currently returns `Box`, so a per-hook allocation also rides on this. **Phase 2 found this already answered by the committed Wasmer 6 code**: `ExperimentalVMHooksBuilder::with_vm_hooks` hands the builder the state and lends the hooks back through a closure, so the state is *bundled* in Rust and *passed as a parameter* in C — the two are compatible, and the generator emits one shape, not two. The allocation question is also already resolved: the builder lends `&mut dyn VMHooks` on its own stack frame. | | **G2** | Does the new executor keep the Go VM's truncating load? | **Settled in Phase 2: yes, it clamps.** `vm_exec2_state_mem_load` writes into the caller's buffer and returns `min(requested, available)`, so the truncation is visible in the return value. Chosen to preserve behaviour contracts have already observed; the strict load stays available on the trait for a caller that wants it. The rule itself moved into `clamped_load_len` in `vm-executor/src/new_traits/memory.rs`, so the FFI and `InstanceState::memory_load_clamped` cannot drift apart. | | **G4** | Which `OpcodeVersion` does each host declare, and does the config stay mutable? | Decides whether `OpcodeConfig` is `Arc>` (2.2 today) or plain (Wasmer 6 today), which decides the shape of the single config struct. **Phase 2 took the immutable branch for the new C API**: the version and the cost table arrive in `vm_exec2_executor_config_t` at `vm_exec2_new_executor` and are never mutated, so there is no `Arc>` on this path and no `set_opcode_config`. The gate stays open for Phase 1.6, which has to reconcile the 2.2 backend's `Arc>`. | +| **G5** | Does one C library carry both backends, selected by a flag? | Decides whether `c-api-new` depends on both backend crates, whether the Go VM links one library or two, and whether the Phase 5 differential run is a runtime flag or a link-time choice. See "Serving both backends" below. | + ## Phase 1 — Close the trait gaps @@ -332,32 +334,145 @@ distinct from early-exit code 0. On the Go side, the package compiling *is* the check that a cgo `//export` function can return a C struct by value — now across all 277 shims rather than the original three-layout spike. -## Phase 4 — Go VM (new `wasmer3/` package, parallel to `wasmer2/`) - -Both executors coexist behind `ExecutorAbstractFactory`, which is what makes differential -testing possible. - -1. `executor.InstanceState` interface — `MemLoad`, `MemStore`, `MemLength`, `MemGrow`, - `GetPointsUsed`, `SetPointsUsed` — implemented by a struct wrapping the `ctx` pointer. -2. `executor.VMHooks` becomes a provider: `CreateVMHooks(state InstanceState) VMHooks`, - mirroring the Rust builder. Replaces the single host-wide `VMHooksImpl`. -3. `VMHooksImpl` gains `state` and `earlyExit`; its `MemLoad`/`MemStore` stop going through - `host.Runtime().GetInstance()`. This is the change that decouples the hooks from the - instance tracker. -4. `FailExecution` / `SignalUserError` record an early exit instead of setting a breakpoint, - and keep burning gas as they do now. -5. `handleBreakpointIfAny` maps `InstanceCallResult` instead of a wasmer global, producing - the same sentinels. -6. `MemLoadFromMemory` / `MemStoreToMemory` are bypassed for wasmer3; kept for wasmer2. -7. The validator switches to imported-function *names* rather than per-name queries. -8. The executor factory passes one config at creation; `SetOpcodeCosts` as a - post-construction mutator goes away. Note Go is already stale here — `wasmer2/bridge2.go` - calls `vm_exec_set_opcode_costs` against a header that now exports - `vm_exec_set_opcode_config`. - -Only the memory and gas paths move to the state handle. `IsFunctionImported`, -`ValidateFunctionArities`, `Reset` and `Cache` stay on the outer `Instance`, and keeping that -split crisp is what makes the Go side match the Rust trait split rather than just wrap it. +## Serving both backends + +The two interface sets cannot coexist in one VM: `Instance` lost most of its methods and the +inside view moved to `InstanceState`, so a host either speaks the old set or the new one. The +Rust side has already resolved this the right way round — **the new set is not "Wasmer 6's +interface", it is the intersection**. Wasmer 2.2 satisfies it through +`vm-executor-wasmer/src/new_traits`, and the Rust VM drives both backends through it; the +reverse does not hold, because nothing in the legacy set can express a call-scoped state. + +That asymmetry is what makes converging safe, and it is worth being explicit about why. The +hook context (`capi_vm_hook_context`) enforces the *stricter* of the two lifetimes: a state +handle is valid for exactly one host call. Wasmer 6 requires that. Wasmer 2.2 does not — its +`WasmerProdInstanceState` holds a `Weak` and stays valid for the whole +instance — but it satisfies the stricter contract trivially. Writing the Go side against the +tighter contract therefore costs nothing on 2.2 and makes it impossible for the VM to +accidentally depend on a lifetime only 2.2 offers. + +### One library, a backend flag + +The target is a single `libvmexeccapi2` carrying both backends, chosen by a field in +`vm_exec2_executor_config_t` — configured once, at creation, like everything else in this API. + +What it buys: + +- **The Phase 5 differential run becomes a runtime flag**, in one process, over one harness, + rather than two builds linked against different libraries. That is the acceptance criterion + for the whole effort, so making it cheap matters more than it looks. +- One FFI surface, one header, one shipped artifact matrix. The platform matrix is already + four libraries; doubling it is worse than doubling one library's size. + +What it costs: roughly 5-6 MB (the current wasmer 2.2 library) on top of ~8 MB (wasmer 6), +which is unremarkable for a shipped node dependency. + +Two things have to be settled before committing to it: + +- **Signal handlers.** Wasmer 2.2 installs trap handlers through `platform_init()` — exposed + as `vm_force_sighandler_reinstall` and called from `wasmer2/bridge2.go` — and Wasmer 6 + installs its own. Both are expected to chain to the previous handler for faults outside + their own code, but that is process-global behaviour nobody here has verified, with Go's own + handlers already in the mix. The failure mode is a mishandled trap under load, not a test + failure. **This needs an experiment in Phase 4a, not an assumption.** +- **Never link both libraries at once.** `libvmexeccapi` (legacy) already contains wasmer 2.2. + A process linking it *and* a `libvmexeccapi2` that also bundles 2.2 would hold two copies, + each with its own globals and trap handlers. The flag design implies the Go VM eventually + links only `libvmexeccapi2`; during migration, one or the other, never both. + +### What the wasmer 2.2 path needs + +`WasmerProdExecutor` already implements the new `Executor` trait, so the gap is in the hooks, +not the instance: + +- `WasmerProdRuntimeRef::vm_hooks` returns `Box` and `opcode_config()` + returns `Arc>`, where the Wasmer 6 side returns a hooks *builder* and a + plain `Arc`. Converging the two (**Phase 1.6**) is therefore a prerequisite for this path, + not an optional tidy-up. +- The generated `CapiVMHooks` is new-style and borrows a per-call context. Wasmer 2.2 asks for + the hooks once per instance. Rather than generating a second 277-method implementation, the + capi should reuse the one it has behind `VMHooksLegacyAdapter`: that adapter already wraps + every hook call, so a capi-specific `VMHooksSetEarlyExit` can **activate a hook context + around each call** and build `CapiVMHooks` there. One generated implementation, two + activation paths, and the Go side cannot tell which backend it is talking to. +- `set_early_exit` on that path must do what `mx-sdk-rs/chain/wasmer-prod/src/wasmer_prod.rs` + does: store the exit *and* trip the breakpoint global, because under 2.2 the global is still + the mechanical halt. See `PLAN-go-breakpoint-removal.md` §3. + +## Phase 4 — Go VM + +Split in two, because the two halves fail for different reasons and only the first one is +cheap to undo. + +### Phase 4a — Make the FFI work, with the VM kept out of it + +A `wasmer3/` package that links the library and drives it directly, with a Go-side test +supplying a minimal hand-written `executor.VMHooks`. It does **not** implement +`executor.Executor` or `executor.Instance` yet. + +The point is to isolate everything that can only fail at the boundary: + +1. Linking: cgo `LDFLAGS`, the per-platform library names, the `rpath`/soname handling that + `wasmer2/bridge2.go` already does for the legacy library. +2. **The struct-return ABI at runtime.** So far it is proven in two halves — Rust's view by + `hook_dispatch`, Go's view by the package compiling against the declared prototypes. Nobody + has yet executed a Go shim called from Rust. This is the first place that happens. +3. The `go_vm_hooks` pointer: how the Go side pins an `executor.VMHooks` so the pointer handed + to `vm_exec2_new_instance` stays valid, under cgo's rule against storing Go pointers in C + memory. The legacy path sidesteps the checker by passing a `uintptr`; decide deliberately + rather than inheriting that. +4. The state operations from inside a real hook, and the stale-context rejection, now with Go + on the other side. +5. **The signal-handler experiment for G5** (see "Serving both backends"). It belongs here + because it needs a real process with both runtimes loaded, and because a bad answer changes + the shape of everything after it. + +None of this touches the VM, so a wrong turn costs a package, not a refactor. + +### Phase 4b — Converge the Go executor interfaces + +The Go side still speaks the legacy interface set, and this is where it stops. It is the Go +mirror of the split the Rust side already made, and it is the bulk of Phase 4. + +The asymmetry to close: `executor.Instance` has 24 methods, against 8 on the new Rust +`Instance` plus the 8 of an `InstanceState` the Go side does not have at all. The consumption is +concentrated — almost all of it is `vmhost/contexts/runtime.go` and `instanceTracker.go`, and +only three implementors matter (`wasmer2.Wasmer2Instance`, `mock.InstanceMock`, +`executor/wrapper.WrapperInstance`) — so the change is wide but shallow. + +1. Add `executor.InstanceState`: `MemLoad`, `MemStore`, `MemLength`, `MemGrow`, + `GetPointsUsed`, `SetPointsUsed`. Implemented for wasmer3 by a struct wrapping the hook + context; implemented for wasmer2 by delegating to its existing instance. +2. Shrink `executor.Instance` to the outside view, and **push the legacy-only methods down + into wasmer2's own types** rather than keeping them on the shared interface: + `SetBreakpointValue`/`GetBreakpointValue` (the new executor reports breakpoints in its call + result — see `PLAN-go-breakpoint-removal.md`), `SetVMHooksPtr`/`GetVMHooksPtr`, + `SetGasLimit`, `MemDump`, `HasMemory`. The census shows several of these have no caller in + the VM at all. +3. `CallFunction` returns an `InstanceCallResult` rather than a bare `error`, which is the + hard prerequisite for the breakpoint removal and is described there. +4. `executor.VMHooks` becomes a per-instance provider, since that is where the Go side keeps + the early-exit slot the shims read. Phase 2 found that the Rust `Executor` trait has + nowhere to put a per-instance host handle; **Phase 1.6 should add it**, at which point the + capi can go back through the trait and serve both backends uniformly. +5. The contract validator switches to imported-function *names*, which the FFI already + exposes as a two-step pair. +6. `MemLoadFromMemory` / `MemStoreToMemory` are bypassed for wasmer3 — bounds checking lives + in Rust — and kept for wasmer2. +7. `SetOpcodeConfig` as a post-construction mutator goes away; config is passed once at + creation. Note Go is already stale here: `wasmer2/bridge2.go` calls + `vm_exec_set_opcode_costs` against a header that now exports `vm_exec_set_opcode_config`. + +Both executors then coexist behind `ExecutorAbstractFactory` speaking the *same* interfaces, +which is what makes the Phase 5 differential run a matter of choosing a factory — or, if G5 +lands as expected, a flag. + +**Why not converge before 4a?** It would mean reshaping a 26-method interface, its three +implementors and the mocks against an FFI nobody has executed yet. **Why not defer it past +Phase 4?** Because then `wasmer3` has to implement the legacy 26 methods, including the +breakpoint accessors the new executor deliberately does not have and the memory accessors that +moved to `InstanceState` — an adapter whose only purpose is to fake the interface being +deleted. Phase 4a is the cheap half and it de-risks the expensive one. ## Phase 5 — Bring-up and validation @@ -366,8 +481,10 @@ split crisp is what makes the Go side match the Rust trait split rather than jus and high store bounds. The three current implementations disagree, so these come before anything else is trusted. The Rust side of these is already covered by `c-api-new/tests/ffi_boundary.rs`; what remains is asserting the Go wrapper agrees. -- **Differential run**: the scenario suite under both factories, asserting identical - `VMOutput` (return code, message, logs, gas used). This is the acceptance criterion. +- **Differential run**: the scenario suite under both backends, asserting identical + `VMOutput` (return code, message, logs, gas used). This is the acceptance criterion. If G5 + lands, "both backends" is a flag on one library in one process rather than two builds, and + after Phase 4b both speak the same Go interfaces, so the harness is the same either way. - Re-entrancy: a contract calling `executeOnDestContext` from inside a hook, confirming nested contexts do not clobber each other. **Covered at the Rust level in Phase 2**; what remains is driving it from a real contract through Go. @@ -402,6 +519,13 @@ and the C ABI is parameter-form regardless, so the trait convergence was not a p after all — it is now better informed by Phase 2, which found the per-instance hooks argument the converged trait needs. -Remaining path: **Phase 4 → Phase 5**, with Phase 1.6/1.7 landing whenever G4 is settled -across both backends. Both sides of the boundary now exist and are tested in isolation; what -is left is the Go VM actually linking the library and driving it. +Remaining path: **Phase 4a → Phase 4b → Phase 5**. + +Phase 4a is independent of everything else and answers two questions that gate the rest: does +the struct-return ABI work at runtime, and can both backends share a process (G5). Phase 4b +depends on G5 only for how the backend is chosen, not for the interface shape — that is already +determined by the Rust side. + +**Phase 1.6/1.7 is now on the critical path rather than beside it.** Serving wasmer 2.2 through +the new FFI needs the two `RuntimeRef` traits converged, and Phase 4b needs the hooks provider +to take a per-instance handle. G4 has to be settled for that, and G1 is already answered. From bc7e13aa2712f24398c182b95782d37cfb5f12d0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 15:25:29 +0300 Subject: [PATCH 09/26] new FFI updates, after Go integration (phase 4a) --- PLAN-go-ffi.md | 192 ++++++++++++++++++++------ c-api-new/libvmexeccapi_new.h | 18 ++- c-api-new/src/capi_instance.rs | 10 +- c-api-new/src/capi_vm_hook_context.rs | 16 ++- c-api-new/tests/ffi_boundary.rs | 4 +- 5 files changed, 182 insertions(+), 58 deletions(-) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 77fdda0a..5997be06 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -1,7 +1,8 @@ # Integrating the new executor traits into the Go VM Working plan for exposing `vm-executor/src/new_traits` over a C FFI and consuming it from -the Go VM. Status as of 2026-10-02: Phase 1 items 1-5, Phase 2 and Phase 3 are implemented. +the Go VM. Status as of 2026-10-02: Phase 1 items 1-5, Phase 2, Phase 3 and Phase 4a are +implemented. ## Why @@ -135,7 +136,7 @@ is what flips. | **G1** | Do the hooks hold the state, or take it as a parameter? | The C ABI passes the context as the first argument of every hook — that *is* the parameter form. If the Rust trait keeps the state bundled, the generator must emit and reconcile two shapes. Deciding after the ABI is frozen turns a refactor into a two-repo migration. The Wasmer 6 builder currently returns `Box`, so a per-hook allocation also rides on this. **Phase 2 found this already answered by the committed Wasmer 6 code**: `ExperimentalVMHooksBuilder::with_vm_hooks` hands the builder the state and lends the hooks back through a closure, so the state is *bundled* in Rust and *passed as a parameter* in C — the two are compatible, and the generator emits one shape, not two. The allocation question is also already resolved: the builder lends `&mut dyn VMHooks` on its own stack frame. | | **G2** | Does the new executor keep the Go VM's truncating load? | **Settled in Phase 2: yes, it clamps.** `vm_exec2_state_mem_load` writes into the caller's buffer and returns `min(requested, available)`, so the truncation is visible in the return value. Chosen to preserve behaviour contracts have already observed; the strict load stays available on the trait for a caller that wants it. The rule itself moved into `clamped_load_len` in `vm-executor/src/new_traits/memory.rs`, so the FFI and `InstanceState::memory_load_clamped` cannot drift apart. | | **G4** | Which `OpcodeVersion` does each host declare, and does the config stay mutable? | Decides whether `OpcodeConfig` is `Arc>` (2.2 today) or plain (Wasmer 6 today), which decides the shape of the single config struct. **Phase 2 took the immutable branch for the new C API**: the version and the cost table arrive in `vm_exec2_executor_config_t` at `vm_exec2_new_executor` and are never mutated, so there is no `Arc>` on this path and no `set_opcode_config`. The gate stays open for Phase 1.6, which has to reconcile the 2.2 backend's `Arc>`. | -| **G5** | Does one C library carry both backends, selected by a flag? | Decides whether `c-api-new` depends on both backend crates, whether the Go VM links one library or two, and whether the Phase 5 differential run is a runtime flag or a link-time choice. See "Serving both backends" below. | +| **G5** | Does one C library carry both backends, selected by a flag? | Decides whether `c-api-new` depends on both backend crates, whether the Go VM links one library or two, and whether the Phase 5 differential run is a runtime flag or a link-time choice. **Phase 4a cleared the part everyone expected to be the problem and found a different one.** Both runtimes' trap handlers coexist and chain, in both initialisation orders (`wasmer3/dualbackend/`). But `wasmer-vm` 2.2 and 6 export the same 32 `#[no_mangle]` `wasmer_vm_*` libcall symbols, which collide, which the Apple linker reports as a *warning* while building the library anyway. The gate now rests on hiding every non-`vm_exec2_*` symbol and confirming it on Linux. See "Serving both backends" below. | ## Phase 1 — Close the trait gaps @@ -367,18 +368,49 @@ What it buys: What it costs: roughly 5-6 MB (the current wasmer 2.2 library) on top of ~8 MB (wasmer 6), which is unremarkable for a shipped node dependency. -Two things have to be settled before committing to it: - -- **Signal handlers.** Wasmer 2.2 installs trap handlers through `platform_init()` — exposed - as `vm_force_sighandler_reinstall` and called from `wasmer2/bridge2.go` — and Wasmer 6 - installs its own. Both are expected to chain to the previous handler for faults outside - their own code, but that is process-global behaviour nobody here has verified, with Go's own - handlers already in the mix. The failure mode is a mishandled trap under load, not a test - failure. **This needs an experiment in Phase 4a, not an assumption.** -- **Never link both libraries at once.** `libvmexeccapi` (legacy) already contains wasmer 2.2. - A process linking it *and* a `libvmexeccapi2` that also bundles 2.2 would hold two copies, - each with its own globals and trap handlers. The flag design implies the Go VM eventually - links only `libvmexeccapi2`; during migration, one or the other, never both. +Two things had to be settled before committing to it. Phase 4a settled the first and +replaced the second with something sharper. + +**Signal handlers: answered, and the answer is yes.** `wasmer3/dualbackend/` links both +libraries into one test binary and traps on purpose in each backend, for both trap kinds that +take different routes to the handler — an illegal instruction (`unreachable`) and an +out-of-range load — in both initialisation orders. All four cases come back as a reported +runtime error, and both backends keep working afterwards. The order that mattered most is +wasmer 2.2 second, because `wasmer2.CreateExecutor` calls `vm_force_sighandler_reinstall` +unconditionally and is therefore the one that can overwrite a handler wasmer 6 has already +installed; it chains correctly. Caveat on the evidence: this was run on darwin/arm64 only, and +it is the two-*library* case, not the bundled one. + +**Symbol collision is the real blocker, and it is not about signal handlers.** Probing the +bundled case — adding the 2.2 backend as a dependency of `c-api-new` and linking — produced 32 +`ld: duplicate symbol` reports, all of the form `_wasmer_vm_f32_ceil`, +`_wasmer_vm_memory32_grow`, `_wasmer_vm_table_init`: the libcalls that JIT-compiled wasm code +calls back into. `wasmer-vm` declares them `#[no_mangle]`, so both versions export the same +names, and both resolve a libcall address the same way, by taking the address of their own +definition (`libcalls.rs`: `LibCall::CeilF32 => wasmer_vm_f32_ceil as usize`). Two things make +this dangerous rather than cosmetic: + +- The Apple linker reported the duplicates as a **warning on the linker's stderr and built the + library anyway**, silently keeping one definition. Nothing fails; the problem would surface + as a wrong answer. +- Whether each backend then gets *its own* address depends on the platform. Under macOS's + two-level namespace the local definition wins, which is why the two-library experiment above + passed. Under ELF's flat namespace a `#[no_mangle]` symbol in a shared object is + preemptible, so the address goes through the GOT and resolves to whichever definition the + dynamic linker picked first — for one of the two backends, the *other* one's libcalls, called + with its own incompatible `VMContext`. That is silent memory corruption, on the platform the + node actually runs on. + +There is a known mitigation, and it is one this library wants regardless: **export nothing but +the `vm_exec2_*` surface.** Hiding the rest (a version script on ELF, `-exported_symbols_list` +on Mach-O, or `-C default-symbol-visibility=hidden`) makes the `wasmer_vm_*` symbols local and +non-preemptible, so each backend binds its own. Until that is in place and verified on Linux, +the constraint stands: **never link `libvmexeccapi` and `libvmexeccapi2` into one binary**, and +note that the reason is the shared `wasmer_vm_*` libcall names, not two copies of wasmer 2.2 — +today's `libvmexeccapi2` contains only wasmer 6. + +So G5 is not closed, but what remains is a link-visibility question with a standard answer, +not an open question about whether two runtimes can coexist. ### What the wasmer 2.2 path needs @@ -404,30 +436,105 @@ not the instance: Split in two, because the two halves fail for different reasons and only the first one is cheap to undo. -### Phase 4a — Make the FFI work, with the VM kept out of it +### Phase 4a — Make the FFI work, with the VM kept out of it — **done** A `wasmer3/` package that links the library and drives it directly, with a Go-side test supplying a minimal hand-written `executor.VMHooks`. It does **not** implement -`executor.Executor` or `executor.Instance` yet. - -The point is to isolate everything that can only fail at the boundary: - -1. Linking: cgo `LDFLAGS`, the per-platform library names, the `rpath`/soname handling that - `wasmer2/bridge2.go` already does for the legacy library. -2. **The struct-return ABI at runtime.** So far it is proven in two halves — Rust's view by - `hook_dispatch`, Go's view by the package compiling against the declared prototypes. Nobody - has yet executed a Go shim called from Rust. This is the first place that happens. -3. The `go_vm_hooks` pointer: how the Go side pins an `executor.VMHooks` so the pointer handed - to `vm_exec2_new_instance` stays valid, under cgo's rule against storing Go pointers in C - memory. The legacy path sidesteps the checker by passing a `uintptr`; decide deliberately - rather than inheriting that. -4. The state operations from inside a real hook, and the stale-context rejection, now with Go - on the other side. -5. **The signal-handler experiment for G5** (see "Serving both backends"). It belongs here - because it needs a real process with both runtimes loaded, and because a bad answer changes - the shape of everything after it. - -None of this touches the VM, so a wrong turn costs a package, not a refactor. +`executor.Executor` or `executor.Instance`, so a wrong turn costs a package rather than a +refactor. + +| File | What it is | +| --- | --- | +| `wasmer3.go` | Package doc and logger | +| `bridge3.go` | cgo linkage and a thin wrapper per C entry point, all 18 of them | +| `error3.go` | The caller-provided error buffer, and status → Go error | +| `wasmer3Executor.go` | `ExecutorConfig`, `CompilationOptions`, executor and instance creation | +| `wasmer3Instance.go` | The outside view: call, gas, introspection, `CallResult` | +| `wasmer3InstanceState.go` | The inside view: the six state operations, over a hook context | +| `vmHooksProvider.go` | `VMHooksProvider`, and the per-instance handle it is reached through | +| `wasmer3ImportsCgoHelper.go` | What every generated shim calls on entry and on failure | +| `opcodeCost.go` | Generated; new writer, see below | +| `bridge3_test.go` | 20 tests (23 with subtests), the Phase 4a acceptance | +| `dualbackend/` | The G5 experiment, behind a build tag | + +What the five questions turned out to be: + +1. **Linking.** The same `${SRCDIR}` rpath shape as `wasmer2/bridge2.go`, against + `libvmexeccapi2[_arm]`. The darwin/arm64 library is built and in place; the other three + platforms need a CI build before they link. +2. **The struct-return ABI at runtime — confirmed.** `TestHookResultShapes` drives an `i64`, + an `i32` and a `void` hook from wasm, and the contract itself compares the value it got + back against what the Go hook promised, signalling an error if they differ. Checked for + non-vacuity by flipping one bit of the hook's return: the test fails, as an early exit with + the contract's own error code. Until this ran, the 16-byte result struct was proven in two + halves and never end to end. +3. **The `go_vm_hooks` pointer — `runtime/cgo.Handle`,** which meant **changing the C type + from `void *` to `uintptr_t`.** The legacy path stores a Go pointer in C memory, which cgo + forbids and which works by luck; a handle is an integer index into a Go-side table, which + is what the rule leaves available. The type change was necessary rather than cosmetic: + with a `void *` field, Go has to convert the handle back with `unsafe.Pointer(uintptr(...))`, + which is exactly the pattern `go vet`'s `unsafeptr` check rejects. The field is also more + honestly typed now, since Rust only stores the word and hands it back. The whole package + passes `go vet` and `GOEXPERIMENT=cgocheck2`, the strictest pointer checking available — + verified to be live by making it catch a deliberate violation. +4. **State operations from inside a hook, and stale-context rejection.** Both work from Go: + a hook writes wasm memory through `MemStore` and reads it through `MemLoad`, with the + contract verifying the bytes itself, and reads gas and memory size through the same + context. `TestStaleContextIsRejected` keeps the state past the hook's return and confirms + all six operations come back as `ErrStaleContext` rather than following a dead borrow. +5. **The signal-handler experiment.** Done, in `dualbackend/`; it passed, and it turned up a + different G5 blocker. Both recorded under "Serving both backends". + +Two risks the phase closed on its own account: + +- **Nesting across the boundary works** (`TestHookReentersTheExecutor`). A hook runs a whole + other instance, which runs hooks of its own, and the outer hook's state is still usable + afterwards and unchanged. That is the property the per-call hooks decision below rests on, + so it is proven here rather than deferred to Phase 5. +- **Re-entering the *same* instance is now an error rather than undefined behaviour.** + `ExperimentalInstance::call` takes `&mut self`, so a second call placed while the first is in + flight would alias a mutable borrow across C, with nothing to catch it. `Wasmer3Instance` + carries an `inCall` flag, so the hook gets `ErrInstanceReentered` and it travels out as an + ordinary early exit. The VM never does this — the instance tracker gives each stack level its + own instance — but it was available to do, and this is the same category of replacement as + the stale-context token: a guarantee the borrow checker used to give, reinstated by hand. + +Two things were decided here that the plan had left open: + +- **The hooks are built per call, not per instance** (`VMHooksProvider.VMHooksForCall`), + mirroring the Rust side exactly: `ExperimentalVMHooksBuilder::with_vm_hooks` builds its + hooks around the state for the duration of one call and lends them out. + + The alternative — one hooks object per instance with a mutable "current state" slot — is + *workable*, and it is worth being straight about why, because the obvious objection to it + turns out to be wrong. A nested call reaches a *different* instance and therefore a + different slot, so the outer hook's slot survives, and re-entering the same instance is now + refused outright. It would not be clobbered. + + The reasons to prefer the parameter anyway are smaller but real. It needs no slot, so there + is no question of when to clear one, and no window in which the hooks object holds a context + belonging to a call that has ended. It makes the state a parameter, which is the shape the C + ABI already has — G1's answer — so the generator emits one shape rather than reconciling + two. And it does not lean on the re-entry guard for a property that construction gives for + free. The cost is one small allocation per hook call, and no generator change at all: the + generated shim still calls `getVMHooksFromHookContext(ctx)` and nothing else. +- **Who holds the error buffer.** Outer calls allocate one per call, which is free at their + frequency. State operations share one buffer per instance, which is sound and not merely + convenient: a state operation is a single cgo call into Rust that touches memory and + returns, never calling back into Go, so the buffer's live range contains no re-entry. A + buffer per operation would be an allocation on every memory access a contract makes. + +Also added: a `WriteWasmer3OpcodeCost` generator writer, so `wasmer3/opcodeCost.go` comes from +the same opcode list as its C counterpart rather than being hand-copied. It emits a +package-level function instead of wasmer2's method on the executor, because this executor takes +its configuration at construction and there is no executor to hang it off of. + +Not done here, deliberately: the `reset`/cache entry points (Phase 6), and the three +non-darwin platform libraries. **The missing libraries have a CI consequence**: `wasmer3` now +links `libvmexeccapi2`, so `go test ./...` fails at the link step on any platform whose library +is absent. `go build ./...` is unaffected, because a non-main package is never linked. Either +the four libraries land before the package is in the default test path, or `wasmer3` goes +behind a build tag until they do. ### Phase 4b — Converge the Go executor interfaces @@ -467,9 +574,9 @@ Both executors then coexist behind `ExecutorAbstractFactory` speaking the *same* which is what makes the Phase 5 differential run a matter of choosing a factory — or, if G5 lands as expected, a flag. -**Why not converge before 4a?** It would mean reshaping a 26-method interface, its three +**Why not converge before 4a?** It would mean reshaping a 24-method interface, its three implementors and the mocks against an FFI nobody has executed yet. **Why not defer it past -Phase 4?** Because then `wasmer3` has to implement the legacy 26 methods, including the +Phase 4?** Because then `wasmer3` has to implement the legacy 24 methods, including the breakpoint accessors the new executor deliberately does not have and the memory accessors that moved to `InstanceState` — an adapter whose only purpose is to fake the interface being deleted. Phase 4a is the cheap half and it de-risks the expensive one. @@ -519,12 +626,13 @@ and the C ABI is parameter-form regardless, so the trait convergence was not a p after all — it is now better informed by Phase 2, which found the per-instance hooks argument the converged trait needs. -Remaining path: **Phase 4a → Phase 4b → Phase 5**. +Remaining path: **Phase 4b → Phase 5**. -Phase 4a is independent of everything else and answers two questions that gate the rest: does -the struct-return ABI work at runtime, and can both backends share a process (G5). Phase 4b -depends on G5 only for how the backend is chosen, not for the interface shape — that is already -determined by the Rust side. +Phase 4a was independent of everything else and answered the two questions that gated the +rest. The struct-return ABI works end to end. Both backends share a process, and what is left +of G5 is a link-visibility question rather than a runtime one. Phase 4b never depended on G5 +for the interface *shape* — that was already determined by the Rust side — only for how the +backend gets chosen, so it is unblocked either way. **Phase 1.6/1.7 is now on the critical path rather than beside it.** Serving wasmer 2.2 through the new FFI needs the two `RuntimeRef` traits converged, and Phase 4b needs the hooks provider diff --git a/c-api-new/libvmexeccapi_new.h b/c-api-new/libvmexeccapi_new.h index 17ba18f4..9556ea1e 100644 --- a/c-api-new/libvmexeccapi_new.h +++ b/c-api-new/libvmexeccapi_new.h @@ -276,9 +276,15 @@ typedef struct vm_exec2_hook_result_t { */ typedef struct vm_exec2_hook_context_t { /** - * The Go VM hooks object for this instance. Read by the Go side, never by Rust. + * The caller's VM hooks handle for this instance. Read by the caller, never by Rust. + * + * An opaque word rather than a pointer, deliberately: Rust only stores it and hands it + * back, and the Go side needs it to be an integer. cgo forbids storing a Go pointer in + * C memory, so what Go puts here is a `runtime/cgo.Handle`, which is a `uintptr` by + * construction. Typing the field as a pointer would force Go to convert it back with + * `unsafe.Pointer(uintptr(...))`, exactly the pattern `go vet` rejects. */ - void *go_vm_hooks; + uintptr_t go_vm_hooks; /** * Erased `&mut ExperimentalInstanceState<'_>`; null while no call is active. */ @@ -653,14 +659,16 @@ void vm_exec2_executor_destroy(struct vm_exec2_executor_t *executor); /** * Creates an instance. * - * `go_vm_hooks` is the Go VM hooks object for *this* instance; it is handed back to every - * VM hook as field 0 of the hook context. It must stay alive for the instance's lifetime. + * `go_vm_hooks` is the caller's VM hooks handle for *this* instance; it is handed back to + * every VM hook as field 0 of the hook context, and is otherwise opaque here. Whatever it + * refers to must stay alive for the instance's lifetime. From Go it is a `cgo.Handle`, + * which is why it is an integer and not a pointer. * * # Safety * * C API function, works with raw object pointers. */ -enum vm_exec2_status_t vm_exec2_new_instance(struct vm_exec2_executor_t *executor, struct vm_exec2_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const struct vm_exec2_compilation_options_t *options, void *go_vm_hooks, char *err_buf, int err_buf_len); +enum vm_exec2_status_t vm_exec2_new_instance(struct vm_exec2_executor_t *executor, struct vm_exec2_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const struct vm_exec2_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); /** * Destroys an instance. Does nothing if `instance` is null. diff --git a/c-api-new/src/capi_instance.rs b/c-api-new/src/capi_instance.rs index 8b6aca12..bdb96eb0 100644 --- a/c-api-new/src/capi_instance.rs +++ b/c-api-new/src/capi_instance.rs @@ -10,7 +10,7 @@ use libc::{c_char, c_int}; use meta::capi_safe_unwind; use multiversx_chain_vm_executor::{BreakpointValue, Instance, InstanceCallResult}; use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalInstance; -use std::ffi::{CStr, c_void}; +use std::ffi::CStr; use std::slice; use crate::capi_config::vm_exec2_compilation_options_t; @@ -82,8 +82,10 @@ impl vm_exec2_call_result_t { /// Creates an instance. /// -/// `go_vm_hooks` is the Go VM hooks object for *this* instance; it is handed back to every -/// VM hook as field 0 of the hook context. It must stay alive for the instance's lifetime. +/// `go_vm_hooks` is the caller's VM hooks handle for *this* instance; it is handed back to +/// every VM hook as field 0 of the hook context, and is otherwise opaque here. Whatever it +/// refers to must stay alive for the instance's lifetime. From Go it is a `cgo.Handle`, +/// which is why it is an integer and not a pointer. /// /// # Safety /// @@ -98,7 +100,7 @@ pub unsafe extern "C" fn vm_exec2_new_instance( wasm_bytes: *const u8, wasm_bytes_len: u32, options: *const vm_exec2_compilation_options_t, - go_vm_hooks: *mut c_void, + go_vm_hooks: usize, err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec2_status_t { diff --git a/c-api-new/src/capi_vm_hook_context.rs b/c-api-new/src/capi_vm_hook_context.rs index 7d10305d..43ae59e3 100644 --- a/c-api-new/src/capi_vm_hook_context.rs +++ b/c-api-new/src/capi_vm_hook_context.rs @@ -106,8 +106,14 @@ impl vm_exec2_hook_result_t { #[allow(non_camel_case_types)] #[repr(C)] pub struct vm_exec2_hook_context_t { - /// The Go VM hooks object for this instance. Read by the Go side, never by Rust. - pub go_vm_hooks: *mut c_void, + /// The caller's VM hooks handle for this instance. Read by the caller, never by Rust. + /// + /// An opaque word rather than a pointer, deliberately: Rust only stores it and hands it + /// back, and the Go side needs it to be an integer. cgo forbids storing a Go pointer in + /// C memory, so what Go puts here is a `runtime/cgo.Handle`, which is a `uintptr` by + /// construction. Typing the field as a pointer would force Go to convert it back with + /// `unsafe.Pointer(uintptr(...))`, exactly the pattern `go vet` rejects. + pub go_vm_hooks: usize, /// Erased `&mut ExperimentalInstanceState<'_>`; null while no call is active. state: *mut c_void, @@ -129,7 +135,7 @@ type ErasedInstanceState = ExperimentalInstanceState<'static>; impl vm_exec2_hook_context_t { fn inactive() -> Self { vm_exec2_hook_context_t { - go_vm_hooks: std::ptr::null_mut(), + go_vm_hooks: 0, state: std::ptr::null_mut(), pool: std::ptr::null(), token: 0, @@ -208,13 +214,13 @@ pub struct CapiVMHookContextPool { /// Builds the VM hooks for one host call, and owns the context lifecycle. pub struct CapiVMHooksBuilder { - go_vm_hooks: *mut c_void, + go_vm_hooks: usize, hook_table: CapiVMHookTable, pool: RefCell, } impl CapiVMHooksBuilder { - pub fn new(go_vm_hooks: *mut c_void, hook_table: CapiVMHookTable) -> Self { + pub fn new(go_vm_hooks: usize, hook_table: CapiVMHookTable) -> Self { CapiVMHooksBuilder { go_vm_hooks, hook_table, diff --git a/c-api-new/tests/ffi_boundary.rs b/c-api-new/tests/ffi_boundary.rs index 46b9bb8a..085b10d9 100644 --- a/c-api-new/tests/ffi_boundary.rs +++ b/c-api-new/tests/ffi_boundary.rs @@ -236,7 +236,7 @@ fn new_executor(err: &mut ErrBuf) -> *mut vm_exec2_executor_t { fn new_instance(executor: *mut vm_exec2_executor_t, err: &mut ErrBuf) -> *mut vm_exec2_instance_t { let options = compilation_options(); let wasm = CONTRACT_WAT.as_bytes(); - let go_vm_hooks = 0xBEEFusize as *mut c_void; + let go_vm_hooks = 0xBEEFusize; let mut instance: *mut vm_exec2_instance_t = std::ptr::null_mut(); let status = unsafe { @@ -723,7 +723,7 @@ fn probe_nesting( fn state_ops() { let outcome = Arc::new(Mutex::new(Outcome::default())); let builder = ProbingHooksBuilder { - inner: CapiVMHooksBuilder::new(0xBEEFusize as *mut c_void, unsafe { + inner: CapiVMHooksBuilder::new(0xBEEFusize, unsafe { CapiVMHookTable::copy_from(mock_hook_table()) }), outcome: outcome.clone(), From 40775c8be210b918b665c1f681a8a67b2e382feb Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 16:10:58 +0300 Subject: [PATCH 10/26] new FFI plan update: bunding --- PLAN-go-ffi.md | 154 +++++++++++++++++++++++++++---------------------- 1 file changed, 85 insertions(+), 69 deletions(-) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 5997be06..39264d03 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -136,7 +136,7 @@ is what flips. | **G1** | Do the hooks hold the state, or take it as a parameter? | The C ABI passes the context as the first argument of every hook — that *is* the parameter form. If the Rust trait keeps the state bundled, the generator must emit and reconcile two shapes. Deciding after the ABI is frozen turns a refactor into a two-repo migration. The Wasmer 6 builder currently returns `Box`, so a per-hook allocation also rides on this. **Phase 2 found this already answered by the committed Wasmer 6 code**: `ExperimentalVMHooksBuilder::with_vm_hooks` hands the builder the state and lends the hooks back through a closure, so the state is *bundled* in Rust and *passed as a parameter* in C — the two are compatible, and the generator emits one shape, not two. The allocation question is also already resolved: the builder lends `&mut dyn VMHooks` on its own stack frame. | | **G2** | Does the new executor keep the Go VM's truncating load? | **Settled in Phase 2: yes, it clamps.** `vm_exec2_state_mem_load` writes into the caller's buffer and returns `min(requested, available)`, so the truncation is visible in the return value. Chosen to preserve behaviour contracts have already observed; the strict load stays available on the trait for a caller that wants it. The rule itself moved into `clamped_load_len` in `vm-executor/src/new_traits/memory.rs`, so the FFI and `InstanceState::memory_load_clamped` cannot drift apart. | | **G4** | Which `OpcodeVersion` does each host declare, and does the config stay mutable? | Decides whether `OpcodeConfig` is `Arc>` (2.2 today) or plain (Wasmer 6 today), which decides the shape of the single config struct. **Phase 2 took the immutable branch for the new C API**: the version and the cost table arrive in `vm_exec2_executor_config_t` at `vm_exec2_new_executor` and are never mutated, so there is no `Arc>` on this path and no `set_opcode_config`. The gate stays open for Phase 1.6, which has to reconcile the 2.2 backend's `Arc>`. | -| **G5** | Does one C library carry both backends, selected by a flag? | Decides whether `c-api-new` depends on both backend crates, whether the Go VM links one library or two, and whether the Phase 5 differential run is a runtime flag or a link-time choice. **Phase 4a cleared the part everyone expected to be the problem and found a different one.** Both runtimes' trap handlers coexist and chain, in both initialisation orders (`wasmer3/dualbackend/`). But `wasmer-vm` 2.2 and 6 export the same 32 `#[no_mangle]` `wasmer_vm_*` libcall symbols, which collide, which the Apple linker reports as a *warning* while building the library anyway. The gate now rests on hiding every non-`vm_exec2_*` symbol and confirming it on Linux. See "Serving both backends" below. | +| **G5** | Does one C library carry both backends, selected by a flag? | **Answered: no — one library per backend, link the one you need.** That is already the pattern: a node links four wasmer runtimes today (v1.2/v1.3/v1.4 share one byte-identical `libwasmer`, v1.5 brings `libvmexeccapi`) with zero shared symbols between them. Bundling would make the 32 duplicate `wasmer_vm_*` libcalls a *static* collision the linker silently collapses, breaking one backend's `vmctx` layout; two libraries make it a *dynamic* one that `-Bsymbolic-functions` fixes. Duplication is near zero: one Rust crate with a backend feature, one Go bridge with a build tag. What remains is **verifying `-Bsymbolic-functions` on Linux, before anything links both** — see "Serving both backends" below. | ## Phase 1 — Close the trait gaps @@ -352,65 +352,73 @@ instance — but it satisfies the stricter contract trivially. Writing the Go si tighter contract therefore costs nothing on 2.2 and makes it impossible for the VM to accidentally depend on a lifetime only 2.2 offers. -### One library, a backend flag - -The target is a single `libvmexeccapi2` carrying both backends, chosen by a field in -`vm_exec2_executor_config_t` — configured once, at creation, like everything else in this API. - -What it buys: - -- **The Phase 5 differential run becomes a runtime flag**, in one process, over one harness, - rather than two builds linked against different libraries. That is the acceptance criterion - for the whole effort, so making it cheap matters more than it looks. -- One FFI surface, one header, one shipped artifact matrix. The platform matrix is already - four libraries; doubling it is worse than doubling one library's size. - -What it costs: roughly 5-6 MB (the current wasmer 2.2 library) on top of ~8 MB (wasmer 6), -which is unremarkable for a shipped node dependency. - -Two things had to be settled before committing to it. Phase 4a settled the first and -replaced the second with something sharper. - -**Signal handlers: answered, and the answer is yes.** `wasmer3/dualbackend/` links both -libraries into one test binary and traps on purpose in each backend, for both trap kinds that -take different routes to the handler — an illegal instruction (`unreachable`) and an -out-of-range load — in both initialisation orders. All four cases come back as a reported -runtime error, and both backends keep working afterwards. The order that mattered most is -wasmer 2.2 second, because `wasmer2.CreateExecutor` calls `vm_force_sighandler_reinstall` -unconditionally and is therefore the one that can overwrite a handler wasmer 6 has already -installed; it chains correctly. Caveat on the evidence: this was run on darwin/arm64 only, and -it is the two-*library* case, not the bundled one. - -**Symbol collision is the real blocker, and it is not about signal handlers.** Probing the -bundled case — adding the 2.2 backend as a dependency of `c-api-new` and linking — produced 32 -`ld: duplicate symbol` reports, all of the form `_wasmer_vm_f32_ceil`, -`_wasmer_vm_memory32_grow`, `_wasmer_vm_table_init`: the libcalls that JIT-compiled wasm code -calls back into. `wasmer-vm` declares them `#[no_mangle]`, so both versions export the same -names, and both resolve a libcall address the same way, by taking the address of their own -definition (`libcalls.rs`: `LibCall::CeilF32 => wasmer_vm_f32_ceil as usize`). Two things make -this dangerous rather than cosmetic: - -- The Apple linker reported the duplicates as a **warning on the linker's stderr and built the - library anyway**, silently keeping one definition. Nothing fails; the problem would surface - as a wrong answer. -- Whether each backend then gets *its own* address depends on the platform. Under macOS's - two-level namespace the local definition wins, which is why the two-library experiment above - passed. Under ELF's flat namespace a `#[no_mangle]` symbol in a shared object is - preemptible, so the address goes through the GOT and resolves to whichever definition the - dynamic linker picked first — for one of the two backends, the *other* one's libcalls, called - with its own incompatible `VMContext`. That is silent memory corruption, on the platform the - node actually runs on. - -There is a known mitigation, and it is one this library wants regardless: **export nothing but -the `vm_exec2_*` surface.** Hiding the rest (a version script on ELF, `-exported_symbols_list` -on Mach-O, or `-C default-symbol-visibility=hidden`) makes the `wasmer_vm_*` symbols local and -non-preemptible, so each backend binds its own. Until that is in place and verified on Linux, -the constraint stands: **never link `libvmexeccapi` and `libvmexeccapi2` into one binary**, and -note that the reason is the shared `wasmer_vm_*` libcall names, not two copies of wasmer 2.2 — -today's `libvmexeccapi2` contains only wasmer 6. - -So G5 is not closed, but what remains is a link-visibility question with a standard answer, -not an open question about whether two runtimes can coexist. +### Two libraries, not one — G5 answered "no" + +The original target was a single `libvmexeccapi2` carrying both backends, chosen by a field in +`vm_exec2_executor_config_t`. **That is dropped.** What replaces it is one library per backend, +with the node linking the one it needs — which is not a compromise but the pattern this +codebase has always used. + +**The established pattern.** A production node already links *four* wasmer runtimes: +`mx-chain-vm-v1_2-go`, `-v1_3-go` and `-v1_4-go` each ship `wasmer/libwasmer_*`, and +`mx-chain-vm-go v1.5` ships `wasmer2/libvmexeccapi*`. The three older copies are +byte-identical (same sha256, same size), and the old `libwasmer` and `libvmexeccapi` share +**zero** symbol names. So the invariant that has quietly held is not "one runtime per process" +— it is **no two *different* runtimes with colliding symbol names**. + +**Why one library is the worse option.** Bundling makes the collision a *static* one: both +`wasmer-vm` versions define the same 32 `#[no_mangle]` libcalls, the linker collapses them to +one definition, and the context-taking ones then reinterpret `vmctx` under the wrong layout. +Fixing that means renaming symbols in one version's rlib or patching `wasmer-vm`. Two +libraries make it a *dynamic* one, which is a link flag — see below. + +**What the duplication actually costs: almost nothing.** `c-api-new` touches its backend in +**three type names across two of thirteen files** — `ExperimentalInstance` in +`capi_instance.rs`, `ExperimentalInstanceState` and `ExperimentalVMHooksBuilder` in +`capi_vm_hook_context.rs`. Status codes, strings, config, opcode cost, the hook context +machinery and the 908-line generated hooks implementation are all backend-agnostic. So the +shape is **one crate with a Cargo feature selecting a backend alias module, built twice**, not +two crates. The one snag is that the aliases are not drop-in: `ExperimentalInstanceState<'a>` +is a borrow of the host-call frame while `WasmerProdInstanceState` is owned, so the lifetime +shapes have to agree first — which is Phase 1.6's job regardless. + +Likewise on the Go side: if both libraries keep the same `vm_exec2_*` ABI, there is **one** +bridge, with a build tag choosing between the `LDFLAGS` lines, and the 3,631-line generated +cgo file stays single-copy. Two Go bridges would only be needed if the libraries were +deliberately given different symbol prefixes, and there is no reason to. + +### The symbol problem does not go away, and it is on the Phase 5 path + +Two libraries does *not* dodge the collision, because the deployment that matters links both. +The migration shape here is the v1.2–v1.4 one: ship the new VM module version alongside the +old, so a node syncing from genesis can re-execute historical blocks under the backend that +produced them. At that point the node links `libvmexeccapi` (wasmer 2.2) **and** +`libvmexeccapi2` (wasmer 6) — and those two shipped libraries have **32 overlapping exported +symbol names, every one of them `wasmer_vm_*`** (measured directly: +`llvm-nm --dynamic` on the shipped `.so`, `nm -gU` on the new dylib). Under ELF's flat +namespace either library's libcall references can bind to the other's definition. macOS's +two-level namespace is why `wasmer3/dualbackend/` passes locally and tells us nothing about +Linux. + +The candidate fix is **`-Wl,-Bsymbolic-functions` on both libraries**: it binds each library's +intra-library references at link time, so each backend reaches its own libcalls regardless of +what else is loaded. It composes with the version script rustc already passes, unlike an +export list (see above). **It needs a Linux verification, and that verification belongs before +anything links both** — which means before the Phase 5 differential harness, not after. + +### The second new-FFI library may not be needed at all + +Worth stating plainly, because it removes work from the critical path. The reason to want +wasmer 2.2 behind the *new* FFI is to delete the legacy C API. It is **not** needed for the +differential run: after Phase 4b, `wasmer2` presents the new Go interfaces over the legacy +`libvmexeccapi`, because 2.2 hands out an instance-lifetime state and a Go-side +`InstanceState` for it is a thin wrapper over the existing instance — which is what Phase 4b +item 1 already says. So the differential run is `wasmer2` against `wasmer3`, both speaking the +new Go interfaces, with no second Rust artifact. + +That makes **Phase 1.6 and the `VMHooksLegacyAdapter` activation trick cleanup rather than +migration**: worth doing when the legacy interface set is retired, not before wasmer 6 is live. +The `-Bsymbolic-functions` work is what takes its place on the critical path. ### What the wasmer 2.2 path needs @@ -626,14 +634,22 @@ and the C ABI is parameter-form regardless, so the trait convergence was not a p after all — it is now better informed by Phase 2, which found the per-instance hooks argument the converged trait needs. -Remaining path: **Phase 4b → Phase 5**. +Remaining path: **Phase 4b → Phase 5**, with the `-Bsymbolic-functions` verification +alongside 4b rather than after it. Phase 4a was independent of everything else and answered the two questions that gated the -rest. The struct-return ABI works end to end. Both backends share a process, and what is left -of G5 is a link-visibility question rather than a runtime one. Phase 4b never depended on G5 -for the interface *shape* — that was already determined by the Rust side — only for how the -backend gets chosen, so it is unblocked either way. - -**Phase 1.6/1.7 is now on the critical path rather than beside it.** Serving wasmer 2.2 through -the new FFI needs the two `RuntimeRef` traits converged, and Phase 4b needs the hooks provider -to take a per-instance handle. G4 has to be settled for that, and G1 is already answered. +rest. The struct-return ABI works end to end, and the backends coexist. Phase 4b never +depended on G5 for the interface *shape* — that was already determined by the Rust side — only +for how the backend gets chosen, so G5's answer does not change it. + +**Phase 1.7 is on the critical path; Phase 1.6 came off it.** Phase 4b needs the hooks +provider to take a per-instance handle, which is 1.7, and G4 has to be settled for that. Phase +1.6 — converging the two `RuntimeRef` traits — was only needed in order to serve wasmer 2.2 +through the *new* FFI, and G5's answer removed that need: the differential run uses `wasmer2` +over the legacy library, presenting the new Go interfaces. 1.6 is now cleanup for when the +legacy interface set is retired. + +**What took its place:** verifying `-Wl,-Bsymbolic-functions` on Linux. Any binary linking both +`libvmexeccapi` and `libvmexeccapi2` — the Phase 5 harness, and later a node that keeps the old +VM module for historical re-execution — has 32 colliding `wasmer_vm_*` symbols between them. +This is the one item that has to land before Phase 5 runs anywhere but macOS. From e570bb0163fb33e894ae0ce45b64caefe31439e2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 16:22:51 +0300 Subject: [PATCH 11/26] new FFI plan update: opcode cost table --- PLAN-go-ffi.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 39264d03..adcb10ea 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -461,7 +461,7 @@ refactor. | `wasmer3InstanceState.go` | The inside view: the six state operations, over a hook context | | `vmHooksProvider.go` | `VMHooksProvider`, and the per-instance handle it is reached through | | `wasmer3ImportsCgoHelper.go` | What every generated shim calls on entry and on failure | -| `opcodeCost.go` | Generated; new writer, see below | +| *(no `opcodeCost.go`)* | The cost table moved to `executor`; see below | | `bridge3_test.go` | 20 tests (23 with subtests), the Phase 4a acceptance | | `dualbackend/` | The G5 experiment, behind a build tag | @@ -532,10 +532,28 @@ Two things were decided here that the plan had left open: returns, never calling back into Go, so the buffer's live range contains no re-entry. A buffer per operation would be an allocation on every memory access a contract makes. -Also added: a `WriteWasmer3OpcodeCost` generator writer, so `wasmer3/opcodeCost.go` comes from -the same opcode list as its C counterpart rather than being hand-copied. It emits a -package-level function instead of wasmer2's method on the executor, because this executor takes -its configuration at construction and there is no executor to hang it off of. +**The opcode cost table became shared rather than per-backend.** It started as a second +generated copy for `wasmer3`, mirroring `wasmer2/opcodeCost.go`; that was then recognised as +duplication with no cause. The opcodes a cost table covers are a property of the gas schedule +and of the metering middleware, not of the runtime underneath, which is exactly why both C +headers already declared an identical `vm_exec_opcode_cost_t`. So: + +- `executor/opcodeCost.go` now holds `OpcodeCost` and `ExtractOpcodeCost`, generated once; + `wasmer2/opcodeCost.go` and `wasmer3/opcodeCost.go` are deleted. +- The two generator writers merged into one `WriteExecutorOpcodeCost`. The pre-existing writer + for the *full* WASM set became `WriteExecutorWASMOpcodeCost`, so each writer is now named + after the type it emits (`OpcodeCost` is the metered subset; `WASMOpcodeCost` is the whole + set as the gas schedule supplies it). +- `ExtractOpcodeCost` is a package-level pure function rather than wasmer2's former method on + the executor, since nothing in the conversion depends on an executor. +- **Each backend keeps its own size tripwire**, and they now carry more weight than before: one + shared Go struct has to match *two* independently generated C headers, and the backends + reinterpret one as the other to cross the boundary. Both assertions were confirmed to fire by + adding a field and watching each package fail to compile. wasmer2 previously had no such + check at all. + +Verified by the full `vmhost/hosttest` contract-execution suite, which exercises wasmer2's +metering through the moved struct. Not done here, deliberately: the `reset`/cache entry points (Phase 6), and the three non-darwin platform libraries. **The missing libraries have a CI consequence**: `wasmer3` now From 40d9ea1015e21aae8ecb801705f485448c632a10 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 17:53:42 +0300 Subject: [PATCH 12/26] new FFI plan update: converge interfaces --- PLAN-go-ffi.md | 136 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 95 insertions(+), 41 deletions(-) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index adcb10ea..bffc71d1 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -2,7 +2,7 @@ Working plan for exposing `vm-executor/src/new_traits` over a C FFI and consuming it from the Go VM. Status as of 2026-10-02: Phase 1 items 1-5, Phase 2, Phase 3 and Phase 4a are -implemented. +implemented; Phase 4b is in progress. ## Why @@ -135,7 +135,7 @@ is what flips. | --- | --- | --- | | **G1** | Do the hooks hold the state, or take it as a parameter? | The C ABI passes the context as the first argument of every hook — that *is* the parameter form. If the Rust trait keeps the state bundled, the generator must emit and reconcile two shapes. Deciding after the ABI is frozen turns a refactor into a two-repo migration. The Wasmer 6 builder currently returns `Box`, so a per-hook allocation also rides on this. **Phase 2 found this already answered by the committed Wasmer 6 code**: `ExperimentalVMHooksBuilder::with_vm_hooks` hands the builder the state and lends the hooks back through a closure, so the state is *bundled* in Rust and *passed as a parameter* in C — the two are compatible, and the generator emits one shape, not two. The allocation question is also already resolved: the builder lends `&mut dyn VMHooks` on its own stack frame. | | **G2** | Does the new executor keep the Go VM's truncating load? | **Settled in Phase 2: yes, it clamps.** `vm_exec2_state_mem_load` writes into the caller's buffer and returns `min(requested, available)`, so the truncation is visible in the return value. Chosen to preserve behaviour contracts have already observed; the strict load stays available on the trait for a caller that wants it. The rule itself moved into `clamped_load_len` in `vm-executor/src/new_traits/memory.rs`, so the FFI and `InstanceState::memory_load_clamped` cannot drift apart. | -| **G4** | Which `OpcodeVersion` does each host declare, and does the config stay mutable? | Decides whether `OpcodeConfig` is `Arc>` (2.2 today) or plain (Wasmer 6 today), which decides the shape of the single config struct. **Phase 2 took the immutable branch for the new C API**: the version and the cost table arrive in `vm_exec2_executor_config_t` at `vm_exec2_new_executor` and are never mutated, so there is no `Arc>` on this path and no `set_opcode_config`. The gate stays open for Phase 1.6, which has to reconcile the 2.2 backend's `Arc>`. | +| **G4** | Which `OpcodeVersion` does each host declare, and does the config stay mutable? | **Resolved in Phase 4b: immutable, with the executor rebuilt on change.** Phase 2 had already taken the immutable branch for the new C API — version and costs arrive in `vm_exec2_executor_config_t`, with no `set_opcode_config`. The obstacle was `GasScheduleChange` (`vmhost/hostCore/host.go:375`), which re-applies both to the *live* executor at an epoch. It is reconcilable because that function already calls `ClearWarmInstanceCache()`, so no instance survives the change: recreating the executor is behaviour-preserving, and is what lets `SetOpcodeConfig` leave the `Executor` interface. Phase 1.6 still has to reconcile the 2.2 backend's `Arc>` if that path is ever built. | | **G5** | Does one C library carry both backends, selected by a flag? | **Answered: no — one library per backend, link the one you need.** That is already the pattern: a node links four wasmer runtimes today (v1.2/v1.3/v1.4 share one byte-identical `libwasmer`, v1.5 brings `libvmexeccapi`) with zero shared symbols between them. Bundling would make the 32 duplicate `wasmer_vm_*` libcalls a *static* collision the linker silently collapses, breaking one backend's `vmctx` layout; two libraries make it a *dynamic* one that `-Bsymbolic-functions` fixes. Duplication is near zero: one Rust crate with a backend feature, one Go bridge with a build tag. What remains is **verifying `-Bsymbolic-functions` on Linux, before anything links both** — see "Serving both backends" below. | @@ -562,50 +562,104 @@ is absent. `go build ./...` is unaffected, because a non-main package is never l the four libraries land before the package is in the default test path, or `wasmer3` goes behind a build tag until they do. -### Phase 4b — Converge the Go executor interfaces +### Phase 4b — Converge the Go executor interfaces — **in progress** The Go side still speaks the legacy interface set, and this is where it stops. It is the Go mirror of the split the Rust side already made, and it is the bulk of Phase 4. The asymmetry to close: `executor.Instance` has 24 methods, against 8 on the new Rust -`Instance` plus the 8 of an `InstanceState` the Go side does not have at all. The consumption is -concentrated — almost all of it is `vmhost/contexts/runtime.go` and `instanceTracker.go`, and -only three implementors matter (`wasmer2.Wasmer2Instance`, `mock.InstanceMock`, -`executor/wrapper.WrapperInstance`) — so the change is wide but shallow. - -1. Add `executor.InstanceState`: `MemLoad`, `MemStore`, `MemLength`, `MemGrow`, - `GetPointsUsed`, `SetPointsUsed`. Implemented for wasmer3 by a struct wrapping the hook - context; implemented for wasmer2 by delegating to its existing instance. -2. Shrink `executor.Instance` to the outside view, and **push the legacy-only methods down - into wasmer2's own types** rather than keeping them on the shared interface: - `SetBreakpointValue`/`GetBreakpointValue` (the new executor reports breakpoints in its call - result — see `PLAN-go-breakpoint-removal.md`), `SetVMHooksPtr`/`GetVMHooksPtr`, - `SetGasLimit`, `MemDump`, `HasMemory`. The census shows several of these have no caller in - the VM at all. -3. `CallFunction` returns an `InstanceCallResult` rather than a bare `error`, which is the - hard prerequisite for the breakpoint removal and is described there. -4. `executor.VMHooks` becomes a per-instance provider, since that is where the Go side keeps - the early-exit slot the shims read. Phase 2 found that the Rust `Executor` trait has - nowhere to put a per-instance host handle; **Phase 1.6 should add it**, at which point the - capi can go back through the trait and serve both backends uniformly. -5. The contract validator switches to imported-function *names*, which the FFI already - exposes as a two-step pair. -6. `MemLoadFromMemory` / `MemStoreToMemory` are bypassed for wasmer3 — bounds checking lives - in Rust — and kept for wasmer2. -7. `SetOpcodeConfig` as a post-construction mutator goes away; config is passed once at - creation. Note Go is already stale here: `wasmer2/bridge2.go` calls - `vm_exec_set_opcode_costs` against a header that now exports `vm_exec_set_opcode_config`. - -Both executors then coexist behind `ExecutorAbstractFactory` speaking the *same* interfaces, -which is what makes the Phase 5 differential run a matter of choosing a factory — or, if G5 -lands as expected, a flag. - -**Why not converge before 4a?** It would mean reshaping a 24-method interface, its three -implementors and the mocks against an FFI nobody has executed yet. **Why not defer it past -Phase 4?** Because then `wasmer3` has to implement the legacy 24 methods, including the -breakpoint accessors the new executor deliberately does not have and the memory accessors that -moved to `InstanceState` — an adapter whose only purpose is to fake the interface being -deleted. Phase 4a is the cheap half and it de-risks the expensive one. +`Instance` plus the 8 of an `InstanceState` the Go side does not have at all. A census of the +call sites confirmed the shape is wide but shallow — almost all consumption is +`vmhost/contexts/runtime.go` and `instanceTracker.go`, and only three implementors matter +(`wasmer2.Wasmer2Instance`, `mock.InstanceMock`, `executor/wrapper.WrapperInstance`). + +#### Done: `executor.InstanceState` and the hooks provider + +The backbone, and it turned out to be far smaller than the method counts suggest. Two +measurements decided the shape: + +- **`GetInstance()` has 8 call sites in the whole repo**, two of which are the memory seam. +- **The `vmhooks` package needs exactly two things from an instance**: `MemLoad` and + `MemStore`, both funnelled through `VMHooksImpl` in `vmhost/vmhooks/vmHooksImpl.go`. Every + one of the 277 hook bodies goes through `context.MemLoad` / `context.MemStore` / + `context.MemLoadMultiple`, so re-pointing two one-line method bodies moves all of them. + +So `executor.InstanceState` is **two methods, not six**. Gas accounting and memory growth were +candidates on symmetry with the Rust trait, but their Go callers (`contexts/metering.go`, +`contexts/runtime.go`) sit outside a host call as often as inside one, and `MemLength`, +`MemGrow` and `MemDump` are marked "only called directly in tests" in wasmer2 itself. Inventing +state surface for symmetry would have been surface nothing uses. + +`executor.VMHooksProvider` has **two** methods, and the asymmetry is deliberate: + +- `VMHooksForCall(state)` — Wasmer 6, which hands each host call a context and nothing longer. +- `VMHooksForInstanceLifetime()` — Wasmer 2.2, whose legacy C API sets the hooks pointer + **per executor, not per instance** (`vm_exec_executor_set_vm_hooks_ptr`; there is no + instance-level equivalent, which is also why `SetVMHooksPtr` on the Go interface never had a + caller). A hook therefore cannot be told which instance called it, so the provider resolves + that through the host's instance stack — which is what the VM has always done. + +This is the one place the Go side cannot mirror Rust, where 2.2's per-instance builder holds +the instance weakly. It is forced by the legacy C API, not chosen, and naming both halves keeps +it visible instead of hidden behind a nil convention. + +`VMHooksImpl` now takes its state at construction. `NewVMHooksImpl(host)` keeps its signature +and supplies `hostInstanceState`, so the ten-odd test call sites were untouched; +`NewVMHooksImplForCall(host, state)` is the call-scoped form. Verified by `vmhost/hosttest` and +`vmhost/vmhookstest`, and checked for non-vacuity by breaking `hostInstanceState.MemLoad` and +watching `vmhookstest` fail. + +#### Done: the validator's imported-function check + +Plan item 5 said the validator should switch to imported-function *names*. The census says +otherwise, and the better answer is the reverse: **the VM keeps asking `IsFunctionImported`, +and the backend answers from a cached set.** + +The validator checks a 21-name blocklist (`mapNewCryptoAPI` 6, `mapBarnardOpcodes` 15) against +every contract. The legacy C API can only answer one name per call, so that is 21 cgo calls per +validation; the new header is the only one with `imported_function_names` at all. So +`Wasmer3Instance` reads the list once at creation — two cgo calls — and answers from a map. +No interface change, identical semantics, and the consensus-relevant check stays exactly as +written. + +#### Still to do + +1. Shrink `executor.Instance` to the outside view, pushing the legacy-only methods down into + wasmer2's own types: `SetBreakpointValue`/`GetBreakpointValue` (the new executor reports + breakpoints in its call result), `SetVMHooksPtr`/`GetVMHooksPtr` (no VM caller at all), + `SetGasLimit`, `MemDump`, `HasMemory`. +2. `CallFunction` returns an `InstanceCallResult` rather than a bare `error` — the hard + prerequisite for `PLAN-go-breakpoint-removal.md`. +3. `MemLoadFromMemory` / `MemStoreToMemory` bypassed for wasmer3, where bounds checking lives + in Rust, and kept for wasmer2. +4. `SetOpcodeConfig` removal — see the G4 resolution below. +5. `wasmer3` implements `executor.Executor` and `executor.Instance`, behind + `ExecutorAbstractFactory`. + +#### G4 resolved: the config can be immutable, if the executor is rebuilt + +The gate asked whether `OpcodeConfig` stays mutable. The new C API took the immutable branch in +Phase 2 — version and costs arrive in `vm_exec2_executor_config_t` and there is no +`set_opcode_config`. The obstacle was whether the Go VM needs to mutate it, and it does: +`vmhost/hostCore/host.go:375`, in `GasScheduleChange`, re-applies the opcode version and costs +to the **live** executor when the gas schedule changes at an epoch. + +That is reconcilable without making the config mutable, because `GasScheduleChange` already +does the hard part: it calls `ClearWarmInstanceCache()`, so no instance survives the change +anyway. **Recreating the executor rather than mutating it is therefore behaviour-preserving**, +and it is what lets `SetOpcodeConfig` leave the `Executor` interface. The cost is that +`RuntimeContext` needs a way to replace its executor, which is what item 4 above covers. + +#### An open gap worth recording: `SetPointsUsed` from outside a host call + +The new FFI exposes `vm_exec2_instance_get_points_used` on the outer instance but **no +`set_points_used`** — writing gas is only reachable through a hook context. The VM does write +it from outside a call, at `vmhost/contexts/runtime.go:311`, where reusing a warm instance +resets its counter to zero. + +For now this is harmless, because warm instances are deferred to Phase 6 for wasmer3. It stops +being harmless the moment they are not, and the fix is a Rust-side addition rather than a Go +workaround. Recorded here so Phase 6 does not rediscover it. ## Phase 5 — Bring-up and validation From 5bae9fe5880576bbcc71ff2fab8556e0f9bd1e33 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 2 Oct 2026 19:33:08 +0300 Subject: [PATCH 13/26] c-api rename: c-api-legacy and c-api-wasmer6 --- Cargo.toml | 4 +- Makefile | 54 +- PLAN-go-ffi.md | 151 +++- {c-api => c-api-legacy}/Cargo.toml | 2 +- {c-api => c-api-legacy}/build.rs | 0 {c-api => c-api-legacy}/libvmexeccapi.h | 0 {c-api => c-api-legacy}/src/basic_types.rs | 0 .../src/capi_breakpoints.rs | 0 .../src/capi_compilation_options.rs | 0 {c-api => c-api-legacy}/src/capi_error.rs | 0 {c-api => c-api-legacy}/src/capi_executor.rs | 0 {c-api => c-api-legacy}/src/capi_instance.rs | 0 .../src/capi_instance_cache.rs | 0 {c-api => c-api-legacy}/src/capi_logger.rs | 0 {c-api => c-api-legacy}/src/capi_memory.rs | 0 {c-api => c-api-legacy}/src/capi_metering.rs | 0 .../src/capi_opcode_cost.rs | 0 .../src/capi_vm_hook_pointers.rs | 0 {c-api => c-api-legacy}/src/capi_vm_hooks.rs | 0 {c-api => c-api-legacy}/src/lib.rs | 0 {c-api => c-api-legacy}/src/macros.rs | 0 .../src/service_singleton.rs | 0 {c-api => c-api-legacy}/src/wasmer_logger.rs | 0 c-api-new/libvmexeccapi_new.h | 832 ------------------ c-api-new/src/capi_vm_hook_pointers.rs | 300 ------- {c-api-new => c-api-wasmer6}/Cargo.toml | 2 +- {c-api-new => c-api-wasmer6}/build.rs | 2 +- {c-api-new => c-api-wasmer6}/cbindgen.toml | 4 +- c-api-wasmer6/libvmexeccapi_w6.h | 832 ++++++++++++++++++ .../src/capi_config.rs | 8 +- .../src/capi_executor.rs | 34 +- .../src/capi_instance.rs | 158 ++-- .../src/capi_instance_state.rs | 76 +- .../src/capi_logger.rs | 2 +- .../src/capi_opcode_cost.rs | 0 .../src/capi_status.rs | 42 +- .../src/capi_strings.rs | 0 .../src/capi_vm_hook_context.rs | 58 +- c-api-wasmer6/src/capi_vm_hook_pointers.rs | 300 +++++++ .../src/capi_vm_hook_table.rs | 8 +- .../src/capi_vm_hooks.rs | 18 +- {c-api-new => c-api-wasmer6}/src/lib.rs | 4 +- .../tests/ffi_boundary.rs | 224 ++--- 43 files changed, 1632 insertions(+), 1483 deletions(-) rename {c-api => c-api-legacy}/Cargo.toml (89%) rename {c-api => c-api-legacy}/build.rs (100%) rename {c-api => c-api-legacy}/libvmexeccapi.h (100%) rename {c-api => c-api-legacy}/src/basic_types.rs (100%) rename {c-api => c-api-legacy}/src/capi_breakpoints.rs (100%) rename {c-api => c-api-legacy}/src/capi_compilation_options.rs (100%) rename {c-api => c-api-legacy}/src/capi_error.rs (100%) rename {c-api => c-api-legacy}/src/capi_executor.rs (100%) rename {c-api => c-api-legacy}/src/capi_instance.rs (100%) rename {c-api => c-api-legacy}/src/capi_instance_cache.rs (100%) rename {c-api => c-api-legacy}/src/capi_logger.rs (100%) rename {c-api => c-api-legacy}/src/capi_memory.rs (100%) rename {c-api => c-api-legacy}/src/capi_metering.rs (100%) rename {c-api-new => c-api-legacy}/src/capi_opcode_cost.rs (100%) rename {c-api => c-api-legacy}/src/capi_vm_hook_pointers.rs (100%) rename {c-api => c-api-legacy}/src/capi_vm_hooks.rs (100%) rename {c-api => c-api-legacy}/src/lib.rs (100%) rename {c-api => c-api-legacy}/src/macros.rs (100%) rename {c-api => c-api-legacy}/src/service_singleton.rs (100%) rename {c-api => c-api-legacy}/src/wasmer_logger.rs (100%) delete mode 100644 c-api-new/libvmexeccapi_new.h delete mode 100644 c-api-new/src/capi_vm_hook_pointers.rs rename {c-api-new => c-api-wasmer6}/Cargo.toml (91%) rename {c-api-new => c-api-wasmer6}/build.rs (96%) rename {c-api-new => c-api-wasmer6}/cbindgen.toml (57%) create mode 100644 c-api-wasmer6/libvmexeccapi_w6.h rename {c-api-new => c-api-wasmer6}/src/capi_config.rs (93%) rename {c-api-new => c-api-wasmer6}/src/capi_executor.rs (74%) rename {c-api-new => c-api-wasmer6}/src/capi_instance.rs (66%) rename {c-api-new => c-api-wasmer6}/src/capi_instance_state.rs (74%) rename {c-api-new => c-api-wasmer6}/src/capi_logger.rs (88%) rename {c-api => c-api-wasmer6}/src/capi_opcode_cost.rs (100%) rename {c-api-new => c-api-wasmer6}/src/capi_status.rs (82%) rename {c-api-new => c-api-wasmer6}/src/capi_strings.rs (100%) rename {c-api-new => c-api-wasmer6}/src/capi_vm_hook_context.rs (88%) create mode 100644 c-api-wasmer6/src/capi_vm_hook_pointers.rs rename {c-api-new => c-api-wasmer6}/src/capi_vm_hook_table.rs (82%) rename {c-api-new => c-api-wasmer6}/src/capi_vm_hooks.rs (99%) rename {c-api-new => c-api-wasmer6}/src/lib.rs (90%) rename {c-api-new => c-api-wasmer6}/tests/ffi_boundary.rs (72%) diff --git a/Cargo.toml b/Cargo.toml index 7a2f9704..6259bf47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,8 @@ resolver = "2" members = [ "meta", - "c-api", - "c-api-new", + "c-api-legacy", + "c-api-wasmer6", "vm-executor", "vm-executor-experimental", "vm-executor-wasmer", diff --git a/Makefile b/Makefile index f88fea44..1d56cba2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: capi capi-linux-amd64 capi-linux-arm capi-osx-amd64 capi-osx-arm \ capi-osx-amd64-cross capi-linux-amd64-docker capi-linux-arm64-docker \ - capi2 capi2-linux-amd64 capi2-linux-arm capi2-osx-amd64 capi2-osx-arm \ + capi-w6 capi-w6-linux-amd64 capi-w6-linux-arm capi-w6-osx-amd64 capi-w6-osx-arm \ ci-local clean check-macos # cargo and docker already parallelise internally, and a parallel make would @@ -14,14 +14,14 @@ CI_LOCAL_DIR ?= target/ci-local UNAME_S := $(shell uname -s) capi: - cargo build -p multiversx-chain-vm-executor-c-api --release + cargo build -p multiversx-chain-vm-executor-c-api-legacy --release capi-linux-amd64: capi - mv target/release/libmultiversx_chain_vm_executor_c_api.so target/release/libvmexeccapi.so + mv target/release/libmultiversx_chain_vm_executor_c_api_legacy.so target/release/libvmexeccapi.so patchelf --set-soname libvmexeccapi.so target/release/libvmexeccapi.so capi-linux-arm: capi - mv target/release/libmultiversx_chain_vm_executor_c_api.so target/release/libvmexeccapi_arm.so + mv target/release/libmultiversx_chain_vm_executor_c_api_legacy.so target/release/libvmexeccapi_arm.so patchelf --set-soname libvmexeccapi_arm.so target/release/libvmexeccapi_arm.so # A prerequisite, not an inline check in the recipe: under .NOTPARALLEL, make @@ -32,11 +32,11 @@ check-macos: @test "$(UNAME_S)" = "Darwin" || { echo "This target requires a macOS host (produces a Mach-O .dylib)." >&2; exit 1; } capi-osx-amd64: check-macos capi - mv target/release/libmultiversx_chain_vm_executor_c_api.dylib target/release/libvmexeccapi.dylib + mv target/release/libmultiversx_chain_vm_executor_c_api_legacy.dylib target/release/libvmexeccapi.dylib install_name_tool -id @rpath/libvmexeccapi.dylib target/release/libvmexeccapi.dylib capi-osx-arm: check-macos capi - mv target/release/libmultiversx_chain_vm_executor_c_api.dylib target/release/libvmexeccapi_arm.dylib + mv target/release/libmultiversx_chain_vm_executor_c_api_legacy.dylib target/release/libvmexeccapi_arm.dylib install_name_tool -id @rpath/libvmexeccapi_arm.dylib target/release/libvmexeccapi_arm.dylib # --- The new C API ------------------------------------------------------------ @@ -49,24 +49,24 @@ capi-osx-arm: check-macos capi # VM starts consuming it, so the release assets do not grow a library nothing # links against. -capi2: - cargo build -p multiversx-chain-vm-executor-c-api-new --release +capi-w6: + cargo build -p multiversx-chain-vm-executor-c-api-wasmer6 --release -capi2-linux-amd64: capi2 - mv target/release/libmultiversx_chain_vm_executor_c_api_new.so target/release/libvmexeccapi2.so - patchelf --set-soname libvmexeccapi2.so target/release/libvmexeccapi2.so +capi-w6-linux-amd64: capi-w6 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer6.so target/release/libvmexeccapi_w6.so + patchelf --set-soname libvmexeccapi_w6.so target/release/libvmexeccapi_w6.so -capi2-linux-arm: capi2 - mv target/release/libmultiversx_chain_vm_executor_c_api_new.so target/release/libvmexeccapi2_arm.so - patchelf --set-soname libvmexeccapi2_arm.so target/release/libvmexeccapi2_arm.so +capi-w6-linux-arm: capi-w6 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer6.so target/release/libvmexeccapi_w6_arm.so + patchelf --set-soname libvmexeccapi_w6_arm.so target/release/libvmexeccapi_w6_arm.so -capi2-osx-amd64: check-macos capi2 - mv target/release/libmultiversx_chain_vm_executor_c_api_new.dylib target/release/libvmexeccapi2.dylib - install_name_tool -id @rpath/libvmexeccapi2.dylib target/release/libvmexeccapi2.dylib +capi-w6-osx-amd64: check-macos capi-w6 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer6.dylib target/release/libvmexeccapi_w6.dylib + install_name_tool -id @rpath/libvmexeccapi_w6.dylib target/release/libvmexeccapi_w6.dylib -capi2-osx-arm: check-macos capi2 - mv target/release/libmultiversx_chain_vm_executor_c_api_new.dylib target/release/libvmexeccapi2_arm.dylib - install_name_tool -id @rpath/libvmexeccapi2_arm.dylib target/release/libvmexeccapi2_arm.dylib +capi-w6-osx-arm: check-macos capi-w6 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer6.dylib target/release/libvmexeccapi_w6_arm.dylib + install_name_tool -id @rpath/libvmexeccapi_w6_arm.dylib target/release/libvmexeccapi_w6_arm.dylib # --- Local reproduction of the CI build matrix ------------------------------- # The four targets above are what CI runs natively, one per runner. The targets @@ -86,8 +86,8 @@ capi-osx-amd64-cross: exit 1; \ } rustup target add x86_64-apple-darwin - cargo build -p multiversx-chain-vm-executor-c-api --release --target x86_64-apple-darwin - cp target/x86_64-apple-darwin/release/libmultiversx_chain_vm_executor_c_api.dylib target/x86_64-apple-darwin/release/libvmexeccapi.dylib + cargo build -p multiversx-chain-vm-executor-c-api-legacy --release --target x86_64-apple-darwin + cp target/x86_64-apple-darwin/release/libmultiversx_chain_vm_executor_c_api_legacy.dylib target/x86_64-apple-darwin/release/libvmexeccapi.dylib install_name_tool -id @rpath/libvmexeccapi.dylib target/x86_64-apple-darwin/release/libvmexeccapi.dylib capi-linux-amd64-docker: @@ -127,10 +127,10 @@ clean: rm -f target/release/libvmexeccapi.dylib rm -f target/release/libvmexeccapi_arm.dylib rm -f c-api/libvmexeccapi.h - rm -f target/release/libvmexeccapi2.so - rm -f target/release/libvmexeccapi2_arm.so - rm -f target/release/libvmexeccapi2.dylib - rm -f target/release/libvmexeccapi2_arm.dylib - rm -f c-api-new/libvmexeccapi_new.h + rm -f target/release/libvmexeccapi_w6.so + rm -f target/release/libvmexeccapi_w6_arm.so + rm -f target/release/libvmexeccapi_w6.dylib + rm -f target/release/libvmexeccapi_w6_arm.dylib + rm -f c-api-wasmer6/libvmexeccapi_w6.h rm -f target/x86_64-apple-darwin/release/libvmexeccapi.dylib rm -rf $(CI_LOCAL_DIR) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index bffc71d1..582a8015 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -17,15 +17,41 @@ one config object rather than a sequence of setters. ## Where things live +Four integrations, because two of them are the *same backend* reached two ways: wasmer 2.2 +through the legacy interfaces and wasmer 2.2 through the new ones. Both are kept until the +migration is shown to be correct, which is what the hook-sequence differential is for. + +| Backend | Interfaces | Crate | Symbols | Library | Go package | +| --- | --- | --- | --- | --- | --- | +| wasmer 2.2 | legacy | `c-api-legacy` | `vm_exec_*` | `libvmexeccapi` | `wasmer2legacy` | +| wasmer 2.2 | new | `c-api-wasmer2` *(planned)* | `vm_exec_w2_*` | `libvmexeccapi_w2` | `wasmer2` *(planned)* | +| wasmer 6 | new | `c-api-wasmer6` | `vm_exec_w6_*` | `libvmexeccapi_w6` | `wasmer6` | +| — | shared | `c-api-common` *(planned)* | `vm_exec_*` | header only | `wasmercommon` *(planned)* | + +The numbers name the **backend**, not the interface generation. The old `vm_exec2_*` prefix +meant "second interface set", which stopped working the moment a second backend joined that +set; it is retired rather than recycled, so no prefix ever changes meaning. The shared types +go into one `libvmexeccapi_common.h` that both backend headers include, which is what lets a +Go package include both and a binary link both without redefinition — and what gives +`wasmercommon` a single set of Go types to wrap. + +One constraint that falls out: a Go package must not include the legacy header together with a +new one. They both declare `vm_exec_compilation_options_t` and `vm_exec_opcode_cost_t`, and the +compilation options differ in layout (seven fields against four). No package does, and the +problem dies with `c-api-legacy`. + +Cgo `//export` shim names are C symbols too, so they also have to be unique per package: +`w2_*` for the legacy shims and `w6_*` for wasmer 6. The new wasmer 2.2 package will need a +third prefix rather than reusing `w2_`. + | Piece | Path | | --- | --- | | Interfaces, legacy + new | `vm-executor/src/` and `vm-executor/src/new_traits/` | | Wasmer 2.2 backend (legacy native, new traits via a shim) | `vm-executor-wasmer/` | | Wasmer 6 backend (new traits native) | `vm-executor-experimental/` | -| Current C API (legacy only) | `c-api/` | -| New C API (new traits, Wasmer 6) | `c-api-new/` | +| C APIs | see the table above | | Code generator (drives **both** repos) | `mx-chain-vm-go/vmhost/vmhooks/generate/` | -| Go VM | `mx-chain-vm-go/`, cgo bridge in `wasmer2/` | +| Go VM | `mx-chain-vm-go/`, cgo bridges in `wasmer2legacy/` and `wasmer6/` | | Rust VM, Wasmer 6 consumer | `mx-sdk-rs/chain/vm/src/executor_impl/we_executor.rs` | | Rust VM, Wasmer 2.2 consumer | `mx-sdk-rs/chain/wasmer-prod/src/wasmer_prod.rs` | @@ -622,6 +648,71 @@ validation; the new header is the only one with `imported_function_names` at all No interface change, identical semantics, and the consensus-relevant check stays exactly as written. +#### Done: wasmer3 is selectable, and the differential harness is running + +The most useful thing to have early, because it replaces reasoning about what the new backend +does with observing it. `wasmer3.ExecutorFactory()` implements +`executor.ExecutorAbstractFactory`, and `$VMEXECUTOR=wasmer3` selects it — the same switch that +carried the Wasmer 1 to Wasmer 2 migration, which had decayed to one option. + +Serving the legacy 24-method `Instance` before it is shrunk needs an adapter, and it is kept in +`wasmer3/wasmer3InstanceLegacy.go` and `wasmer3ExecutorLegacy.go` so that the split stays +visible and those files can be deleted rather than untangled. Three groups of method: plain +forwarding with an error the legacy signature cannot report; memory and gas, which this +executor can only address from inside a host call; and the things it has no notion of — +breakpoint globals, the per-instance hooks pointer, reset and cache. + +The middle group needed one mechanism: `instanceHooks.activeState`, the state of the hook +currently running on the instance, published by the single helper the generated shims already +call. No generated code changed. It is safe without clearing, for the reasons the re-entry guard +established in 4a: a nested call runs on a different instance, re-entering the same one is +refused, and a context used after its call fails the token check. + +**Shared, not duplicated.** `functionNames` existed in two copies (wasmer2 and the mock) and a +third was about to be generated; it is now `executor.VMHookNames`, generated once by +`WriteExecutorVMHookNames`. Same argument as the opcode cost table: the hook names are a +property of the executor interface, not of what runs below it. + +**One VM change was needed, and it is a latent bug fix.** `runtimeContext.saveCompiledCode` +serialised the module and saved the warm instance behind a shared early return, so an executor +that cannot serialise lost its warm instance too. Invisible on wasmer2, where serialising does +not fail; under wasmer3 it was the single cause of **all 42** `vmhookstest` failures, reported as +`unclosed cold instances remaining`. Split into `saveCompiledCodeToStorage` plus +`saveWarmInstance`; the warm path already degrades correctly when `Reset()` fails. + +Where the suites stand under `$VMEXECUTOR=wasmer3`: + +| Suite | Result | +| --- | --- | +| `vmhost/vmhookstest` | **passes in full** | +| `vmhost/hostCore` | **passes in full** | +| `vmhost/contexts` | 8 failures, all `TestRuntimeContext_Mem*` — tests that address memory through `Instance` from *outside* a host call | +| `vmhost/hosttest` | 17 failures: 8 async-call, 2 memory reset, the rest gas and bad-contract cases | + +Every remaining failure is in a named, already-planned bucket: the breakpoint-as-halt work +(async call is the `BreakpointAsyncCall` producer), `Reset()` and the module cache (Phase 6), +and memory reached through `Instance` from outside a call (item 1 below). None is a surprise and +none needed a new gate. + +#### Done: the hook-sequence differential, and it already agrees + +`integrationTests/json/scenariosExecutorDiff_test.go` runs a scenario folder under both +backends with the `executorwrapper` logger and compares the traces. The wrapper records every +hook with its arguments and its result, so agreement there is a much stronger statement than +matching `VMOutput`s. + +On the `erc20-c` suite — a **9,135-line** trace — the two backends differ on **96 lines**, and +every one of them is instance lifecycle: `Reset`, `Clean`, and the `SetPointsUsed` / +`SetGasLimit` / `SetBreakpointValue` triple that follows a successful warm reuse. **Every VM +hook call and every gas value is identical**, which is the first direct evidence that the two +metering middlewares agree. The difference is entirely `Reset()` returning false, which is +Phase 6. + +The test is skipped unless `VMEXECUTOR_DIFF=1`, since it cannot pass until Phase 6 lands, and +`VMEXECUTOR_DIFF_FOLDER` chooses the scenario. `TestCErc20Executors` was also revived to its +original purpose: wasmer2, wasmer2, wasmer3, wasmer3, wasmer2 in one process, which passes and +exercises backend coexistence through the real VM rather than through a trap-handler probe. + #### Still to do 1. Shrink `executor.Instance` to the outside view, pushing the legacy-only methods down into @@ -633,8 +724,8 @@ written. 3. `MemLoadFromMemory` / `MemStoreToMemory` bypassed for wasmer3, where bounds checking lives in Rust, and kept for wasmer2. 4. `SetOpcodeConfig` removal — see the G4 resolution below. -5. `wasmer3` implements `executor.Executor` and `executor.Instance`, behind - `ExecutorAbstractFactory`. +5. ~~`wasmer3` implements `executor.Executor` and `executor.Instance`~~ — **done**, as a + transitional adapter; it is item 1 that lets the adapter go away. #### G4 resolved: the config can be immutable, if the executor is rebuilt @@ -661,6 +752,56 @@ For now this is harmless, because warm instances are deferred to Phase 6 for was being harmless the moment they are not, and the fix is a Rust-side addition rather than a Go workaround. Recorded here so Phase 6 does not rediscover it. +## Phase 4c — Two libraries per backend, and the shared layer + +Driven by the conclusion that bundling is out (gate G5) and that the two Wasmer 2.2 +integrations have to coexist until the new one is trusted. + +### Done: the renames, and the prefix scheme + +Mechanical but load-bearing, because every later stage inherits the names. + +- Rust: `c-api` → `c-api-legacy`, `c-api-new` → `c-api-wasmer6`, with `vm_exec2_*` → + `vm_exec_w6_*` and `libvmexeccapi_new.h` → `libvmexeccapi_w6.h`. Makefile targets + `capi2-*` → `capi-w6-*`, artifacts `libvmexeccapi2*` → `libvmexeccapi_w6*`. +- Generator: the `Capi2` writers became `CapiW6`, `WriteWasmer3Cgo` became `WriteWasmer6Cgo`, + the emitted shim prefix `w3_` became `w6_`, and every copy target moved with its crate. Also + the legacy cgo writer's emitted package, which is what the first regeneration caught. +- Go: `wasmer3` → `wasmer6` (directory, package, files, `Wasmer3*` → `Wasmer6*`), and + `wasmer2` → `wasmer2legacy`. The node does not import either package — checked against + `mx-chain-go`, which reaches the VM only through `vmhost` — so this was internal, 13 files. +- `$VMEXECUTOR` values now name backends unambiguously: `wasmer2legacy` (the default, what + production runs), `wasmer6`, and `wasmer2` **reserved** for the new Wasmer 2.2 integration so + the two can never be confused. + +Verified by regenerating — which must move nothing — and by running the full suite under both +`wasmer2legacy` and `wasmer6`, plus the dual-backend trap tests and the hook-sequence +differential. + +### Still to do + +1. **`c-api-common`.** The extraction is small: of the thirteen modules in `c-api-wasmer6`, + **ten already reference no backend type at all**. Only `capi_vm_hook_context.rs` (10 + references), `capi_instance.rs` (3) and `capi_vm_hook_table.rs` (1) are coupled. So status + codes, strings, config, opcode cost, the logger, the hook result struct and the generated + hook tables move as-is; the context and the instance stay behind, parameterised by backend. +2. **`c-api-wasmer2`.** Wasmer 2.2 through the new interfaces. This is what puts **Phase 1.6 + back on the critical path**: `WasmerProdRuntimeRef::vm_hooks` returns + `Box` and `Arc>` where the Wasmer 6 side returns a + builder and a plain `Arc`, and the two have to converge before one crate body can serve + both. The `VMHooksLegacyAdapter` activation path described under "What the wasmer 2.2 path + needs" is the other half. +3. **`wasmercommon` in Go**, wrapping the common header: the error buffer, the status mapping, + the call result and the hook result, so `wasmer2` and `wasmer6` share them instead of each + carrying a copy. Worth doing *after* `c-api-common`, since the Go types mirror it. +4. A third cgo shim prefix for the new `wasmer2` package, since `w2_` belongs to the legacy one + for as long as both exist. + +The payoff is the comparison the whole structure exists for: **wasmer 2.2 against wasmer 2.2**, +one through each interface set, under the hook-sequence differential. That isolates the +interface migration from the runtime upgrade — if those two agree, any remaining difference +against `wasmer6` is Wasmer's doing and not the new FFI's. + ## Phase 5 — Bring-up and validation - Unit tests per Phase 1 item, plus the FFI boundary test from Phase 2. diff --git a/c-api/Cargo.toml b/c-api-legacy/Cargo.toml similarity index 89% rename from c-api/Cargo.toml rename to c-api-legacy/Cargo.toml index c26d229d..bf3adbc2 100644 --- a/c-api/Cargo.toml +++ b/c-api-legacy/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "multiversx-chain-vm-executor-c-api" +name = "multiversx-chain-vm-executor-c-api-legacy" version = "0.0.0" edition = "2024" publish = false diff --git a/c-api/build.rs b/c-api-legacy/build.rs similarity index 100% rename from c-api/build.rs rename to c-api-legacy/build.rs diff --git a/c-api/libvmexeccapi.h b/c-api-legacy/libvmexeccapi.h similarity index 100% rename from c-api/libvmexeccapi.h rename to c-api-legacy/libvmexeccapi.h diff --git a/c-api/src/basic_types.rs b/c-api-legacy/src/basic_types.rs similarity index 100% rename from c-api/src/basic_types.rs rename to c-api-legacy/src/basic_types.rs diff --git a/c-api/src/capi_breakpoints.rs b/c-api-legacy/src/capi_breakpoints.rs similarity index 100% rename from c-api/src/capi_breakpoints.rs rename to c-api-legacy/src/capi_breakpoints.rs diff --git a/c-api/src/capi_compilation_options.rs b/c-api-legacy/src/capi_compilation_options.rs similarity index 100% rename from c-api/src/capi_compilation_options.rs rename to c-api-legacy/src/capi_compilation_options.rs diff --git a/c-api/src/capi_error.rs b/c-api-legacy/src/capi_error.rs similarity index 100% rename from c-api/src/capi_error.rs rename to c-api-legacy/src/capi_error.rs diff --git a/c-api/src/capi_executor.rs b/c-api-legacy/src/capi_executor.rs similarity index 100% rename from c-api/src/capi_executor.rs rename to c-api-legacy/src/capi_executor.rs diff --git a/c-api/src/capi_instance.rs b/c-api-legacy/src/capi_instance.rs similarity index 100% rename from c-api/src/capi_instance.rs rename to c-api-legacy/src/capi_instance.rs diff --git a/c-api/src/capi_instance_cache.rs b/c-api-legacy/src/capi_instance_cache.rs similarity index 100% rename from c-api/src/capi_instance_cache.rs rename to c-api-legacy/src/capi_instance_cache.rs diff --git a/c-api/src/capi_logger.rs b/c-api-legacy/src/capi_logger.rs similarity index 100% rename from c-api/src/capi_logger.rs rename to c-api-legacy/src/capi_logger.rs diff --git a/c-api/src/capi_memory.rs b/c-api-legacy/src/capi_memory.rs similarity index 100% rename from c-api/src/capi_memory.rs rename to c-api-legacy/src/capi_memory.rs diff --git a/c-api/src/capi_metering.rs b/c-api-legacy/src/capi_metering.rs similarity index 100% rename from c-api/src/capi_metering.rs rename to c-api-legacy/src/capi_metering.rs diff --git a/c-api-new/src/capi_opcode_cost.rs b/c-api-legacy/src/capi_opcode_cost.rs similarity index 100% rename from c-api-new/src/capi_opcode_cost.rs rename to c-api-legacy/src/capi_opcode_cost.rs diff --git a/c-api/src/capi_vm_hook_pointers.rs b/c-api-legacy/src/capi_vm_hook_pointers.rs similarity index 100% rename from c-api/src/capi_vm_hook_pointers.rs rename to c-api-legacy/src/capi_vm_hook_pointers.rs diff --git a/c-api/src/capi_vm_hooks.rs b/c-api-legacy/src/capi_vm_hooks.rs similarity index 100% rename from c-api/src/capi_vm_hooks.rs rename to c-api-legacy/src/capi_vm_hooks.rs diff --git a/c-api/src/lib.rs b/c-api-legacy/src/lib.rs similarity index 100% rename from c-api/src/lib.rs rename to c-api-legacy/src/lib.rs diff --git a/c-api/src/macros.rs b/c-api-legacy/src/macros.rs similarity index 100% rename from c-api/src/macros.rs rename to c-api-legacy/src/macros.rs diff --git a/c-api/src/service_singleton.rs b/c-api-legacy/src/service_singleton.rs similarity index 100% rename from c-api/src/service_singleton.rs rename to c-api-legacy/src/service_singleton.rs diff --git a/c-api/src/wasmer_logger.rs b/c-api-legacy/src/wasmer_logger.rs similarity index 100% rename from c-api/src/wasmer_logger.rs rename to c-api-legacy/src/wasmer_logger.rs diff --git a/c-api-new/libvmexeccapi_new.h b/c-api-new/libvmexeccapi_new.h deleted file mode 100644 index 9556ea1e..00000000 --- a/c-api-new/libvmexeccapi_new.h +++ /dev/null @@ -1,832 +0,0 @@ -#include -#include -#include -#include - -/** - * The value of `vm_exec2_hook_result_t::early_exit` that means "the hook returned normally". - * - * A dedicated sentinel is needed because `0` is a real early-exit code: the Rust VM uses - * `ASYNC_CALL_EARLY_EXIT_CODE = 0` and the Go VM has the same async-call case, so "code 0" - * and "no code" have to stay distinguishable. - */ -#define VM_EXEC2_NO_EARLY_EXIT UINT64_MAX - -/** - * Result of a C API call. - * - * `VM_EXEC2_OK` is zero and every failure is negative, so the functions that return a - * count (the memory loads) can share the same code space: a non-negative result is a - * count, a negative one is one of these. - */ -typedef enum vm_exec2_status_t { - /** - * The call succeeded. - */ - VM_EXEC2_OK = 0, - /** - * The call failed; the error message was written to the provided error buffer. - */ - VM_EXEC2_ERROR = -1, - /** - * A pointer argument that must not be null was null. No message is produced, because - * a null pointer may well be the error buffer itself. - */ - VM_EXEC2_NULL_POINTER = -2, - /** - * A hook context was used outside the host call it belongs to. See - * [`crate::capi_vm_hook_context`]. - */ - VM_EXEC2_STALE_CONTEXT = -3, - /** - * A panic was caught at the boundary. Rust panics must never unwind into C, so every - * entry point traps them and reports this instead. - */ - VM_EXEC2_PANIC = -4, -} vm_exec2_status_t; - -/** - * How a call ended. - * - * This replaces the legacy arrangement, where a call returned a bare ok/error and the VM - * then polled a wasm global to find out what had actually happened. - */ -typedef enum vm_exec2_call_result_kind_t { - /** - * The function returned normally. - */ - VM_EXEC2_CALL_OK = 0, - /** - * No exported function by that name. - */ - VM_EXEC2_CALL_FUNCTION_NOT_FOUND = 1, - /** - * The instance trapped for a reason that is neither a VM hook's doing nor a - * middleware breakpoint. The message is in the error buffer. - */ - VM_EXEC2_CALL_RUNTIME_ERROR = 2, - /** - * A VM hook asked for execution to stop. `early_exit_code` says which; the message, if - * any, is in the error buffer, and is informational — the VM has already recorded its - * own. - */ - VM_EXEC2_CALL_EARLY_EXIT = 3, - /** - * A middleware stopped execution. `breakpoint` says which. - */ - VM_EXEC2_CALL_BREAKPOINT = 4, -} vm_exec2_call_result_kind_t; - -/** - * Opaque handle to an executor. - */ -typedef struct vm_exec2_executor_t { - -} vm_exec2_executor_t; - -/** - * FFI-safe mirror of `OpcodeCost`. - * - * This is a distinct type rather than a cast of `OpcodeCost` itself, so that - * the general-purpose struct is free to change shape without touching the C - * ABI, and vice versa. - */ -typedef struct vm_exec_opcode_cost_t { - uint32_t opcode_block; - uint32_t opcode_br; - uint32_t opcode_brif; - uint32_t opcode_brtable; - uint32_t opcode_call; - uint32_t opcode_callindirect; - uint32_t opcode_catch; - uint32_t opcode_catchall; - uint32_t opcode_delegate; - uint32_t opcode_drop; - uint32_t opcode_else; - uint32_t opcode_end; - uint32_t opcode_globalget; - uint32_t opcode_globalset; - uint32_t opcode_i32add; - uint32_t opcode_i32and; - uint32_t opcode_i32clz; - uint32_t opcode_i32const; - uint32_t opcode_i32ctz; - uint32_t opcode_i32divs; - uint32_t opcode_i32divu; - uint32_t opcode_i32eq; - uint32_t opcode_i32eqz; - uint32_t opcode_i32extend16s; - uint32_t opcode_i32extend8s; - uint32_t opcode_i32ges; - uint32_t opcode_i32geu; - uint32_t opcode_i32gts; - uint32_t opcode_i32gtu; - uint32_t opcode_i32les; - uint32_t opcode_i32leu; - uint32_t opcode_i32load; - uint32_t opcode_i32load16s; - uint32_t opcode_i32load16u; - uint32_t opcode_i32load8s; - uint32_t opcode_i32load8u; - uint32_t opcode_i32lts; - uint32_t opcode_i32ltu; - uint32_t opcode_i32mul; - uint32_t opcode_i32ne; - uint32_t opcode_i32or; - uint32_t opcode_i32popcnt; - uint32_t opcode_i32rems; - uint32_t opcode_i32remu; - uint32_t opcode_i32rotl; - uint32_t opcode_i32rotr; - uint32_t opcode_i32shl; - uint32_t opcode_i32shrs; - uint32_t opcode_i32shru; - uint32_t opcode_i32store; - uint32_t opcode_i32store16; - uint32_t opcode_i32store8; - uint32_t opcode_i32sub; - uint32_t opcode_i32wrapi64; - uint32_t opcode_i32xor; - uint32_t opcode_i64add; - uint32_t opcode_i64and; - uint32_t opcode_i64clz; - uint32_t opcode_i64const; - uint32_t opcode_i64ctz; - uint32_t opcode_i64divs; - uint32_t opcode_i64divu; - uint32_t opcode_i64eq; - uint32_t opcode_i64eqz; - uint32_t opcode_i64extend16s; - uint32_t opcode_i64extend32s; - uint32_t opcode_i64extend8s; - uint32_t opcode_i64extendi32s; - uint32_t opcode_i64extendi32u; - uint32_t opcode_i64ges; - uint32_t opcode_i64geu; - uint32_t opcode_i64gts; - uint32_t opcode_i64gtu; - uint32_t opcode_i64les; - uint32_t opcode_i64leu; - uint32_t opcode_i64load; - uint32_t opcode_i64load16s; - uint32_t opcode_i64load16u; - uint32_t opcode_i64load32s; - uint32_t opcode_i64load32u; - uint32_t opcode_i64load8s; - uint32_t opcode_i64load8u; - uint32_t opcode_i64lts; - uint32_t opcode_i64ltu; - uint32_t opcode_i64mul; - uint32_t opcode_i64ne; - uint32_t opcode_i64or; - uint32_t opcode_i64popcnt; - uint32_t opcode_i64rems; - uint32_t opcode_i64remu; - uint32_t opcode_i64rotl; - uint32_t opcode_i64rotr; - uint32_t opcode_i64shl; - uint32_t opcode_i64shrs; - uint32_t opcode_i64shru; - uint32_t opcode_i64store; - uint32_t opcode_i64store16; - uint32_t opcode_i64store32; - uint32_t opcode_i64store8; - uint32_t opcode_i64sub; - uint32_t opcode_i64xor; - uint32_t opcode_if; - uint32_t opcode_localget; - uint32_t opcode_localset; - uint32_t opcode_localtee; - uint32_t opcode_loop; - uint32_t opcode_memorygrow; - uint32_t opcode_memorysize; - uint32_t opcode_nop; - uint32_t opcode_reffunc; - uint32_t opcode_refisnull; - uint32_t opcode_refnull; - uint32_t opcode_rethrow; - uint32_t opcode_return; - uint32_t opcode_returncall; - uint32_t opcode_returncallindirect; - uint32_t opcode_select; - uint32_t opcode_tableget; - uint32_t opcode_tablegrow; - uint32_t opcode_tableinit; - uint32_t opcode_tableset; - uint32_t opcode_tablesize; - uint32_t opcode_throw; - uint32_t opcode_try; - uint32_t opcode_typedselect; - uint32_t opcode_unreachable; - uint32_t opcode_unwind; - uint32_t opcode_memorycopy; - uint32_t opcode_memoryfill; - uint32_t opcode_memorycopyperbyte; - uint32_t opcode_memoryfillperbyte; - uint32_t opcode_localallocate; -} vm_exec_opcode_cost_t; - -/** - * Everything an executor is configured with. - * - * The opcode cost table is referenced rather than inlined, because it is large and the - * caller already holds one. It is read during the call and not retained. - */ -typedef struct vm_exec2_executor_config_t { - /** - * Which opcode set and whitelist to apply; see `OpcodeVersion` on the Rust side. - */ - int32_t opcode_version; - /** - * The per-opcode gas costs. Must not be null. - */ - const struct vm_exec_opcode_cost_t *opcode_cost; -} vm_exec2_executor_config_t; - -/** - * What every VM hook returns. - * - * One 16-byte struct covers every hook shape — `void`, `i32`, `i64` — and is returned in - * registers under both SysV AMD64 (RAX:RDX) and AAPCS64 (X0:X1), so the early-exit channel - * costs no memory traffic. A cgo `//export` function may return a C struct by value, which - * is what makes this shape available to the Go side. - * - * The early-exit *message* does not travel in this struct. A pointer cannot: cgo forbids - * returning an unpinned Go pointer, and a fixed buffer cannot work because the messages are - * unbounded user text. The Go VM does not need it to travel, because the hook has already - * recorded the message in its own output state by the time it returns. - */ -typedef struct vm_exec2_hook_result_t { - /** - * The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for - * any hook that reports an early exit. - */ - int64_t value; - /** - * The early-exit code, or `VM_EXEC2_NO_EARLY_EXIT`. - */ - uint64_t early_exit; -} vm_exec2_hook_result_t; - -/** - * What a VM hook is handed as its first argument. - * - * Only `go_vm_hooks` is part of the layout the Go side mirrors; the rest is private to - * Rust, and the whole pointer is what travels back into the state functions. - */ -typedef struct vm_exec2_hook_context_t { - /** - * The caller's VM hooks handle for this instance. Read by the caller, never by Rust. - * - * An opaque word rather than a pointer, deliberately: Rust only stores it and hands it - * back, and the Go side needs it to be an integer. cgo forbids storing a Go pointer in - * C memory, so what Go puts here is a `runtime/cgo.Handle`, which is a `uintptr` by - * construction. Typing the field as a pointer would force Go to convert it back with - * `unsafe.Pointer(uintptr(...))`, exactly the pattern `go vet` rejects. - */ - uintptr_t go_vm_hooks; - /** - * Erased `&mut ExperimentalInstanceState<'_>`; null while no call is active. - */ - void *state; - /** - * Erased `&RefCell`, for the token check. - */ - const void *pool; - /** - * This activation's call token; `0` while no call is active. - */ - uint64_t token; -} vm_exec2_hook_context_t; - -/** - * The Go VM's hook implementations, as a table of C function pointers. - * - * Copied into Rust at executor creation; see CapiVMHookTable. - */ -typedef struct vm_exec2_vm_hook_c_func_pointers { - struct vm_exec2_hook_result_t (*get_gas_left_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_sc_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); - struct vm_exec2_hook_result_t (*get_owner_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); - struct vm_exec2_hook_result_t (*get_shard_of_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset); - struct vm_exec2_hook_result_t (*is_smart_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset); - struct vm_exec2_hook_result_t (*signal_error_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t message_offset, int32_t message_length); - struct vm_exec2_hook_result_t (*get_external_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t result_offset); - struct vm_exec2_hook_result_t (*get_block_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t nonce, int32_t result_offset); - struct vm_exec2_hook_result_t (*get_esdt_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_offset); - struct vm_exec2_hook_result_t (*get_esdt_nft_name_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); - struct vm_exec2_hook_result_t (*get_esdt_nft_attribute_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); - struct vm_exec2_hook_result_t (*get_esdt_nft_uri_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); - struct vm_exec2_hook_result_t (*get_esdt_token_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t value_handle, int32_t properties_offset, int32_t hash_offset, int32_t name_offset, int32_t attributes_offset, int32_t creator_offset, int32_t royalties_handle, int32_t uris_offset); - struct vm_exec2_hook_result_t (*get_esdt_local_roles_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t token_id_handle); - struct vm_exec2_hook_result_t (*validate_token_identifier_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t token_id_handle); - struct vm_exec2_hook_result_t (*transfer_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); - struct vm_exec2_hook_result_t (*transfer_value_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*transfer_esdt_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*transfer_esdt_nft_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t nonce, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t num_token_transfers, int32_t token_transfers_args_length_offset, int32_t token_transfer_data_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*create_async_call_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t data_length, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback); - struct vm_exec2_hook_result_t (*set_async_context_callback_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t callback, int32_t callback_length, int32_t data, int32_t data_length, int64_t gas); - struct vm_exec2_hook_result_t (*upgrade_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*upgrade_from_source_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*delete_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*async_call_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); - struct vm_exec2_hook_result_t (*get_argument_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id); - struct vm_exec2_hook_result_t (*get_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id, int32_t arg_offset); - struct vm_exec2_hook_result_t (*get_function_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t function_offset); - struct vm_exec2_hook_result_t (*get_num_arguments_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*storage_store_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset, int32_t data_length); - struct vm_exec2_hook_result_t (*storage_load_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec2_hook_result_t (*storage_load_from_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t key_offset, int32_t key_length, int32_t data_offset); - struct vm_exec2_hook_result_t (*storage_load_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset); - struct vm_exec2_hook_result_t (*set_storage_lock_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t lock_timestamp); - struct vm_exec2_hook_result_t (*get_storage_lock_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec2_hook_result_t (*is_storage_locked_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec2_hook_result_t (*clear_storage_lock_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec2_hook_result_t (*get_caller_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); - struct vm_exec2_hook_result_t (*check_no_payment_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); - struct vm_exec2_hook_result_t (*get_esdt_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); - struct vm_exec2_hook_result_t (*get_esdt_value_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset, int32_t index); - struct vm_exec2_hook_result_t (*get_esdt_token_name_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset); - struct vm_exec2_hook_result_t (*get_esdt_token_name_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_offset, int32_t index); - struct vm_exec2_hook_result_t (*get_esdt_token_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_esdt_token_nonce_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t index); - struct vm_exec2_hook_result_t (*get_current_esdt_nft_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len); - struct vm_exec2_hook_result_t (*get_esdt_token_type_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_esdt_token_type_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t index); - struct vm_exec2_hook_result_t (*get_num_esdt_transfers_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_call_value_token_name_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset); - struct vm_exec2_hook_result_t (*get_call_value_token_name_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset, int32_t index); - struct vm_exec2_hook_result_t (*is_reserved_function_name_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t name_handle); - struct vm_exec2_hook_result_t (*write_log_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_pointer, int32_t data_length, int32_t topic_ptr, int32_t num_topics); - struct vm_exec2_hook_result_t (*write_event_log_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t num_topics, int32_t topic_lengths_offset, int32_t topic_offset, int32_t data_offset, int32_t data_length); - struct vm_exec2_hook_result_t (*get_block_timestamp_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_block_timestamp_ms_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_block_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_block_round_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_block_epoch_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_block_random_seed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t pointer); - struct vm_exec2_hook_result_t (*get_state_root_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t pointer); - struct vm_exec2_hook_result_t (*get_prev_block_timestamp_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_prev_block_timestamp_ms_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_prev_block_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_prev_block_round_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_prev_block_epoch_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_prev_block_random_seed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t pointer); - struct vm_exec2_hook_result_t (*get_block_round_time_ms_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*epoch_start_block_timestamp_ms_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*epoch_start_block_nonce_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*epoch_start_block_round_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*finish_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t pointer, int32_t length); - struct vm_exec2_hook_result_t (*execute_on_same_context_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*execute_on_dest_context_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*execute_read_only_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*create_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t result_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*deploy_from_source_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t result_address_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec2_hook_result_t (*get_num_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*get_return_data_size_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_id); - struct vm_exec2_hook_result_t (*get_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_id, int32_t data_offset); - struct vm_exec2_hook_result_t (*clean_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*delete_from_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_id); - struct vm_exec2_hook_result_t (*get_original_tx_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset); - struct vm_exec2_hook_result_t (*get_current_tx_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset); - struct vm_exec2_hook_result_t (*get_prev_tx_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset); - struct vm_exec2_hook_result_t (*managed_sc_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*managed_owner_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*managed_caller_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*managed_get_original_caller_addr_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*managed_get_relayer_addr_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*managed_signal_error_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t err_handle); - struct vm_exec2_hook_result_t (*managed_write_log_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t topics_handle, int32_t data_handle); - struct vm_exec2_hook_result_t (*managed_get_original_tx_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_get_state_root_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_get_block_random_seed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_get_prev_block_random_seed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_get_return_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t result_id, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_get_multi_esdt_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t multi_call_value_handle); - struct vm_exec2_hook_result_t (*managed_get_all_transfers_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t transfer_call_values_list_handle); - struct vm_exec2_hook_result_t (*managed_get_back_transfers_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t esdt_transfers_value_handle, int32_t egld_value_handle); - struct vm_exec2_hook_result_t (*managed_get_esdt_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle); - struct vm_exec2_hook_result_t (*managed_get_esdt_token_data_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle, int32_t properties_handle, int32_t hash_handle, int32_t name_handle, int32_t attributes_handle, int32_t creator_handle, int32_t royalties_handle, int32_t uris_handle); - struct vm_exec2_hook_result_t (*managed_get_esdt_token_type_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t type_handle); - struct vm_exec2_hook_result_t (*managed_async_call_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle); - struct vm_exec2_hook_result_t (*managed_create_async_call_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback, int32_t callback_closure_handle); - struct vm_exec2_hook_result_t (*managed_get_callback_closure_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t callback_closure_handle); - struct vm_exec2_hook_result_t (*managed_upgrade_from_source_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_upgrade_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_delete_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_handle, int64_t gas_limit, int32_t arguments_handle); - struct vm_exec2_hook_result_t (*managed_deploy_from_source_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_create_contract_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_execute_read_only_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_execute_on_same_context_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_execute_on_dest_context_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_execute_on_dest_context_with_error_return_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*managed_multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); - struct vm_exec2_hook_result_t (*managed_multi_transfer_esdt_nft_execute_with_return_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); - struct vm_exec2_hook_result_t (*managed_multi_transfer_esdt_nft_execute_by_user_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t user_handle, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); - struct vm_exec2_hook_result_t (*managed_transfer_value_execute_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dst_handle, int32_t value_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); - struct vm_exec2_hook_result_t (*managed_is_esdt_frozen_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce); - struct vm_exec2_hook_result_t (*managed_is_esdt_limited_transfer_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t token_id_handle); - struct vm_exec2_hook_result_t (*managed_is_esdt_paused_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t token_id_handle); - struct vm_exec2_hook_result_t (*managed_buffer_to_hex_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t source_handle, int32_t dest_handle); - struct vm_exec2_hook_result_t (*managed_get_code_metadata_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t response_handle); - struct vm_exec2_hook_result_t (*managed_get_code_hash_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t code_hash_handle); - struct vm_exec2_hook_result_t (*managed_is_builtin_function_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t function_name_handle); - struct vm_exec2_hook_result_t (*big_float_new_from_parts_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t integral_part, int32_t fractional_part, int32_t exponent); - struct vm_exec2_hook_result_t (*big_float_new_from_frac_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t numerator, int64_t denominator); - struct vm_exec2_hook_result_t (*big_float_new_from_sci_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t significand, int64_t exponent); - struct vm_exec2_hook_result_t (*big_float_add_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_float_sub_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_float_mul_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_float_div_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_float_neg_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_float_clone_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_float_cmp_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_float_abs_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_float_sign_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_float_sqrt_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_float_pow_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t exponent); - struct vm_exec2_hook_result_t (*big_float_floor_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_float_ceil_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_float_truncate_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_float_set_int64_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int64_t value); - struct vm_exec2_hook_result_t (*big_float_is_int_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_float_set_big_int_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t big_int_handle); - struct vm_exec2_hook_result_t (*big_float_get_const_pi_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*big_float_get_const_e_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*big_int_get_unsigned_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id, int32_t destination_handle); - struct vm_exec2_hook_result_t (*big_int_get_signed_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id, int32_t destination_handle); - struct vm_exec2_hook_result_t (*big_int_storage_store_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t source_handle); - struct vm_exec2_hook_result_t (*big_int_storage_load_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t destination_handle); - struct vm_exec2_hook_result_t (*big_int_get_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*big_int_get_esdt_call_value_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination); - struct vm_exec2_hook_result_t (*big_int_get_esdt_call_value_by_index_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t index); - struct vm_exec2_hook_result_t (*big_int_get_external_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t result); - struct vm_exec2_hook_result_t (*big_int_get_esdt_external_balance_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_handle); - struct vm_exec2_hook_result_t (*big_int_new_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t small_value); - struct vm_exec2_hook_result_t (*big_int_unsigned_byte_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle); - struct vm_exec2_hook_result_t (*big_int_signed_byte_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle); - struct vm_exec2_hook_result_t (*big_int_get_unsigned_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); - struct vm_exec2_hook_result_t (*big_int_get_signed_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); - struct vm_exec2_hook_result_t (*big_int_set_unsigned_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); - struct vm_exec2_hook_result_t (*big_int_set_signed_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); - struct vm_exec2_hook_result_t (*big_int_is_int64_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*big_int_get_int64_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec2_hook_result_t (*big_int_set_int64_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int64_t value); - struct vm_exec2_hook_result_t (*big_int_add_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_sub_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_mul_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_tdiv_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_tmod_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_ediv_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_emod_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_sqrt_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_int_pow_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_log2_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op1_handle); - struct vm_exec2_hook_result_t (*big_int_abs_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_int_neg_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_int_sign_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_int_cmp_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_not_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec2_hook_result_t (*big_int_and_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_or_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_xor_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec2_hook_result_t (*big_int_shr_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); - struct vm_exec2_hook_result_t (*big_int_shl_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); - struct vm_exec2_hook_result_t (*big_int_finish_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle); - struct vm_exec2_hook_result_t (*big_int_finish_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t reference_handle); - struct vm_exec2_hook_result_t (*big_int_to_string_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t big_int_handle, int32_t destination_handle); - struct vm_exec2_hook_result_t (*mbuffer_new_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*mbuffer_new_from_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t data_length); - struct vm_exec2_hook_result_t (*mbuffer_get_length_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle); - struct vm_exec2_hook_result_t (*mbuffer_get_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t result_offset); - struct vm_exec2_hook_result_t (*mbuffer_get_byte_slice_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t result_offset); - struct vm_exec2_hook_result_t (*mbuffer_copy_byte_slice_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t destination_handle); - struct vm_exec2_hook_result_t (*mbuffer_eq_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle1, int32_t m_buffer_handle2); - struct vm_exec2_hook_result_t (*mbuffer_set_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t data_offset, int32_t data_length); - struct vm_exec2_hook_result_t (*mbuffer_set_byte_slice_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t starting_position, int32_t data_length, int32_t data_offset); - struct vm_exec2_hook_result_t (*mbuffer_append_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_handle); - struct vm_exec2_hook_result_t (*mbuffer_append_bytes_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_offset, int32_t data_length); - struct vm_exec2_hook_result_t (*mbuffer_to_big_int_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); - struct vm_exec2_hook_result_t (*mbuffer_to_big_int_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); - struct vm_exec2_hook_result_t (*mbuffer_from_big_int_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); - struct vm_exec2_hook_result_t (*mbuffer_from_big_int_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); - struct vm_exec2_hook_result_t (*mbuffer_to_small_int_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle); - struct vm_exec2_hook_result_t (*mbuffer_to_small_int_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle); - struct vm_exec2_hook_result_t (*mbuffer_from_small_int_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); - struct vm_exec2_hook_result_t (*mbuffer_from_small_int_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); - struct vm_exec2_hook_result_t (*mbuffer_to_big_float_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); - struct vm_exec2_hook_result_t (*mbuffer_from_big_float_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); - struct vm_exec2_hook_result_t (*mbuffer_storage_store_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t source_handle); - struct vm_exec2_hook_result_t (*mbuffer_storage_load_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t destination_handle); - struct vm_exec2_hook_result_t (*mbuffer_storage_load_from_address_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t address_handle, int32_t key_handle, int32_t destination_handle); - struct vm_exec2_hook_result_t (*mbuffer_get_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id, int32_t destination_handle); - struct vm_exec2_hook_result_t (*mbuffer_finish_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t source_handle); - struct vm_exec2_hook_result_t (*mbuffer_set_random_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t destination_handle, int32_t length); - struct vm_exec2_hook_result_t (*managed_map_new_func_ptr)(struct vm_exec2_hook_context_t *ctx); - struct vm_exec2_hook_result_t (*managed_map_put_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t value_handle); - struct vm_exec2_hook_result_t (*managed_map_get_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); - struct vm_exec2_hook_result_t (*managed_map_remove_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); - struct vm_exec2_hook_result_t (*managed_map_contains_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle); - struct vm_exec2_hook_result_t (*small_int_get_unsigned_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id); - struct vm_exec2_hook_result_t (*small_int_get_signed_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id); - struct vm_exec2_hook_result_t (*small_int_finish_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t value); - struct vm_exec2_hook_result_t (*small_int_finish_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t value); - struct vm_exec2_hook_result_t (*small_int_storage_store_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); - struct vm_exec2_hook_result_t (*small_int_storage_store_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); - struct vm_exec2_hook_result_t (*small_int_storage_load_unsigned_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec2_hook_result_t (*small_int_storage_load_signed_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec2_hook_result_t (*int64get_argument_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t id); - struct vm_exec2_hook_result_t (*int64finish_func_ptr)(struct vm_exec2_hook_context_t *ctx, int64_t value); - struct vm_exec2_hook_result_t (*int64storage_store_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); - struct vm_exec2_hook_result_t (*int64storage_load_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec2_hook_result_t (*sha256_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); - struct vm_exec2_hook_result_t (*managed_sha256_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); - struct vm_exec2_hook_result_t (*keccak256_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); - struct vm_exec2_hook_result_t (*managed_keccak256_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); - struct vm_exec2_hook_result_t (*ripemd160_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); - struct vm_exec2_hook_result_t (*managed_ripemd160_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); - struct vm_exec2_hook_result_t (*verify_bls_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); - struct vm_exec2_hook_result_t (*managed_verify_bls_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec2_hook_result_t (*verify_ed25519_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); - struct vm_exec2_hook_result_t (*managed_verify_ed25519_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec2_hook_result_t (*verify_custom_secp256k1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset, int32_t hash_type); - struct vm_exec2_hook_result_t (*managed_verify_custom_secp256k1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle, int32_t hash_type); - struct vm_exec2_hook_result_t (*verify_secp256k1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset); - struct vm_exec2_hook_result_t (*managed_verify_secp256k1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec2_hook_result_t (*encode_secp256k1_der_signature_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t r_offset, int32_t r_length, int32_t s_offset, int32_t s_length, int32_t sig_offset); - struct vm_exec2_hook_result_t (*managed_encode_secp256k1_der_signature_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t r_handle, int32_t s_handle, int32_t sig_handle); - struct vm_exec2_hook_result_t (*add_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t fst_point_xhandle, int32_t fst_point_yhandle, int32_t snd_point_xhandle, int32_t snd_point_yhandle); - struct vm_exec2_hook_result_t (*double_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); - struct vm_exec2_hook_result_t (*is_on_curve_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); - struct vm_exec2_hook_result_t (*scalar_base_mult_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); - struct vm_exec2_hook_result_t (*managed_scalar_base_mult_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); - struct vm_exec2_hook_result_t (*scalar_mult_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_offset, int32_t length); - struct vm_exec2_hook_result_t (*managed_scalar_mult_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_handle); - struct vm_exec2_hook_result_t (*marshal_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); - struct vm_exec2_hook_result_t (*managed_marshal_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*marshal_compressed_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); - struct vm_exec2_hook_result_t (*managed_marshal_compressed_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*unmarshal_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); - struct vm_exec2_hook_result_t (*managed_unmarshal_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); - struct vm_exec2_hook_result_t (*unmarshal_compressed_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); - struct vm_exec2_hook_result_t (*managed_unmarshal_compressed_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); - struct vm_exec2_hook_result_t (*generate_key_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_offset); - struct vm_exec2_hook_result_t (*managed_generate_key_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_handle); - struct vm_exec2_hook_result_t (*create_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_offset, int32_t data_length); - struct vm_exec2_hook_result_t (*managed_create_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t data_handle); - struct vm_exec2_hook_result_t (*get_curve_length_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t ec_handle); - struct vm_exec2_hook_result_t (*get_priv_key_byte_length_ec_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t ec_handle); - struct vm_exec2_hook_result_t (*elliptic_curve_get_values_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t ec_handle, int32_t field_order_handle, int32_t base_point_order_handle, int32_t eq_constant_handle, int32_t x_base_point_handle, int32_t y_base_point_handle); - struct vm_exec2_hook_result_t (*managed_verify_secp256r1_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec2_hook_result_t (*managed_verify_blssignature_share_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec2_hook_result_t (*managed_verify_blsaggregated_signature_func_ptr)(struct vm_exec2_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); -} vm_exec2_vm_hook_c_func_pointers; - -/** - * Opaque handle to an instance. - */ -typedef struct vm_exec2_instance_t { - -} vm_exec2_instance_t; - -/** - * FFI-safe mirror of the new `CompilationOptions`. - * - * Four fields, against the legacy seven: `gas_limit` moved to the call, and metering and - * runtime breakpoints are no longer optional. Fields that are `usize` on the Rust side are - * fixed to `u64` here, since `usize` has no guaranteed width across host platforms. - */ -typedef struct vm_exec2_compilation_options_t { - uint64_t unmetered_locals; - uint64_t max_memory_grow; - uint64_t max_memory_grow_delta; - bool opcode_trace; -} vm_exec2_compilation_options_t; - -/** - * The out-struct of `vm_exec2_instance_call`. - */ -typedef struct vm_exec2_call_result_t { - enum vm_exec2_call_result_kind_t kind; - /** - * Meaningful only for `VM_EXEC2_CALL_EARLY_EXIT`. - */ - uint64_t early_exit_code; - /** - * Meaningful only for `VM_EXEC2_CALL_BREAKPOINT`. Values match `BreakpointValue`: - * 4 = out of gas, 5 = memory limit. - */ - uint64_t breakpoint; -} vm_exec2_call_result_t; - -/** - * A buffer of this size holds every message the executor currently produces. - * - * Exported so the Go side has one place to take the number from rather than picking its - * own. - */ -extern const int VM_EXEC2_ERROR_BUF_RECOMMENDED_LEN; - -/** - * Creates an executor from one configuration struct and one hook table. - * - * `hook_table` is copied, so the caller is free to release it as soon as this returns. Every - * entry must be populated; see `CapiVMHookTable::copy_from`. - * - * On success writes the executor to `executor_out` and returns `VM_EXEC2_OK`. The caller - * owns the result and must release it with `vm_exec2_executor_destroy`. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -enum vm_exec2_status_t vm_exec2_new_executor(struct vm_exec2_executor_t **executor_out, const struct vm_exec2_executor_config_t *config, const struct vm_exec2_vm_hook_c_func_pointers *hook_table, char *err_buf, int err_buf_len); - -/** - * Destroys an executor. Does nothing if `executor` is null. - * - * Instances created from an executor do not borrow it, so the order of destruction between - * the two does not matter. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -void vm_exec2_executor_destroy(struct vm_exec2_executor_t *executor); - -/** - * Creates an instance. - * - * `go_vm_hooks` is the caller's VM hooks handle for *this* instance; it is handed back to - * every VM hook as field 0 of the hook context, and is otherwise opaque here. Whatever it - * refers to must stay alive for the instance's lifetime. From Go it is a `cgo.Handle`, - * which is why it is an integer and not a pointer. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -enum vm_exec2_status_t vm_exec2_new_instance(struct vm_exec2_executor_t *executor, struct vm_exec2_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const struct vm_exec2_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); - -/** - * Destroys an instance. Does nothing if `instance` is null. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -void vm_exec2_instance_destroy(struct vm_exec2_instance_t *instance); - -/** - * Calls an exported function, with a gas limit for this call. - * - * Always writes `result_out` when it returns `VM_EXEC2_OK`. A non-`OK` status means the - * call could not be attempted at all, which is distinct from a call that was attempted and - * failed — the latter is reported through `result_out.kind`. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -enum vm_exec2_status_t vm_exec2_instance_call(struct vm_exec2_instance_t *instance, const char *func_name, uint64_t points_limit, struct vm_exec2_call_result_t *result_out, char *err_buf, int err_buf_len); - -/** - * Returns the number of points (gas) used so far. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -enum vm_exec2_status_t vm_exec2_instance_get_points_used(struct vm_exec2_instance_t *instance, uint64_t *points_out, char *err_buf, int err_buf_len); - -/** - * Checks that every exported function takes no arguments and returns nothing. - * - * Returns 1 if so, 0 if not, or a negative status. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -int vm_exec2_instance_check_signatures(const struct vm_exec2_instance_t *instance); - -/** - * Checks whether the contract exports a function with the given name. - * - * Returns 1 if so, 0 if not, or a negative status. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -int vm_exec2_instance_has_function(const struct vm_exec2_instance_t *instance, const char *func_name); - -/** - * Buffer length needed by `vm_exec2_instance_exported_function_names`. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -int vm_exec2_instance_exported_function_names_length(const struct vm_exec2_instance_t *instance); - -/** - * Writes the exported function names, pipe-separated, e.g. `"init|endpoint1|endpoint2"`. - * - * No ordering is guaranteed. Returns the number of bytes written including the terminator, - * or `-1` if the buffer is absent or too small. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -int vm_exec2_instance_exported_function_names(const struct vm_exec2_instance_t *instance, char *dest, int dest_len); - -/** - * Buffer length needed by `vm_exec2_instance_imported_function_names`. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -int vm_exec2_instance_imported_function_names_length(const struct vm_exec2_instance_t *instance); - -/** - * Writes the names of the VM hooks the contract imports, pipe-separated. - * - * All of them at once, rather than a per-name query, because the Go contract validator - * checks the module against whole sets of allowed names. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -int vm_exec2_instance_imported_function_names(const struct vm_exec2_instance_t *instance, char *dest, int dest_len); - -/** - * Loads from instance memory into a caller-provided buffer, **truncating** at the end of - * the memory. - * - * Returns the number of bytes actually written, which may be fewer than `dest_len`, or a - * negative status. The truncation is where the Go VM's historical load semantics live: a - * load whose offset is valid but whose end lies past the end of the memory yields the bytes - * that are available and no error. Contracts have observed that, so the executor keeps - * offering it. An offset outside the memory, or a negative length, is still an error. - * - * `dest_len` doubles as the requested length, which is what makes the clamping visible to - * the caller: Go allocates `length` bytes and gets back `min(length, available)`. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -int64_t vm_exec2_state_mem_load(struct vm_exec2_hook_context_t *ctx, int32_t mem_ptr, uint8_t *dest, int32_t dest_len, char *err_buf, int err_buf_len); - -/** - * Writes to instance memory. The range must fit entirely inside the memory. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -enum vm_exec2_status_t vm_exec2_state_mem_store(struct vm_exec2_hook_context_t *ctx, int32_t mem_ptr, const uint8_t *src, int32_t src_len, char *err_buf, int err_buf_len); - -/** - * Returns the size in bytes of the instance memory. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -enum vm_exec2_status_t vm_exec2_state_mem_length(struct vm_exec2_hook_context_t *ctx, uint64_t *length_out, char *err_buf, int err_buf_len); - -/** - * Grows the instance memory by `by_num_pages` pages of 64Kb each. - * - * Writes the number of pages the memory had *before* growing to `prev_pages_out`. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -enum vm_exec2_status_t vm_exec2_state_mem_grow(struct vm_exec2_hook_context_t *ctx, uint32_t by_num_pages, uint32_t *prev_pages_out, char *err_buf, int err_buf_len); - -/** - * Returns the number of points (gas) used by the instance so far. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -enum vm_exec2_status_t vm_exec2_state_get_points_used(struct vm_exec2_hook_context_t *ctx, uint64_t *points_out, char *err_buf, int err_buf_len); - -/** - * Sets the number of points (gas) used by the instance. - * - * # Safety - * - * C API function, works with raw object pointers. - */ -enum vm_exec2_status_t vm_exec2_state_set_points_used(struct vm_exec2_hook_context_t *ctx, uint64_t points, char *err_buf, int err_buf_len); diff --git a/c-api-new/src/capi_vm_hook_pointers.rs b/c-api-new/src/capi_vm_hook_pointers.rs deleted file mode 100644 index f32df131..00000000 --- a/c-api-new/src/capi_vm_hook_pointers.rs +++ /dev/null @@ -1,300 +0,0 @@ -// Code generated by vmhooks generator. DO NOT EDIT. - -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -use crate::capi_vm_hook_context::{vm_exec2_hook_context_t, vm_exec2_hook_result_t}; - -/// The Go VM's hook implementations, as a table of C function pointers. -/// -/// Copied into Rust at executor creation; see CapiVMHookTable. -#[repr(C)] -#[derive(Clone)] -#[allow(non_camel_case_types)] -#[rustfmt::skip] -pub struct vm_exec2_vm_hook_c_func_pointers { - pub get_gas_left_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, - pub get_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, - pub get_shard_of_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32) -> vm_exec2_hook_result_t, - pub is_smart_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32) -> vm_exec2_hook_result_t, - pub signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, message_offset: i32, message_length: i32) -> vm_exec2_hook_result_t, - pub get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, result_offset: i32) -> vm_exec2_hook_result_t, - pub get_block_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, nonce: i64, result_offset: i32) -> vm_exec2_hook_result_t, - pub get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_offset: i32) -> vm_exec2_hook_result_t, - pub get_esdt_nft_name_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec2_hook_result_t, - pub get_esdt_nft_attribute_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec2_hook_result_t, - pub get_esdt_nft_uri_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec2_hook_result_t, - pub get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, value_handle: i32, properties_offset: i32, hash_offset: i32, name_offset: i32, attributes_offset: i32, creator_offset: i32, royalties_handle: i32, uris_offset: i32) -> vm_exec2_hook_result_t, - pub get_esdt_local_roles_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, token_id_handle: i32) -> vm_exec2_hook_result_t, - pub validate_token_identifier_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, token_id_handle: i32) -> vm_exec2_hook_result_t, - pub transfer_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, - pub transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub transfer_esdt_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, nonce: i64, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, num_token_transfers: i32, token_transfers_args_length_offset: i32, token_transfer_data_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, data_length: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64) -> vm_exec2_hook_result_t, - pub set_async_context_callback_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, callback: i32, callback_length: i32, data: i32, data_length: i32, gas: i64) -> vm_exec2_hook_result_t, - pub upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, gas_limit: i64, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, - pub get_argument_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32) -> vm_exec2_hook_result_t, - pub get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32, arg_offset: i32) -> vm_exec2_hook_result_t, - pub get_function_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, function_offset: i32) -> vm_exec2_hook_result_t, - pub get_num_arguments_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, - pub storage_load_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, - pub storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub set_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, lock_timestamp: i64) -> vm_exec2_hook_result_t, - pub get_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, - pub is_storage_locked_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, - pub clear_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, - pub get_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, - pub check_no_payment_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, - pub get_esdt_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, - pub get_esdt_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32, index: i32) -> vm_exec2_hook_result_t, - pub get_esdt_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32) -> vm_exec2_hook_result_t, - pub get_esdt_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_offset: i32, index: i32) -> vm_exec2_hook_result_t, - pub get_esdt_token_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_esdt_token_nonce_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, index: i32) -> vm_exec2_hook_result_t, - pub get_current_esdt_nft_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32) -> vm_exec2_hook_result_t, - pub get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_esdt_token_type_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, index: i32) -> vm_exec2_hook_result_t, - pub get_num_esdt_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_call_value_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, call_value_offset: i32, token_name_offset: i32) -> vm_exec2_hook_result_t, - pub get_call_value_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, call_value_offset: i32, token_name_offset: i32, index: i32) -> vm_exec2_hook_result_t, - pub is_reserved_function_name_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, name_handle: i32) -> vm_exec2_hook_result_t, - pub write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_pointer: i32, data_length: i32, topic_ptr: i32, num_topics: i32) -> vm_exec2_hook_result_t, - pub write_event_log_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, num_topics: i32, topic_lengths_offset: i32, topic_offset: i32, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, - pub get_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, pointer: i32) -> vm_exec2_hook_result_t, - pub get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, pointer: i32) -> vm_exec2_hook_result_t, - pub get_prev_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_prev_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_prev_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_prev_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_prev_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, pointer: i32) -> vm_exec2_hook_result_t, - pub get_block_round_time_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub epoch_start_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub epoch_start_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub epoch_start_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub finish_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, pointer: i32, length: i32) -> vm_exec2_hook_result_t, - pub execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, address_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, result_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, result_address_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub get_num_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub get_return_data_size_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_id: i32) -> vm_exec2_hook_result_t, - pub get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_id: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub clean_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub delete_from_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_id: i32) -> vm_exec2_hook_result_t, - pub get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32) -> vm_exec2_hook_result_t, - pub get_current_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32) -> vm_exec2_hook_result_t, - pub get_prev_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32) -> vm_exec2_hook_result_t, - pub managed_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub managed_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub managed_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_original_caller_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_relayer_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub managed_signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, err_handle: i32) -> vm_exec2_hook_result_t, - pub managed_write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, topics_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, result_id: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_multi_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, multi_call_value_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_all_transfers_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, transfer_call_values_list_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_back_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, esdt_transfers_value_handle: i32, egld_value_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32, properties_handle: i32, hash_handle: i32, name_handle: i32, attributes_handle: i32, creator_handle: i32, royalties_handle: i32, uris_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, type_handle: i32) -> vm_exec2_hook_result_t, - pub managed_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, - pub managed_create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64, callback_closure_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_callback_closure_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, callback_closure_handle: i32) -> vm_exec2_hook_result_t, - pub managed_upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_handle: i32, gas_limit: i64, arguments_handle: i32) -> vm_exec2_hook_result_t, - pub managed_deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, address_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_execute_on_dest_context_with_error_return_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub managed_multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, - pub managed_multi_transfer_esdt_nft_execute_with_return_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, - pub managed_multi_transfer_esdt_nft_execute_by_user_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, user_handle: i32, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, - pub managed_transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dst_handle: i32, value_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec2_hook_result_t, - pub managed_is_esdt_frozen_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64) -> vm_exec2_hook_result_t, - pub managed_is_esdt_limited_transfer_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, token_id_handle: i32) -> vm_exec2_hook_result_t, - pub managed_is_esdt_paused_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, token_id_handle: i32) -> vm_exec2_hook_result_t, - pub managed_buffer_to_hex_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, source_handle: i32, dest_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_code_metadata_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, response_handle: i32) -> vm_exec2_hook_result_t, - pub managed_get_code_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, code_hash_handle: i32) -> vm_exec2_hook_result_t, - pub managed_is_builtin_function_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, function_name_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_new_from_parts_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, integral_part: i32, fractional_part: i32, exponent: i32) -> vm_exec2_hook_result_t, - pub big_float_new_from_frac_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, numerator: i64, denominator: i64) -> vm_exec2_hook_result_t, - pub big_float_new_from_sci_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, significand: i64, exponent: i64) -> vm_exec2_hook_result_t, - pub big_float_add_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_div_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_clone_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32, exponent: i32) -> vm_exec2_hook_result_t, - pub big_float_floor_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_ceil_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_truncate_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, value: i64) -> vm_exec2_hook_result_t, - pub big_float_is_int_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_set_big_int_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_get_const_pi_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub big_float_get_const_e_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32, destination_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32, destination_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, source_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, destination_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_get_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination: i32) -> vm_exec2_hook_result_t, - pub big_int_get_esdt_call_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, index: i32) -> vm_exec2_hook_result_t, - pub big_int_get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, result: i32) -> vm_exec2_hook_result_t, - pub big_int_get_esdt_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_new_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, small_value: i64) -> vm_exec2_hook_result_t, - pub big_int_unsigned_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_signed_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_get_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec2_hook_result_t, - pub big_int_get_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec2_hook_result_t, - pub big_int_set_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec2_hook_result_t, - pub big_int_set_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec2_hook_result_t, - pub big_int_is_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_get_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, value: i64) -> vm_exec2_hook_result_t, - pub big_int_add_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_tdiv_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_tmod_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_ediv_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_emod_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_log2_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op1_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_not_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_and_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_or_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_xor_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_shr_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec2_hook_result_t, - pub big_int_shl_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec2_hook_result_t, - pub big_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, reference_handle: i32) -> vm_exec2_hook_result_t, - pub big_int_to_string_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, big_int_handle: i32, destination_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_new_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub mbuffer_new_from_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, - pub mbuffer_get_length_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_get_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, result_offset: i32) -> vm_exec2_hook_result_t, - pub mbuffer_get_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, result_offset: i32) -> vm_exec2_hook_result_t, - pub mbuffer_copy_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, destination_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_eq_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle1: i32, m_buffer_handle2: i32) -> vm_exec2_hook_result_t, - pub mbuffer_set_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, - pub mbuffer_set_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, starting_position: i32, data_length: i32, data_offset: i32) -> vm_exec2_hook_result_t, - pub mbuffer_append_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, accumulator_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_append_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, accumulator_handle: i32, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, - pub mbuffer_to_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_to_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_from_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_from_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_to_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_to_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_from_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec2_hook_result_t, - pub mbuffer_from_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec2_hook_result_t, - pub mbuffer_to_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_from_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, source_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, destination_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, address_handle: i32, key_handle: i32, destination_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32, destination_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_finish_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, source_handle: i32) -> vm_exec2_hook_result_t, - pub mbuffer_set_random_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, destination_handle: i32, length: i32) -> vm_exec2_hook_result_t, - pub managed_map_new_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t, - pub managed_map_put_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_map_handle: i32, key_handle: i32, value_handle: i32) -> vm_exec2_hook_result_t, - pub managed_map_get_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec2_hook_result_t, - pub managed_map_remove_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec2_hook_result_t, - pub managed_map_contains_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, m_map_handle: i32, key_handle: i32) -> vm_exec2_hook_result_t, - pub small_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32) -> vm_exec2_hook_result_t, - pub small_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32) -> vm_exec2_hook_result_t, - pub small_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, value: i64) -> vm_exec2_hook_result_t, - pub small_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, value: i64) -> vm_exec2_hook_result_t, - pub small_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec2_hook_result_t, - pub small_int_storage_store_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec2_hook_result_t, - pub small_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, - pub small_int_storage_load_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, - pub int64get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, id: i32) -> vm_exec2_hook_result_t, - pub int64finish_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, value: i64) -> vm_exec2_hook_result_t, - pub int64storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec2_hook_result_t, - pub int64storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec2_hook_result_t, - pub sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec2_hook_result_t, - pub managed_sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec2_hook_result_t, - pub keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec2_hook_result_t, - pub managed_keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec2_hook_result_t, - pub ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec2_hook_result_t, - pub managed_ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec2_hook_result_t, - pub verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec2_hook_result_t, - pub managed_verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, - pub verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec2_hook_result_t, - pub managed_verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, - pub verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32, hash_type: i32) -> vm_exec2_hook_result_t, - pub managed_verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32, hash_type: i32) -> vm_exec2_hook_result_t, - pub verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec2_hook_result_t, - pub managed_verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, - pub encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, r_offset: i32, r_length: i32, s_offset: i32, s_length: i32, sig_offset: i32) -> vm_exec2_hook_result_t, - pub managed_encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, r_handle: i32, s_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, - pub add_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, fst_point_xhandle: i32, fst_point_yhandle: i32, snd_point_xhandle: i32, snd_point_yhandle: i32) -> vm_exec2_hook_result_t, - pub double_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec2_hook_result_t, - pub is_on_curve_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec2_hook_result_t, - pub scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, - pub managed_scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, - pub scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, - pub managed_scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_handle: i32) -> vm_exec2_hook_result_t, - pub marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec2_hook_result_t, - pub managed_marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec2_hook_result_t, - pub managed_marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, - pub managed_unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, - pub unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec2_hook_result_t, - pub managed_unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec2_hook_result_t, - pub generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec2_hook_result_t, - pub managed_generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec2_hook_result_t, - pub create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec2_hook_result_t, - pub managed_create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, data_handle: i32) -> vm_exec2_hook_result_t, - pub get_curve_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, ec_handle: i32) -> vm_exec2_hook_result_t, - pub get_priv_key_byte_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, ec_handle: i32) -> vm_exec2_hook_result_t, - pub elliptic_curve_get_values_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, ec_handle: i32, field_order_handle: i32, base_point_order_handle: i32, eq_constant_handle: i32, x_base_point_handle: i32, y_base_point_handle: i32) -> vm_exec2_hook_result_t, - pub managed_verify_secp256r1_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, - pub managed_verify_blssignature_share_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, - pub managed_verify_blsaggregated_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec2_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec2_hook_result_t, -} - -impl std::fmt::Debug for vm_exec2_vm_hook_c_func_pointers { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "vm_exec2_vm_hook_c_func_pointers") - } -} diff --git a/c-api-new/Cargo.toml b/c-api-wasmer6/Cargo.toml similarity index 91% rename from c-api-new/Cargo.toml rename to c-api-wasmer6/Cargo.toml index 5c017ead..598850f6 100644 --- a/c-api-new/Cargo.toml +++ b/c-api-wasmer6/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "multiversx-chain-vm-executor-c-api-new" +name = "multiversx-chain-vm-executor-c-api-wasmer6" version = "0.0.0" edition = "2024" publish = false diff --git a/c-api-new/build.rs b/c-api-wasmer6/build.rs similarity index 96% rename from c-api-new/build.rs rename to c-api-wasmer6/build.rs index 522eba98..938962ea 100644 --- a/c-api-new/build.rs +++ b/c-api-wasmer6/build.rs @@ -3,7 +3,7 @@ extern crate cbindgen; use cbindgen::{Builder, Config, Language, Style}; use std::{env, fs, path::PathBuf}; -const HEADER_FILE_NAME: &str = "libvmexeccapi_new"; +const HEADER_FILE_NAME: &str = "libvmexeccapi_w6"; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); diff --git a/c-api-new/cbindgen.toml b/c-api-wasmer6/cbindgen.toml similarity index 57% rename from c-api-new/cbindgen.toml rename to c-api-wasmer6/cbindgen.toml index 7a40ddd7..09495340 100644 --- a/c-api-new/cbindgen.toml +++ b/c-api-wasmer6/cbindgen.toml @@ -1,5 +1,5 @@ -# `vm_exec2_hook_result_t` is the return type of every generated VM hook pointer, so the Go +# `vm_exec_w6_hook_result_t` is the return type of every generated VM hook pointer, so the Go # side and the Phase 3 generator both need its typedef — but no function exported from this # crate mentions it yet, so cbindgen would not emit it on reachability alone. [export] -include = ["vm_exec2_hook_result_t"] +include = ["vm_exec_w6_hook_result_t"] diff --git a/c-api-wasmer6/libvmexeccapi_w6.h b/c-api-wasmer6/libvmexeccapi_w6.h new file mode 100644 index 00000000..3b2bca80 --- /dev/null +++ b/c-api-wasmer6/libvmexeccapi_w6.h @@ -0,0 +1,832 @@ +#include +#include +#include +#include + +/** + * The value of `vm_exec_w6_hook_result_t::early_exit` that means "the hook returned normally". + * + * A dedicated sentinel is needed because `0` is a real early-exit code: the Rust VM uses + * `ASYNC_CALL_EARLY_EXIT_CODE = 0` and the Go VM has the same async-call case, so "code 0" + * and "no code" have to stay distinguishable. + */ +#define VM_EXEC_W6_NO_EARLY_EXIT UINT64_MAX + +/** + * Result of a C API call. + * + * `VM_EXEC_W6_OK` is zero and every failure is negative, so the functions that return a + * count (the memory loads) can share the same code space: a non-negative result is a + * count, a negative one is one of these. + */ +typedef enum vm_exec_w6_status_t { + /** + * The call succeeded. + */ + VM_EXEC_W6_OK = 0, + /** + * The call failed; the error message was written to the provided error buffer. + */ + VM_EXEC_W6_ERROR = -1, + /** + * A pointer argument that must not be null was null. No message is produced, because + * a null pointer may well be the error buffer itself. + */ + VM_EXEC_W6_NULL_POINTER = -2, + /** + * A hook context was used outside the host call it belongs to. See + * [`crate::capi_vm_hook_context`]. + */ + VM_EXEC_W6_STALE_CONTEXT = -3, + /** + * A panic was caught at the boundary. Rust panics must never unwind into C, so every + * entry point traps them and reports this instead. + */ + VM_EXEC_W6_PANIC = -4, +} vm_exec_w6_status_t; + +/** + * How a call ended. + * + * This replaces the legacy arrangement, where a call returned a bare ok/error and the VM + * then polled a wasm global to find out what had actually happened. + */ +typedef enum vm_exec_w6_call_result_kind_t { + /** + * The function returned normally. + */ + VM_EXEC_W6_CALL_OK = 0, + /** + * No exported function by that name. + */ + VM_EXEC_W6_CALL_FUNCTION_NOT_FOUND = 1, + /** + * The instance trapped for a reason that is neither a VM hook's doing nor a + * middleware breakpoint. The message is in the error buffer. + */ + VM_EXEC_W6_CALL_RUNTIME_ERROR = 2, + /** + * A VM hook asked for execution to stop. `early_exit_code` says which; the message, if + * any, is in the error buffer, and is informational — the VM has already recorded its + * own. + */ + VM_EXEC_W6_CALL_EARLY_EXIT = 3, + /** + * A middleware stopped execution. `breakpoint` says which. + */ + VM_EXEC_W6_CALL_BREAKPOINT = 4, +} vm_exec_w6_call_result_kind_t; + +/** + * Opaque handle to an executor. + */ +typedef struct vm_exec_w6_executor_t { + +} vm_exec_w6_executor_t; + +/** + * FFI-safe mirror of `OpcodeCost`. + * + * This is a distinct type rather than a cast of `OpcodeCost` itself, so that + * the general-purpose struct is free to change shape without touching the C + * ABI, and vice versa. + */ +typedef struct vm_exec_opcode_cost_t { + uint32_t opcode_block; + uint32_t opcode_br; + uint32_t opcode_brif; + uint32_t opcode_brtable; + uint32_t opcode_call; + uint32_t opcode_callindirect; + uint32_t opcode_catch; + uint32_t opcode_catchall; + uint32_t opcode_delegate; + uint32_t opcode_drop; + uint32_t opcode_else; + uint32_t opcode_end; + uint32_t opcode_globalget; + uint32_t opcode_globalset; + uint32_t opcode_i32add; + uint32_t opcode_i32and; + uint32_t opcode_i32clz; + uint32_t opcode_i32const; + uint32_t opcode_i32ctz; + uint32_t opcode_i32divs; + uint32_t opcode_i32divu; + uint32_t opcode_i32eq; + uint32_t opcode_i32eqz; + uint32_t opcode_i32extend16s; + uint32_t opcode_i32extend8s; + uint32_t opcode_i32ges; + uint32_t opcode_i32geu; + uint32_t opcode_i32gts; + uint32_t opcode_i32gtu; + uint32_t opcode_i32les; + uint32_t opcode_i32leu; + uint32_t opcode_i32load; + uint32_t opcode_i32load16s; + uint32_t opcode_i32load16u; + uint32_t opcode_i32load8s; + uint32_t opcode_i32load8u; + uint32_t opcode_i32lts; + uint32_t opcode_i32ltu; + uint32_t opcode_i32mul; + uint32_t opcode_i32ne; + uint32_t opcode_i32or; + uint32_t opcode_i32popcnt; + uint32_t opcode_i32rems; + uint32_t opcode_i32remu; + uint32_t opcode_i32rotl; + uint32_t opcode_i32rotr; + uint32_t opcode_i32shl; + uint32_t opcode_i32shrs; + uint32_t opcode_i32shru; + uint32_t opcode_i32store; + uint32_t opcode_i32store16; + uint32_t opcode_i32store8; + uint32_t opcode_i32sub; + uint32_t opcode_i32wrapi64; + uint32_t opcode_i32xor; + uint32_t opcode_i64add; + uint32_t opcode_i64and; + uint32_t opcode_i64clz; + uint32_t opcode_i64const; + uint32_t opcode_i64ctz; + uint32_t opcode_i64divs; + uint32_t opcode_i64divu; + uint32_t opcode_i64eq; + uint32_t opcode_i64eqz; + uint32_t opcode_i64extend16s; + uint32_t opcode_i64extend32s; + uint32_t opcode_i64extend8s; + uint32_t opcode_i64extendi32s; + uint32_t opcode_i64extendi32u; + uint32_t opcode_i64ges; + uint32_t opcode_i64geu; + uint32_t opcode_i64gts; + uint32_t opcode_i64gtu; + uint32_t opcode_i64les; + uint32_t opcode_i64leu; + uint32_t opcode_i64load; + uint32_t opcode_i64load16s; + uint32_t opcode_i64load16u; + uint32_t opcode_i64load32s; + uint32_t opcode_i64load32u; + uint32_t opcode_i64load8s; + uint32_t opcode_i64load8u; + uint32_t opcode_i64lts; + uint32_t opcode_i64ltu; + uint32_t opcode_i64mul; + uint32_t opcode_i64ne; + uint32_t opcode_i64or; + uint32_t opcode_i64popcnt; + uint32_t opcode_i64rems; + uint32_t opcode_i64remu; + uint32_t opcode_i64rotl; + uint32_t opcode_i64rotr; + uint32_t opcode_i64shl; + uint32_t opcode_i64shrs; + uint32_t opcode_i64shru; + uint32_t opcode_i64store; + uint32_t opcode_i64store16; + uint32_t opcode_i64store32; + uint32_t opcode_i64store8; + uint32_t opcode_i64sub; + uint32_t opcode_i64xor; + uint32_t opcode_if; + uint32_t opcode_localget; + uint32_t opcode_localset; + uint32_t opcode_localtee; + uint32_t opcode_loop; + uint32_t opcode_memorygrow; + uint32_t opcode_memorysize; + uint32_t opcode_nop; + uint32_t opcode_reffunc; + uint32_t opcode_refisnull; + uint32_t opcode_refnull; + uint32_t opcode_rethrow; + uint32_t opcode_return; + uint32_t opcode_returncall; + uint32_t opcode_returncallindirect; + uint32_t opcode_select; + uint32_t opcode_tableget; + uint32_t opcode_tablegrow; + uint32_t opcode_tableinit; + uint32_t opcode_tableset; + uint32_t opcode_tablesize; + uint32_t opcode_throw; + uint32_t opcode_try; + uint32_t opcode_typedselect; + uint32_t opcode_unreachable; + uint32_t opcode_unwind; + uint32_t opcode_memorycopy; + uint32_t opcode_memoryfill; + uint32_t opcode_memorycopyperbyte; + uint32_t opcode_memoryfillperbyte; + uint32_t opcode_localallocate; +} vm_exec_opcode_cost_t; + +/** + * Everything an executor is configured with. + * + * The opcode cost table is referenced rather than inlined, because it is large and the + * caller already holds one. It is read during the call and not retained. + */ +typedef struct vm_exec_w6_executor_config_t { + /** + * Which opcode set and whitelist to apply; see `OpcodeVersion` on the Rust side. + */ + int32_t opcode_version; + /** + * The per-opcode gas costs. Must not be null. + */ + const struct vm_exec_opcode_cost_t *opcode_cost; +} vm_exec_w6_executor_config_t; + +/** + * What every VM hook returns. + * + * One 16-byte struct covers every hook shape — `void`, `i32`, `i64` — and is returned in + * registers under both SysV AMD64 (RAX:RDX) and AAPCS64 (X0:X1), so the early-exit channel + * costs no memory traffic. A cgo `//export` function may return a C struct by value, which + * is what makes this shape available to the Go side. + * + * The early-exit *message* does not travel in this struct. A pointer cannot: cgo forbids + * returning an unpinned Go pointer, and a fixed buffer cannot work because the messages are + * unbounded user text. The Go VM does not need it to travel, because the hook has already + * recorded the message in its own output state by the time it returns. + */ +typedef struct vm_exec_w6_hook_result_t { + /** + * The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for + * any hook that reports an early exit. + */ + int64_t value; + /** + * The early-exit code, or `VM_EXEC_W6_NO_EARLY_EXIT`. + */ + uint64_t early_exit; +} vm_exec_w6_hook_result_t; + +/** + * What a VM hook is handed as its first argument. + * + * Only `go_vm_hooks` is part of the layout the Go side mirrors; the rest is private to + * Rust, and the whole pointer is what travels back into the state functions. + */ +typedef struct vm_exec_w6_hook_context_t { + /** + * The caller's VM hooks handle for this instance. Read by the caller, never by Rust. + * + * An opaque word rather than a pointer, deliberately: Rust only stores it and hands it + * back, and the Go side needs it to be an integer. cgo forbids storing a Go pointer in + * C memory, so what Go puts here is a `runtime/cgo.Handle`, which is a `uintptr` by + * construction. Typing the field as a pointer would force Go to convert it back with + * `unsafe.Pointer(uintptr(...))`, exactly the pattern `go vet` rejects. + */ + uintptr_t go_vm_hooks; + /** + * Erased `&mut ExperimentalInstanceState<'_>`; null while no call is active. + */ + void *state; + /** + * Erased `&RefCell`, for the token check. + */ + const void *pool; + /** + * This activation's call token; `0` while no call is active. + */ + uint64_t token; +} vm_exec_w6_hook_context_t; + +/** + * The Go VM's hook implementations, as a table of C function pointers. + * + * Copied into Rust at executor creation; see CapiVMHookTable. + */ +typedef struct vm_exec_w6_vm_hook_c_func_pointers { + struct vm_exec_w6_hook_result_t (*get_gas_left_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_sc_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*get_owner_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*get_shard_of_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset); + struct vm_exec_w6_hook_result_t (*is_smart_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset); + struct vm_exec_w6_hook_result_t (*signal_error_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t message_offset, int32_t message_length); + struct vm_exec_w6_hook_result_t (*get_external_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*get_block_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t nonce, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*get_esdt_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*get_esdt_nft_name_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); + struct vm_exec_w6_hook_result_t (*get_esdt_nft_attribute_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); + struct vm_exec_w6_hook_result_t (*get_esdt_nft_uri_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); + struct vm_exec_w6_hook_result_t (*get_esdt_token_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t value_handle, int32_t properties_offset, int32_t hash_offset, int32_t name_offset, int32_t attributes_offset, int32_t creator_offset, int32_t royalties_handle, int32_t uris_offset); + struct vm_exec_w6_hook_result_t (*get_esdt_local_roles_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec_w6_hook_result_t (*validate_token_identifier_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec_w6_hook_result_t (*transfer_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); + struct vm_exec_w6_hook_result_t (*transfer_value_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*transfer_esdt_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*transfer_esdt_nft_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t nonce, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t num_token_transfers, int32_t token_transfers_args_length_offset, int32_t token_transfer_data_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*create_async_call_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t data_length, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback); + struct vm_exec_w6_hook_result_t (*set_async_context_callback_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t callback, int32_t callback_length, int32_t data, int32_t data_length, int64_t gas); + struct vm_exec_w6_hook_result_t (*upgrade_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*upgrade_from_source_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*delete_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*async_call_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); + struct vm_exec_w6_hook_result_t (*get_argument_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id); + struct vm_exec_w6_hook_result_t (*get_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id, int32_t arg_offset); + struct vm_exec_w6_hook_result_t (*get_function_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t function_offset); + struct vm_exec_w6_hook_result_t (*get_num_arguments_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*storage_store_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset, int32_t data_length); + struct vm_exec_w6_hook_result_t (*storage_load_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_w6_hook_result_t (*storage_load_from_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t key_offset, int32_t key_length, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*storage_load_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*set_storage_lock_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t lock_timestamp); + struct vm_exec_w6_hook_result_t (*get_storage_lock_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_w6_hook_result_t (*is_storage_locked_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_w6_hook_result_t (*clear_storage_lock_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_w6_hook_result_t (*get_caller_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*check_no_payment_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*get_esdt_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*get_esdt_value_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset, int32_t index); + struct vm_exec_w6_hook_result_t (*get_esdt_token_name_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*get_esdt_token_name_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset, int32_t index); + struct vm_exec_w6_hook_result_t (*get_esdt_token_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_esdt_token_nonce_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t index); + struct vm_exec_w6_hook_result_t (*get_current_esdt_nft_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len); + struct vm_exec_w6_hook_result_t (*get_esdt_token_type_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_esdt_token_type_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t index); + struct vm_exec_w6_hook_result_t (*get_num_esdt_transfers_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_call_value_token_name_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset); + struct vm_exec_w6_hook_result_t (*get_call_value_token_name_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset, int32_t index); + struct vm_exec_w6_hook_result_t (*is_reserved_function_name_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t name_handle); + struct vm_exec_w6_hook_result_t (*write_log_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_pointer, int32_t data_length, int32_t topic_ptr, int32_t num_topics); + struct vm_exec_w6_hook_result_t (*write_event_log_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t num_topics, int32_t topic_lengths_offset, int32_t topic_offset, int32_t data_offset, int32_t data_length); + struct vm_exec_w6_hook_result_t (*get_block_timestamp_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_block_timestamp_ms_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_block_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_block_round_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_block_epoch_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_block_random_seed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t pointer); + struct vm_exec_w6_hook_result_t (*get_state_root_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t pointer); + struct vm_exec_w6_hook_result_t (*get_prev_block_timestamp_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_prev_block_timestamp_ms_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_prev_block_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_prev_block_round_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_prev_block_epoch_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_prev_block_random_seed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t pointer); + struct vm_exec_w6_hook_result_t (*get_block_round_time_ms_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*epoch_start_block_timestamp_ms_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*epoch_start_block_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*epoch_start_block_round_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*finish_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t pointer, int32_t length); + struct vm_exec_w6_hook_result_t (*execute_on_same_context_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*execute_on_dest_context_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*execute_read_only_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*create_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t result_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*deploy_from_source_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t result_address_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*get_num_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*get_return_data_size_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_id); + struct vm_exec_w6_hook_result_t (*get_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_id, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*clean_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*delete_from_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_id); + struct vm_exec_w6_hook_result_t (*get_original_tx_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*get_current_tx_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*get_prev_tx_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*managed_sc_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*managed_owner_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*managed_caller_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*managed_get_original_caller_addr_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*managed_get_relayer_addr_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*managed_signal_error_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t err_handle); + struct vm_exec_w6_hook_result_t (*managed_write_log_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t topics_handle, int32_t data_handle); + struct vm_exec_w6_hook_result_t (*managed_get_original_tx_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_get_state_root_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_get_block_random_seed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_get_prev_block_random_seed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_get_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_id, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_get_multi_esdt_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t multi_call_value_handle); + struct vm_exec_w6_hook_result_t (*managed_get_all_transfers_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t transfer_call_values_list_handle); + struct vm_exec_w6_hook_result_t (*managed_get_back_transfers_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t esdt_transfers_value_handle, int32_t egld_value_handle); + struct vm_exec_w6_hook_result_t (*managed_get_esdt_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle); + struct vm_exec_w6_hook_result_t (*managed_get_esdt_token_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle, int32_t properties_handle, int32_t hash_handle, int32_t name_handle, int32_t attributes_handle, int32_t creator_handle, int32_t royalties_handle, int32_t uris_handle); + struct vm_exec_w6_hook_result_t (*managed_get_esdt_token_type_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t type_handle); + struct vm_exec_w6_hook_result_t (*managed_async_call_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_w6_hook_result_t (*managed_create_async_call_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback, int32_t callback_closure_handle); + struct vm_exec_w6_hook_result_t (*managed_get_callback_closure_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t callback_closure_handle); + struct vm_exec_w6_hook_result_t (*managed_upgrade_from_source_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_upgrade_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_delete_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int64_t gas_limit, int32_t arguments_handle); + struct vm_exec_w6_hook_result_t (*managed_deploy_from_source_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_create_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_execute_read_only_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_execute_on_same_context_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_execute_on_dest_context_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_execute_on_dest_context_with_error_return_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*managed_multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_w6_hook_result_t (*managed_multi_transfer_esdt_nft_execute_with_return_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_w6_hook_result_t (*managed_multi_transfer_esdt_nft_execute_by_user_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t user_handle, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_w6_hook_result_t (*managed_transfer_value_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dst_handle, int32_t value_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_w6_hook_result_t (*managed_is_esdt_frozen_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce); + struct vm_exec_w6_hook_result_t (*managed_is_esdt_limited_transfer_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec_w6_hook_result_t (*managed_is_esdt_paused_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec_w6_hook_result_t (*managed_buffer_to_hex_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t source_handle, int32_t dest_handle); + struct vm_exec_w6_hook_result_t (*managed_get_code_metadata_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t response_handle); + struct vm_exec_w6_hook_result_t (*managed_get_code_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t code_hash_handle); + struct vm_exec_w6_hook_result_t (*managed_is_builtin_function_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t function_name_handle); + struct vm_exec_w6_hook_result_t (*big_float_new_from_parts_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t integral_part, int32_t fractional_part, int32_t exponent); + struct vm_exec_w6_hook_result_t (*big_float_new_from_frac_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t numerator, int64_t denominator); + struct vm_exec_w6_hook_result_t (*big_float_new_from_sci_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t significand, int64_t exponent); + struct vm_exec_w6_hook_result_t (*big_float_add_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_float_sub_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_float_mul_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_float_div_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_float_neg_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_float_clone_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_float_cmp_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_float_abs_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_float_sign_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_float_sqrt_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_float_pow_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t exponent); + struct vm_exec_w6_hook_result_t (*big_float_floor_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_float_ceil_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_float_truncate_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_float_set_int64_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int64_t value); + struct vm_exec_w6_hook_result_t (*big_float_is_int_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_float_set_big_int_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t big_int_handle); + struct vm_exec_w6_hook_result_t (*big_float_get_const_pi_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*big_float_get_const_e_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*big_int_get_unsigned_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*big_int_get_signed_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*big_int_storage_store_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t source_handle); + struct vm_exec_w6_hook_result_t (*big_int_storage_load_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*big_int_get_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*big_int_get_esdt_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination); + struct vm_exec_w6_hook_result_t (*big_int_get_esdt_call_value_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t index); + struct vm_exec_w6_hook_result_t (*big_int_get_external_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t result); + struct vm_exec_w6_hook_result_t (*big_int_get_esdt_external_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*big_int_new_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t small_value); + struct vm_exec_w6_hook_result_t (*big_int_unsigned_byte_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec_w6_hook_result_t (*big_int_signed_byte_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec_w6_hook_result_t (*big_int_get_unsigned_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); + struct vm_exec_w6_hook_result_t (*big_int_get_signed_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); + struct vm_exec_w6_hook_result_t (*big_int_set_unsigned_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); + struct vm_exec_w6_hook_result_t (*big_int_set_signed_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); + struct vm_exec_w6_hook_result_t (*big_int_is_int64_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*big_int_get_int64_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*big_int_set_int64_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int64_t value); + struct vm_exec_w6_hook_result_t (*big_int_add_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_sub_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_mul_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_tdiv_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_tmod_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_ediv_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_emod_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_sqrt_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_int_pow_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_log2_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op1_handle); + struct vm_exec_w6_hook_result_t (*big_int_abs_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_int_neg_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_int_sign_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_int_cmp_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_not_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_w6_hook_result_t (*big_int_and_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_or_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_xor_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_w6_hook_result_t (*big_int_shr_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); + struct vm_exec_w6_hook_result_t (*big_int_shl_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); + struct vm_exec_w6_hook_result_t (*big_int_finish_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec_w6_hook_result_t (*big_int_finish_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec_w6_hook_result_t (*big_int_to_string_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t big_int_handle, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_new_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*mbuffer_new_from_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t data_length); + struct vm_exec_w6_hook_result_t (*mbuffer_get_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_get_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*mbuffer_get_byte_slice_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*mbuffer_copy_byte_slice_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_eq_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle1, int32_t m_buffer_handle2); + struct vm_exec_w6_hook_result_t (*mbuffer_set_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t data_offset, int32_t data_length); + struct vm_exec_w6_hook_result_t (*mbuffer_set_byte_slice_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t starting_position, int32_t data_length, int32_t data_offset); + struct vm_exec_w6_hook_result_t (*mbuffer_append_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_append_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_offset, int32_t data_length); + struct vm_exec_w6_hook_result_t (*mbuffer_to_big_int_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_to_big_int_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_from_big_int_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_from_big_int_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_to_small_int_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_to_small_int_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_from_small_int_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); + struct vm_exec_w6_hook_result_t (*mbuffer_from_small_int_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); + struct vm_exec_w6_hook_result_t (*mbuffer_to_big_float_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_from_big_float_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_storage_store_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t source_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_storage_load_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_storage_load_from_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t key_handle, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_get_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id, int32_t destination_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_finish_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t source_handle); + struct vm_exec_w6_hook_result_t (*mbuffer_set_random_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t length); + struct vm_exec_w6_hook_result_t (*managed_map_new_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); + struct vm_exec_w6_hook_result_t (*managed_map_put_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t value_handle); + struct vm_exec_w6_hook_result_t (*managed_map_get_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); + struct vm_exec_w6_hook_result_t (*managed_map_remove_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); + struct vm_exec_w6_hook_result_t (*managed_map_contains_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle); + struct vm_exec_w6_hook_result_t (*small_int_get_unsigned_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id); + struct vm_exec_w6_hook_result_t (*small_int_get_signed_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id); + struct vm_exec_w6_hook_result_t (*small_int_finish_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t value); + struct vm_exec_w6_hook_result_t (*small_int_finish_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t value); + struct vm_exec_w6_hook_result_t (*small_int_storage_store_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); + struct vm_exec_w6_hook_result_t (*small_int_storage_store_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); + struct vm_exec_w6_hook_result_t (*small_int_storage_load_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_w6_hook_result_t (*small_int_storage_load_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_w6_hook_result_t (*int64get_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id); + struct vm_exec_w6_hook_result_t (*int64finish_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t value); + struct vm_exec_w6_hook_result_t (*int64storage_store_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); + struct vm_exec_w6_hook_result_t (*int64storage_load_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_w6_hook_result_t (*sha256_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*managed_sha256_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); + struct vm_exec_w6_hook_result_t (*keccak256_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*managed_keccak256_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); + struct vm_exec_w6_hook_result_t (*ripemd160_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*managed_ripemd160_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); + struct vm_exec_w6_hook_result_t (*verify_bls_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); + struct vm_exec_w6_hook_result_t (*managed_verify_bls_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_w6_hook_result_t (*verify_ed25519_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); + struct vm_exec_w6_hook_result_t (*managed_verify_ed25519_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_w6_hook_result_t (*verify_custom_secp256k1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset, int32_t hash_type); + struct vm_exec_w6_hook_result_t (*managed_verify_custom_secp256k1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle, int32_t hash_type); + struct vm_exec_w6_hook_result_t (*verify_secp256k1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset); + struct vm_exec_w6_hook_result_t (*managed_verify_secp256k1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_w6_hook_result_t (*encode_secp256k1_der_signature_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t r_offset, int32_t r_length, int32_t s_offset, int32_t s_length, int32_t sig_offset); + struct vm_exec_w6_hook_result_t (*managed_encode_secp256k1_der_signature_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t r_handle, int32_t s_handle, int32_t sig_handle); + struct vm_exec_w6_hook_result_t (*add_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t fst_point_xhandle, int32_t fst_point_yhandle, int32_t snd_point_xhandle, int32_t snd_point_yhandle); + struct vm_exec_w6_hook_result_t (*double_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); + struct vm_exec_w6_hook_result_t (*is_on_curve_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); + struct vm_exec_w6_hook_result_t (*scalar_base_mult_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); + struct vm_exec_w6_hook_result_t (*managed_scalar_base_mult_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); + struct vm_exec_w6_hook_result_t (*scalar_mult_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_offset, int32_t length); + struct vm_exec_w6_hook_result_t (*managed_scalar_mult_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_handle); + struct vm_exec_w6_hook_result_t (*marshal_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*managed_marshal_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*marshal_compressed_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*managed_marshal_compressed_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*unmarshal_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); + struct vm_exec_w6_hook_result_t (*managed_unmarshal_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); + struct vm_exec_w6_hook_result_t (*unmarshal_compressed_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); + struct vm_exec_w6_hook_result_t (*managed_unmarshal_compressed_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); + struct vm_exec_w6_hook_result_t (*generate_key_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_offset); + struct vm_exec_w6_hook_result_t (*managed_generate_key_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_handle); + struct vm_exec_w6_hook_result_t (*create_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t data_length); + struct vm_exec_w6_hook_result_t (*managed_create_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_handle); + struct vm_exec_w6_hook_result_t (*get_curve_length_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t ec_handle); + struct vm_exec_w6_hook_result_t (*get_priv_key_byte_length_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t ec_handle); + struct vm_exec_w6_hook_result_t (*elliptic_curve_get_values_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t ec_handle, int32_t field_order_handle, int32_t base_point_order_handle, int32_t eq_constant_handle, int32_t x_base_point_handle, int32_t y_base_point_handle); + struct vm_exec_w6_hook_result_t (*managed_verify_secp256r1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_w6_hook_result_t (*managed_verify_blssignature_share_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_w6_hook_result_t (*managed_verify_blsaggregated_signature_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); +} vm_exec_w6_vm_hook_c_func_pointers; + +/** + * Opaque handle to an instance. + */ +typedef struct vm_exec_w6_instance_t { + +} vm_exec_w6_instance_t; + +/** + * FFI-safe mirror of the new `CompilationOptions`. + * + * Four fields, against the legacy seven: `gas_limit` moved to the call, and metering and + * runtime breakpoints are no longer optional. Fields that are `usize` on the Rust side are + * fixed to `u64` here, since `usize` has no guaranteed width across host platforms. + */ +typedef struct vm_exec_w6_compilation_options_t { + uint64_t unmetered_locals; + uint64_t max_memory_grow; + uint64_t max_memory_grow_delta; + bool opcode_trace; +} vm_exec_w6_compilation_options_t; + +/** + * The out-struct of `vm_exec_w6_instance_call`. + */ +typedef struct vm_exec_w6_call_result_t { + enum vm_exec_w6_call_result_kind_t kind; + /** + * Meaningful only for `VM_EXEC_W6_CALL_EARLY_EXIT`. + */ + uint64_t early_exit_code; + /** + * Meaningful only for `VM_EXEC_W6_CALL_BREAKPOINT`. Values match `BreakpointValue`: + * 4 = out of gas, 5 = memory limit. + */ + uint64_t breakpoint; +} vm_exec_w6_call_result_t; + +/** + * A buffer of this size holds every message the executor currently produces. + * + * Exported so the Go side has one place to take the number from rather than picking its + * own. + */ +extern const int VM_EXEC_W6_ERROR_BUF_RECOMMENDED_LEN; + +/** + * Creates an executor from one configuration struct and one hook table. + * + * `hook_table` is copied, so the caller is free to release it as soon as this returns. Every + * entry must be populated; see `CapiVMHookTable::copy_from`. + * + * On success writes the executor to `executor_out` and returns `VM_EXEC_W6_OK`. The caller + * owns the result and must release it with `vm_exec_w6_executor_destroy`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec_w6_status_t vm_exec_w6_new_executor(struct vm_exec_w6_executor_t **executor_out, const struct vm_exec_w6_executor_config_t *config, const struct vm_exec_w6_vm_hook_c_func_pointers *hook_table, char *err_buf, int err_buf_len); + +/** + * Destroys an executor. Does nothing if `executor` is null. + * + * Instances created from an executor do not borrow it, so the order of destruction between + * the two does not matter. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +void vm_exec_w6_executor_destroy(struct vm_exec_w6_executor_t *executor); + +/** + * Creates an instance. + * + * `go_vm_hooks` is the caller's VM hooks handle for *this* instance; it is handed back to + * every VM hook as field 0 of the hook context, and is otherwise opaque here. Whatever it + * refers to must stay alive for the instance's lifetime. From Go it is a `cgo.Handle`, + * which is why it is an integer and not a pointer. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec_w6_status_t vm_exec_w6_new_instance(struct vm_exec_w6_executor_t *executor, struct vm_exec_w6_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const struct vm_exec_w6_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); + +/** + * Destroys an instance. Does nothing if `instance` is null. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +void vm_exec_w6_instance_destroy(struct vm_exec_w6_instance_t *instance); + +/** + * Calls an exported function, with a gas limit for this call. + * + * Always writes `result_out` when it returns `VM_EXEC_W6_OK`. A non-`OK` status means the + * call could not be attempted at all, which is distinct from a call that was attempted and + * failed — the latter is reported through `result_out.kind`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec_w6_status_t vm_exec_w6_instance_call(struct vm_exec_w6_instance_t *instance, const char *func_name, uint64_t points_limit, struct vm_exec_w6_call_result_t *result_out, char *err_buf, int err_buf_len); + +/** + * Returns the number of points (gas) used so far. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec_w6_status_t vm_exec_w6_instance_get_points_used(struct vm_exec_w6_instance_t *instance, uint64_t *points_out, char *err_buf, int err_buf_len); + +/** + * Checks that every exported function takes no arguments and returns nothing. + * + * Returns 1 if so, 0 if not, or a negative status. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w6_instance_check_signatures(const struct vm_exec_w6_instance_t *instance); + +/** + * Checks whether the contract exports a function with the given name. + * + * Returns 1 if so, 0 if not, or a negative status. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w6_instance_has_function(const struct vm_exec_w6_instance_t *instance, const char *func_name); + +/** + * Buffer length needed by `vm_exec_w6_instance_exported_function_names`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w6_instance_exported_function_names_length(const struct vm_exec_w6_instance_t *instance); + +/** + * Writes the exported function names, pipe-separated, e.g. `"init|endpoint1|endpoint2"`. + * + * No ordering is guaranteed. Returns the number of bytes written including the terminator, + * or `-1` if the buffer is absent or too small. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w6_instance_exported_function_names(const struct vm_exec_w6_instance_t *instance, char *dest, int dest_len); + +/** + * Buffer length needed by `vm_exec_w6_instance_imported_function_names`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w6_instance_imported_function_names_length(const struct vm_exec_w6_instance_t *instance); + +/** + * Writes the names of the VM hooks the contract imports, pipe-separated. + * + * All of them at once, rather than a per-name query, because the Go contract validator + * checks the module against whole sets of allowed names. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w6_instance_imported_function_names(const struct vm_exec_w6_instance_t *instance, char *dest, int dest_len); + +/** + * Loads from instance memory into a caller-provided buffer, **truncating** at the end of + * the memory. + * + * Returns the number of bytes actually written, which may be fewer than `dest_len`, or a + * negative status. The truncation is where the Go VM's historical load semantics live: a + * load whose offset is valid but whose end lies past the end of the memory yields the bytes + * that are available and no error. Contracts have observed that, so the executor keeps + * offering it. An offset outside the memory, or a negative length, is still an error. + * + * `dest_len` doubles as the requested length, which is what makes the clamping visible to + * the caller: Go allocates `length` bytes and gets back `min(length, available)`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int64_t vm_exec_w6_state_mem_load(struct vm_exec_w6_hook_context_t *ctx, int32_t mem_ptr, uint8_t *dest, int32_t dest_len, char *err_buf, int err_buf_len); + +/** + * Writes to instance memory. The range must fit entirely inside the memory. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec_w6_status_t vm_exec_w6_state_mem_store(struct vm_exec_w6_hook_context_t *ctx, int32_t mem_ptr, const uint8_t *src, int32_t src_len, char *err_buf, int err_buf_len); + +/** + * Returns the size in bytes of the instance memory. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec_w6_status_t vm_exec_w6_state_mem_length(struct vm_exec_w6_hook_context_t *ctx, uint64_t *length_out, char *err_buf, int err_buf_len); + +/** + * Grows the instance memory by `by_num_pages` pages of 64Kb each. + * + * Writes the number of pages the memory had *before* growing to `prev_pages_out`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec_w6_status_t vm_exec_w6_state_mem_grow(struct vm_exec_w6_hook_context_t *ctx, uint32_t by_num_pages, uint32_t *prev_pages_out, char *err_buf, int err_buf_len); + +/** + * Returns the number of points (gas) used by the instance so far. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec_w6_status_t vm_exec_w6_state_get_points_used(struct vm_exec_w6_hook_context_t *ctx, uint64_t *points_out, char *err_buf, int err_buf_len); + +/** + * Sets the number of points (gas) used by the instance. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +enum vm_exec_w6_status_t vm_exec_w6_state_set_points_used(struct vm_exec_w6_hook_context_t *ctx, uint64_t points, char *err_buf, int err_buf_len); diff --git a/c-api-new/src/capi_config.rs b/c-api-wasmer6/src/capi_config.rs similarity index 93% rename from c-api-new/src/capi_config.rs rename to c-api-wasmer6/src/capi_config.rs index e093aa30..dd7ad8f3 100644 --- a/c-api-new/src/capi_config.rs +++ b/c-api-wasmer6/src/capi_config.rs @@ -15,7 +15,7 @@ use crate::capi_opcode_cost::vm_exec_opcode_cost_t; /// caller already holds one. It is read during the call and not retained. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec2_executor_config_t { +pub struct vm_exec_w6_executor_config_t { /// Which opcode set and whitelist to apply; see `OpcodeVersion` on the Rust side. pub opcode_version: i32, @@ -23,7 +23,7 @@ pub struct vm_exec2_executor_config_t { pub opcode_cost: *const vm_exec_opcode_cost_t, } -impl vm_exec2_executor_config_t { +impl vm_exec_w6_executor_config_t { /// Converts to the executor's own config type. /// /// # Safety @@ -55,14 +55,14 @@ impl vm_exec2_executor_config_t { /// fixed to `u64` here, since `usize` has no guaranteed width across host platforms. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec2_compilation_options_t { +pub struct vm_exec_w6_compilation_options_t { pub unmetered_locals: u64, pub max_memory_grow: u64, pub max_memory_grow_delta: u64, pub opcode_trace: bool, } -impl vm_exec2_compilation_options_t { +impl vm_exec_w6_compilation_options_t { pub(crate) fn to_compilation_options(&self) -> CompilationOptions { CompilationOptions { unmetered_locals: self.unmetered_locals as usize, diff --git a/c-api-new/src/capi_executor.rs b/c-api-wasmer6/src/capi_executor.rs similarity index 74% rename from c-api-new/src/capi_executor.rs rename to c-api-wasmer6/src/capi_executor.rs index 7ff24e3f..bbfcc035 100644 --- a/c-api-new/src/capi_executor.rs +++ b/c-api-wasmer6/src/capi_executor.rs @@ -10,7 +10,7 @@ //! executor's own runtime reference — but this API needs one Go hooks object *per instance*, //! because that is where the Go side keeps the per-instance early-exit slot. So instances are //! built directly against the Wasmer 6 backend, and the per-instance pointer is supplied at -//! `vm_exec2_new_instance`. Converging the two backends' runtime-reference traits (Phase 1.6) +//! `vm_exec_w6_new_instance`. Converging the two backends' runtime-reference traits (Phase 1.6) //! should give the hooks provider that per-instance argument, at which point this can go back //! through the trait and serve both backends. @@ -19,15 +19,15 @@ use meta::capi_safe_unwind; use multiversx_chain_vm_executor::OpcodeConfig; use std::sync::Arc; -use crate::capi_config::vm_exec2_executor_config_t; -use crate::capi_status::{vm_exec2_status_t, write_error}; -use crate::capi_vm_hook_pointers::vm_exec2_vm_hook_c_func_pointers; +use crate::capi_config::vm_exec_w6_executor_config_t; +use crate::capi_status::{vm_exec_w6_status_t, write_error}; +use crate::capi_vm_hook_pointers::vm_exec_w6_vm_hook_c_func_pointers; use crate::capi_vm_hook_table::CapiVMHookTable; /// Opaque handle to an executor. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec2_executor_t; +pub struct vm_exec_w6_executor_t; pub struct CapiExecutor { pub(crate) opcode_config: Arc, @@ -39,24 +39,24 @@ pub struct CapiExecutor { /// `hook_table` is copied, so the caller is free to release it as soon as this returns. Every /// entry must be populated; see `CapiVMHookTable::copy_from`. /// -/// On success writes the executor to `executor_out` and returns `VM_EXEC2_OK`. The caller -/// owns the result and must release it with `vm_exec2_executor_destroy`. +/// On success writes the executor to `executor_out` and returns `VM_EXEC_W6_OK`. The caller +/// owns the result and must release it with `vm_exec_w6_executor_destroy`. /// /// # Safety /// /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] -pub unsafe extern "C" fn vm_exec2_new_executor( - executor_out: *mut *mut vm_exec2_executor_t, - config: *const vm_exec2_executor_config_t, - hook_table: *const vm_exec2_vm_hook_c_func_pointers, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +pub unsafe extern "C" fn vm_exec_w6_new_executor( + executor_out: *mut *mut vm_exec_w6_executor_t, + config: *const vm_exec_w6_executor_config_t, + hook_table: *const vm_exec_w6_vm_hook_c_func_pointers, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { if executor_out.is_null() || config.is_null() || hook_table.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; } let opcode_config = match unsafe { (*config).to_opcode_config() } { @@ -70,10 +70,10 @@ pub unsafe extern "C" fn vm_exec2_new_executor( }; unsafe { - *executor_out = Box::into_raw(Box::new(capi_executor)) as *mut vm_exec2_executor_t; + *executor_out = Box::into_raw(Box::new(capi_executor)) as *mut vm_exec_w6_executor_t; } - vm_exec2_status_t::VM_EXEC2_OK + vm_exec_w6_status_t::VM_EXEC_W6_OK } /// Destroys an executor. Does nothing if `executor` is null. @@ -86,7 +86,7 @@ pub unsafe extern "C" fn vm_exec2_new_executor( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -pub unsafe extern "C" fn vm_exec2_executor_destroy(executor: *mut vm_exec2_executor_t) { +pub unsafe extern "C" fn vm_exec_w6_executor_destroy(executor: *mut vm_exec_w6_executor_t) { if !executor.is_null() { drop(unsafe { Box::from_raw(executor as *mut CapiExecutor) }); } diff --git a/c-api-new/src/capi_instance.rs b/c-api-wasmer6/src/capi_instance.rs similarity index 66% rename from c-api-new/src/capi_instance.rs rename to c-api-wasmer6/src/capi_instance.rs index bdb96eb0..c1a070c3 100644 --- a/c-api-new/src/capi_instance.rs +++ b/c-api-wasmer6/src/capi_instance.rs @@ -13,16 +13,16 @@ use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalInstance; use std::ffi::CStr; use std::slice; -use crate::capi_config::vm_exec2_compilation_options_t; -use crate::capi_executor::{CapiExecutor, vm_exec2_executor_t}; -use crate::capi_status::{vm_exec2_status_t, write_error}; +use crate::capi_config::vm_exec_w6_compilation_options_t; +use crate::capi_executor::{CapiExecutor, vm_exec_w6_executor_t}; +use crate::capi_status::{vm_exec_w6_status_t, write_error}; use crate::capi_strings::{copy_pipe_joined, pipe_joined_length}; use crate::capi_vm_hook_context::CapiVMHooksBuilder; /// Opaque handle to an instance. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec2_instance_t; +pub struct vm_exec_w6_instance_t; pub struct CapiInstance { pub(crate) content: ExperimentalInstance, @@ -35,44 +35,44 @@ pub struct CapiInstance { #[allow(non_camel_case_types)] #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum vm_exec2_call_result_kind_t { +pub enum vm_exec_w6_call_result_kind_t { /// The function returned normally. - VM_EXEC2_CALL_OK = 0, + VM_EXEC_W6_CALL_OK = 0, /// No exported function by that name. - VM_EXEC2_CALL_FUNCTION_NOT_FOUND = 1, + VM_EXEC_W6_CALL_FUNCTION_NOT_FOUND = 1, /// The instance trapped for a reason that is neither a VM hook's doing nor a /// middleware breakpoint. The message is in the error buffer. - VM_EXEC2_CALL_RUNTIME_ERROR = 2, + VM_EXEC_W6_CALL_RUNTIME_ERROR = 2, /// A VM hook asked for execution to stop. `early_exit_code` says which; the message, if /// any, is in the error buffer, and is informational — the VM has already recorded its /// own. - VM_EXEC2_CALL_EARLY_EXIT = 3, + VM_EXEC_W6_CALL_EARLY_EXIT = 3, /// A middleware stopped execution. `breakpoint` says which. - VM_EXEC2_CALL_BREAKPOINT = 4, + VM_EXEC_W6_CALL_BREAKPOINT = 4, } -/// The out-struct of `vm_exec2_instance_call`. +/// The out-struct of `vm_exec_w6_instance_call`. #[allow(non_camel_case_types)] #[repr(C)] #[derive(Debug, Clone, Copy)] -pub struct vm_exec2_call_result_t { - pub kind: vm_exec2_call_result_kind_t, +pub struct vm_exec_w6_call_result_t { + pub kind: vm_exec_w6_call_result_kind_t, - /// Meaningful only for `VM_EXEC2_CALL_EARLY_EXIT`. + /// Meaningful only for `VM_EXEC_W6_CALL_EARLY_EXIT`. pub early_exit_code: u64, - /// Meaningful only for `VM_EXEC2_CALL_BREAKPOINT`. Values match `BreakpointValue`: + /// Meaningful only for `VM_EXEC_W6_CALL_BREAKPOINT`. Values match `BreakpointValue`: /// 4 = out of gas, 5 = memory limit. pub breakpoint: u64, } -impl vm_exec2_call_result_t { - fn of_kind(kind: vm_exec2_call_result_kind_t) -> Self { - vm_exec2_call_result_t { +impl vm_exec_w6_call_result_t { + fn of_kind(kind: vm_exec_w6_call_result_kind_t) -> Self { + vm_exec_w6_call_result_t { kind, early_exit_code: 0, breakpoint: 0, @@ -93,19 +93,19 @@ impl vm_exec2_call_result_t { #[allow(clippy::cast_ptr_alignment)] #[allow(clippy::too_many_arguments)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] -pub unsafe extern "C" fn vm_exec2_new_instance( - executor: *mut vm_exec2_executor_t, - instance_out: *mut *mut vm_exec2_instance_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +pub unsafe extern "C" fn vm_exec_w6_new_instance( + executor: *mut vm_exec_w6_executor_t, + instance_out: *mut *mut vm_exec_w6_instance_t, wasm_bytes: *const u8, wasm_bytes_len: u32, - options: *const vm_exec2_compilation_options_t, + options: *const vm_exec_w6_compilation_options_t, go_vm_hooks: usize, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { if executor.is_null() || instance_out.is_null() || wasm_bytes.is_null() || options.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; } let capi_executor = unsafe { &*(executor as *const CapiExecutor) }; @@ -129,11 +129,11 @@ pub unsafe extern "C" fn vm_exec2_new_instance( }; unsafe { - *instance_out = - Box::into_raw(Box::new(CapiInstance { content: instance })) as *mut vm_exec2_instance_t; + *instance_out = Box::into_raw(Box::new(CapiInstance { content: instance })) + as *mut vm_exec_w6_instance_t; } - vm_exec2_status_t::VM_EXEC2_OK + vm_exec_w6_status_t::VM_EXEC_W6_OK } /// Destroys an instance. Does nothing if `instance` is null. @@ -143,7 +143,7 @@ pub unsafe extern "C" fn vm_exec2_new_instance( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -pub unsafe extern "C" fn vm_exec2_instance_destroy(instance: *mut vm_exec2_instance_t) { +pub unsafe extern "C" fn vm_exec_w6_instance_destroy(instance: *mut vm_exec_w6_instance_t) { if !instance.is_null() { drop(unsafe { Box::from_raw(instance as *mut CapiInstance) }); } @@ -151,7 +151,7 @@ pub unsafe extern "C" fn vm_exec2_instance_destroy(instance: *mut vm_exec2_insta /// Calls an exported function, with a gas limit for this call. /// -/// Always writes `result_out` when it returns `VM_EXEC2_OK`. A non-`OK` status means the +/// Always writes `result_out` when it returns `VM_EXEC_W6_OK`. A non-`OK` status means the /// call could not be attempted at all, which is distinct from a call that was attempted and /// failed — the latter is reported through `result_out.kind`. /// @@ -160,17 +160,17 @@ pub unsafe extern "C" fn vm_exec2_instance_destroy(instance: *mut vm_exec2_insta /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] -pub unsafe extern "C" fn vm_exec2_instance_call( - instance: *mut vm_exec2_instance_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +pub unsafe extern "C" fn vm_exec_w6_instance_call( + instance: *mut vm_exec_w6_instance_t, func_name: *const c_char, points_limit: u64, - result_out: *mut vm_exec2_call_result_t, + result_out: *mut vm_exec_w6_call_result_t, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { if instance.is_null() || func_name.is_null() || result_out.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; } let capi_instance = unsafe { &mut *(instance as *mut CapiInstance) }; @@ -181,28 +181,28 @@ pub unsafe extern "C" fn vm_exec2_instance_call( let result = match capi_instance.content.call(func_name, points_limit) { InstanceCallResult::Ok => { - vm_exec2_call_result_t::of_kind(vm_exec2_call_result_kind_t::VM_EXEC2_CALL_OK) + vm_exec_w6_call_result_t::of_kind(vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_OK) } - InstanceCallResult::FunctionNotFound => vm_exec2_call_result_t::of_kind( - vm_exec2_call_result_kind_t::VM_EXEC2_CALL_FUNCTION_NOT_FOUND, + InstanceCallResult::FunctionNotFound => vm_exec_w6_call_result_t::of_kind( + vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_FUNCTION_NOT_FOUND, ), InstanceCallResult::RuntimeError(err) => { write_error(err, err_buf, err_buf_len); - vm_exec2_call_result_t::of_kind( - vm_exec2_call_result_kind_t::VM_EXEC2_CALL_RUNTIME_ERROR, + vm_exec_w6_call_result_t::of_kind( + vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_RUNTIME_ERROR, ) } InstanceCallResult::VMHooksEarlyExit(early_exit) => { // Informational: the hook has already recorded its own message on the VM side. write_error(&early_exit.message, err_buf, err_buf_len); - vm_exec2_call_result_t { - kind: vm_exec2_call_result_kind_t::VM_EXEC2_CALL_EARLY_EXIT, + vm_exec_w6_call_result_t { + kind: vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_EARLY_EXIT, early_exit_code: early_exit.code, breakpoint: 0, } } - InstanceCallResult::Breakpoint(breakpoint) => vm_exec2_call_result_t { - kind: vm_exec2_call_result_kind_t::VM_EXEC2_CALL_BREAKPOINT, + InstanceCallResult::Breakpoint(breakpoint) => vm_exec_w6_call_result_t { + kind: vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_BREAKPOINT, early_exit_code: 0, breakpoint: breakpoint_as_u64(breakpoint), }, @@ -212,7 +212,7 @@ pub unsafe extern "C" fn vm_exec2_instance_call( *result_out = result; } - vm_exec2_status_t::VM_EXEC2_OK + vm_exec_w6_status_t::VM_EXEC_W6_OK } fn breakpoint_as_u64(breakpoint: BreakpointValue) -> u64 { @@ -226,15 +226,15 @@ fn breakpoint_as_u64(breakpoint: BreakpointValue) -> u64 { /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] -pub unsafe extern "C" fn vm_exec2_instance_get_points_used( - instance: *mut vm_exec2_instance_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +pub unsafe extern "C" fn vm_exec_w6_instance_get_points_used( + instance: *mut vm_exec_w6_instance_t, points_out: *mut u64, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { if instance.is_null() || points_out.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; } let capi_instance = unsafe { &mut *(instance as *mut CapiInstance) }; @@ -243,7 +243,7 @@ pub unsafe extern "C" fn vm_exec2_instance_get_points_used( unsafe { *points_out = points; } - vm_exec2_status_t::VM_EXEC2_OK + vm_exec_w6_status_t::VM_EXEC_W6_OK } Err(err) => write_error(err, err_buf, err_buf_len), } @@ -258,12 +258,12 @@ pub unsafe extern "C" fn vm_exec2_instance_get_points_used( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec2_instance_check_signatures( - instance: *const vm_exec2_instance_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w6_instance_check_signatures( + instance: *const vm_exec_w6_instance_t, ) -> c_int { if instance.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; @@ -279,36 +279,36 @@ pub unsafe extern "C" fn vm_exec2_instance_check_signatures( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec2_instance_has_function( - instance: *const vm_exec2_instance_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w6_instance_has_function( + instance: *const vm_exec_w6_instance_t, func_name: *const c_char, ) -> c_int { if instance.is_null() || func_name.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; let Ok(func_name) = unsafe { CStr::from_ptr(func_name) }.to_str() else { - return vm_exec2_status_t::VM_EXEC2_ERROR.as_c_int(); + return vm_exec_w6_status_t::VM_EXEC_W6_ERROR.as_c_int(); }; c_int::from(capi_instance.content.has_function(func_name)) } -/// Buffer length needed by `vm_exec2_instance_exported_function_names`. +/// Buffer length needed by `vm_exec_w6_instance_exported_function_names`. /// /// # Safety /// /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec2_instance_exported_function_names_length( - instance: *const vm_exec2_instance_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names_length( + instance: *const vm_exec_w6_instance_t, ) -> c_int { if instance.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; @@ -325,14 +325,14 @@ pub unsafe extern "C" fn vm_exec2_instance_exported_function_names_length( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec2_instance_exported_function_names( - instance: *const vm_exec2_instance_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names( + instance: *const vm_exec_w6_instance_t, dest: *mut c_char, dest_len: c_int, ) -> c_int { if instance.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; @@ -345,19 +345,19 @@ pub unsafe extern "C" fn vm_exec2_instance_exported_function_names( } } -/// Buffer length needed by `vm_exec2_instance_imported_function_names`. +/// Buffer length needed by `vm_exec_w6_instance_imported_function_names`. /// /// # Safety /// /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec2_instance_imported_function_names_length( - instance: *const vm_exec2_instance_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names_length( + instance: *const vm_exec_w6_instance_t, ) -> c_int { if instance.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; @@ -374,14 +374,14 @@ pub unsafe extern "C" fn vm_exec2_instance_imported_function_names_length( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec2_instance_imported_function_names( - instance: *const vm_exec2_instance_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names( + instance: *const vm_exec_w6_instance_t, dest: *mut c_char, dest_len: c_int, ) -> c_int { if instance.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int(); + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; diff --git a/c-api-new/src/capi_instance_state.rs b/c-api-wasmer6/src/capi_instance_state.rs similarity index 74% rename from c-api-new/src/capi_instance_state.rs rename to c-api-wasmer6/src/capi_instance_state.rs index 64406909..83b45be3 100644 --- a/c-api-new/src/capi_instance_state.rs +++ b/c-api-wasmer6/src/capi_instance_state.rs @@ -2,7 +2,7 @@ //! //! Every function here takes a hook context and is valid only while the hook that was handed //! that context is still running. See `capi_vm_hook_context` for how that is enforced; a stale -//! context yields `VM_EXEC2_STALE_CONTEXT` rather than a dereference of a dead borrow. +//! context yields `VM_EXEC_W6_STALE_CONTEXT` rather than a dereference of a dead borrow. //! //! Bounds checking lives on this side of the boundary, not in Go. The executor is the only //! side that knows the real extent of the instance memory, and the rules are observable by @@ -16,14 +16,14 @@ use multiversx_chain_vm_executor::{ }; use std::slice; -use crate::capi_status::{vm_exec2_status_t, write_error, write_error_count}; -use crate::capi_vm_hook_context::{ContextError, vm_exec2_hook_context_t, with_instance_state}; +use crate::capi_status::{vm_exec_w6_status_t, write_error, write_error_count}; +use crate::capi_vm_hook_context::{ContextError, vm_exec_w6_hook_context_t, with_instance_state}; impl ContextError { - fn as_status(self) -> vm_exec2_status_t { + fn as_status(self) -> vm_exec_w6_status_t { match self { - ContextError::NullContext => vm_exec2_status_t::VM_EXEC2_NULL_POINTER, - ContextError::Stale => vm_exec2_status_t::VM_EXEC2_STALE_CONTEXT, + ContextError::NullContext => vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER, + ContextError::Stale => vm_exec_w6_status_t::VM_EXEC_W6_STALE_CONTEXT, } } } @@ -44,9 +44,9 @@ impl ContextError { /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC.as_i64())] -pub unsafe extern "C" fn vm_exec2_state_mem_load( - ctx: *mut vm_exec2_hook_context_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_i64())] +pub unsafe extern "C" fn vm_exec_w6_state_mem_load( + ctx: *mut vm_exec_w6_hook_context_t, mem_ptr: i32, dest: *mut u8, dest_len: i32, @@ -85,20 +85,20 @@ pub unsafe extern "C" fn vm_exec2_state_mem_load( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] -pub unsafe extern "C" fn vm_exec2_state_mem_store( - ctx: *mut vm_exec2_hook_context_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +pub unsafe extern "C" fn vm_exec_w6_state_mem_store( + ctx: *mut vm_exec_w6_hook_context_t, mem_ptr: i32, src: *const u8, src_len: i32, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { if src_len < 0 { return write_error("negative length", err_buf, err_buf_len); } if src.is_null() && src_len > 0 { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; } let stored = unsafe { @@ -115,7 +115,7 @@ pub unsafe extern "C" fn vm_exec2_state_mem_store( match stored { Err(context_err) => context_err.as_status(), Ok(Err(err)) => write_error(err, err_buf, err_buf_len), - Ok(Ok(())) => vm_exec2_status_t::VM_EXEC2_OK, + Ok(Ok(())) => vm_exec_w6_status_t::VM_EXEC_W6_OK, } } @@ -125,15 +125,15 @@ pub unsafe extern "C" fn vm_exec2_state_mem_store( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] -pub unsafe extern "C" fn vm_exec2_state_mem_length( - ctx: *mut vm_exec2_hook_context_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +pub unsafe extern "C" fn vm_exec_w6_state_mem_length( + ctx: *mut vm_exec_w6_hook_context_t, length_out: *mut u64, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { if length_out.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; } let length = unsafe { with_instance_state(ctx, |state| state.memory_length()) }; @@ -145,7 +145,7 @@ pub unsafe extern "C" fn vm_exec2_state_mem_length( unsafe { *length_out = length; } - vm_exec2_status_t::VM_EXEC2_OK + vm_exec_w6_status_t::VM_EXEC_W6_OK } } } @@ -158,16 +158,16 @@ pub unsafe extern "C" fn vm_exec2_state_mem_length( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] -pub unsafe extern "C" fn vm_exec2_state_mem_grow( - ctx: *mut vm_exec2_hook_context_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +pub unsafe extern "C" fn vm_exec_w6_state_mem_grow( + ctx: *mut vm_exec_w6_hook_context_t, by_num_pages: u32, prev_pages_out: *mut u32, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { if prev_pages_out.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; } let grown = unsafe { with_instance_state(ctx, |state| state.memory_grow(by_num_pages)) }; @@ -179,7 +179,7 @@ pub unsafe extern "C" fn vm_exec2_state_mem_grow( unsafe { *prev_pages_out = prev_pages; } - vm_exec2_status_t::VM_EXEC2_OK + vm_exec_w6_status_t::VM_EXEC_W6_OK } } } @@ -190,15 +190,15 @@ pub unsafe extern "C" fn vm_exec2_state_mem_grow( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] -pub unsafe extern "C" fn vm_exec2_state_get_points_used( - ctx: *mut vm_exec2_hook_context_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +pub unsafe extern "C" fn vm_exec_w6_state_get_points_used( + ctx: *mut vm_exec_w6_hook_context_t, points_out: *mut u64, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { if points_out.is_null() { - return vm_exec2_status_t::VM_EXEC2_NULL_POINTER; + return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; } let points = unsafe { with_instance_state(ctx, |state| state.get_points_used()) }; @@ -210,7 +210,7 @@ pub unsafe extern "C" fn vm_exec2_state_get_points_used( unsafe { *points_out = points; } - vm_exec2_status_t::VM_EXEC2_OK + vm_exec_w6_status_t::VM_EXEC_W6_OK } } } @@ -221,18 +221,18 @@ pub unsafe extern "C" fn vm_exec2_state_get_points_used( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec2_status_t::VM_EXEC2_PANIC)] -pub unsafe extern "C" fn vm_exec2_state_set_points_used( - ctx: *mut vm_exec2_hook_context_t, +#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +pub unsafe extern "C" fn vm_exec_w6_state_set_points_used( + ctx: *mut vm_exec_w6_hook_context_t, points: u64, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { let set = unsafe { with_instance_state(ctx, |state| state.set_points_used(points)) }; match set { Err(context_err) => context_err.as_status(), Ok(Err(err)) => write_error(err, err_buf, err_buf_len), - Ok(Ok(())) => vm_exec2_status_t::VM_EXEC2_OK, + Ok(Ok(())) => vm_exec_w6_status_t::VM_EXEC_W6_OK, } } diff --git a/c-api-new/src/capi_logger.rs b/c-api-wasmer6/src/capi_logger.rs similarity index 88% rename from c-api-new/src/capi_logger.rs rename to c-api-wasmer6/src/capi_logger.rs index 90a238e7..33365a8b 100644 --- a/c-api-new/src/capi_logger.rs +++ b/c-api-wasmer6/src/capi_logger.rs @@ -12,7 +12,7 @@ static PANIC_HANDLER: Once = Once::new(); /// Installs a no-op panic hook, so a caught panic does not also print a backtrace. /// /// A panic must never unwind across `extern "C"`; the `capi_safe_unwind` attribute catches -/// it and turns it into `VM_EXEC2_PANIC`. This only silences the default hook's output. +/// it and turns it into `VM_EXEC_W6_PANIC`. This only silences the default hook's output. pub fn set_panic_handler() { PANIC_HANDLER.call_once(|| { std::panic::set_hook(Box::new(|_| {})); diff --git a/c-api/src/capi_opcode_cost.rs b/c-api-wasmer6/src/capi_opcode_cost.rs similarity index 100% rename from c-api/src/capi_opcode_cost.rs rename to c-api-wasmer6/src/capi_opcode_cost.rs diff --git a/c-api-new/src/capi_status.rs b/c-api-wasmer6/src/capi_status.rs similarity index 82% rename from c-api-new/src/capi_status.rs rename to c-api-wasmer6/src/capi_status.rs index fe5db97b..bfdcd5d1 100644 --- a/c-api-new/src/capi_status.rs +++ b/c-api-wasmer6/src/capi_status.rs @@ -13,33 +13,33 @@ use std::{ptr, slice}; /// Result of a C API call. /// -/// `VM_EXEC2_OK` is zero and every failure is negative, so the functions that return a +/// `VM_EXEC_W6_OK` is zero and every failure is negative, so the functions that return a /// count (the memory loads) can share the same code space: a non-negative result is a /// count, a negative one is one of these. #[allow(non_camel_case_types)] #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum vm_exec2_status_t { +pub enum vm_exec_w6_status_t { /// The call succeeded. - VM_EXEC2_OK = 0, + VM_EXEC_W6_OK = 0, /// The call failed; the error message was written to the provided error buffer. - VM_EXEC2_ERROR = -1, + VM_EXEC_W6_ERROR = -1, /// A pointer argument that must not be null was null. No message is produced, because /// a null pointer may well be the error buffer itself. - VM_EXEC2_NULL_POINTER = -2, + VM_EXEC_W6_NULL_POINTER = -2, /// A hook context was used outside the host call it belongs to. See /// [`crate::capi_vm_hook_context`]. - VM_EXEC2_STALE_CONTEXT = -3, + VM_EXEC_W6_STALE_CONTEXT = -3, /// A panic was caught at the boundary. Rust panics must never unwind into C, so every /// entry point traps them and reports this instead. - VM_EXEC2_PANIC = -4, + VM_EXEC_W6_PANIC = -4, } -impl vm_exec2_status_t { +impl vm_exec_w6_status_t { pub fn as_c_int(self) -> c_int { self as c_int } @@ -54,7 +54,7 @@ impl vm_exec2_status_t { /// The message is truncated to fit and always NUL-terminated. Truncation is silent and /// deliberate: the alternative is a second call to ask for the length, which is the /// round-trip this API is getting rid of. Callers size the buffer once; see -/// `VM_EXEC2_ERROR_BUF_RECOMMENDED_LEN`. +/// `VM_EXEC_W6_ERROR_BUF_RECOMMENDED_LEN`. /// /// A null buffer, or a buffer with no room for the terminator, is accepted and ignored, so /// that a caller that does not care about the message can pass `(NULL, 0)`. @@ -62,7 +62,7 @@ pub(crate) fn write_error( message: impl Display, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec2_status_t { +) -> vm_exec_w6_status_t { if !err_buf.is_null() && err_buf_len > 0 { let message = message.to_string(); let capacity = err_buf_len as usize - 1; // room for the NUL @@ -81,7 +81,7 @@ pub(crate) fn write_error( } } - vm_exec2_status_t::VM_EXEC2_ERROR + vm_exec_w6_status_t::VM_EXEC_W6_ERROR } /// Writes a message and returns it as a negative count, for the functions that return one. @@ -98,13 +98,13 @@ pub(crate) fn write_error_count( /// Exported so the Go side has one place to take the number from rather than picking its /// own. #[unsafe(no_mangle)] -pub static VM_EXEC2_ERROR_BUF_RECOMMENDED_LEN: c_int = 1024; +pub static VM_EXEC_W6_ERROR_BUF_RECOMMENDED_LEN: c_int = 1024; #[cfg(test)] mod tests { use super::*; - fn write_to_buf(message: &str, len: usize) -> (vm_exec2_status_t, String) { + fn write_to_buf(message: &str, len: usize) -> (vm_exec_w6_status_t, String) { let mut buf = vec![0i8; len]; let status = write_error(message, buf.as_mut_ptr() as *mut c_char, len as c_int); let bytes: Vec = buf @@ -118,7 +118,7 @@ mod tests { #[test] fn error_message_round_trips() { let (status, written) = write_to_buf("boom", 64); - assert_eq!(status, vm_exec2_status_t::VM_EXEC2_ERROR); + assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_ERROR); assert_eq!(written, "boom"); } @@ -139,22 +139,22 @@ mod tests { fn error_message_tolerates_no_buffer() { assert_eq!( write_error("boom", std::ptr::null_mut(), 0), - vm_exec2_status_t::VM_EXEC2_ERROR + vm_exec_w6_status_t::VM_EXEC_W6_ERROR ); assert_eq!( write_error("boom", std::ptr::null_mut(), 64), - vm_exec2_status_t::VM_EXEC2_ERROR + vm_exec_w6_status_t::VM_EXEC_W6_ERROR ); } #[test] fn status_codes_leave_room_for_counts() { - assert_eq!(vm_exec2_status_t::VM_EXEC2_OK.as_i64(), 0); + assert_eq!(vm_exec_w6_status_t::VM_EXEC_W6_OK.as_i64(), 0); for status in [ - vm_exec2_status_t::VM_EXEC2_ERROR, - vm_exec2_status_t::VM_EXEC2_NULL_POINTER, - vm_exec2_status_t::VM_EXEC2_STALE_CONTEXT, - vm_exec2_status_t::VM_EXEC2_PANIC, + vm_exec_w6_status_t::VM_EXEC_W6_ERROR, + vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER, + vm_exec_w6_status_t::VM_EXEC_W6_STALE_CONTEXT, + vm_exec_w6_status_t::VM_EXEC_W6_PANIC, ] { assert!(status.as_i64() < 0, "{status:?} must be negative"); } diff --git a/c-api-new/src/capi_strings.rs b/c-api-wasmer6/src/capi_strings.rs similarity index 100% rename from c-api-new/src/capi_strings.rs rename to c-api-wasmer6/src/capi_strings.rs diff --git a/c-api-new/src/capi_vm_hook_context.rs b/c-api-wasmer6/src/capi_vm_hook_context.rs similarity index 88% rename from c-api-new/src/capi_vm_hook_context.rs rename to c-api-wasmer6/src/capi_vm_hook_context.rs index 43ae59e3..8cf6d464 100644 --- a/c-api-new/src/capi_vm_hook_context.rs +++ b/c-api-wasmer6/src/capi_vm_hook_context.rs @@ -10,7 +10,7 @@ //! 1. **Lifetime.** Each activation gets a fresh, non-zero call token. The context records //! it, and the pool records which token is currently innermost. A state function accepts //! a context only while the two agree, so a context used after its hook returned is -//! rejected with `VM_EXEC2_STALE_CONTEXT` instead of dereferencing a dead borrow. +//! rejected with `VM_EXEC_W6_STALE_CONTEXT` instead of dereferencing a dead borrow. //! //! 2. **Exclusivity.** Contexts are activated in a stack, one per nesting level, so a hook //! that re-enters the VM (`executeOnDestContext` running a contract from inside a hook) @@ -42,12 +42,12 @@ use multiversx_chain_vm_executor_wasmer_experimental::{ use crate::capi_vm_hook_table::CapiVMHookTable; use crate::capi_vm_hooks::CapiVMHooks; -/// The value of `vm_exec2_hook_result_t::early_exit` that means "the hook returned normally". +/// The value of `vm_exec_w6_hook_result_t::early_exit` that means "the hook returned normally". /// /// A dedicated sentinel is needed because `0` is a real early-exit code: the Rust VM uses /// `ASYNC_CALL_EARLY_EXIT_CODE = 0` and the Go VM has the same async-call case, so "code 0" /// and "no code" have to stay distinguishable. -pub const VM_EXEC2_NO_EARLY_EXIT: u64 = u64::MAX; +pub const VM_EXEC_W6_NO_EARLY_EXIT: u64 = u64::MAX; /// What every VM hook returns. /// @@ -63,27 +63,27 @@ pub const VM_EXEC2_NO_EARLY_EXIT: u64 = u64::MAX; #[allow(non_camel_case_types)] #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct vm_exec2_hook_result_t { +pub struct vm_exec_w6_hook_result_t { /// The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for /// any hook that reports an early exit. pub value: i64, - /// The early-exit code, or `VM_EXEC2_NO_EARLY_EXIT`. + /// The early-exit code, or `VM_EXEC_W6_NO_EARLY_EXIT`. pub early_exit: u64, } -impl vm_exec2_hook_result_t { +impl vm_exec_w6_hook_result_t { /// The hook returned normally, with the given value (`0` for a `void` hook). pub fn ok(value: i64) -> Self { - vm_exec2_hook_result_t { + vm_exec_w6_hook_result_t { value, - early_exit: VM_EXEC2_NO_EARLY_EXIT, + early_exit: VM_EXEC_W6_NO_EARLY_EXIT, } } /// The hook requested an early exit with the given code. pub fn early_exit(code: u64) -> Self { - vm_exec2_hook_result_t { + vm_exec_w6_hook_result_t { value: 0, early_exit: code, } @@ -91,7 +91,7 @@ impl vm_exec2_hook_result_t { /// The early-exit code, if there is one. pub fn early_exit_code(self) -> Option { - if self.early_exit == VM_EXEC2_NO_EARLY_EXIT { + if self.early_exit == VM_EXEC_W6_NO_EARLY_EXIT { None } else { Some(self.early_exit) @@ -105,7 +105,7 @@ impl vm_exec2_hook_result_t { /// Rust, and the whole pointer is what travels back into the state functions. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec2_hook_context_t { +pub struct vm_exec_w6_hook_context_t { /// The caller's VM hooks handle for this instance. Read by the caller, never by Rust. /// /// An opaque word rather than a pointer, deliberately: Rust only stores it and hands it @@ -132,9 +132,9 @@ pub struct vm_exec2_hook_context_t { /// by the call token. type ErasedInstanceState = ExperimentalInstanceState<'static>; -impl vm_exec2_hook_context_t { +impl vm_exec_w6_hook_context_t { fn inactive() -> Self { - vm_exec2_hook_context_t { + vm_exec_w6_hook_context_t { go_vm_hooks: 0, state: std::ptr::null_mut(), pool: std::ptr::null(), @@ -150,11 +150,11 @@ impl vm_exec2_hook_context_t { /// /// # Safety /// -/// `ctx` must either be null or point to a `vm_exec2_hook_context_t` that this crate +/// `ctx` must either be null or point to a `vm_exec_w6_hook_context_t` that this crate /// activated. Pooled contexts are never freed before their instance, so a context pointer /// obtained from a hook always satisfies this, live or not. pub(crate) unsafe fn with_instance_state( - ctx: *mut vm_exec2_hook_context_t, + ctx: *mut vm_exec_w6_hook_context_t, f: impl FnOnce(&mut &mut ExperimentalInstanceState<'static>) -> R, ) -> Result { if ctx.is_null() { @@ -194,12 +194,12 @@ pub struct CapiVMHookContextPool { /// One boxed context per nesting level. /// /// The `Box` is load-bearing and not the redundancy clippy takes it for: a - /// `Vec` would move its elements when it grows, invalidating + /// `Vec` would move its elements when it grows, invalidating /// context pointers that outer hook invocations are still holding. Boxing each context /// individually pins its address for the instance's lifetime, which is what lets a /// stale pointer be read and rejected instead of being undefined behaviour. #[allow(clippy::vec_box)] - slots: Vec>, + slots: Vec>, /// Nesting depth of the currently active call. depth: usize, @@ -236,7 +236,7 @@ impl CapiVMHooksBuilder { pub fn with_hook_context( &self, instance_state: &mut ExperimentalInstanceState<'_>, - f: impl FnOnce(*mut vm_exec2_hook_context_t) -> R, + f: impl FnOnce(*mut vm_exec_w6_hook_context_t) -> R, ) -> R { let state_ptr = (instance_state as *mut ExperimentalInstanceState<'_>).cast::(); @@ -258,7 +258,7 @@ impl CapiVMHooksBuilder { pub unsafe fn with_hook_context_raw( &self, state: *mut c_void, - f: impl FnOnce(*mut vm_exec2_hook_context_t) -> R, + f: impl FnOnce(*mut vm_exec_w6_hook_context_t) -> R, ) -> R { let (ctx_ptr, parent_token) = { let mut pool = self.pool.borrow_mut(); @@ -266,7 +266,7 @@ impl CapiVMHooksBuilder { let depth = pool.depth; if depth == pool.slots.len() { pool.slots - .push(Box::new(vm_exec2_hook_context_t::inactive())); + .push(Box::new(vm_exec_w6_hook_context_t::inactive())); } pool.next_token += 1; @@ -283,7 +283,7 @@ impl CapiVMHooksBuilder { slot.pool = pool_ptr; slot.token = token; - (&mut **slot as *mut vm_exec2_hook_context_t, parent_token) + (&mut **slot as *mut vm_exec_w6_hook_context_t, parent_token) }; // The pool borrow is released across `f`, because the hook will call back into the @@ -299,7 +299,7 @@ impl CapiVMHooksBuilder { result } - fn deactivate(&self, ctx_ptr: *mut vm_exec2_hook_context_t, parent_token: u64) { + fn deactivate(&self, ctx_ptr: *mut vm_exec_w6_hook_context_t, parent_token: u64) { let mut pool = self.pool.borrow_mut(); pool.depth = pool.depth.saturating_sub(1); pool.active_token = parent_token; @@ -316,7 +316,7 @@ impl CapiVMHooksBuilder { /// Runs the deactivation even if the hook body unwinds. struct DeactivateOnDrop<'a> { builder: &'a CapiVMHooksBuilder, - ctx_ptr: *mut vm_exec2_hook_context_t, + ctx_ptr: *mut vm_exec_w6_hook_context_t, parent_token: u64, } @@ -346,21 +346,21 @@ mod tests { #[test] fn hook_result_distinguishes_code_zero_from_no_code() { - assert_eq!(vm_exec2_hook_result_t::ok(7).early_exit_code(), None); + assert_eq!(vm_exec_w6_hook_result_t::ok(7).early_exit_code(), None); assert_eq!( - vm_exec2_hook_result_t::early_exit(0).early_exit_code(), + vm_exec_w6_hook_result_t::early_exit(0).early_exit_code(), Some(0) ); assert_eq!( - vm_exec2_hook_result_t::early_exit(4).early_exit_code(), + vm_exec_w6_hook_result_t::early_exit(4).early_exit_code(), Some(4) ); } #[test] fn hook_result_fits_in_two_registers() { - assert_eq!(std::mem::size_of::(), 16); - assert_eq!(std::mem::align_of::(), 8); + assert_eq!(std::mem::size_of::(), 16); + assert_eq!(std::mem::align_of::(), 8); } #[test] @@ -371,7 +371,7 @@ mod tests { #[test] fn inactive_context_is_rejected() { - let mut ctx = vm_exec2_hook_context_t::inactive(); + let mut ctx = vm_exec_w6_hook_context_t::inactive(); let result = unsafe { with_instance_state(&mut ctx, |_| ()) }; assert_eq!(result, Err(ContextError::Stale)); } diff --git a/c-api-wasmer6/src/capi_vm_hook_pointers.rs b/c-api-wasmer6/src/capi_vm_hook_pointers.rs new file mode 100644 index 00000000..3c12fef9 --- /dev/null +++ b/c-api-wasmer6/src/capi_vm_hook_pointers.rs @@ -0,0 +1,300 @@ +// Code generated by vmhooks generator. DO NOT EDIT. + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +use crate::capi_vm_hook_context::{vm_exec_w6_hook_context_t, vm_exec_w6_hook_result_t}; + +/// The Go VM's hook implementations, as a table of C function pointers. +/// +/// Copied into Rust at executor creation; see CapiVMHookTable. +#[repr(C)] +#[derive(Clone)] +#[allow(non_camel_case_types)] +#[rustfmt::skip] +pub struct vm_exec_w6_vm_hook_c_func_pointers { + pub get_gas_left_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_shard_of_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32) -> vm_exec_w6_hook_result_t, + pub is_smart_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32) -> vm_exec_w6_hook_result_t, + pub signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, message_offset: i32, message_length: i32) -> vm_exec_w6_hook_result_t, + pub get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_block_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, nonce: i64, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_esdt_nft_name_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec_w6_hook_result_t, + pub get_esdt_nft_attribute_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec_w6_hook_result_t, + pub get_esdt_nft_uri_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec_w6_hook_result_t, + pub get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, value_handle: i32, properties_offset: i32, hash_offset: i32, name_offset: i32, attributes_offset: i32, creator_offset: i32, royalties_handle: i32, uris_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_esdt_local_roles_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, token_id_handle: i32) -> vm_exec_w6_hook_result_t, + pub validate_token_identifier_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, token_id_handle: i32) -> vm_exec_w6_hook_result_t, + pub transfer_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, + pub transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub transfer_esdt_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, nonce: i64, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, num_token_transfers: i32, token_transfers_args_length_offset: i32, token_transfer_data_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, data_length: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64) -> vm_exec_w6_hook_result_t, + pub set_async_context_callback_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, callback: i32, callback_length: i32, data: i32, data_length: i32, gas: i64) -> vm_exec_w6_hook_result_t, + pub upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, gas_limit: i64, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, + pub get_argument_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32) -> vm_exec_w6_hook_result_t, + pub get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32, arg_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_function_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, function_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_num_arguments_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, + pub storage_load_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, + pub storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub set_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, lock_timestamp: i64) -> vm_exec_w6_hook_result_t, + pub get_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, + pub is_storage_locked_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, + pub clear_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, + pub get_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub check_no_payment_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_esdt_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_esdt_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32, index: i32) -> vm_exec_w6_hook_result_t, + pub get_esdt_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_esdt_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32, index: i32) -> vm_exec_w6_hook_result_t, + pub get_esdt_token_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_esdt_token_nonce_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, index: i32) -> vm_exec_w6_hook_result_t, + pub get_current_esdt_nft_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32) -> vm_exec_w6_hook_result_t, + pub get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_esdt_token_type_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, index: i32) -> vm_exec_w6_hook_result_t, + pub get_num_esdt_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_call_value_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, call_value_offset: i32, token_name_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_call_value_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, call_value_offset: i32, token_name_offset: i32, index: i32) -> vm_exec_w6_hook_result_t, + pub is_reserved_function_name_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, name_handle: i32) -> vm_exec_w6_hook_result_t, + pub write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_pointer: i32, data_length: i32, topic_ptr: i32, num_topics: i32) -> vm_exec_w6_hook_result_t, + pub write_event_log_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, num_topics: i32, topic_lengths_offset: i32, topic_offset: i32, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, + pub get_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, pointer: i32) -> vm_exec_w6_hook_result_t, + pub get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, pointer: i32) -> vm_exec_w6_hook_result_t, + pub get_prev_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_prev_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_prev_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_prev_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_prev_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, pointer: i32) -> vm_exec_w6_hook_result_t, + pub get_block_round_time_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub epoch_start_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub epoch_start_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub epoch_start_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub finish_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, pointer: i32, length: i32) -> vm_exec_w6_hook_result_t, + pub execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, address_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, result_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, result_address_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_num_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub get_return_data_size_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_id: i32) -> vm_exec_w6_hook_result_t, + pub get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_id: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub clean_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub delete_from_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_id: i32) -> vm_exec_w6_hook_result_t, + pub get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_current_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub get_prev_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_original_caller_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_relayer_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, err_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, topics_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_id: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_multi_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, multi_call_value_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_all_transfers_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, transfer_call_values_list_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_back_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, esdt_transfers_value_handle: i32, egld_value_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32, properties_handle: i32, hash_handle: i32, name_handle: i32, attributes_handle: i32, creator_handle: i32, royalties_handle: i32, uris_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, type_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64, callback_closure_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_callback_closure_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, callback_closure_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, gas_limit: i64, arguments_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, address_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_execute_on_dest_context_with_error_return_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_multi_transfer_esdt_nft_execute_with_return_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_multi_transfer_esdt_nft_execute_by_user_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, user_handle: i32, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dst_handle: i32, value_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_is_esdt_frozen_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64) -> vm_exec_w6_hook_result_t, + pub managed_is_esdt_limited_transfer_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, token_id_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_is_esdt_paused_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, token_id_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_buffer_to_hex_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, source_handle: i32, dest_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_code_metadata_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, response_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_get_code_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, code_hash_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_is_builtin_function_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, function_name_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_new_from_parts_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, integral_part: i32, fractional_part: i32, exponent: i32) -> vm_exec_w6_hook_result_t, + pub big_float_new_from_frac_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, numerator: i64, denominator: i64) -> vm_exec_w6_hook_result_t, + pub big_float_new_from_sci_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, significand: i64, exponent: i64) -> vm_exec_w6_hook_result_t, + pub big_float_add_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_div_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_clone_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32, exponent: i32) -> vm_exec_w6_hook_result_t, + pub big_float_floor_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_ceil_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_truncate_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, value: i64) -> vm_exec_w6_hook_result_t, + pub big_float_is_int_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_set_big_int_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_get_const_pi_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_float_get_const_e_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, source_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_esdt_call_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, index: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, result: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_esdt_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_new_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, small_value: i64) -> vm_exec_w6_hook_result_t, + pub big_int_unsigned_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_signed_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec_w6_hook_result_t, + pub big_int_set_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec_w6_hook_result_t, + pub big_int_set_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec_w6_hook_result_t, + pub big_int_is_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_get_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, value: i64) -> vm_exec_w6_hook_result_t, + pub big_int_add_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_tdiv_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_tmod_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_ediv_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_emod_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_log2_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op1_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_not_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_and_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_or_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_xor_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_shr_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec_w6_hook_result_t, + pub big_int_shl_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec_w6_hook_result_t, + pub big_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32) -> vm_exec_w6_hook_result_t, + pub big_int_to_string_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, big_int_handle: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_new_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub mbuffer_new_from_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_get_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_get_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_get_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_copy_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_eq_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle1: i32, m_buffer_handle2: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_set_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_set_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, starting_position: i32, data_length: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_append_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, accumulator_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_append_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, accumulator_handle: i32, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_to_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_to_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_from_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_from_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_to_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_to_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_from_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec_w6_hook_result_t, + pub mbuffer_from_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec_w6_hook_result_t, + pub mbuffer_to_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_from_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, source_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, key_handle: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_finish_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, source_handle: i32) -> vm_exec_w6_hook_result_t, + pub mbuffer_set_random_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, length: i32) -> vm_exec_w6_hook_result_t, + pub managed_map_new_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, + pub managed_map_put_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_map_handle: i32, key_handle: i32, value_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_map_get_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_map_remove_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_map_contains_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_map_handle: i32, key_handle: i32) -> vm_exec_w6_hook_result_t, + pub small_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32) -> vm_exec_w6_hook_result_t, + pub small_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32) -> vm_exec_w6_hook_result_t, + pub small_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, value: i64) -> vm_exec_w6_hook_result_t, + pub small_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, value: i64) -> vm_exec_w6_hook_result_t, + pub small_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec_w6_hook_result_t, + pub small_int_storage_store_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec_w6_hook_result_t, + pub small_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, + pub small_int_storage_load_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, + pub int64get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32) -> vm_exec_w6_hook_result_t, + pub int64finish_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, value: i64) -> vm_exec_w6_hook_result_t, + pub int64storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec_w6_hook_result_t, + pub int64storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, + pub sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec_w6_hook_result_t, + pub keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec_w6_hook_result_t, + pub ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec_w6_hook_result_t, + pub verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, + pub verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, + pub verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32, hash_type: i32) -> vm_exec_w6_hook_result_t, + pub managed_verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32, hash_type: i32) -> vm_exec_w6_hook_result_t, + pub verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, + pub encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, r_offset: i32, r_length: i32, s_offset: i32, s_length: i32, sig_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, r_handle: i32, s_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, + pub add_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, fst_point_xhandle: i32, fst_point_yhandle: i32, snd_point_xhandle: i32, snd_point_yhandle: i32) -> vm_exec_w6_hook_result_t, + pub double_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec_w6_hook_result_t, + pub is_on_curve_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec_w6_hook_result_t, + pub scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, + pub managed_scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, + pub scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, + pub managed_scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, + pub marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, + pub managed_unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, + pub unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, + pub managed_unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, + pub generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, + pub managed_generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, + pub create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, + pub managed_create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_handle: i32) -> vm_exec_w6_hook_result_t, + pub get_curve_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, ec_handle: i32) -> vm_exec_w6_hook_result_t, + pub get_priv_key_byte_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, ec_handle: i32) -> vm_exec_w6_hook_result_t, + pub elliptic_curve_get_values_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, ec_handle: i32, field_order_handle: i32, base_point_order_handle: i32, eq_constant_handle: i32, x_base_point_handle: i32, y_base_point_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_verify_secp256r1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_verify_blssignature_share_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, + pub managed_verify_blsaggregated_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, +} + +impl std::fmt::Debug for vm_exec_w6_vm_hook_c_func_pointers { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "vm_exec_w6_vm_hook_c_func_pointers") + } +} diff --git a/c-api-new/src/capi_vm_hook_table.rs b/c-api-wasmer6/src/capi_vm_hook_table.rs similarity index 82% rename from c-api-new/src/capi_vm_hook_table.rs rename to c-api-wasmer6/src/capi_vm_hook_table.rs index 1cb32f43..d548d4f6 100644 --- a/c-api-new/src/capi_vm_hook_table.rs +++ b/c-api-wasmer6/src/capi_vm_hook_table.rs @@ -7,14 +7,14 @@ use std::sync::Arc; -use crate::capi_vm_hook_pointers::vm_exec2_vm_hook_c_func_pointers; +use crate::capi_vm_hook_pointers::vm_exec_w6_vm_hook_c_func_pointers; /// An owned copy of the caller's hook table. /// /// Cloning is cheap and shares the copy; each instance's hooks builder holds one. #[derive(Clone, Debug)] pub struct CapiVMHookTable { - func_pointers: Arc, + func_pointers: Arc, } // The table is a block of plain function pointers with no interior mutability, and is only @@ -32,13 +32,13 @@ impl CapiVMHookTable { /// entry is called without a further null check, because a partially filled table is not /// something this API can detect: a null function pointer and a wrong one are /// indistinguishable here. - pub unsafe fn copy_from(func_pointers: *const vm_exec2_vm_hook_c_func_pointers) -> Self { + pub unsafe fn copy_from(func_pointers: *const vm_exec_w6_vm_hook_c_func_pointers) -> Self { CapiVMHookTable { func_pointers: Arc::new(unsafe { (*func_pointers).clone() }), } } - pub fn func_pointers(&self) -> &vm_exec2_vm_hook_c_func_pointers { + pub fn func_pointers(&self) -> &vm_exec_w6_vm_hook_c_func_pointers { &self.func_pointers } } diff --git a/c-api-new/src/capi_vm_hooks.rs b/c-api-wasmer6/src/capi_vm_hooks.rs similarity index 99% rename from c-api-new/src/capi_vm_hooks.rs rename to c-api-wasmer6/src/capi_vm_hooks.rs index 15fecd23..2000cf9e 100644 --- a/c-api-new/src/capi_vm_hooks.rs +++ b/c-api-wasmer6/src/capi_vm_hooks.rs @@ -6,8 +6,8 @@ use multiversx_chain_vm_executor::{MemLength, MemPtr, VMHooks, VMHooksEarlyExit}; -use crate::capi_vm_hook_context::{vm_exec2_hook_context_t, vm_exec2_hook_result_t}; -use crate::capi_vm_hook_pointers::vm_exec2_vm_hook_c_func_pointers; +use crate::capi_vm_hook_context::{vm_exec_w6_hook_context_t, vm_exec_w6_hook_result_t}; +use crate::capi_vm_hook_pointers::vm_exec_w6_vm_hook_c_func_pointers; /// Services one host call by dispatching each VM hook to the Go side. /// @@ -15,14 +15,14 @@ use crate::capi_vm_hook_pointers::vm_exec2_vm_hook_c_func_pointers; /// the Go side hands back to the state functions. It borrows the table rather than owning a /// copy, because the executor already owns one for the lifetime of the instance. pub struct CapiVMHooks<'a> { - ctx: *mut vm_exec2_hook_context_t, - func_pointers: &'a vm_exec2_vm_hook_c_func_pointers, + ctx: *mut vm_exec_w6_hook_context_t, + func_pointers: &'a vm_exec_w6_vm_hook_c_func_pointers, } impl<'a> CapiVMHooks<'a> { pub fn new( - ctx: *mut vm_exec2_hook_context_t, - func_pointers: &'a vm_exec2_vm_hook_c_func_pointers, + ctx: *mut vm_exec_w6_hook_context_t, + func_pointers: &'a vm_exec_w6_vm_hook_c_func_pointers, ) -> Self { CapiVMHooks { ctx, func_pointers } } @@ -51,21 +51,21 @@ fn early_exit(code: u64) -> VMHooksEarlyExit { VMHooksEarlyExit::new(code) } -fn unpack_void(result: vm_exec2_hook_result_t) -> Result<(), VMHooksEarlyExit> { +fn unpack_void(result: vm_exec_w6_hook_result_t) -> Result<(), VMHooksEarlyExit> { match result.early_exit_code() { Some(code) => Err(early_exit(code)), None => Ok(()), } } -fn unpack_i32(result: vm_exec2_hook_result_t) -> Result { +fn unpack_i32(result: vm_exec_w6_hook_result_t) -> Result { match result.early_exit_code() { Some(code) => Err(early_exit(code)), None => Ok(result.value as i32), } } -fn unpack_i64(result: vm_exec2_hook_result_t) -> Result { +fn unpack_i64(result: vm_exec_w6_hook_result_t) -> Result { match result.early_exit_code() { Some(code) => Err(early_exit(code)), None => Ok(result.value), diff --git a/c-api-new/src/lib.rs b/c-api-wasmer6/src/lib.rs similarity index 90% rename from c-api-new/src/lib.rs rename to c-api-wasmer6/src/lib.rs index b88863ae..4bdeebd2 100644 --- a/c-api-new/src/lib.rs +++ b/c-api-wasmer6/src/lib.rs @@ -4,14 +4,14 @@ //! shipping while this one is brought up. The two differ in four ways that matter to a //! caller: //! -//! - **Exported symbols are prefixed `vm_exec2_`**, so both libraries can be linked into one +//! - **Exported symbols are prefixed `vm_exec_w6_`**, so both libraries can be linked into one //! binary. The legacy API already owns `vm_exec_new_executor` and friends. //! - **Errors travel through a caller-provided buffer**, not a thread-local last-error //! singleton. See `capi_status`. //! - **Configuration is passed once**, at the point the thing it configures is created. //! There are no post-construction setters. //! - **A call reports what happened in its result**, instead of leaving the caller to poll a -//! wasm global afterwards. See `vm_exec2_call_result_t`. +//! wasm global afterwards. See `vm_exec_w6_call_result_t`. //! //! The hook dispatch — the typed function-pointer table (`capi_vm_hook_pointers`) and the //! `VMHooks` implementation that calls into it (`capi_vm_hooks`) — is generated from the Go diff --git a/c-api-new/tests/ffi_boundary.rs b/c-api-wasmer6/tests/ffi_boundary.rs similarity index 72% rename from c-api-new/tests/ffi_boundary.rs rename to c-api-wasmer6/tests/ffi_boundary.rs index 085b10d9..3e1a1ac1 100644 --- a/c-api-new/tests/ffi_boundary.rs +++ b/c-api-wasmer6/tests/ffi_boundary.rs @@ -15,21 +15,21 @@ use std::ffi::{CString, c_char, c_int, c_void}; use std::sync::{Arc, Mutex, OnceLock}; use multiversx_chain_vm_executor::{VMHooks, VMHooksDefault}; -use multiversx_chain_vm_executor_c_api_new::capi_config::{ - vm_exec2_compilation_options_t, vm_exec2_executor_config_t, +use multiversx_chain_vm_executor_c_api_wasmer6::capi_config::{ + vm_exec_w6_compilation_options_t, vm_exec_w6_executor_config_t, }; -use multiversx_chain_vm_executor_c_api_new::capi_executor::{ - vm_exec2_executor_destroy, vm_exec2_executor_t, vm_exec2_new_executor, +use multiversx_chain_vm_executor_c_api_wasmer6::capi_executor::{ + vm_exec_w6_executor_destroy, vm_exec_w6_executor_t, vm_exec_w6_new_executor, }; -use multiversx_chain_vm_executor_c_api_new::capi_instance::*; -use multiversx_chain_vm_executor_c_api_new::capi_instance_state::*; -use multiversx_chain_vm_executor_c_api_new::capi_opcode_cost::vm_exec_opcode_cost_t; -use multiversx_chain_vm_executor_c_api_new::capi_status::vm_exec2_status_t; -use multiversx_chain_vm_executor_c_api_new::capi_vm_hook_context::{ - CapiVMHooksBuilder, vm_exec2_hook_context_t, vm_exec2_hook_result_t, +use multiversx_chain_vm_executor_c_api_wasmer6::capi_instance::*; +use multiversx_chain_vm_executor_c_api_wasmer6::capi_instance_state::*; +use multiversx_chain_vm_executor_c_api_wasmer6::capi_opcode_cost::vm_exec_opcode_cost_t; +use multiversx_chain_vm_executor_c_api_wasmer6::capi_status::vm_exec_w6_status_t; +use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_context::{ + CapiVMHooksBuilder, vm_exec_w6_hook_context_t, vm_exec_w6_hook_result_t, }; -use multiversx_chain_vm_executor_c_api_new::capi_vm_hook_pointers::vm_exec2_vm_hook_c_func_pointers; -use multiversx_chain_vm_executor_c_api_new::capi_vm_hook_table::CapiVMHookTable; +use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_pointers::vm_exec_w6_vm_hook_c_func_pointers; +use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_table::CapiVMHookTable; use multiversx_chain_vm_executor_wasmer_experimental::{ ExperimentalInstance, ExperimentalInstanceState, ExperimentalVMHooksBuilder, }; @@ -83,8 +83,8 @@ fn opcode_cost() -> vm_exec_opcode_cost_t { unsafe { std::mem::zeroed() } } -fn compilation_options() -> vm_exec2_compilation_options_t { - vm_exec2_compilation_options_t { +fn compilation_options() -> vm_exec_w6_compilation_options_t { + vm_exec_w6_compilation_options_t { unmetered_locals: 0, max_memory_grow: 10, max_memory_grow_delta: 10, @@ -100,8 +100,8 @@ fn compilation_options() -> vm_exec2_compilation_options_t { // `extern "C" fn` is a non-nullable type. Calling one of the stubs would be a bug in the test // contract, and it reports a code the assertions would catch. -extern "C" fn stub_hook(_ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t { - vm_exec2_hook_result_t::early_exit(UNEXPECTED_HOOK_CODE) +extern "C" fn stub_hook(_ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t { + vm_exec_w6_hook_result_t::early_exit(UNEXPECTED_HOOK_CODE) } /// What the mock hooks record, for assertions after the call has returned. @@ -126,13 +126,13 @@ fn hook_log() -> &'static Mutex { /// Loads from the contract's memory through the hook context it was handed. fn load_through_context( - ctx: *mut vm_exec2_hook_context_t, + ctx: *mut vm_exec_w6_hook_context_t, mem_ptr: i32, len: i32, ) -> Result, i64> { let mut buf = vec![0u8; len.max(0) as usize]; let loaded = unsafe { - vm_exec2_state_mem_load(ctx, mem_ptr, buf.as_mut_ptr(), len, std::ptr::null_mut(), 0) + vm_exec_w6_state_mem_load(ctx, mem_ptr, buf.as_mut_ptr(), len, std::ptr::null_mut(), 0) }; if loaded < 0 { return Err(loaded); @@ -141,7 +141,7 @@ fn load_through_context( Ok(buf) } -extern "C" fn mock_get_gas_left(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_hook_result_t { +extern "C" fn mock_get_gas_left(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t { let mut log = hook_log().lock().unwrap(); log.get_gas_left_calls += 1; @@ -155,20 +155,20 @@ extern "C" fn mock_get_gas_left(ctx: *mut vm_exec2_hook_context_t) -> vm_exec2_h } let mut length = 0u64; - let status = unsafe { vm_exec2_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; - if status != vm_exec2_status_t::VM_EXEC2_OK || length != 65536 { + let status = unsafe { vm_exec_w6_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; + if status != vm_exec_w6_status_t::VM_EXEC_W6_OK || length != 65536 { log.failures .push(format!("mem_length in getGasLeft: {status:?}, {length}")); } - vm_exec2_hook_result_t::ok(4242) + vm_exec_w6_hook_result_t::ok(4242) } extern "C" fn mock_signal_error( - ctx: *mut vm_exec2_hook_context_t, + ctx: *mut vm_exec_w6_hook_context_t, message_offset: i32, message_length: i32, -) -> vm_exec2_hook_result_t { +) -> vm_exec_w6_hook_result_t { let mut log = hook_log().lock().unwrap(); log.signal_error_calls += 1; @@ -181,22 +181,23 @@ extern "C" fn mock_signal_error( // A hook that wants execution to stop says so in its result. Nothing is written to a // breakpoint global on the way out. - vm_exec2_hook_result_t::early_exit(USER_ERROR_CODE) + vm_exec_w6_hook_result_t::early_exit(USER_ERROR_CODE) } /// Builds a table whose every entry is a valid function pointer. -fn mock_hook_table() -> &'static vm_exec2_vm_hook_c_func_pointers { +fn mock_hook_table() -> &'static vm_exec_w6_vm_hook_c_func_pointers { const WORD: usize = std::mem::size_of::(); - let words = std::mem::size_of::() / WORD; + let words = std::mem::size_of::() / WORD; assert_eq!( - std::mem::size_of::() % WORD, + std::mem::size_of::() % WORD, 0, "the hook table should be a flat block of function pointers" ); let filled = vec![stub_hook as *const () as usize; words].into_boxed_slice(); - let table = - unsafe { &mut *(Box::leak(filled).as_mut_ptr() as *mut vm_exec2_vm_hook_c_func_pointers) }; + let table = unsafe { + &mut *(Box::leak(filled).as_mut_ptr() as *mut vm_exec_w6_vm_hook_c_func_pointers) + }; table.get_gas_left_func_ptr = mock_get_gas_left; table.signal_error_func_ptr = mock_signal_error; @@ -205,16 +206,16 @@ fn mock_hook_table() -> &'static vm_exec2_vm_hook_c_func_pointers { // --- Driving the exported functions ------------------------------------------------------- -fn new_executor(err: &mut ErrBuf) -> *mut vm_exec2_executor_t { +fn new_executor(err: &mut ErrBuf) -> *mut vm_exec_w6_executor_t { let cost = opcode_cost(); - let config = vm_exec2_executor_config_t { + let config = vm_exec_w6_executor_config_t { opcode_version: 1, opcode_cost: &cost, }; - let mut executor: *mut vm_exec2_executor_t = std::ptr::null_mut(); + let mut executor: *mut vm_exec_w6_executor_t = std::ptr::null_mut(); let status = unsafe { - vm_exec2_new_executor( + vm_exec_w6_new_executor( &mut executor, &config, mock_hook_table(), @@ -225,7 +226,7 @@ fn new_executor(err: &mut ErrBuf) -> *mut vm_exec2_executor_t { assert_eq!( status, - vm_exec2_status_t::VM_EXEC2_OK, + vm_exec_w6_status_t::VM_EXEC_W6_OK, "new_executor failed: {}", err.message() ); @@ -233,14 +234,17 @@ fn new_executor(err: &mut ErrBuf) -> *mut vm_exec2_executor_t { executor } -fn new_instance(executor: *mut vm_exec2_executor_t, err: &mut ErrBuf) -> *mut vm_exec2_instance_t { +fn new_instance( + executor: *mut vm_exec_w6_executor_t, + err: &mut ErrBuf, +) -> *mut vm_exec_w6_instance_t { let options = compilation_options(); let wasm = CONTRACT_WAT.as_bytes(); let go_vm_hooks = 0xBEEFusize; - let mut instance: *mut vm_exec2_instance_t = std::ptr::null_mut(); + let mut instance: *mut vm_exec_w6_instance_t = std::ptr::null_mut(); let status = unsafe { - vm_exec2_new_instance( + vm_exec_w6_new_instance( executor, &mut instance, wasm.as_ptr(), @@ -254,7 +258,7 @@ fn new_instance(executor: *mut vm_exec2_executor_t, err: &mut ErrBuf) -> *mut vm assert_eq!( status, - vm_exec2_status_t::VM_EXEC2_OK, + vm_exec_w6_status_t::VM_EXEC_W6_OK, "new_instance failed: {}", err.message() ); @@ -263,19 +267,19 @@ fn new_instance(executor: *mut vm_exec2_executor_t, err: &mut ErrBuf) -> *mut vm } fn call( - instance: *mut vm_exec2_instance_t, + instance: *mut vm_exec_w6_instance_t, func_name: &str, err: &mut ErrBuf, -) -> vm_exec2_call_result_t { +) -> vm_exec_w6_call_result_t { let name = CString::new(func_name).unwrap(); - let mut result = vm_exec2_call_result_t { - kind: vm_exec2_call_result_kind_t::VM_EXEC2_CALL_RUNTIME_ERROR, + let mut result = vm_exec_w6_call_result_t { + kind: vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_RUNTIME_ERROR, early_exit_code: u64::MAX, breakpoint: u64::MAX, }; let status = unsafe { - vm_exec2_instance_call( + vm_exec_w6_instance_call( instance, name.as_ptr(), 1_000_000, @@ -284,14 +288,14 @@ fn call( ERR_BUF_LEN, ) }; - assert_eq!(status, vm_exec2_status_t::VM_EXEC2_OK); + assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_OK); result } fn names( - instance: *mut vm_exec2_instance_t, - length: unsafe extern "C" fn(*const vm_exec2_instance_t) -> c_int, - write: unsafe extern "C" fn(*const vm_exec2_instance_t, *mut c_char, c_int) -> c_int, + instance: *mut vm_exec_w6_instance_t, + length: unsafe extern "C" fn(*const vm_exec_w6_instance_t) -> c_int, + write: unsafe extern "C" fn(*const vm_exec_w6_instance_t, *mut c_char, c_int) -> c_int, ) -> Vec { let needed = unsafe { length(instance) }; assert!(needed > 0, "expected a non-empty name list"); @@ -321,39 +325,43 @@ fn lifecycle() { let instance = new_instance(executor, &mut err); let result = call(instance, "doNothing", &mut err); - assert_eq!(result.kind, vm_exec2_call_result_kind_t::VM_EXEC2_CALL_OK); + assert_eq!( + result.kind, + vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_OK + ); // A missing endpoint is reported in the result, not as a failure of the call itself. let result = call(instance, "noSuchEndpoint", &mut err); assert_eq!( result.kind, - vm_exec2_call_result_kind_t::VM_EXEC2_CALL_FUNCTION_NOT_FOUND + vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_FUNCTION_NOT_FOUND ); let mut points = u64::MAX; - let status = - unsafe { vm_exec2_instance_get_points_used(instance, &mut points, err.ptr(), ERR_BUF_LEN) }; - assert_eq!(status, vm_exec2_status_t::VM_EXEC2_OK); + let status = unsafe { + vm_exec_w6_instance_get_points_used(instance, &mut points, err.ptr(), ERR_BUF_LEN) + }; + assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_OK); assert_ne!(points, u64::MAX, "points_used was not written"); - assert_eq!(unsafe { vm_exec2_instance_check_signatures(instance) }, 1); + assert_eq!(unsafe { vm_exec_w6_instance_check_signatures(instance) }, 1); let present = CString::new("doNothing").unwrap(); let absent = CString::new("noSuchEndpoint").unwrap(); assert_eq!( - unsafe { vm_exec2_instance_has_function(instance, present.as_ptr()) }, + unsafe { vm_exec_w6_instance_has_function(instance, present.as_ptr()) }, 1 ); assert_eq!( - unsafe { vm_exec2_instance_has_function(instance, absent.as_ptr()) }, + unsafe { vm_exec_w6_instance_has_function(instance, absent.as_ptr()) }, 0 ); assert_eq!( names( instance, - vm_exec2_instance_exported_function_names_length, - vm_exec2_instance_exported_function_names, + vm_exec_w6_instance_exported_function_names_length, + vm_exec_w6_instance_exported_function_names, ), vec![ "callGetGasLeft".to_string(), @@ -366,15 +374,15 @@ fn lifecycle() { assert_eq!( names( instance, - vm_exec2_instance_imported_function_names_length, - vm_exec2_instance_imported_function_names, + vm_exec_w6_instance_imported_function_names_length, + vm_exec_w6_instance_imported_function_names, ), vec!["getGasLeft".to_string(), "signalError".to_string()] ); unsafe { - vm_exec2_instance_destroy(instance); - vm_exec2_executor_destroy(executor); + vm_exec_w6_instance_destroy(instance); + vm_exec_w6_executor_destroy(executor); } } @@ -388,7 +396,7 @@ fn hook_dispatch() { let result = call(instance, "callGetGasLeft", &mut err); assert_eq!( result.kind, - vm_exec2_call_result_kind_t::VM_EXEC2_CALL_OK, + vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_OK, "calling a hook that returns normally should not stop execution" ); @@ -397,7 +405,7 @@ fn hook_dispatch() { let result = call(instance, "callSignalError", &mut err); assert_eq!( result.kind, - vm_exec2_call_result_kind_t::VM_EXEC2_CALL_EARLY_EXIT + vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_EARLY_EXIT ); assert_eq!(result.early_exit_code, USER_ERROR_CODE); @@ -416,20 +424,20 @@ fn hook_dispatch() { drop(log); unsafe { - vm_exec2_instance_destroy(instance); - vm_exec2_executor_destroy(executor); + vm_exec_w6_instance_destroy(instance); + vm_exec_w6_executor_destroy(executor); } } #[test] fn null_arguments_are_refused_without_a_message() { let mut err = ErrBuf::new(); - let mut executor: *mut vm_exec2_executor_t = std::ptr::null_mut(); + let mut executor: *mut vm_exec_w6_executor_t = std::ptr::null_mut(); // A null config cannot be reported through the error buffer, because the buffer itself may // be the thing that is null, so these return a dedicated status instead. let status = unsafe { - vm_exec2_new_executor( + vm_exec_w6_new_executor( &mut executor, std::ptr::null(), std::ptr::null(), @@ -437,12 +445,12 @@ fn null_arguments_are_refused_without_a_message() { ERR_BUF_LEN, ) }; - assert_eq!(status, vm_exec2_status_t::VM_EXEC2_NULL_POINTER); + assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER); assert!(executor.is_null()); assert_eq!( - unsafe { vm_exec2_instance_check_signatures(std::ptr::null()) }, - vm_exec2_status_t::VM_EXEC2_NULL_POINTER.as_c_int() + unsafe { vm_exec_w6_instance_check_signatures(std::ptr::null()) }, + vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int() ); } @@ -450,14 +458,14 @@ fn null_arguments_are_refused_without_a_message() { fn an_invalid_opcode_version_is_reported_through_the_buffer() { let mut err = ErrBuf::new(); let cost = opcode_cost(); - let config = vm_exec2_executor_config_t { + let config = vm_exec_w6_executor_config_t { opcode_version: 99, opcode_cost: &cost, }; - let mut executor: *mut vm_exec2_executor_t = std::ptr::null_mut(); + let mut executor: *mut vm_exec_w6_executor_t = std::ptr::null_mut(); let status = unsafe { - vm_exec2_new_executor( + vm_exec_w6_new_executor( &mut executor, &config, mock_hook_table(), @@ -466,7 +474,7 @@ fn an_invalid_opcode_version_is_reported_through_the_buffer() { ) }; - assert_eq!(status, vm_exec2_status_t::VM_EXEC2_ERROR); + assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_ERROR); assert_eq!(err.message(), "invalid opcode version code: 99"); assert!(executor.is_null()); } @@ -477,22 +485,22 @@ fn a_context_is_rejected_when_no_call_is_active() { assert_eq!( unsafe { - vm_exec2_state_mem_length(std::ptr::null_mut(), &mut 0u64, err.ptr(), ERR_BUF_LEN) + vm_exec_w6_state_mem_length(std::ptr::null_mut(), &mut 0u64, err.ptr(), ERR_BUF_LEN) }, - vm_exec2_status_t::VM_EXEC2_NULL_POINTER + vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER ); let mut points = 0u64; assert_eq!( unsafe { - vm_exec2_state_get_points_used( + vm_exec_w6_state_get_points_used( std::ptr::null_mut(), &mut points, err.ptr(), ERR_BUF_LEN, ) }, - vm_exec2_status_t::VM_EXEC2_NULL_POINTER + vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER ); } @@ -543,9 +551,9 @@ impl ExperimentalVMHooksBuilder for ProbingHooksBuilder { } } -fn mem_load(ctx: *mut vm_exec2_hook_context_t, mem_ptr: i32, dest: &mut [u8], len: i32) -> i64 { +fn mem_load(ctx: *mut vm_exec_w6_hook_context_t, mem_ptr: i32, dest: &mut [u8], len: i32) -> i64 { unsafe { - vm_exec2_state_mem_load( + vm_exec_w6_state_mem_load( ctx, mem_ptr, dest.as_mut_ptr(), @@ -556,9 +564,9 @@ fn mem_load(ctx: *mut vm_exec2_hook_context_t, mem_ptr: i32, dest: &mut [u8], le } } -fn mem_store(ctx: *mut vm_exec2_hook_context_t, mem_ptr: i32, src: &[u8]) -> vm_exec2_status_t { +fn mem_store(ctx: *mut vm_exec_w6_hook_context_t, mem_ptr: i32, src: &[u8]) -> vm_exec_w6_status_t { unsafe { - vm_exec2_state_mem_store( + vm_exec_w6_state_mem_store( ctx, mem_ptr, src.as_ptr(), @@ -569,14 +577,14 @@ fn mem_store(ctx: *mut vm_exec2_hook_context_t, mem_ptr: i32, src: &[u8]) -> vm_ } } -fn probe_state_ops(ctx: *mut vm_exec2_hook_context_t, outcome: &mut Outcome) { +fn probe_state_ops(ctx: *mut vm_exec_w6_hook_context_t, outcome: &mut Outcome) { const PAGE: u64 = 65536; let mut length = 0u64; - let status = unsafe { vm_exec2_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; + let status = unsafe { vm_exec_w6_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; outcome.check( "mem_length", - status == vm_exec2_status_t::VM_EXEC2_OK && length == PAGE, + status == vm_exec_w6_status_t::VM_EXEC_W6_OK && length == PAGE, format!("{status:?}, length {length}"), ); @@ -610,21 +618,21 @@ fn probe_state_ops(ctx: *mut vm_exec2_hook_context_t, outcome: &mut Outcome) { let loaded = mem_load(ctx, (PAGE + 1) as i32, &mut buf, 1); outcome.check( "mem_load rejects an offset past the end", - loaded == vm_exec2_status_t::VM_EXEC2_ERROR.as_i64(), + loaded == vm_exec_w6_status_t::VM_EXEC_W6_ERROR.as_i64(), format!("loaded {loaded}"), ); let loaded = mem_load(ctx, -1, &mut buf, 1); outcome.check( "mem_load rejects a negative offset", - loaded == vm_exec2_status_t::VM_EXEC2_ERROR.as_i64(), + loaded == vm_exec_w6_status_t::VM_EXEC_W6_ERROR.as_i64(), format!("loaded {loaded}"), ); let loaded = mem_load(ctx, 0, &mut buf, -1); outcome.check( "mem_load rejects a negative length", - loaded == vm_exec2_status_t::VM_EXEC2_ERROR.as_i64(), + loaded == vm_exec_w6_status_t::VM_EXEC_W6_ERROR.as_i64(), format!("loaded {loaded}"), ); @@ -634,7 +642,7 @@ fn probe_state_ops(ctx: *mut vm_exec2_hook_context_t, outcome: &mut Outcome) { let loaded = mem_load(ctx, 16, &mut buf, 3); outcome.check( "mem_store round-trips", - status == vm_exec2_status_t::VM_EXEC2_OK && loaded == 3 && &buf == b"ABC", + status == vm_exec_w6_status_t::VM_EXEC_W6_OK && loaded == 3 && &buf == b"ABC", format!( "{status:?}, loaded {loaded}, {:?}", std::str::from_utf8(&buf) @@ -645,19 +653,19 @@ fn probe_state_ops(ctx: *mut vm_exec2_hook_context_t, outcome: &mut Outcome) { let status = mem_store(ctx, (PAGE - 1) as i32, b"ABC"); outcome.check( "mem_store rejects a range past the end", - status == vm_exec2_status_t::VM_EXEC2_ERROR, + status == vm_exec_w6_status_t::VM_EXEC_W6_ERROR, format!("{status:?}"), ); // Gas, through the state handle rather than the instance handle. - let status = unsafe { vm_exec2_state_set_points_used(ctx, 42, std::ptr::null_mut(), 0) }; + let status = unsafe { vm_exec_w6_state_set_points_used(ctx, 42, std::ptr::null_mut(), 0) }; let mut points = 0u64; let get_status = - unsafe { vm_exec2_state_get_points_used(ctx, &mut points, std::ptr::null_mut(), 0) }; + unsafe { vm_exec_w6_state_get_points_used(ctx, &mut points, std::ptr::null_mut(), 0) }; outcome.check( "points used round-trip", - status == vm_exec2_status_t::VM_EXEC2_OK - && get_status == vm_exec2_status_t::VM_EXEC2_OK + status == vm_exec_w6_status_t::VM_EXEC_W6_OK + && get_status == vm_exec_w6_status_t::VM_EXEC_W6_OK && points == 42, format!("{status:?}, {get_status:?}, points {points}"), ); @@ -665,12 +673,12 @@ fn probe_state_ops(ctx: *mut vm_exec2_hook_context_t, outcome: &mut Outcome) { // Growing reports the page count from before the growth. let mut prev_pages = 0u32; let status = - unsafe { vm_exec2_state_mem_grow(ctx, 1, &mut prev_pages, std::ptr::null_mut(), 0) }; + unsafe { vm_exec_w6_state_mem_grow(ctx, 1, &mut prev_pages, std::ptr::null_mut(), 0) }; let mut length_after = 0u64; - unsafe { vm_exec2_state_mem_length(ctx, &mut length_after, std::ptr::null_mut(), 0) }; + unsafe { vm_exec_w6_state_mem_length(ctx, &mut length_after, std::ptr::null_mut(), 0) }; outcome.check( "mem_grow", - status == vm_exec2_status_t::VM_EXEC2_OK && prev_pages == 1 && length_after == 2 * PAGE, + status == vm_exec_w6_status_t::VM_EXEC_W6_OK && prev_pages == 1 && length_after == 2 * PAGE, format!("{status:?}, prev_pages {prev_pages}, length {length_after}"), ); } @@ -679,7 +687,7 @@ fn probe_state_ops(ctx: *mut vm_exec2_hook_context_t, outcome: &mut Outcome) { fn probe_nesting( builder: &CapiVMHooksBuilder, state_ptr: *mut c_void, - outer_ctx: *mut vm_exec2_hook_context_t, + outer_ctx: *mut vm_exec_w6_hook_context_t, outcome: &mut Outcome, ) { unsafe { @@ -692,28 +700,28 @@ fn probe_nesting( let mut length = 0u64; let inner_status = - vm_exec2_state_mem_length(inner_ctx, &mut length, std::ptr::null_mut(), 0); + vm_exec_w6_state_mem_length(inner_ctx, &mut length, std::ptr::null_mut(), 0); outcome.check( "the innermost context works", - inner_status == vm_exec2_status_t::VM_EXEC2_OK, + inner_status == vm_exec_w6_status_t::VM_EXEC_W6_OK, format!("{inner_status:?}"), ); let outer_status = - vm_exec2_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); + vm_exec_w6_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); outcome.check( "an outer context is rejected while a nested call runs", - outer_status == vm_exec2_status_t::VM_EXEC2_STALE_CONTEXT, + outer_status == vm_exec_w6_status_t::VM_EXEC_W6_STALE_CONTEXT, format!("{outer_status:?}"), ); }); // ... and is usable again once the nested call has returned. let mut length = 0u64; - let status = vm_exec2_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); + let status = vm_exec_w6_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); outcome.check( "the outer context works again after the nested call", - status == vm_exec2_status_t::VM_EXEC2_OK, + status == vm_exec_w6_status_t::VM_EXEC_W6_OK, format!("{status:?}"), ); } @@ -764,13 +772,13 @@ fn state_ops() { // The context that escaped the host call must now be refused. Contexts are pooled for the // instance's lifetime, so this reads live memory and fails the token check, rather than // being undefined behaviour. - let escaped = outcome.escaped_context as *mut vm_exec2_hook_context_t; + let escaped = outcome.escaped_context as *mut vm_exec_w6_hook_context_t; let mut length = 0u64; let status = - unsafe { vm_exec2_state_mem_length(escaped, &mut length, std::ptr::null_mut(), 0) }; + unsafe { vm_exec_w6_state_mem_length(escaped, &mut length, std::ptr::null_mut(), 0) }; assert_eq!( status, - vm_exec2_status_t::VM_EXEC2_STALE_CONTEXT, + vm_exec_w6_status_t::VM_EXEC_W6_STALE_CONTEXT, "a context used after its call must be rejected" ); } From 0c2d5afb7dfa19cf0f3cd9f1b57dbe53978d621e Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sat, 3 Oct 2026 01:14:32 +0300 Subject: [PATCH 14/26] c-api-common crate, common headers, C prefixes renamed --- Cargo.toml | 1 + PLAN-go-ffi.md | 53 +- c-api-common/Cargo.toml | 16 + c-api-common/build.rs | 39 ++ c-api-common/cbindgen.toml | 16 + c-api-common/libvmexeccapi_common.h | 621 +++++++++++++++++ c-api-common/src/capi_call_result.rs | 115 ++++ .../src/capi_config.rs | 12 +- c-api-common/src/capi_logger.rs | 20 + .../src/capi_opcode_cost.rs | 0 .../src/capi_status.rs | 50 +- .../src/capi_strings.rs | 8 +- .../src/capi_vm_hook_context.rs | 148 ++-- c-api-common/src/capi_vm_hook_pointers.rs | 300 ++++++++ .../src/capi_vm_hook_table.rs | 8 +- .../src/capi_vm_hooks.rs | 18 +- c-api-common/src/lib.rs | 39 ++ c-api-legacy/libvmexeccapi.h | 98 +-- c-api-legacy/src/basic_types.rs | 6 +- c-api-legacy/src/capi_breakpoints.rs | 20 +- c-api-legacy/src/capi_compilation_options.rs | 4 +- c-api-legacy/src/capi_executor.rs | 34 +- c-api-legacy/src/capi_instance.rs | 74 +- c-api-legacy/src/capi_instance_cache.rs | 36 +- c-api-legacy/src/capi_logger.rs | 10 +- c-api-legacy/src/capi_memory.rs | 18 +- c-api-legacy/src/capi_metering.rs | 56 +- c-api-legacy/src/capi_opcode_cost.rs | 4 +- c-api-legacy/src/capi_vm_hook_pointers.rs | 6 +- c-api-legacy/src/capi_vm_hooks.rs | 6 +- c-api-legacy/src/macros.rs | 6 +- c-api-wasmer6/Cargo.toml | 1 + c-api-wasmer6/cbindgen.toml | 20 +- c-api-wasmer6/libvmexeccapi_w6.h | 638 +----------------- c-api-wasmer6/src/capi_backend.rs | 41 ++ c-api-wasmer6/src/capi_executor.rs | 20 +- c-api-wasmer6/src/capi_instance.rs | 156 ++--- c-api-wasmer6/src/capi_instance_state.rs | 121 ++-- c-api-wasmer6/src/capi_logger.rs | 24 +- c-api-wasmer6/src/capi_vm_hook_pointers.rs | 300 -------- c-api-wasmer6/src/lib.rs | 18 +- c-api-wasmer6/tests/ffi_boundary.rs | 119 ++-- 42 files changed, 1789 insertions(+), 1511 deletions(-) create mode 100644 c-api-common/Cargo.toml create mode 100644 c-api-common/build.rs create mode 100644 c-api-common/cbindgen.toml create mode 100644 c-api-common/libvmexeccapi_common.h create mode 100644 c-api-common/src/capi_call_result.rs rename {c-api-wasmer6 => c-api-common}/src/capi_config.rs (88%) create mode 100644 c-api-common/src/capi_logger.rs rename {c-api-wasmer6 => c-api-common}/src/capi_opcode_cost.rs (100%) rename {c-api-wasmer6 => c-api-common}/src/capi_status.rs (79%) rename {c-api-wasmer6 => c-api-common}/src/capi_strings.rs (94%) rename {c-api-wasmer6 => c-api-common}/src/capi_vm_hook_context.rs (74%) create mode 100644 c-api-common/src/capi_vm_hook_pointers.rs rename {c-api-wasmer6 => c-api-common}/src/capi_vm_hook_table.rs (82%) rename {c-api-wasmer6 => c-api-common}/src/capi_vm_hooks.rs (99%) create mode 100644 c-api-common/src/lib.rs create mode 100644 c-api-wasmer6/src/capi_backend.rs delete mode 100644 c-api-wasmer6/src/capi_vm_hook_pointers.rs diff --git a/Cargo.toml b/Cargo.toml index 6259bf47..1231c4ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "meta", + "c-api-common", "c-api-legacy", "c-api-wasmer6", "vm-executor", diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 582a8015..a402a6c5 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -778,13 +778,58 @@ Verified by regenerating — which must move nothing — and by running the full `wasmer2legacy` and `wasmer6`, plus the dual-backend trap tests and the hook-sequence differential. +### Done: the prefix separation, and `c-api-common` + +**Shim and legacy-type prefixes.** The cgo `//export` names are C symbols, so the legacy +shims moved to `w2l_*`, freeing `w2_*` for the new Wasmer 2.2 integration. And the legacy C +*types* became `vm_legacy_*` — `vm_legacy_compilation_options_t`, `vm_legacy_result_t`, +`vm_legacy_executor_t`, `vm_legacy_instance_t`, `vm_legacy_opcode_cost_t`, +`vm_legacy_vm_hook_c_func_pointers`, plus `VM_LEGACY_OK` / `VM_LEGACY_ERROR` — which removes +the collision class entirely rather than relying on no package including both headers. The +legacy *functions* keep `vm_exec_*`, since the common header declares no functions and nothing +of theirs can collide. The shared opcode-cost writer had to be parameterised by C type name, +because one output fed both the legacy crate and the new one. + +**`c-api-common` is extracted, and it took more than the types.** The result: + +| | Lines | Contents | +| --- | --- | --- | +| `c-api-common` | **2,435** | status, strings, config, opcode cost, panic suppression, call result, and the *whole* hook dispatch path — context ABI, context pool, builder, table, and the generated `VMHooks` implementation | +| `c-api-wasmer6` | **734** | the two opaque handles, the prefixed entry points, and one file naming the backend | + +The hook dispatch moved because the context carries the instance state as an **erased pointer**: +nothing in the pool, the table or the 900-odd generated lines ever names a backend type. Making +`with_instance_state` and `with_hook_context` generic over `S` was enough, and the backend crate +now names its state type in exactly one place — `capi_backend.rs`, which defines +`BackendInstanceState` and wraps the shared builder to implement its own builder trait. So +`c-api-wasmer2` will not duplicate the generated hooks; it gets a `capi_backend.rs` of its own. + +Two things the compiler forced, both worth keeping: + +- **The orphan rule.** With the builder in `c-api-common`, both it and `ExperimentalVMHooksBuilder` + are foreign to the backend crate, so the trait cannot be implemented directly. A newtype + (`BackendVMHooksBuilder`) fixes it and makes the seam explicit rather than incidental. +- **`capi_logger` has to exist in every crate**, because `meta::capi_safe_unwind` expands to + `crate::capi_logger::set_panic_handler()`. Each backend crate carries a one-line re-export. + +**The shared header works.** `c-api-common` generates `libvmexeccapi_common.h` with an include +guard and an explicit export list (nothing is reachable from an entry point, since the crate +exports no functions); `c-api-wasmer6`'s cbindgen excludes those types and emits +`#include "libvmexeccapi_common.h"`. Verified: nine shared types appear once, in the common +header, and zero times in the backend header. + +Shared types are published under the neutral `vm_exec_*` prefix. The rule that fell out is +worth stating, because it is what the next crate follows: **types shared → `vm_exec_*`; +opaque handles and functions → `vm_exec_w6_*`.** + ### Still to do -1. **`c-api-common`.** The extraction is small: of the thirteen modules in `c-api-wasmer6`, +1. ~~**`c-api-common`.**~~ **Done** — see above. What remains of the original estimate: The extraction is small: of the thirteen modules in `c-api-wasmer6`, **ten already reference no backend type at all**. Only `capi_vm_hook_context.rs` (10 - references), `capi_instance.rs` (3) and `capi_vm_hook_table.rs` (1) are coupled. So status - codes, strings, config, opcode cost, the logger, the hook result struct and the generated - hook tables move as-is; the context and the instance stay behind, parameterised by backend. + the three `#[no_mangle]` entry-point files (`capi_executor.rs`, `capi_instance.rs`, + `capi_instance_state.rs`, ~700 lines) are still per-backend, because each needs its own + symbol prefix. `c-api-wasmer2` will duplicate them unless they are generated from a macro + parameterised by prefix — worth deciding when that crate is written, not before. 2. **`c-api-wasmer2`.** Wasmer 2.2 through the new interfaces. This is what puts **Phase 1.6 back on the critical path**: `WasmerProdRuntimeRef::vm_hooks` returns `Box` and `Arc>` where the Wasmer 6 side returns a diff --git a/c-api-common/Cargo.toml b/c-api-common/Cargo.toml new file mode 100644 index 00000000..4ce35d95 --- /dev/null +++ b/c-api-common/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "multiversx-chain-vm-executor-c-api-common" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["rlib"] + +[dependencies] +multiversx-chain-vm-executor = { path = "../vm-executor" } + +libc = { version = "^0.2", default-features = false } + +[build-dependencies] +cbindgen = "0.29" diff --git a/c-api-common/build.rs b/c-api-common/build.rs new file mode 100644 index 00000000..16c99ab6 --- /dev/null +++ b/c-api-common/build.rs @@ -0,0 +1,39 @@ +extern crate cbindgen; + +use cbindgen::{Builder, Config, Language, Style}; +use std::{env, fs, path::PathBuf}; + +const HEADER_FILE_NAME: &str = "libvmexeccapi_common"; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut crate_header_file = PathBuf::from(&crate_dir); + crate_header_file.push(HEADER_FILE_NAME); + + let out_dir = env::var("OUT_DIR").unwrap(); + let mut out_header_file = PathBuf::from(&out_dir); + out_header_file.push(HEADER_FILE_NAME); + + // Generate the C bindings in the `OUT_DIR`. + out_header_file.set_extension("h"); + // Reads `cbindgen.toml`, which force-exports the types no exported function mentions. + let config = Config::from_root_or_default(&crate_dir); + + Builder::new() + .with_config(config) + .with_crate(&crate_dir) + .with_language(Language::C) + // Same formatting choices as the legacy `c-api` header, so the two are easy to + // diff against each other. + .with_line_length(usize::MAX) + .with_style(Style::Both) + .generate() + .expect("Unable to generate C bindings") + .write_to_file(out_header_file.as_path()); + + // Copy the generated C bindings from `OUT_DIR` to `CARGO_MANIFEST_DIR`. + crate_header_file.set_extension("h"); + out_header_file.set_extension("h"); + fs::copy(out_header_file.as_path(), crate_header_file.as_path()) + .expect("Unable to copy the generated C bindings"); +} diff --git a/c-api-common/cbindgen.toml b/c-api-common/cbindgen.toml new file mode 100644 index 00000000..96855f71 --- /dev/null +++ b/c-api-common/cbindgen.toml @@ -0,0 +1,16 @@ +# This crate exports no C functions, only the shared vocabulary the backend APIs are written +# in, so nothing is reachable from an entry point and everything has to be named explicitly. +include_guard = "VM_EXEC_COMMON_H" + +[export] +include = [ + "vm_exec_status_t", + "vm_exec_opcode_cost_t", + "vm_exec_compilation_options_t", + "vm_exec_executor_config_t", + "vm_exec_call_result_kind_t", + "vm_exec_call_result_t", + "vm_exec_hook_result_t", + "vm_exec_hook_context_t", + "vm_exec_vm_hook_c_func_pointers", +] diff --git a/c-api-common/libvmexeccapi_common.h b/c-api-common/libvmexeccapi_common.h new file mode 100644 index 00000000..fc73793f --- /dev/null +++ b/c-api-common/libvmexeccapi_common.h @@ -0,0 +1,621 @@ +#ifndef VM_EXEC_COMMON_H +#define VM_EXEC_COMMON_H + +#include +#include +#include +#include + +/** + * The value of `vm_exec_hook_result_t::early_exit` that means "the hook returned normally". + * + * A dedicated sentinel is needed because `0` is a real early-exit code: the Rust VM uses + * `ASYNC_CALL_EARLY_EXIT_CODE = 0` and the Go VM has the same async-call case, so "code 0" + * and "no code" have to stay distinguishable. + */ +#define VM_EXEC_NO_EARLY_EXIT UINT64_MAX + +/** + * Result of a C API call. + * + * `VM_EXEC_OK` is zero and every failure is negative, so the functions that return a + * count (the memory loads) can share the same code space: a non-negative result is a + * count, a negative one is one of these. + */ +typedef enum vm_exec_status_t { + /** + * The call succeeded. + */ + VM_EXEC_OK = 0, + /** + * The call failed; the error message was written to the provided error buffer. + */ + VM_EXEC_ERROR = -1, + /** + * A pointer argument that must not be null was null. No message is produced, because + * a null pointer may well be the error buffer itself. + */ + VM_EXEC_NULL_POINTER = -2, + /** + * A hook context was used outside the host call it belongs to. See + * [`crate::capi_vm_hook_context`]. + */ + VM_EXEC_STALE_CONTEXT = -3, + /** + * A panic was caught at the boundary. Rust panics must never unwind into C, so every + * entry point traps them and reports this instead. + */ + VM_EXEC_PANIC = -4, +} vm_exec_status_t; + +/** + * How a call ended. + * + * This replaces the legacy arrangement, where a call returned a bare ok/error and the VM + * then polled a wasm global to find out what had actually happened. + */ +typedef enum vm_exec_call_result_kind_t { + /** + * The function returned normally. + */ + VM_EXEC_CALL_OK = 0, + /** + * No exported function by that name. + */ + VM_EXEC_CALL_FUNCTION_NOT_FOUND = 1, + /** + * The instance trapped for a reason that is neither a VM hook's doing nor a + * middleware breakpoint. The message is in the error buffer. + */ + VM_EXEC_CALL_RUNTIME_ERROR = 2, + /** + * A VM hook asked for execution to stop. `early_exit_code` says which; the message, if + * any, is in the error buffer, and is informational — the VM has already recorded its + * own. + */ + VM_EXEC_CALL_EARLY_EXIT = 3, + /** + * A middleware stopped execution. `breakpoint` says which. + */ + VM_EXEC_CALL_BREAKPOINT = 4, +} vm_exec_call_result_kind_t; + +/** + * FFI-safe mirror of `OpcodeCost`. + * + * This is a distinct type rather than a cast of `OpcodeCost` itself, so that + * the general-purpose struct is free to change shape without touching the C + * ABI, and vice versa. + */ +typedef struct vm_exec_opcode_cost_t { + uint32_t opcode_block; + uint32_t opcode_br; + uint32_t opcode_brif; + uint32_t opcode_brtable; + uint32_t opcode_call; + uint32_t opcode_callindirect; + uint32_t opcode_catch; + uint32_t opcode_catchall; + uint32_t opcode_delegate; + uint32_t opcode_drop; + uint32_t opcode_else; + uint32_t opcode_end; + uint32_t opcode_globalget; + uint32_t opcode_globalset; + uint32_t opcode_i32add; + uint32_t opcode_i32and; + uint32_t opcode_i32clz; + uint32_t opcode_i32const; + uint32_t opcode_i32ctz; + uint32_t opcode_i32divs; + uint32_t opcode_i32divu; + uint32_t opcode_i32eq; + uint32_t opcode_i32eqz; + uint32_t opcode_i32extend16s; + uint32_t opcode_i32extend8s; + uint32_t opcode_i32ges; + uint32_t opcode_i32geu; + uint32_t opcode_i32gts; + uint32_t opcode_i32gtu; + uint32_t opcode_i32les; + uint32_t opcode_i32leu; + uint32_t opcode_i32load; + uint32_t opcode_i32load16s; + uint32_t opcode_i32load16u; + uint32_t opcode_i32load8s; + uint32_t opcode_i32load8u; + uint32_t opcode_i32lts; + uint32_t opcode_i32ltu; + uint32_t opcode_i32mul; + uint32_t opcode_i32ne; + uint32_t opcode_i32or; + uint32_t opcode_i32popcnt; + uint32_t opcode_i32rems; + uint32_t opcode_i32remu; + uint32_t opcode_i32rotl; + uint32_t opcode_i32rotr; + uint32_t opcode_i32shl; + uint32_t opcode_i32shrs; + uint32_t opcode_i32shru; + uint32_t opcode_i32store; + uint32_t opcode_i32store16; + uint32_t opcode_i32store8; + uint32_t opcode_i32sub; + uint32_t opcode_i32wrapi64; + uint32_t opcode_i32xor; + uint32_t opcode_i64add; + uint32_t opcode_i64and; + uint32_t opcode_i64clz; + uint32_t opcode_i64const; + uint32_t opcode_i64ctz; + uint32_t opcode_i64divs; + uint32_t opcode_i64divu; + uint32_t opcode_i64eq; + uint32_t opcode_i64eqz; + uint32_t opcode_i64extend16s; + uint32_t opcode_i64extend32s; + uint32_t opcode_i64extend8s; + uint32_t opcode_i64extendi32s; + uint32_t opcode_i64extendi32u; + uint32_t opcode_i64ges; + uint32_t opcode_i64geu; + uint32_t opcode_i64gts; + uint32_t opcode_i64gtu; + uint32_t opcode_i64les; + uint32_t opcode_i64leu; + uint32_t opcode_i64load; + uint32_t opcode_i64load16s; + uint32_t opcode_i64load16u; + uint32_t opcode_i64load32s; + uint32_t opcode_i64load32u; + uint32_t opcode_i64load8s; + uint32_t opcode_i64load8u; + uint32_t opcode_i64lts; + uint32_t opcode_i64ltu; + uint32_t opcode_i64mul; + uint32_t opcode_i64ne; + uint32_t opcode_i64or; + uint32_t opcode_i64popcnt; + uint32_t opcode_i64rems; + uint32_t opcode_i64remu; + uint32_t opcode_i64rotl; + uint32_t opcode_i64rotr; + uint32_t opcode_i64shl; + uint32_t opcode_i64shrs; + uint32_t opcode_i64shru; + uint32_t opcode_i64store; + uint32_t opcode_i64store16; + uint32_t opcode_i64store32; + uint32_t opcode_i64store8; + uint32_t opcode_i64sub; + uint32_t opcode_i64xor; + uint32_t opcode_if; + uint32_t opcode_localget; + uint32_t opcode_localset; + uint32_t opcode_localtee; + uint32_t opcode_loop; + uint32_t opcode_memorygrow; + uint32_t opcode_memorysize; + uint32_t opcode_nop; + uint32_t opcode_reffunc; + uint32_t opcode_refisnull; + uint32_t opcode_refnull; + uint32_t opcode_rethrow; + uint32_t opcode_return; + uint32_t opcode_returncall; + uint32_t opcode_returncallindirect; + uint32_t opcode_select; + uint32_t opcode_tableget; + uint32_t opcode_tablegrow; + uint32_t opcode_tableinit; + uint32_t opcode_tableset; + uint32_t opcode_tablesize; + uint32_t opcode_throw; + uint32_t opcode_try; + uint32_t opcode_typedselect; + uint32_t opcode_unreachable; + uint32_t opcode_unwind; + uint32_t opcode_memorycopy; + uint32_t opcode_memoryfill; + uint32_t opcode_memorycopyperbyte; + uint32_t opcode_memoryfillperbyte; + uint32_t opcode_localallocate; +} vm_exec_opcode_cost_t; + +/** + * FFI-safe mirror of the new `CompilationOptions`. + * + * Four fields, against the legacy seven: `gas_limit` moved to the call, and metering and + * runtime breakpoints are no longer optional. Fields that are `usize` on the Rust side are + * fixed to `u64` here, since `usize` has no guaranteed width across host platforms. + */ +typedef struct vm_exec_compilation_options_t { + uint64_t unmetered_locals; + uint64_t max_memory_grow; + uint64_t max_memory_grow_delta; + bool opcode_trace; +} vm_exec_compilation_options_t; + +/** + * Everything an executor is configured with. + * + * The opcode cost table is referenced rather than inlined, because it is large and the + * caller already holds one. It is read during the call and not retained. + */ +typedef struct vm_exec_executor_config_t { + /** + * Which opcode set and whitelist to apply; see `OpcodeVersion` on the Rust side. + */ + int32_t opcode_version; + /** + * The per-opcode gas costs. Must not be null. + */ + const struct vm_exec_opcode_cost_t *opcode_cost; +} vm_exec_executor_config_t; + +/** + * The out-struct of `vm_exec_w6_instance_call`. + */ +typedef struct vm_exec_call_result_t { + enum vm_exec_call_result_kind_t kind; + /** + * Meaningful only for `VM_EXEC_CALL_EARLY_EXIT`. + */ + uint64_t early_exit_code; + /** + * Meaningful only for `VM_EXEC_CALL_BREAKPOINT`. Values match `BreakpointValue`: + * 4 = out of gas, 5 = memory limit. + */ + uint64_t breakpoint; +} vm_exec_call_result_t; + +/** + * What every VM hook returns. + * + * One 16-byte struct covers every hook shape — `void`, `i32`, `i64` — and is returned in + * registers under both SysV AMD64 (RAX:RDX) and AAPCS64 (X0:X1), so the early-exit channel + * costs no memory traffic. A cgo `//export` function may return a C struct by value, which + * is what makes this shape available to the Go side. + * + * The early-exit *message* does not travel in this struct. A pointer cannot: cgo forbids + * returning an unpinned Go pointer, and a fixed buffer cannot work because the messages are + * unbounded user text. The Go VM does not need it to travel, because the hook has already + * recorded the message in its own output state by the time it returns. + */ +typedef struct vm_exec_hook_result_t { + /** + * The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for + * any hook that reports an early exit. + */ + int64_t value; + /** + * The early-exit code, or `VM_EXEC_NO_EARLY_EXIT`. + */ + uint64_t early_exit; +} vm_exec_hook_result_t; + +/** + * What a VM hook is handed as its first argument. + * + * Only `go_vm_hooks` is part of the layout the Go side mirrors; the rest is private to + * Rust, and the whole pointer is what travels back into the state functions. + */ +typedef struct vm_exec_hook_context_t { + /** + * The caller's VM hooks handle for this instance. Read by the caller, never by Rust. + * + * An opaque word rather than a pointer, deliberately: Rust only stores it and hands it + * back, and the Go side needs it to be an integer. cgo forbids storing a Go pointer in + * C memory, so what Go puts here is a `runtime/cgo.Handle`, which is a `uintptr` by + * construction. Typing the field as a pointer would force Go to convert it back with + * `unsafe.Pointer(uintptr(...))`, exactly the pattern `go vet` rejects. + */ + uintptr_t go_vm_hooks; + /** + * Erased `&mut S` for the backend's instance state; null while no call is active. + */ + void *state; + /** + * Erased `&RefCell`, for the token check. + */ + const void *pool; + /** + * This activation's call token; `0` while no call is active. + */ + uint64_t token; +} vm_exec_hook_context_t; + +/** + * The Go VM's hook implementations, as a table of C function pointers. + * + * Copied into Rust at executor creation; see CapiVMHookTable. + */ +typedef struct vm_exec_vm_hook_c_func_pointers { + struct vm_exec_hook_result_t (*get_gas_left_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_sc_address_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_hook_result_t (*get_owner_address_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_hook_result_t (*get_shard_of_address_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset); + struct vm_exec_hook_result_t (*is_smart_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset); + struct vm_exec_hook_result_t (*signal_error_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t message_offset, int32_t message_length); + struct vm_exec_hook_result_t (*get_external_balance_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t result_offset); + struct vm_exec_hook_result_t (*get_block_hash_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t nonce, int32_t result_offset); + struct vm_exec_hook_result_t (*get_esdt_balance_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_offset); + struct vm_exec_hook_result_t (*get_esdt_nft_name_length_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); + struct vm_exec_hook_result_t (*get_esdt_nft_attribute_length_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); + struct vm_exec_hook_result_t (*get_esdt_nft_uri_length_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); + struct vm_exec_hook_result_t (*get_esdt_token_data_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t value_handle, int32_t properties_offset, int32_t hash_offset, int32_t name_offset, int32_t attributes_offset, int32_t creator_offset, int32_t royalties_handle, int32_t uris_offset); + struct vm_exec_hook_result_t (*get_esdt_local_roles_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec_hook_result_t (*validate_token_identifier_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec_hook_result_t (*transfer_value_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); + struct vm_exec_hook_result_t (*transfer_value_execute_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*transfer_esdt_execute_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*transfer_esdt_nft_execute_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t nonce, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int32_t num_token_transfers, int32_t token_transfers_args_length_offset, int32_t token_transfer_data_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*create_async_call_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t data_length, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback); + struct vm_exec_hook_result_t (*set_async_context_callback_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t callback, int32_t callback_length, int32_t data, int32_t data_length, int64_t gas); + struct vm_exec_hook_result_t (*upgrade_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*upgrade_from_source_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*delete_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*async_call_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); + struct vm_exec_hook_result_t (*get_argument_length_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t id); + struct vm_exec_hook_result_t (*get_argument_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t id, int32_t arg_offset); + struct vm_exec_hook_result_t (*get_function_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t function_offset); + struct vm_exec_hook_result_t (*get_num_arguments_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*storage_store_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset, int32_t data_length); + struct vm_exec_hook_result_t (*storage_load_length_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_hook_result_t (*storage_load_from_address_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t key_offset, int32_t key_length, int32_t data_offset); + struct vm_exec_hook_result_t (*storage_load_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset); + struct vm_exec_hook_result_t (*set_storage_lock_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t lock_timestamp); + struct vm_exec_hook_result_t (*get_storage_lock_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_hook_result_t (*is_storage_locked_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_hook_result_t (*clear_storage_lock_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_hook_result_t (*get_caller_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_hook_result_t (*check_no_payment_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_call_value_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_hook_result_t (*get_esdt_value_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_hook_result_t (*get_esdt_value_by_index_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_offset, int32_t index); + struct vm_exec_hook_result_t (*get_esdt_token_name_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_offset); + struct vm_exec_hook_result_t (*get_esdt_token_name_by_index_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_offset, int32_t index); + struct vm_exec_hook_result_t (*get_esdt_token_nonce_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_esdt_token_nonce_by_index_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t index); + struct vm_exec_hook_result_t (*get_current_esdt_nft_nonce_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len); + struct vm_exec_hook_result_t (*get_esdt_token_type_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_esdt_token_type_by_index_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t index); + struct vm_exec_hook_result_t (*get_num_esdt_transfers_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_call_value_token_name_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset); + struct vm_exec_hook_result_t (*get_call_value_token_name_by_index_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset, int32_t index); + struct vm_exec_hook_result_t (*is_reserved_function_name_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t name_handle); + struct vm_exec_hook_result_t (*write_log_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_pointer, int32_t data_length, int32_t topic_ptr, int32_t num_topics); + struct vm_exec_hook_result_t (*write_event_log_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t num_topics, int32_t topic_lengths_offset, int32_t topic_offset, int32_t data_offset, int32_t data_length); + struct vm_exec_hook_result_t (*get_block_timestamp_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_block_timestamp_ms_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_block_nonce_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_block_round_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_block_epoch_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_block_random_seed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t pointer); + struct vm_exec_hook_result_t (*get_state_root_hash_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t pointer); + struct vm_exec_hook_result_t (*get_prev_block_timestamp_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_prev_block_timestamp_ms_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_prev_block_nonce_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_prev_block_round_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_prev_block_epoch_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_prev_block_random_seed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t pointer); + struct vm_exec_hook_result_t (*get_block_round_time_ms_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*epoch_start_block_timestamp_ms_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*epoch_start_block_nonce_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*epoch_start_block_round_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*finish_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t pointer, int32_t length); + struct vm_exec_hook_result_t (*execute_on_same_context_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*execute_on_dest_context_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*execute_read_only_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*create_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t result_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*deploy_from_source_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t result_address_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); + struct vm_exec_hook_result_t (*get_num_return_data_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*get_return_data_size_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_id); + struct vm_exec_hook_result_t (*get_return_data_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_id, int32_t data_offset); + struct vm_exec_hook_result_t (*clean_return_data_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*delete_from_return_data_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_id); + struct vm_exec_hook_result_t (*get_original_tx_hash_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_offset); + struct vm_exec_hook_result_t (*get_current_tx_hash_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_offset); + struct vm_exec_hook_result_t (*get_prev_tx_hash_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_offset); + struct vm_exec_hook_result_t (*managed_sc_address_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*managed_owner_address_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*managed_caller_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*managed_get_original_caller_addr_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*managed_get_relayer_addr_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*managed_signal_error_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t err_handle); + struct vm_exec_hook_result_t (*managed_write_log_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t topics_handle, int32_t data_handle); + struct vm_exec_hook_result_t (*managed_get_original_tx_hash_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_get_state_root_hash_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_get_block_random_seed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_get_prev_block_random_seed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_get_return_data_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t result_id, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_get_multi_esdt_call_value_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t multi_call_value_handle); + struct vm_exec_hook_result_t (*managed_get_all_transfers_call_value_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t transfer_call_values_list_handle); + struct vm_exec_hook_result_t (*managed_get_back_transfers_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t esdt_transfers_value_handle, int32_t egld_value_handle); + struct vm_exec_hook_result_t (*managed_get_esdt_balance_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle); + struct vm_exec_hook_result_t (*managed_get_esdt_token_data_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle, int32_t properties_handle, int32_t hash_handle, int32_t name_handle, int32_t attributes_handle, int32_t creator_handle, int32_t royalties_handle, int32_t uris_handle); + struct vm_exec_hook_result_t (*managed_get_esdt_token_type_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t type_handle); + struct vm_exec_hook_result_t (*managed_async_call_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_hook_result_t (*managed_create_async_call_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback, int32_t callback_closure_handle); + struct vm_exec_hook_result_t (*managed_get_callback_closure_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t callback_closure_handle); + struct vm_exec_hook_result_t (*managed_upgrade_from_source_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_upgrade_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_delete_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_handle, int64_t gas_limit, int32_t arguments_handle); + struct vm_exec_hook_result_t (*managed_deploy_from_source_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_create_contract_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_execute_read_only_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_execute_on_same_context_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_execute_on_dest_context_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_execute_on_dest_context_with_error_return_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*managed_multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_hook_result_t (*managed_multi_transfer_esdt_nft_execute_with_return_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_hook_result_t (*managed_multi_transfer_esdt_nft_execute_by_user_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t user_handle, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_hook_result_t (*managed_transfer_value_execute_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dst_handle, int32_t value_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); + struct vm_exec_hook_result_t (*managed_is_esdt_frozen_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce); + struct vm_exec_hook_result_t (*managed_is_esdt_limited_transfer_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec_hook_result_t (*managed_is_esdt_paused_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t token_id_handle); + struct vm_exec_hook_result_t (*managed_buffer_to_hex_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t source_handle, int32_t dest_handle); + struct vm_exec_hook_result_t (*managed_get_code_metadata_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_handle, int32_t response_handle); + struct vm_exec_hook_result_t (*managed_get_code_hash_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_handle, int32_t code_hash_handle); + struct vm_exec_hook_result_t (*managed_is_builtin_function_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t function_name_handle); + struct vm_exec_hook_result_t (*big_float_new_from_parts_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t integral_part, int32_t fractional_part, int32_t exponent); + struct vm_exec_hook_result_t (*big_float_new_from_frac_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t numerator, int64_t denominator); + struct vm_exec_hook_result_t (*big_float_new_from_sci_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t significand, int64_t exponent); + struct vm_exec_hook_result_t (*big_float_add_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_float_sub_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_float_mul_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_float_div_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_float_neg_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_float_clone_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_float_cmp_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_float_abs_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_float_sign_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t op_handle); + struct vm_exec_hook_result_t (*big_float_sqrt_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_float_pow_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t exponent); + struct vm_exec_hook_result_t (*big_float_floor_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_float_ceil_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_float_truncate_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_float_set_int64_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int64_t value); + struct vm_exec_hook_result_t (*big_float_is_int_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t op_handle); + struct vm_exec_hook_result_t (*big_float_set_big_int_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t big_int_handle); + struct vm_exec_hook_result_t (*big_float_get_const_pi_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*big_float_get_const_e_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*big_int_get_unsigned_argument_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t id, int32_t destination_handle); + struct vm_exec_hook_result_t (*big_int_get_signed_argument_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t id, int32_t destination_handle); + struct vm_exec_hook_result_t (*big_int_storage_store_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t source_handle); + struct vm_exec_hook_result_t (*big_int_storage_load_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t destination_handle); + struct vm_exec_hook_result_t (*big_int_get_call_value_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*big_int_get_esdt_call_value_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination); + struct vm_exec_hook_result_t (*big_int_get_esdt_call_value_by_index_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t index); + struct vm_exec_hook_result_t (*big_int_get_external_balance_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t result); + struct vm_exec_hook_result_t (*big_int_get_esdt_external_balance_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_handle); + struct vm_exec_hook_result_t (*big_int_new_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t small_value); + struct vm_exec_hook_result_t (*big_int_unsigned_byte_length_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec_hook_result_t (*big_int_signed_byte_length_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec_hook_result_t (*big_int_get_unsigned_bytes_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); + struct vm_exec_hook_result_t (*big_int_get_signed_bytes_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); + struct vm_exec_hook_result_t (*big_int_set_unsigned_bytes_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); + struct vm_exec_hook_result_t (*big_int_set_signed_bytes_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); + struct vm_exec_hook_result_t (*big_int_is_int64_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*big_int_get_int64_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle); + struct vm_exec_hook_result_t (*big_int_set_int64_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int64_t value); + struct vm_exec_hook_result_t (*big_int_add_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_sub_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_mul_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_tdiv_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_tmod_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_ediv_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_emod_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_sqrt_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_int_pow_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_log2_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t op1_handle); + struct vm_exec_hook_result_t (*big_int_abs_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_int_neg_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_int_sign_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t op_handle); + struct vm_exec_hook_result_t (*big_int_cmp_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_not_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); + struct vm_exec_hook_result_t (*big_int_and_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_or_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_xor_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); + struct vm_exec_hook_result_t (*big_int_shr_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); + struct vm_exec_hook_result_t (*big_int_shl_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); + struct vm_exec_hook_result_t (*big_int_finish_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec_hook_result_t (*big_int_finish_signed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t reference_handle); + struct vm_exec_hook_result_t (*big_int_to_string_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t big_int_handle, int32_t destination_handle); + struct vm_exec_hook_result_t (*mbuffer_new_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*mbuffer_new_from_bytes_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_offset, int32_t data_length); + struct vm_exec_hook_result_t (*mbuffer_get_length_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle); + struct vm_exec_hook_result_t (*mbuffer_get_bytes_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int32_t result_offset); + struct vm_exec_hook_result_t (*mbuffer_get_byte_slice_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t result_offset); + struct vm_exec_hook_result_t (*mbuffer_copy_byte_slice_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t destination_handle); + struct vm_exec_hook_result_t (*mbuffer_eq_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle1, int32_t m_buffer_handle2); + struct vm_exec_hook_result_t (*mbuffer_set_bytes_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int32_t data_offset, int32_t data_length); + struct vm_exec_hook_result_t (*mbuffer_set_byte_slice_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int32_t starting_position, int32_t data_length, int32_t data_offset); + struct vm_exec_hook_result_t (*mbuffer_append_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_handle); + struct vm_exec_hook_result_t (*mbuffer_append_bytes_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_offset, int32_t data_length); + struct vm_exec_hook_result_t (*mbuffer_to_big_int_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec_hook_result_t (*mbuffer_to_big_int_signed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec_hook_result_t (*mbuffer_from_big_int_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec_hook_result_t (*mbuffer_from_big_int_signed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); + struct vm_exec_hook_result_t (*mbuffer_to_small_int_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle); + struct vm_exec_hook_result_t (*mbuffer_to_small_int_signed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle); + struct vm_exec_hook_result_t (*mbuffer_from_small_int_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); + struct vm_exec_hook_result_t (*mbuffer_from_small_int_signed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); + struct vm_exec_hook_result_t (*mbuffer_to_big_float_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); + struct vm_exec_hook_result_t (*mbuffer_from_big_float_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); + struct vm_exec_hook_result_t (*mbuffer_storage_store_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_handle, int32_t source_handle); + struct vm_exec_hook_result_t (*mbuffer_storage_load_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_handle, int32_t destination_handle); + struct vm_exec_hook_result_t (*mbuffer_storage_load_from_address_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t address_handle, int32_t key_handle, int32_t destination_handle); + struct vm_exec_hook_result_t (*mbuffer_get_argument_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t id, int32_t destination_handle); + struct vm_exec_hook_result_t (*mbuffer_finish_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t source_handle); + struct vm_exec_hook_result_t (*mbuffer_set_random_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t destination_handle, int32_t length); + struct vm_exec_hook_result_t (*managed_map_new_func_ptr)(struct vm_exec_hook_context_t *ctx); + struct vm_exec_hook_result_t (*managed_map_put_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t value_handle); + struct vm_exec_hook_result_t (*managed_map_get_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); + struct vm_exec_hook_result_t (*managed_map_remove_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); + struct vm_exec_hook_result_t (*managed_map_contains_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle); + struct vm_exec_hook_result_t (*small_int_get_unsigned_argument_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t id); + struct vm_exec_hook_result_t (*small_int_get_signed_argument_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t id); + struct vm_exec_hook_result_t (*small_int_finish_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t value); + struct vm_exec_hook_result_t (*small_int_finish_signed_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t value); + struct vm_exec_hook_result_t (*small_int_storage_store_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); + struct vm_exec_hook_result_t (*small_int_storage_store_signed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); + struct vm_exec_hook_result_t (*small_int_storage_load_unsigned_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_hook_result_t (*small_int_storage_load_signed_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_hook_result_t (*int64get_argument_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t id); + struct vm_exec_hook_result_t (*int64finish_func_ptr)(struct vm_exec_hook_context_t *ctx, int64_t value); + struct vm_exec_hook_result_t (*int64storage_store_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); + struct vm_exec_hook_result_t (*int64storage_load_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length); + struct vm_exec_hook_result_t (*sha256_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); + struct vm_exec_hook_result_t (*managed_sha256_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); + struct vm_exec_hook_result_t (*keccak256_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); + struct vm_exec_hook_result_t (*managed_keccak256_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); + struct vm_exec_hook_result_t (*ripemd160_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); + struct vm_exec_hook_result_t (*managed_ripemd160_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); + struct vm_exec_hook_result_t (*verify_bls_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); + struct vm_exec_hook_result_t (*managed_verify_bls_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_hook_result_t (*verify_ed25519_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); + struct vm_exec_hook_result_t (*managed_verify_ed25519_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_hook_result_t (*verify_custom_secp256k1_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset, int32_t hash_type); + struct vm_exec_hook_result_t (*managed_verify_custom_secp256k1_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle, int32_t hash_type); + struct vm_exec_hook_result_t (*verify_secp256k1_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset); + struct vm_exec_hook_result_t (*managed_verify_secp256k1_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_hook_result_t (*encode_secp256k1_der_signature_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t r_offset, int32_t r_length, int32_t s_offset, int32_t s_length, int32_t sig_offset); + struct vm_exec_hook_result_t (*managed_encode_secp256k1_der_signature_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t r_handle, int32_t s_handle, int32_t sig_handle); + struct vm_exec_hook_result_t (*add_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t fst_point_xhandle, int32_t fst_point_yhandle, int32_t snd_point_xhandle, int32_t snd_point_yhandle); + struct vm_exec_hook_result_t (*double_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); + struct vm_exec_hook_result_t (*is_on_curve_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); + struct vm_exec_hook_result_t (*scalar_base_mult_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); + struct vm_exec_hook_result_t (*managed_scalar_base_mult_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); + struct vm_exec_hook_result_t (*scalar_mult_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_offset, int32_t length); + struct vm_exec_hook_result_t (*managed_scalar_mult_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_handle); + struct vm_exec_hook_result_t (*marshal_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); + struct vm_exec_hook_result_t (*managed_marshal_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*marshal_compressed_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); + struct vm_exec_hook_result_t (*managed_marshal_compressed_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*unmarshal_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); + struct vm_exec_hook_result_t (*managed_unmarshal_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); + struct vm_exec_hook_result_t (*unmarshal_compressed_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); + struct vm_exec_hook_result_t (*managed_unmarshal_compressed_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); + struct vm_exec_hook_result_t (*generate_key_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_offset); + struct vm_exec_hook_result_t (*managed_generate_key_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_handle); + struct vm_exec_hook_result_t (*create_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_offset, int32_t data_length); + struct vm_exec_hook_result_t (*managed_create_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t data_handle); + struct vm_exec_hook_result_t (*get_curve_length_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t ec_handle); + struct vm_exec_hook_result_t (*get_priv_key_byte_length_ec_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t ec_handle); + struct vm_exec_hook_result_t (*elliptic_curve_get_values_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t ec_handle, int32_t field_order_handle, int32_t base_point_order_handle, int32_t eq_constant_handle, int32_t x_base_point_handle, int32_t y_base_point_handle); + struct vm_exec_hook_result_t (*managed_verify_secp256r1_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_hook_result_t (*managed_verify_blssignature_share_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); + struct vm_exec_hook_result_t (*managed_verify_blsaggregated_signature_func_ptr)(struct vm_exec_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); +} vm_exec_vm_hook_c_func_pointers; + +/** + * A buffer of this size holds every message the executor currently produces. + * + * Exported so the Go side has one place to take the number from rather than picking its + * own. + */ +extern const int VM_EXEC_ERROR_BUF_RECOMMENDED_LEN; + +#endif /* VM_EXEC_COMMON_H */ diff --git a/c-api-common/src/capi_call_result.rs b/c-api-common/src/capi_call_result.rs new file mode 100644 index 00000000..d4d127b2 --- /dev/null +++ b/c-api-common/src/capi_call_result.rs @@ -0,0 +1,115 @@ +//! How a call ended, as it crosses the boundary. +//! +//! This replaces the legacy arrangement, where a call returned a bare ok/error and the VM then +//! polled a wasm global to find out what had actually happened. The reason now travels with +//! the result. +//! +//! Shared between backends: an `InstanceCallResult` means the same thing whichever runtime +//! produced it, and both report it in the same struct so that a caller linking both sees one +//! definition. + +use multiversx_chain_vm_executor::{BreakpointValue, InstanceCallResult}; + +use crate::capi_status::write_error; +use libc::{c_char, c_int}; + +/// How a call ended. +/// +/// This replaces the legacy arrangement, where a call returned a bare ok/error and the VM +/// then polled a wasm global to find out what had actually happened. +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum vm_exec_call_result_kind_t { + /// The function returned normally. + VM_EXEC_CALL_OK = 0, + + /// No exported function by that name. + VM_EXEC_CALL_FUNCTION_NOT_FOUND = 1, + + /// The instance trapped for a reason that is neither a VM hook's doing nor a + /// middleware breakpoint. The message is in the error buffer. + VM_EXEC_CALL_RUNTIME_ERROR = 2, + + /// A VM hook asked for execution to stop. `early_exit_code` says which; the message, if + /// any, is in the error buffer, and is informational — the VM has already recorded its + /// own. + VM_EXEC_CALL_EARLY_EXIT = 3, + + /// A middleware stopped execution. `breakpoint` says which. + VM_EXEC_CALL_BREAKPOINT = 4, +} + +/// The out-struct of `vm_exec_w6_instance_call`. +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct vm_exec_call_result_t { + pub kind: vm_exec_call_result_kind_t, + + /// Meaningful only for `VM_EXEC_CALL_EARLY_EXIT`. + pub early_exit_code: u64, + + /// Meaningful only for `VM_EXEC_CALL_BREAKPOINT`. Values match `BreakpointValue`: + /// 4 = out of gas, 5 = memory limit. + pub breakpoint: u64, +} + +impl vm_exec_call_result_t { + pub fn of_kind(kind: vm_exec_call_result_kind_t) -> Self { + vm_exec_call_result_t { + kind, + early_exit_code: 0, + breakpoint: 0, + } + } +} + +impl vm_exec_call_result_t { + /// Converts a call outcome, writing any message the caller might want into `err_buf`. + /// + /// The message is informational in both reporting cases. For an early exit the VM has + /// already recorded its own, which is why the text does not have to cross the boundary at + /// all; what arrives here is whatever the executor had to say about it. + /// + /// # Safety + /// + /// `err_buf` must be null or writable for `err_buf_len` bytes. + pub unsafe fn from_call_result( + result: InstanceCallResult, + err_buf: *mut c_char, + err_buf_len: c_int, + ) -> Self { + match result { + InstanceCallResult::Ok => { + vm_exec_call_result_t::of_kind(vm_exec_call_result_kind_t::VM_EXEC_CALL_OK) + } + InstanceCallResult::FunctionNotFound => vm_exec_call_result_t::of_kind( + vm_exec_call_result_kind_t::VM_EXEC_CALL_FUNCTION_NOT_FOUND, + ), + InstanceCallResult::RuntimeError(err) => { + write_error(err, err_buf, err_buf_len); + vm_exec_call_result_t::of_kind( + vm_exec_call_result_kind_t::VM_EXEC_CALL_RUNTIME_ERROR, + ) + } + InstanceCallResult::VMHooksEarlyExit(early_exit) => { + write_error(&early_exit.message, err_buf, err_buf_len); + vm_exec_call_result_t { + kind: vm_exec_call_result_kind_t::VM_EXEC_CALL_EARLY_EXIT, + early_exit_code: early_exit.code, + breakpoint: 0, + } + } + InstanceCallResult::Breakpoint(breakpoint) => vm_exec_call_result_t { + kind: vm_exec_call_result_kind_t::VM_EXEC_CALL_BREAKPOINT, + early_exit_code: 0, + breakpoint: breakpoint_as_u64(breakpoint), + }, + } + } +} + +fn breakpoint_as_u64(breakpoint: BreakpointValue) -> u64 { + breakpoint.as_u64() +} diff --git a/c-api-wasmer6/src/capi_config.rs b/c-api-common/src/capi_config.rs similarity index 88% rename from c-api-wasmer6/src/capi_config.rs rename to c-api-common/src/capi_config.rs index dd7ad8f3..8e4da713 100644 --- a/c-api-wasmer6/src/capi_config.rs +++ b/c-api-common/src/capi_config.rs @@ -15,7 +15,7 @@ use crate::capi_opcode_cost::vm_exec_opcode_cost_t; /// caller already holds one. It is read during the call and not retained. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec_w6_executor_config_t { +pub struct vm_exec_executor_config_t { /// Which opcode set and whitelist to apply; see `OpcodeVersion` on the Rust side. pub opcode_version: i32, @@ -23,13 +23,13 @@ pub struct vm_exec_w6_executor_config_t { pub opcode_cost: *const vm_exec_opcode_cost_t, } -impl vm_exec_w6_executor_config_t { +impl vm_exec_executor_config_t { /// Converts to the executor's own config type. /// /// # Safety /// /// `self.opcode_cost` must be a valid, non-null pointer for the duration of the call. - pub(crate) unsafe fn to_opcode_config(&self) -> Result { + pub unsafe fn to_opcode_config(&self) -> Result { let Some(opcode_version) = OpcodeVersion::from_i32(self.opcode_version) else { return Err(format!( "invalid opcode version code: {}", @@ -55,15 +55,15 @@ impl vm_exec_w6_executor_config_t { /// fixed to `u64` here, since `usize` has no guaranteed width across host platforms. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec_w6_compilation_options_t { +pub struct vm_exec_compilation_options_t { pub unmetered_locals: u64, pub max_memory_grow: u64, pub max_memory_grow_delta: u64, pub opcode_trace: bool, } -impl vm_exec_w6_compilation_options_t { - pub(crate) fn to_compilation_options(&self) -> CompilationOptions { +impl vm_exec_compilation_options_t { + pub fn to_compilation_options(&self) -> CompilationOptions { CompilationOptions { unmetered_locals: self.unmetered_locals as usize, max_memory_grow: self.max_memory_grow as usize, diff --git a/c-api-common/src/capi_logger.rs b/c-api-common/src/capi_logger.rs new file mode 100644 index 00000000..0c07171f --- /dev/null +++ b/c-api-common/src/capi_logger.rs @@ -0,0 +1,20 @@ +//! Panic suppression at the C boundary. +//! +//! The module is named `capi_logger` because that is the path the shared +//! `meta::capi_safe_unwind` attribute expands to. It deliberately does not carry the legacy +//! crate's logging FFI: log configuration belongs to the process, and the legacy `c-api` +//! already owns those symbols. + +use std::sync::Once; + +static PANIC_HANDLER: Once = Once::new(); + +/// Installs a no-op panic hook, so a caught panic does not also print a backtrace. +/// +/// A panic must never unwind across `extern "C"`; the `capi_safe_unwind` attribute catches +/// it and turns it into `VM_EXEC_PANIC`. This only silences the default hook's output. +pub fn set_panic_handler() { + PANIC_HANDLER.call_once(|| { + std::panic::set_hook(Box::new(|_| {})); + }); +} diff --git a/c-api-wasmer6/src/capi_opcode_cost.rs b/c-api-common/src/capi_opcode_cost.rs similarity index 100% rename from c-api-wasmer6/src/capi_opcode_cost.rs rename to c-api-common/src/capi_opcode_cost.rs diff --git a/c-api-wasmer6/src/capi_status.rs b/c-api-common/src/capi_status.rs similarity index 79% rename from c-api-wasmer6/src/capi_status.rs rename to c-api-common/src/capi_status.rs index bfdcd5d1..102016c7 100644 --- a/c-api-wasmer6/src/capi_status.rs +++ b/c-api-common/src/capi_status.rs @@ -13,33 +13,33 @@ use std::{ptr, slice}; /// Result of a C API call. /// -/// `VM_EXEC_W6_OK` is zero and every failure is negative, so the functions that return a +/// `VM_EXEC_OK` is zero and every failure is negative, so the functions that return a /// count (the memory loads) can share the same code space: a non-negative result is a /// count, a negative one is one of these. #[allow(non_camel_case_types)] #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum vm_exec_w6_status_t { +pub enum vm_exec_status_t { /// The call succeeded. - VM_EXEC_W6_OK = 0, + VM_EXEC_OK = 0, /// The call failed; the error message was written to the provided error buffer. - VM_EXEC_W6_ERROR = -1, + VM_EXEC_ERROR = -1, /// A pointer argument that must not be null was null. No message is produced, because /// a null pointer may well be the error buffer itself. - VM_EXEC_W6_NULL_POINTER = -2, + VM_EXEC_NULL_POINTER = -2, /// A hook context was used outside the host call it belongs to. See /// [`crate::capi_vm_hook_context`]. - VM_EXEC_W6_STALE_CONTEXT = -3, + VM_EXEC_STALE_CONTEXT = -3, /// A panic was caught at the boundary. Rust panics must never unwind into C, so every /// entry point traps them and reports this instead. - VM_EXEC_W6_PANIC = -4, + VM_EXEC_PANIC = -4, } -impl vm_exec_w6_status_t { +impl vm_exec_status_t { pub fn as_c_int(self) -> c_int { self as c_int } @@ -54,15 +54,15 @@ impl vm_exec_w6_status_t { /// The message is truncated to fit and always NUL-terminated. Truncation is silent and /// deliberate: the alternative is a second call to ask for the length, which is the /// round-trip this API is getting rid of. Callers size the buffer once; see -/// `VM_EXEC_W6_ERROR_BUF_RECOMMENDED_LEN`. +/// `VM_EXEC_ERROR_BUF_RECOMMENDED_LEN`. /// /// A null buffer, or a buffer with no room for the terminator, is accepted and ignored, so /// that a caller that does not care about the message can pass `(NULL, 0)`. -pub(crate) fn write_error( +pub fn write_error( message: impl Display, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { +) -> vm_exec_status_t { if !err_buf.is_null() && err_buf_len > 0 { let message = message.to_string(); let capacity = err_buf_len as usize - 1; // room for the NUL @@ -81,15 +81,11 @@ pub(crate) fn write_error( } } - vm_exec_w6_status_t::VM_EXEC_W6_ERROR + vm_exec_status_t::VM_EXEC_ERROR } /// Writes a message and returns it as a negative count, for the functions that return one. -pub(crate) fn write_error_count( - message: impl Display, - err_buf: *mut c_char, - err_buf_len: c_int, -) -> i64 { +pub fn write_error_count(message: impl Display, err_buf: *mut c_char, err_buf_len: c_int) -> i64 { write_error(message, err_buf, err_buf_len).as_i64() } @@ -98,13 +94,13 @@ pub(crate) fn write_error_count( /// Exported so the Go side has one place to take the number from rather than picking its /// own. #[unsafe(no_mangle)] -pub static VM_EXEC_W6_ERROR_BUF_RECOMMENDED_LEN: c_int = 1024; +pub static VM_EXEC_ERROR_BUF_RECOMMENDED_LEN: c_int = 1024; #[cfg(test)] mod tests { use super::*; - fn write_to_buf(message: &str, len: usize) -> (vm_exec_w6_status_t, String) { + fn write_to_buf(message: &str, len: usize) -> (vm_exec_status_t, String) { let mut buf = vec![0i8; len]; let status = write_error(message, buf.as_mut_ptr() as *mut c_char, len as c_int); let bytes: Vec = buf @@ -118,7 +114,7 @@ mod tests { #[test] fn error_message_round_trips() { let (status, written) = write_to_buf("boom", 64); - assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_ERROR); + assert_eq!(status, vm_exec_status_t::VM_EXEC_ERROR); assert_eq!(written, "boom"); } @@ -139,22 +135,22 @@ mod tests { fn error_message_tolerates_no_buffer() { assert_eq!( write_error("boom", std::ptr::null_mut(), 0), - vm_exec_w6_status_t::VM_EXEC_W6_ERROR + vm_exec_status_t::VM_EXEC_ERROR ); assert_eq!( write_error("boom", std::ptr::null_mut(), 64), - vm_exec_w6_status_t::VM_EXEC_W6_ERROR + vm_exec_status_t::VM_EXEC_ERROR ); } #[test] fn status_codes_leave_room_for_counts() { - assert_eq!(vm_exec_w6_status_t::VM_EXEC_W6_OK.as_i64(), 0); + assert_eq!(vm_exec_status_t::VM_EXEC_OK.as_i64(), 0); for status in [ - vm_exec_w6_status_t::VM_EXEC_W6_ERROR, - vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER, - vm_exec_w6_status_t::VM_EXEC_W6_STALE_CONTEXT, - vm_exec_w6_status_t::VM_EXEC_W6_PANIC, + vm_exec_status_t::VM_EXEC_ERROR, + vm_exec_status_t::VM_EXEC_NULL_POINTER, + vm_exec_status_t::VM_EXEC_STALE_CONTEXT, + vm_exec_status_t::VM_EXEC_PANIC, ] { assert!(status.as_i64() < 0, "{status:?} must be negative"); } diff --git a/c-api-wasmer6/src/capi_strings.rs b/c-api-common/src/capi_strings.rs similarity index 94% rename from c-api-wasmer6/src/capi_strings.rs rename to c-api-common/src/capi_strings.rs index 6995283f..f3846b50 100644 --- a/c-api-wasmer6/src/capi_strings.rs +++ b/c-api-common/src/capi_strings.rs @@ -8,7 +8,7 @@ use libc::{c_char, c_int}; use std::{ptr, slice}; /// The length, including the NUL terminator, that `copy_pipe_joined` needs for `names`. -pub(crate) fn pipe_joined_length(names: &[String]) -> c_int { +pub fn pipe_joined_length(names: &[String]) -> c_int { if names.is_empty() { return 0; } @@ -26,11 +26,7 @@ pub(crate) fn pipe_joined_length(names: &[String]) -> c_int { /// # Safety /// /// `dest` must be null or writable for `dest_len` bytes. -pub(crate) unsafe fn copy_pipe_joined( - names: &[String], - dest: *mut c_char, - dest_len: c_int, -) -> c_int { +pub unsafe fn copy_pipe_joined(names: &[String], dest: *mut c_char, dest_len: c_int) -> c_int { let joined = names.join("|"); if dest.is_null() || dest_len <= 0 { diff --git a/c-api-wasmer6/src/capi_vm_hook_context.rs b/c-api-common/src/capi_vm_hook_context.rs similarity index 74% rename from c-api-wasmer6/src/capi_vm_hook_context.rs rename to c-api-common/src/capi_vm_hook_context.rs index 8cf6d464..09788ad4 100644 --- a/c-api-wasmer6/src/capi_vm_hook_context.rs +++ b/c-api-common/src/capi_vm_hook_context.rs @@ -10,7 +10,7 @@ //! 1. **Lifetime.** Each activation gets a fresh, non-zero call token. The context records //! it, and the pool records which token is currently innermost. A state function accepts //! a context only while the two agree, so a context used after its hook returned is -//! rejected with `VM_EXEC_W6_STALE_CONTEXT` instead of dereferencing a dead borrow. +//! rejected with `VM_EXEC_STALE_CONTEXT` instead of dereferencing a dead borrow. //! //! 2. **Exclusivity.** Contexts are activated in a stack, one per nesting level, so a hook //! that re-enters the VM (`executeOnDestContext` running a contract from inside a hook) @@ -35,19 +35,18 @@ use std::cell::RefCell; use std::ffi::c_void; use multiversx_chain_vm_executor::VMHooks; -use multiversx_chain_vm_executor_wasmer_experimental::{ - ExperimentalInstanceState, ExperimentalVMHooksBuilder, -}; + +use crate::capi_status::vm_exec_status_t; use crate::capi_vm_hook_table::CapiVMHookTable; use crate::capi_vm_hooks::CapiVMHooks; -/// The value of `vm_exec_w6_hook_result_t::early_exit` that means "the hook returned normally". +/// The value of `vm_exec_hook_result_t::early_exit` that means "the hook returned normally". /// /// A dedicated sentinel is needed because `0` is a real early-exit code: the Rust VM uses /// `ASYNC_CALL_EARLY_EXIT_CODE = 0` and the Go VM has the same async-call case, so "code 0" /// and "no code" have to stay distinguishable. -pub const VM_EXEC_W6_NO_EARLY_EXIT: u64 = u64::MAX; +pub const VM_EXEC_NO_EARLY_EXIT: u64 = u64::MAX; /// What every VM hook returns. /// @@ -63,27 +62,27 @@ pub const VM_EXEC_W6_NO_EARLY_EXIT: u64 = u64::MAX; #[allow(non_camel_case_types)] #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct vm_exec_w6_hook_result_t { +pub struct vm_exec_hook_result_t { /// The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for /// any hook that reports an early exit. pub value: i64, - /// The early-exit code, or `VM_EXEC_W6_NO_EARLY_EXIT`. + /// The early-exit code, or `VM_EXEC_NO_EARLY_EXIT`. pub early_exit: u64, } -impl vm_exec_w6_hook_result_t { +impl vm_exec_hook_result_t { /// The hook returned normally, with the given value (`0` for a `void` hook). pub fn ok(value: i64) -> Self { - vm_exec_w6_hook_result_t { + vm_exec_hook_result_t { value, - early_exit: VM_EXEC_W6_NO_EARLY_EXIT, + early_exit: VM_EXEC_NO_EARLY_EXIT, } } /// The hook requested an early exit with the given code. pub fn early_exit(code: u64) -> Self { - vm_exec_w6_hook_result_t { + vm_exec_hook_result_t { value: 0, early_exit: code, } @@ -91,7 +90,7 @@ impl vm_exec_w6_hook_result_t { /// The early-exit code, if there is one. pub fn early_exit_code(self) -> Option { - if self.early_exit == VM_EXEC_W6_NO_EARLY_EXIT { + if self.early_exit == VM_EXEC_NO_EARLY_EXIT { None } else { Some(self.early_exit) @@ -105,7 +104,7 @@ impl vm_exec_w6_hook_result_t { /// Rust, and the whole pointer is what travels back into the state functions. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec_w6_hook_context_t { +pub struct vm_exec_hook_context_t { /// The caller's VM hooks handle for this instance. Read by the caller, never by Rust. /// /// An opaque word rather than a pointer, deliberately: Rust only stores it and hands it @@ -115,7 +114,7 @@ pub struct vm_exec_w6_hook_context_t { /// `unsafe.Pointer(uintptr(...))`, exactly the pattern `go vet` rejects. pub go_vm_hooks: usize, - /// Erased `&mut ExperimentalInstanceState<'_>`; null while no call is active. + /// Erased `&mut S` for the backend's instance state; null while no call is active. state: *mut c_void, /// Erased `&RefCell`, for the token check. @@ -125,16 +124,9 @@ pub struct vm_exec_w6_hook_context_t { token: u64, } -/// Lifetime-erased form of the instance state pointer kept in a context. -/// -/// The real type is `&mut ExperimentalInstanceState<'s>` for the `'s` of the host-call frame. -/// That lifetime cannot be expressed across C, so it is erased here and enforced at runtime -/// by the call token. -type ErasedInstanceState = ExperimentalInstanceState<'static>; - -impl vm_exec_w6_hook_context_t { +impl vm_exec_hook_context_t { fn inactive() -> Self { - vm_exec_w6_hook_context_t { + vm_exec_hook_context_t { go_vm_hooks: 0, state: std::ptr::null_mut(), pool: std::ptr::null(), @@ -150,12 +142,12 @@ impl vm_exec_w6_hook_context_t { /// /// # Safety /// -/// `ctx` must either be null or point to a `vm_exec_w6_hook_context_t` that this crate +/// `ctx` must either be null or point to a `vm_exec_hook_context_t` that this crate /// activated. Pooled contexts are never freed before their instance, so a context pointer /// obtained from a hook always satisfies this, live or not. -pub(crate) unsafe fn with_instance_state( - ctx: *mut vm_exec_w6_hook_context_t, - f: impl FnOnce(&mut &mut ExperimentalInstanceState<'static>) -> R, +pub unsafe fn with_instance_state( + ctx: *mut vm_exec_hook_context_t, + f: impl FnOnce(&mut &mut S) -> R, ) -> Result { if ctx.is_null() { return Err(ContextError::NullContext); @@ -176,30 +168,45 @@ pub(crate) unsafe fn with_instance_state( return Err(ContextError::Stale); } - let mut state: &mut ErasedInstanceState = - unsafe { &mut *(ctx.state as *mut ErasedInstanceState) }; + // The caller names `S`, and is responsible for naming the same type the matching + // `with_hook_context` erased. Within one backend crate there is only one such type, which + // is what makes that obligation checkable by reading a single file. + let mut state: &mut S = unsafe { &mut *(ctx.state as *mut S) }; Ok(f(&mut state)) } /// Why a context could not be used. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ContextError { +pub enum ContextError { NullContext, Stale, } +impl ContextError { + /// The status a state function reports for this failure. + /// + /// Lives here rather than with the state functions, because every backend's copy of those + /// maps it the same way and the mapping is part of what the error means. + pub fn as_status(self) -> vm_exec_status_t { + match self { + ContextError::NullContext => vm_exec_status_t::VM_EXEC_NULL_POINTER, + ContextError::Stale => vm_exec_status_t::VM_EXEC_STALE_CONTEXT, + } + } +} + /// The per-instance stack of hook contexts. #[derive(Default)] pub struct CapiVMHookContextPool { /// One boxed context per nesting level. /// /// The `Box` is load-bearing and not the redundancy clippy takes it for: a - /// `Vec` would move its elements when it grows, invalidating + /// `Vec` would move its elements when it grows, invalidating /// context pointers that outer hook invocations are still holding. Boxing each context /// individually pins its address for the instance's lifetime, which is what lets a /// stale pointer be read and rejected instead of being undefined behaviour. #[allow(clippy::vec_box)] - slots: Vec>, + slots: Vec>, /// Nesting depth of the currently active call. depth: usize, @@ -213,6 +220,10 @@ pub struct CapiVMHookContextPool { } /// Builds the VM hooks for one host call, and owns the context lifecycle. +/// +/// Backend-independent: the state pointer it stores is erased, so nothing here names the +/// backend's state type. Each backend crate supplies the `with_vm_hooks` implementation that +/// its builder trait requires, naming its own state type at that one point. pub struct CapiVMHooksBuilder { go_vm_hooks: usize, hook_table: CapiVMHookTable, @@ -233,12 +244,12 @@ impl CapiVMHooksBuilder { /// The context is deactivated before this returns, whether `f` panics or not, so a /// pointer that escapes is guaranteed to fail the token check rather than reach a dead /// borrow. - pub fn with_hook_context( + pub fn with_hook_context( &self, - instance_state: &mut ExperimentalInstanceState<'_>, - f: impl FnOnce(*mut vm_exec_w6_hook_context_t) -> R, + instance_state: &mut S, + f: impl FnOnce(*mut vm_exec_hook_context_t) -> R, ) -> R { - let state_ptr = (instance_state as *mut ExperimentalInstanceState<'_>).cast::(); + let state_ptr = (instance_state as *mut S).cast::(); // Safety: the pointer is derived from a live `&mut` that outlives this call, and the // context holding it is deactivated before we return. @@ -253,12 +264,13 @@ impl CapiVMHooksBuilder { /// /// # Safety /// - /// `state` must point to a live `ExperimentalInstanceState` for the whole of `f`, unless + /// `state` must point to a live instance state of the backend's own type for the whole of + /// `f`, unless /// `f` never reaches the state through the context. pub unsafe fn with_hook_context_raw( &self, state: *mut c_void, - f: impl FnOnce(*mut vm_exec_w6_hook_context_t) -> R, + f: impl FnOnce(*mut vm_exec_hook_context_t) -> R, ) -> R { let (ctx_ptr, parent_token) = { let mut pool = self.pool.borrow_mut(); @@ -266,7 +278,7 @@ impl CapiVMHooksBuilder { let depth = pool.depth; if depth == pool.slots.len() { pool.slots - .push(Box::new(vm_exec_w6_hook_context_t::inactive())); + .push(Box::new(vm_exec_hook_context_t::inactive())); } pool.next_token += 1; @@ -283,7 +295,7 @@ impl CapiVMHooksBuilder { slot.pool = pool_ptr; slot.token = token; - (&mut **slot as *mut vm_exec_w6_hook_context_t, parent_token) + (&mut **slot as *mut vm_exec_hook_context_t, parent_token) }; // The pool borrow is released across `f`, because the hook will call back into the @@ -299,7 +311,7 @@ impl CapiVMHooksBuilder { result } - fn deactivate(&self, ctx_ptr: *mut vm_exec_w6_hook_context_t, parent_token: u64) { + fn deactivate(&self, ctx_ptr: *mut vm_exec_hook_context_t, parent_token: u64) { let mut pool = self.pool.borrow_mut(); pool.depth = pool.depth.saturating_sub(1); pool.active_token = parent_token; @@ -313,10 +325,30 @@ impl CapiVMHooksBuilder { } } +impl CapiVMHooksBuilder { + /// Activates a context around `instance_state` and lends the generated hooks to `f`. + /// + /// This is the whole of hook dispatch, and it names no backend: the state is erased on the + /// way in and reconstituted by `with_instance_state` on the way back. A backend crate + /// wraps this in the builder trait its executor expects, which is the one place its state + /// type appears. + pub fn with_vm_hooks_for_state( + &self, + instance_state: &mut S, + f: &mut dyn FnMut(&mut dyn VMHooks), + ) { + self.with_hook_context(instance_state, |ctx| { + // The hooks borrow the context, so they live exactly as long as this activation + // — which is also exactly as long as the instance state they reach through it. + f(&mut CapiVMHooks::new(ctx, self.hook_table.func_pointers())); + }); + } +} + /// Runs the deactivation even if the hook body unwinds. struct DeactivateOnDrop<'a> { builder: &'a CapiVMHooksBuilder, - ctx_ptr: *mut vm_exec_w6_hook_context_t, + ctx_ptr: *mut vm_exec_hook_context_t, parent_token: u64, } @@ -326,53 +358,39 @@ impl Drop for DeactivateOnDrop<'_> { } } -impl ExperimentalVMHooksBuilder for CapiVMHooksBuilder { - fn with_vm_hooks( - &self, - instance_state_ref: &mut ExperimentalInstanceState, - f: &mut dyn FnMut(&mut dyn VMHooks), - ) { - self.with_hook_context(instance_state_ref, |ctx| { - // The hooks borrow the context, so they live exactly as long as this activation - // — which is also exactly as long as the instance state they reach through it. - f(&mut CapiVMHooks::new(ctx, self.hook_table.func_pointers())); - }); - } -} - #[cfg(test)] mod tests { use super::*; #[test] fn hook_result_distinguishes_code_zero_from_no_code() { - assert_eq!(vm_exec_w6_hook_result_t::ok(7).early_exit_code(), None); + assert_eq!(vm_exec_hook_result_t::ok(7).early_exit_code(), None); assert_eq!( - vm_exec_w6_hook_result_t::early_exit(0).early_exit_code(), + vm_exec_hook_result_t::early_exit(0).early_exit_code(), Some(0) ); assert_eq!( - vm_exec_w6_hook_result_t::early_exit(4).early_exit_code(), + vm_exec_hook_result_t::early_exit(4).early_exit_code(), Some(4) ); } #[test] fn hook_result_fits_in_two_registers() { - assert_eq!(std::mem::size_of::(), 16); - assert_eq!(std::mem::align_of::(), 8); + assert_eq!(std::mem::size_of::(), 16); + assert_eq!(std::mem::align_of::(), 8); } #[test] fn null_context_is_rejected() { - let result = unsafe { with_instance_state(std::ptr::null_mut(), |_| ()) }; + let result = unsafe { with_instance_state::<(), _>(std::ptr::null_mut(), |_| ()) }; assert_eq!(result, Err(ContextError::NullContext)); } #[test] fn inactive_context_is_rejected() { - let mut ctx = vm_exec_w6_hook_context_t::inactive(); - let result = unsafe { with_instance_state(&mut ctx, |_| ()) }; + let mut ctx = vm_exec_hook_context_t::inactive(); + let result = unsafe { with_instance_state::<(), _>(&mut ctx, |_| ()) }; assert_eq!(result, Err(ContextError::Stale)); } } diff --git a/c-api-common/src/capi_vm_hook_pointers.rs b/c-api-common/src/capi_vm_hook_pointers.rs new file mode 100644 index 00000000..f87a4d63 --- /dev/null +++ b/c-api-common/src/capi_vm_hook_pointers.rs @@ -0,0 +1,300 @@ +// Code generated by vmhooks generator. DO NOT EDIT. + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +use crate::capi_vm_hook_context::{vm_exec_hook_context_t, vm_exec_hook_result_t}; + +/// The Go VM's hook implementations, as a table of C function pointers. +/// +/// Copied into Rust at executor creation; see CapiVMHookTable. +#[repr(C)] +#[derive(Clone)] +#[allow(non_camel_case_types)] +#[rustfmt::skip] +pub struct vm_exec_vm_hook_c_func_pointers { + pub get_gas_left_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_offset: i32) -> vm_exec_hook_result_t, + pub get_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_offset: i32) -> vm_exec_hook_result_t, + pub get_shard_of_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32) -> vm_exec_hook_result_t, + pub is_smart_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32) -> vm_exec_hook_result_t, + pub signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, message_offset: i32, message_length: i32) -> vm_exec_hook_result_t, + pub get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, result_offset: i32) -> vm_exec_hook_result_t, + pub get_block_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, nonce: i64, result_offset: i32) -> vm_exec_hook_result_t, + pub get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_offset: i32) -> vm_exec_hook_result_t, + pub get_esdt_nft_name_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec_hook_result_t, + pub get_esdt_nft_attribute_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec_hook_result_t, + pub get_esdt_nft_uri_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec_hook_result_t, + pub get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, value_handle: i32, properties_offset: i32, hash_offset: i32, name_offset: i32, attributes_offset: i32, creator_offset: i32, royalties_handle: i32, uris_offset: i32) -> vm_exec_hook_result_t, + pub get_esdt_local_roles_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, token_id_handle: i32) -> vm_exec_hook_result_t, + pub validate_token_identifier_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, token_id_handle: i32) -> vm_exec_hook_result_t, + pub transfer_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec_hook_result_t, + pub transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub transfer_esdt_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, nonce: i64, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, num_token_transfers: i32, token_transfers_args_length_offset: i32, token_transfer_data_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, data_length: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64) -> vm_exec_hook_result_t, + pub set_async_context_callback_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, callback: i32, callback_length: i32, data: i32, data_length: i32, gas: i64) -> vm_exec_hook_result_t, + pub upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, gas_limit: i64, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec_hook_result_t, + pub get_argument_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, id: i32) -> vm_exec_hook_result_t, + pub get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, id: i32, arg_offset: i32) -> vm_exec_hook_result_t, + pub get_function_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, function_offset: i32) -> vm_exec_hook_result_t, + pub get_num_arguments_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32, data_length: i32) -> vm_exec_hook_result_t, + pub storage_load_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_hook_result_t, + pub storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub set_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, lock_timestamp: i64) -> vm_exec_hook_result_t, + pub get_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_hook_result_t, + pub is_storage_locked_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_hook_result_t, + pub clear_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_hook_result_t, + pub get_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_offset: i32) -> vm_exec_hook_result_t, + pub check_no_payment_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_offset: i32) -> vm_exec_hook_result_t, + pub get_esdt_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_offset: i32) -> vm_exec_hook_result_t, + pub get_esdt_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_offset: i32, index: i32) -> vm_exec_hook_result_t, + pub get_esdt_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_offset: i32) -> vm_exec_hook_result_t, + pub get_esdt_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_offset: i32, index: i32) -> vm_exec_hook_result_t, + pub get_esdt_token_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_esdt_token_nonce_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, index: i32) -> vm_exec_hook_result_t, + pub get_current_esdt_nft_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32) -> vm_exec_hook_result_t, + pub get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_esdt_token_type_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, index: i32) -> vm_exec_hook_result_t, + pub get_num_esdt_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_call_value_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, call_value_offset: i32, token_name_offset: i32) -> vm_exec_hook_result_t, + pub get_call_value_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, call_value_offset: i32, token_name_offset: i32, index: i32) -> vm_exec_hook_result_t, + pub is_reserved_function_name_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, name_handle: i32) -> vm_exec_hook_result_t, + pub write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_pointer: i32, data_length: i32, topic_ptr: i32, num_topics: i32) -> vm_exec_hook_result_t, + pub write_event_log_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, num_topics: i32, topic_lengths_offset: i32, topic_offset: i32, data_offset: i32, data_length: i32) -> vm_exec_hook_result_t, + pub get_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, pointer: i32) -> vm_exec_hook_result_t, + pub get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, pointer: i32) -> vm_exec_hook_result_t, + pub get_prev_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_prev_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_prev_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_prev_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_prev_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, pointer: i32) -> vm_exec_hook_result_t, + pub get_block_round_time_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub epoch_start_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub epoch_start_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub epoch_start_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub finish_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, pointer: i32, length: i32) -> vm_exec_hook_result_t, + pub execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas_limit: i64, address_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, result_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, result_address_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub get_num_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub get_return_data_size_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_id: i32) -> vm_exec_hook_result_t, + pub get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_id: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub clean_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub delete_from_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_id: i32) -> vm_exec_hook_result_t, + pub get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_offset: i32) -> vm_exec_hook_result_t, + pub get_current_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_offset: i32) -> vm_exec_hook_result_t, + pub get_prev_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_offset: i32) -> vm_exec_hook_result_t, + pub managed_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub managed_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub managed_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_original_caller_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_relayer_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub managed_signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, err_handle: i32) -> vm_exec_hook_result_t, + pub managed_write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, topics_handle: i32, data_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, result_id: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_multi_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, multi_call_value_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_all_transfers_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, transfer_call_values_list_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_back_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, esdt_transfers_value_handle: i32, egld_value_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32, properties_handle: i32, hash_handle: i32, name_handle: i32, attributes_handle: i32, creator_handle: i32, royalties_handle: i32, uris_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, type_handle: i32) -> vm_exec_hook_result_t, + pub managed_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32) -> vm_exec_hook_result_t, + pub managed_create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64, callback_closure_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_callback_closure_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, callback_closure_handle: i32) -> vm_exec_hook_result_t, + pub managed_upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_handle: i32, gas_limit: i64, arguments_handle: i32) -> vm_exec_hook_result_t, + pub managed_deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas: i64, address_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_execute_on_dest_context_with_error_return_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub managed_multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_hook_result_t, + pub managed_multi_transfer_esdt_nft_execute_with_return_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_hook_result_t, + pub managed_multi_transfer_esdt_nft_execute_by_user_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, user_handle: i32, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_hook_result_t, + pub managed_transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dst_handle: i32, value_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_hook_result_t, + pub managed_is_esdt_frozen_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64) -> vm_exec_hook_result_t, + pub managed_is_esdt_limited_transfer_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, token_id_handle: i32) -> vm_exec_hook_result_t, + pub managed_is_esdt_paused_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, token_id_handle: i32) -> vm_exec_hook_result_t, + pub managed_buffer_to_hex_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, source_handle: i32, dest_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_code_metadata_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_handle: i32, response_handle: i32) -> vm_exec_hook_result_t, + pub managed_get_code_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_handle: i32, code_hash_handle: i32) -> vm_exec_hook_result_t, + pub managed_is_builtin_function_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, function_name_handle: i32) -> vm_exec_hook_result_t, + pub big_float_new_from_parts_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, integral_part: i32, fractional_part: i32, exponent: i32) -> vm_exec_hook_result_t, + pub big_float_new_from_frac_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, numerator: i64, denominator: i64) -> vm_exec_hook_result_t, + pub big_float_new_from_sci_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, significand: i64, exponent: i64) -> vm_exec_hook_result_t, + pub big_float_add_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_float_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_float_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_float_div_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_float_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_float_clone_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_float_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_float_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_float_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, op_handle: i32) -> vm_exec_hook_result_t, + pub big_float_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_float_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32, exponent: i32) -> vm_exec_hook_result_t, + pub big_float_floor_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_float_ceil_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_float_truncate_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_float_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, value: i64) -> vm_exec_hook_result_t, + pub big_float_is_int_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, op_handle: i32) -> vm_exec_hook_result_t, + pub big_float_set_big_int_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, big_int_handle: i32) -> vm_exec_hook_result_t, + pub big_float_get_const_pi_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub big_float_get_const_e_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub big_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, id: i32, destination_handle: i32) -> vm_exec_hook_result_t, + pub big_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, id: i32, destination_handle: i32) -> vm_exec_hook_result_t, + pub big_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, source_handle: i32) -> vm_exec_hook_result_t, + pub big_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, destination_handle: i32) -> vm_exec_hook_result_t, + pub big_int_get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub big_int_get_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination: i32) -> vm_exec_hook_result_t, + pub big_int_get_esdt_call_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, index: i32) -> vm_exec_hook_result_t, + pub big_int_get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, result: i32) -> vm_exec_hook_result_t, + pub big_int_get_esdt_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_handle: i32) -> vm_exec_hook_result_t, + pub big_int_new_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, small_value: i64) -> vm_exec_hook_result_t, + pub big_int_unsigned_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, reference_handle: i32) -> vm_exec_hook_result_t, + pub big_int_signed_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, reference_handle: i32) -> vm_exec_hook_result_t, + pub big_int_get_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec_hook_result_t, + pub big_int_get_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec_hook_result_t, + pub big_int_set_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec_hook_result_t, + pub big_int_set_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec_hook_result_t, + pub big_int_is_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub big_int_get_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32) -> vm_exec_hook_result_t, + pub big_int_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, value: i64) -> vm_exec_hook_result_t, + pub big_int_add_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_tdiv_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_tmod_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_ediv_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_emod_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_int_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_log2_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, op1_handle: i32) -> vm_exec_hook_result_t, + pub big_int_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_int_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_int_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, op_handle: i32) -> vm_exec_hook_result_t, + pub big_int_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_not_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_hook_result_t, + pub big_int_and_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_or_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_xor_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_hook_result_t, + pub big_int_shr_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec_hook_result_t, + pub big_int_shl_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec_hook_result_t, + pub big_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, reference_handle: i32) -> vm_exec_hook_result_t, + pub big_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, reference_handle: i32) -> vm_exec_hook_result_t, + pub big_int_to_string_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, big_int_handle: i32, destination_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_new_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub mbuffer_new_from_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec_hook_result_t, + pub mbuffer_get_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_get_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, result_offset: i32) -> vm_exec_hook_result_t, + pub mbuffer_get_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, result_offset: i32) -> vm_exec_hook_result_t, + pub mbuffer_copy_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, destination_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_eq_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle1: i32, m_buffer_handle2: i32) -> vm_exec_hook_result_t, + pub mbuffer_set_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, data_offset: i32, data_length: i32) -> vm_exec_hook_result_t, + pub mbuffer_set_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, starting_position: i32, data_length: i32, data_offset: i32) -> vm_exec_hook_result_t, + pub mbuffer_append_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, accumulator_handle: i32, data_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_append_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, accumulator_handle: i32, data_offset: i32, data_length: i32) -> vm_exec_hook_result_t, + pub mbuffer_to_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_to_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_from_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_from_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_to_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_to_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_from_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec_hook_result_t, + pub mbuffer_from_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec_hook_result_t, + pub mbuffer_to_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_from_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_handle: i32, source_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_handle: i32, destination_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, address_handle: i32, key_handle: i32, destination_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, id: i32, destination_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_finish_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, source_handle: i32) -> vm_exec_hook_result_t, + pub mbuffer_set_random_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, destination_handle: i32, length: i32) -> vm_exec_hook_result_t, + pub managed_map_new_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t, + pub managed_map_put_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_map_handle: i32, key_handle: i32, value_handle: i32) -> vm_exec_hook_result_t, + pub managed_map_get_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec_hook_result_t, + pub managed_map_remove_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec_hook_result_t, + pub managed_map_contains_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, m_map_handle: i32, key_handle: i32) -> vm_exec_hook_result_t, + pub small_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, id: i32) -> vm_exec_hook_result_t, + pub small_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, id: i32) -> vm_exec_hook_result_t, + pub small_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, value: i64) -> vm_exec_hook_result_t, + pub small_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, value: i64) -> vm_exec_hook_result_t, + pub small_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec_hook_result_t, + pub small_int_storage_store_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec_hook_result_t, + pub small_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_hook_result_t, + pub small_int_storage_load_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_hook_result_t, + pub int64get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, id: i32) -> vm_exec_hook_result_t, + pub int64finish_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, value: i64) -> vm_exec_hook_result_t, + pub int64storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec_hook_result_t, + pub int64storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_hook_result_t, + pub sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec_hook_result_t, + pub managed_sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec_hook_result_t, + pub keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec_hook_result_t, + pub managed_keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec_hook_result_t, + pub ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec_hook_result_t, + pub managed_ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec_hook_result_t, + pub verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec_hook_result_t, + pub managed_verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_hook_result_t, + pub verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec_hook_result_t, + pub managed_verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_hook_result_t, + pub verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32, hash_type: i32) -> vm_exec_hook_result_t, + pub managed_verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32, hash_type: i32) -> vm_exec_hook_result_t, + pub verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec_hook_result_t, + pub managed_verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_hook_result_t, + pub encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, r_offset: i32, r_length: i32, s_offset: i32, s_length: i32, sig_offset: i32) -> vm_exec_hook_result_t, + pub managed_encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, r_handle: i32, s_handle: i32, sig_handle: i32) -> vm_exec_hook_result_t, + pub add_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, fst_point_xhandle: i32, fst_point_yhandle: i32, snd_point_xhandle: i32, snd_point_yhandle: i32) -> vm_exec_hook_result_t, + pub double_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec_hook_result_t, + pub is_on_curve_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec_hook_result_t, + pub scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec_hook_result_t, + pub managed_scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec_hook_result_t, + pub scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_offset: i32, length: i32) -> vm_exec_hook_result_t, + pub managed_scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_handle: i32) -> vm_exec_hook_result_t, + pub marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec_hook_result_t, + pub managed_marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec_hook_result_t, + pub managed_marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec_hook_result_t, + pub managed_unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec_hook_result_t, + pub unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec_hook_result_t, + pub managed_unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec_hook_result_t, + pub generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec_hook_result_t, + pub managed_generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec_hook_result_t, + pub create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec_hook_result_t, + pub managed_create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, data_handle: i32) -> vm_exec_hook_result_t, + pub get_curve_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, ec_handle: i32) -> vm_exec_hook_result_t, + pub get_priv_key_byte_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, ec_handle: i32) -> vm_exec_hook_result_t, + pub elliptic_curve_get_values_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, ec_handle: i32, field_order_handle: i32, base_point_order_handle: i32, eq_constant_handle: i32, x_base_point_handle: i32, y_base_point_handle: i32) -> vm_exec_hook_result_t, + pub managed_verify_secp256r1_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_hook_result_t, + pub managed_verify_blssignature_share_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_hook_result_t, + pub managed_verify_blsaggregated_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_hook_result_t, +} + +impl std::fmt::Debug for vm_exec_vm_hook_c_func_pointers { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "vm_exec_vm_hook_c_func_pointers") + } +} diff --git a/c-api-wasmer6/src/capi_vm_hook_table.rs b/c-api-common/src/capi_vm_hook_table.rs similarity index 82% rename from c-api-wasmer6/src/capi_vm_hook_table.rs rename to c-api-common/src/capi_vm_hook_table.rs index d548d4f6..d15fe3ab 100644 --- a/c-api-wasmer6/src/capi_vm_hook_table.rs +++ b/c-api-common/src/capi_vm_hook_table.rs @@ -7,14 +7,14 @@ use std::sync::Arc; -use crate::capi_vm_hook_pointers::vm_exec_w6_vm_hook_c_func_pointers; +use crate::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; /// An owned copy of the caller's hook table. /// /// Cloning is cheap and shares the copy; each instance's hooks builder holds one. #[derive(Clone, Debug)] pub struct CapiVMHookTable { - func_pointers: Arc, + func_pointers: Arc, } // The table is a block of plain function pointers with no interior mutability, and is only @@ -32,13 +32,13 @@ impl CapiVMHookTable { /// entry is called without a further null check, because a partially filled table is not /// something this API can detect: a null function pointer and a wrong one are /// indistinguishable here. - pub unsafe fn copy_from(func_pointers: *const vm_exec_w6_vm_hook_c_func_pointers) -> Self { + pub unsafe fn copy_from(func_pointers: *const vm_exec_vm_hook_c_func_pointers) -> Self { CapiVMHookTable { func_pointers: Arc::new(unsafe { (*func_pointers).clone() }), } } - pub fn func_pointers(&self) -> &vm_exec_w6_vm_hook_c_func_pointers { + pub fn func_pointers(&self) -> &vm_exec_vm_hook_c_func_pointers { &self.func_pointers } } diff --git a/c-api-wasmer6/src/capi_vm_hooks.rs b/c-api-common/src/capi_vm_hooks.rs similarity index 99% rename from c-api-wasmer6/src/capi_vm_hooks.rs rename to c-api-common/src/capi_vm_hooks.rs index 2000cf9e..726b75d5 100644 --- a/c-api-wasmer6/src/capi_vm_hooks.rs +++ b/c-api-common/src/capi_vm_hooks.rs @@ -6,8 +6,8 @@ use multiversx_chain_vm_executor::{MemLength, MemPtr, VMHooks, VMHooksEarlyExit}; -use crate::capi_vm_hook_context::{vm_exec_w6_hook_context_t, vm_exec_w6_hook_result_t}; -use crate::capi_vm_hook_pointers::vm_exec_w6_vm_hook_c_func_pointers; +use crate::capi_vm_hook_context::{vm_exec_hook_context_t, vm_exec_hook_result_t}; +use crate::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; /// Services one host call by dispatching each VM hook to the Go side. /// @@ -15,14 +15,14 @@ use crate::capi_vm_hook_pointers::vm_exec_w6_vm_hook_c_func_pointers; /// the Go side hands back to the state functions. It borrows the table rather than owning a /// copy, because the executor already owns one for the lifetime of the instance. pub struct CapiVMHooks<'a> { - ctx: *mut vm_exec_w6_hook_context_t, - func_pointers: &'a vm_exec_w6_vm_hook_c_func_pointers, + ctx: *mut vm_exec_hook_context_t, + func_pointers: &'a vm_exec_vm_hook_c_func_pointers, } impl<'a> CapiVMHooks<'a> { pub fn new( - ctx: *mut vm_exec_w6_hook_context_t, - func_pointers: &'a vm_exec_w6_vm_hook_c_func_pointers, + ctx: *mut vm_exec_hook_context_t, + func_pointers: &'a vm_exec_vm_hook_c_func_pointers, ) -> Self { CapiVMHooks { ctx, func_pointers } } @@ -51,21 +51,21 @@ fn early_exit(code: u64) -> VMHooksEarlyExit { VMHooksEarlyExit::new(code) } -fn unpack_void(result: vm_exec_w6_hook_result_t) -> Result<(), VMHooksEarlyExit> { +fn unpack_void(result: vm_exec_hook_result_t) -> Result<(), VMHooksEarlyExit> { match result.early_exit_code() { Some(code) => Err(early_exit(code)), None => Ok(()), } } -fn unpack_i32(result: vm_exec_w6_hook_result_t) -> Result { +fn unpack_i32(result: vm_exec_hook_result_t) -> Result { match result.early_exit_code() { Some(code) => Err(early_exit(code)), None => Ok(result.value as i32), } } -fn unpack_i64(result: vm_exec_w6_hook_result_t) -> Result { +fn unpack_i64(result: vm_exec_hook_result_t) -> Result { match result.early_exit_code() { Some(code) => Err(early_exit(code)), None => Ok(result.value), diff --git a/c-api-common/src/lib.rs b/c-api-common/src/lib.rs new file mode 100644 index 00000000..dbc41f21 --- /dev/null +++ b/c-api-common/src/lib.rs @@ -0,0 +1,39 @@ +//! Everything the new C APIs share, independent of which wasm runtime is underneath. +//! +//! There are two C APIs built on the second executor interface set — one per backend, with +//! distinct symbol prefixes so both can be linked into one binary — and they are near +//! identical, because they are built on the same traits. What differs between them is only +//! the backend wiring: which `Executor` is instantiated, and what the instance state is. +//! Everything else lives here and is compiled once. +//! +//! What that covers: +//! +//! - **The shared C vocabulary**: status codes, the error-buffer convention, configuration, +//! the opcode cost table. These are published under the neutral `vm_exec_*` prefix and +//! emitted into a single `libvmexeccapi_common.h` that each backend's header includes, so a +//! caller that links both libraries sees one definition of each rather than two. +//! - **Helpers with no ABI of their own**: the two-step string-out convention, panic +//! suppression. +//! - **The whole hook dispatch path**: the result and context ABI, the per-instance context +//! pool, the function-pointer table, and the generated `VMHooks` implementation that calls +//! through it. Over 1,200 generated lines, compiled once instead of once per backend. None +//! of it names a backend, because the context carries the instance state as an erased +//! pointer and only the backend's own builder implementation ever names its type. +//! +//! The backend crates keep only their opaque handles and their prefixed entry points. The +//! legacy C API is not part of this: its types were renamed to `vm_legacy_*` precisely so +//! that it can coexist with these without colliding, and it is on its way out. + +mod capi_logger; + +pub mod capi_call_result; +pub mod capi_config; +pub mod capi_opcode_cost; +pub mod capi_status; +pub mod capi_strings; +pub mod capi_vm_hook_context; +pub mod capi_vm_hook_pointers; +pub mod capi_vm_hook_table; +pub mod capi_vm_hooks; + +pub use capi_logger::set_panic_handler; diff --git a/c-api-legacy/libvmexeccapi.h b/c-api-legacy/libvmexeccapi.h index 9d7097b6..97ec9825 100644 --- a/c-api-legacy/libvmexeccapi.h +++ b/c-api-legacy/libvmexeccapi.h @@ -7,16 +7,16 @@ * The `wasmer_result_t` enum is a type that represents either a * success, or a failure. */ -typedef enum vm_exec_result_t { +typedef enum vm_legacy_result_t { /** * Represents a success. */ - VM_EXEC_OK = 1, + VM_LEGACY_OK = 1, /** * Represents a failure. */ - VM_EXEC_ERROR = 2, -} vm_exec_result_t; + VM_LEGACY_ERROR = 2, +} vm_legacy_result_t; /** * Opaque pointer to a `wasmer_runtime::Instance` value in Rust. @@ -25,15 +25,15 @@ typedef enum vm_exec_result_t { * is generally generated by the `wasmer_instantiate()` function, or by * the `wasmer_module_instantiate()` function for the most common paths. */ -typedef struct vm_exec_instance_t { +typedef struct vm_legacy_instance_t { -} vm_exec_instance_t; +} vm_legacy_instance_t; -typedef struct vm_exec_executor_t { +typedef struct vm_legacy_executor_t { -} vm_exec_executor_t; +} vm_legacy_executor_t; -typedef struct vm_exec_vm_hook_c_func_pointers { +typedef struct vm_legacy_vm_hook_c_func_pointers { int64_t (*get_gas_left_func_ptr)(void *context); void (*get_sc_address_func_ptr)(void *context, int32_t result_offset); void (*get_owner_address_func_ptr)(void *context, int32_t result_offset); @@ -311,7 +311,7 @@ typedef struct vm_exec_vm_hook_c_func_pointers { int32_t (*managed_verify_secp256r1_func_ptr)(void *context, int32_t key_handle, int32_t message_handle, int32_t sig_handle); int32_t (*managed_verify_blssignature_share_func_ptr)(void *context, int32_t key_handle, int32_t message_handle, int32_t sig_handle); int32_t (*managed_verify_blsaggregated_signature_func_ptr)(void *context, int32_t key_handle, int32_t message_handle, int32_t sig_handle); -} vm_exec_vm_hook_c_func_pointers; +} vm_legacy_vm_hook_c_func_pointers; /** * FFI-safe mirror of `CompilationOptionsLegacy`. @@ -322,7 +322,7 @@ typedef struct vm_exec_vm_hook_c_func_pointers { * the Rust side are fixed to `u64` here, since `usize` has no guaranteed * width across host platforms. */ -typedef struct vm_exec_compilation_options_t { +typedef struct vm_legacy_compilation_options_t { uint64_t gas_limit; uint64_t unmetered_locals; uint64_t max_memory_grow; @@ -330,7 +330,7 @@ typedef struct vm_exec_compilation_options_t { bool opcode_trace; bool metering; bool runtime_breakpoints; -} vm_exec_compilation_options_t; +} vm_legacy_compilation_options_t; /** * FFI-safe mirror of `OpcodeCost`. @@ -339,7 +339,7 @@ typedef struct vm_exec_compilation_options_t { * the general-purpose struct is free to change shape without touching the C * ABI, and vice versa. */ -typedef struct vm_exec_opcode_cost_t { +typedef struct vm_legacy_opcode_cost_t { uint32_t opcode_block; uint32_t opcode_br; uint32_t opcode_brif; @@ -472,20 +472,20 @@ typedef struct vm_exec_opcode_cost_t { uint32_t opcode_memorycopyperbyte; uint32_t opcode_memoryfillperbyte; uint32_t opcode_localallocate; -} vm_exec_opcode_cost_t; +} vm_legacy_opcode_cost_t; /** * Sets the runtime breakpoint value for the given instance. * - * This function returns `vm_exec_result_t::WASMER_OK` upon success, - * `vm_exec_result_t::WASMER_ERROR` otherwise. You can use + * This function returns `vm_legacy_result_t::WASMER_OK` upon success, + * `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use * `wasmer_last_error_message()` to get the generated error message. * * # Safety * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_instance_set_breakpoint_value(const struct vm_exec_instance_t *instance_ptr, uint64_t value); +enum vm_legacy_result_t vm_exec_instance_set_breakpoint_value(const struct vm_legacy_instance_t *instance_ptr, uint64_t value); /** * Returns the runtime breakpoint value from the given instance. @@ -494,7 +494,7 @@ enum vm_exec_result_t vm_exec_instance_set_breakpoint_value(const struct vm_exec * * C API function, works with raw object pointers. */ -uint64_t vm_exec_instance_get_breakpoint_value(const struct vm_exec_instance_t *instance_ptr); +uint64_t vm_exec_instance_get_breakpoint_value(const struct vm_legacy_instance_t *instance_ptr); /** * Gets the length in bytes of the last error if any. @@ -533,7 +533,7 @@ int vm_exec_last_error_message(char *dest_buffer, int dest_buffer_len); * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_new_executor(struct vm_exec_executor_t **executor, struct vm_exec_vm_hook_c_func_pointers **vm_hook_pointers_ptr_ptr); +enum vm_legacy_result_t vm_exec_new_executor(struct vm_legacy_executor_t **executor, struct vm_legacy_vm_hook_c_func_pointers **vm_hook_pointers_ptr_ptr); /** * Forces reinstalling the sighandlers. @@ -558,7 +558,7 @@ void vm_force_sighandler_reinstall(void); * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_executor_set_vm_hooks_ptr(struct vm_exec_executor_t *executor_ptr, void *vm_hooks_ptr); +enum vm_legacy_result_t vm_exec_executor_set_vm_hooks_ptr(struct vm_legacy_executor_t *executor_ptr, void *vm_hooks_ptr); /** * Destroys a VM executor object. @@ -567,7 +567,7 @@ enum vm_exec_result_t vm_exec_executor_set_vm_hooks_ptr(struct vm_exec_executor_ * * C API function, works with raw object pointers. */ -void vm_exec_executor_destroy(struct vm_exec_executor_t *executor_ptr); +void vm_exec_executor_destroy(struct vm_legacy_executor_t *executor_ptr); /** * Creates a new VM executor instance. @@ -578,15 +578,15 @@ void vm_exec_executor_destroy(struct vm_exec_executor_t *executor_ptr); * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_new_instance(struct vm_exec_executor_t *executor_ptr, struct vm_exec_instance_t **instance_ptr_ptr, uint8_t *wasm_bytes_ptr, uint32_t wasm_bytes_len, const struct vm_exec_compilation_options_t *options_ptr); +enum vm_legacy_result_t vm_exec_new_instance(struct vm_legacy_executor_t *executor_ptr, struct vm_legacy_instance_t **instance_ptr_ptr, uint8_t *wasm_bytes_ptr, uint32_t wasm_bytes_len, const struct vm_legacy_compilation_options_t *options_ptr); /** * Calls an exported function of a WebAssembly instance by `name` * with the provided parameters. The exported function results are * stored on the provided `results` pointer. * - * This function returns `vm_exec_result_t::WASMER_OK` upon success, - * `vm_exec_result_t::WASMER_ERROR` otherwise. You can use + * This function returns `vm_legacy_result_t::WASMER_OK` upon success, + * `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use * `wasmer_last_error_message()` to get the generated error message. * * Potential errors are the following: @@ -599,7 +599,7 @@ enum vm_exec_result_t vm_exec_new_instance(struct vm_exec_executor_t *executor_p * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_instance_call(struct vm_exec_instance_t *instance_ptr, const char *func_name_ptr); +enum vm_legacy_result_t vm_exec_instance_call(struct vm_legacy_instance_t *instance_ptr, const char *func_name_ptr); /** * Checks that all public module functions (SC endpoints) have no arguments or results. @@ -610,7 +610,7 @@ enum vm_exec_result_t vm_exec_instance_call(struct vm_exec_instance_t *instance_ * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_check_signatures(struct vm_exec_instance_t *instance_ptr); +enum vm_legacy_result_t vm_check_signatures(struct vm_legacy_instance_t *instance_ptr); /** * Checks whether SC has an endpoint with given name. @@ -619,7 +619,7 @@ enum vm_exec_result_t vm_check_signatures(struct vm_exec_instance_t *instance_pt * * C API function, works with raw object pointers. */ -int vm_exec_instance_has_function(struct vm_exec_instance_t *instance_ptr, const char *func_name_ptr); +int vm_exec_instance_has_function(struct vm_legacy_instance_t *instance_ptr, const char *func_name_ptr); /** * Checks whether SC has an endpoint with given name. @@ -628,7 +628,7 @@ int vm_exec_instance_has_function(struct vm_exec_instance_t *instance_ptr, const * * C API function, works with raw object pointers. */ -int vm_exec_instance_has_imported_function(struct vm_exec_instance_t *instance_ptr, const char *func_name_ptr); +int vm_exec_instance_has_imported_function(struct vm_legacy_instance_t *instance_ptr, const char *func_name_ptr); /** * Required to be able to extract all SC endpoint names. See `vm_exported_function_names`. @@ -637,7 +637,7 @@ int vm_exec_instance_has_imported_function(struct vm_exec_instance_t *instance_p * * C API function, works with raw object pointers. */ -int vm_exported_function_names_length(struct vm_exec_instance_t *instance_ptr); +int vm_exported_function_names_length(struct vm_legacy_instance_t *instance_ptr); /** * Returns all SC endpoint names, separated by pipes. @@ -652,10 +652,10 @@ int vm_exported_function_names_length(struct vm_exec_instance_t *instance_ptr); * * C API function, works with raw object pointers. */ -int vm_exported_function_names(struct vm_exec_instance_t *instance_ptr, char *dest_buffer, int dest_buffer_len); +int vm_exported_function_names(struct vm_legacy_instance_t *instance_ptr, char *dest_buffer, int dest_buffer_len); /** - * Frees memory for the given `vm_exec_instance_t`. + * Frees memory for the given `vm_legacy_instance_t`. * * Check the `wasmer_instantiate()` function to get a complete * example. @@ -666,7 +666,7 @@ int vm_exported_function_names(struct vm_exec_instance_t *instance_ptr, char *de * * C API function, works with raw object pointers. */ -void vm_exec_instance_destroy(struct vm_exec_instance_t *instance_ptr); +void vm_exec_instance_destroy(struct vm_legacy_instance_t *instance_ptr); /** * Resets an instance, cleaning memories and globals. @@ -675,7 +675,7 @@ void vm_exec_instance_destroy(struct vm_exec_instance_t *instance_ptr); * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_instance_reset(struct vm_exec_instance_t *instance_ptr); +enum vm_legacy_result_t vm_exec_instance_reset(struct vm_legacy_instance_t *instance_ptr); /** * Caches an instance. @@ -684,7 +684,7 @@ enum vm_exec_result_t vm_exec_instance_reset(struct vm_exec_instance_t *instance * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_instance_cache(const struct vm_exec_instance_t *instance_ptr, const uint8_t **cache_bytes_ptr, uint32_t *cache_bytes_len); +enum vm_legacy_result_t vm_exec_instance_cache(const struct vm_legacy_instance_t *instance_ptr, const uint8_t **cache_bytes_ptr, uint32_t *cache_bytes_len); /** * Creates a new VM executor instance from cache. @@ -695,12 +695,12 @@ enum vm_exec_result_t vm_exec_instance_cache(const struct vm_exec_instance_t *in * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_instance_from_cache(struct vm_exec_executor_t *executor_ptr, struct vm_exec_instance_t **instance_ptr_ptr, uint8_t *cache_bytes_ptr, uint32_t cache_bytes_len, const struct vm_exec_compilation_options_t *options_ptr); +enum vm_legacy_result_t vm_exec_instance_from_cache(struct vm_legacy_executor_t *executor_ptr, struct vm_legacy_instance_t **instance_ptr_ptr, uint8_t *cache_bytes_ptr, uint32_t cache_bytes_len, const struct vm_legacy_compilation_options_t *options_ptr); /** * Sets the log level. */ -enum vm_exec_result_t vm_exec_set_log_level(uint64_t value); +enum vm_legacy_result_t vm_exec_set_log_level(uint64_t value); /** * Gets the size in bytes of the memory data. @@ -711,7 +711,7 @@ enum vm_exec_result_t vm_exec_set_log_level(uint64_t value); * * C API function, works with raw object pointers. */ -uint64_t vm_exec_instance_memory_data_length(struct vm_exec_instance_t *instance_ptr); +uint64_t vm_exec_instance_memory_data_length(struct vm_legacy_instance_t *instance_ptr); /** * Gets a pointer to the beginning of the contiguous memory data @@ -726,7 +726,7 @@ uint64_t vm_exec_instance_memory_data_length(struct vm_exec_instance_t *instance * * C API function, works with raw object pointers. */ -uint8_t *vm_exec_instance_memory_data(struct vm_exec_instance_t *instance_ptr); +uint8_t *vm_exec_instance_memory_data(struct vm_legacy_instance_t *instance_ptr); /** * Grows a memory by the given number of pages (of 65Kb each). @@ -740,46 +740,46 @@ uint8_t *vm_exec_instance_memory_data(struct vm_exec_instance_t *instance_ptr); * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_instance_memory_grow(struct vm_exec_instance_t *instance_ptr, uint32_t by_num_pages); +enum vm_legacy_result_t vm_exec_instance_memory_grow(struct vm_legacy_instance_t *instance_ptr, uint32_t by_num_pages); /** * Sets the opcode costs for the given executor. * - * This function returns `vm_exec_result_t::WASMER_OK` upon success, - * `vm_exec_result_t::WASMER_ERROR` otherwise. You can use + * This function returns `vm_legacy_result_t::WASMER_OK` upon success, + * `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use * `wasmer_last_error_message()` to get the generated error message. * * # Safety * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_set_opcode_config(struct vm_exec_executor_t *executor_ptr, int32_t opcode_version_code, const struct vm_exec_opcode_cost_t *opcode_cost_ptr); +enum vm_legacy_result_t vm_exec_set_opcode_config(struct vm_legacy_executor_t *executor_ptr, int32_t opcode_version_code, const struct vm_legacy_opcode_cost_t *opcode_cost_ptr); /** * Sets the number of points(gas) limit for the given instance. * - * This function returns `vm_exec_result_t::WASMER_OK` upon success, - * `vm_exec_result_t::WASMER_ERROR` otherwise. You can use + * This function returns `vm_legacy_result_t::WASMER_OK` upon success, + * `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use * `wasmer_last_error_message()` to get the generated error message. * * # Safety * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_instance_set_points_limit(const struct vm_exec_instance_t *instance_ptr, uint64_t limit); +enum vm_legacy_result_t vm_exec_instance_set_points_limit(const struct vm_legacy_instance_t *instance_ptr, uint64_t limit); /** * Sets the number of points(gas) for the given instance. * - * This function returns `vm_exec_result_t::WASMER_OK` upon success, - * `vm_exec_result_t::WASMER_ERROR` otherwise. You can use + * This function returns `vm_legacy_result_t::WASMER_OK` upon success, + * `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use * `wasmer_last_error_message()` to get the generated error message. * * # Safety * * C API function, works with raw object pointers. */ -enum vm_exec_result_t vm_exec_instance_set_points_used(const struct vm_exec_instance_t *instance_ptr, uint64_t points); +enum vm_legacy_result_t vm_exec_instance_set_points_used(const struct vm_legacy_instance_t *instance_ptr, uint64_t points); /** * Returns the number of points(gas) used by the given instance. @@ -788,4 +788,4 @@ enum vm_exec_result_t vm_exec_instance_set_points_used(const struct vm_exec_inst * * C API function, works with raw object pointers. */ -uint64_t vm_exec_instance_get_points_used(const struct vm_exec_instance_t *instance_ptr); +uint64_t vm_exec_instance_get_points_used(const struct vm_legacy_instance_t *instance_ptr); diff --git a/c-api-legacy/src/basic_types.rs b/c-api-legacy/src/basic_types.rs index 92f1fc71..7aa6cf65 100644 --- a/c-api-legacy/src/basic_types.rs +++ b/c-api-legacy/src/basic_types.rs @@ -5,12 +5,12 @@ use std::{ptr, slice}; /// success, or a failure. #[allow(non_camel_case_types)] #[repr(C)] -pub enum vm_exec_result_t { +pub enum vm_legacy_result_t { /// Represents a success. - VM_EXEC_OK = 1, + VM_LEGACY_OK = 1, /// Represents a failure. - VM_EXEC_ERROR = 2, + VM_LEGACY_ERROR = 2, } #[allow(non_camel_case_types)] diff --git a/c-api-legacy/src/capi_breakpoints.rs b/c-api-legacy/src/capi_breakpoints.rs index c5e2cdcc..c2956ff4 100644 --- a/c-api-legacy/src/capi_breakpoints.rs +++ b/c-api-legacy/src/capi_breakpoints.rs @@ -1,14 +1,14 @@ use meta::capi_safe_unwind; use multiversx_chain_vm_executor::InstanceLegacy; -use crate::capi_instance::{CapiInstance, vm_exec_instance_t}; +use crate::capi_instance::{CapiInstance, vm_legacy_instance_t}; use crate::service_singleton::with_service; -use crate::vm_exec_result_t; +use crate::vm_legacy_result_t; /// Sets the runtime breakpoint value for the given instance. /// -/// This function returns `vm_exec_result_t::WASMER_OK` upon success, -/// `vm_exec_result_t::WASMER_ERROR` otherwise. You can use +/// This function returns `vm_legacy_result_t::WASMER_OK` upon success, +/// `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use /// `wasmer_last_error_message()` to get the generated error message. /// /// # Safety @@ -16,18 +16,18 @@ use crate::vm_exec_result_t; /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_instance_set_breakpoint_value( - instance_ptr: *const vm_exec_instance_t, + instance_ptr: *const vm_legacy_instance_t, value: u64, -) -> vm_exec_result_t { +) -> vm_legacy_result_t { let capi_instance = cast_input_const_ptr!(instance_ptr, CapiInstance, "instance ptr is null"); let result = set_breakpoint_value_u64(capi_instance.content.as_ref(), value); match result { - Ok(()) => vm_exec_result_t::VM_EXEC_OK, + Ok(()) => vm_legacy_result_t::VM_LEGACY_OK, Err(message) => { with_service(|service| service.update_last_error_str(message)); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } @@ -41,7 +41,7 @@ pub unsafe extern "C" fn vm_exec_instance_set_breakpoint_value( #[unsafe(no_mangle)] #[capi_safe_unwind(0)] pub unsafe extern "C" fn vm_exec_instance_get_breakpoint_value( - instance_ptr: *const vm_exec_instance_t, + instance_ptr: *const vm_legacy_instance_t, ) -> u64 { let capi_instance = cast_input_const_ptr!(instance_ptr, CapiInstance, "instance ptr is null", 0); diff --git a/c-api-legacy/src/capi_compilation_options.rs b/c-api-legacy/src/capi_compilation_options.rs index 704bf89d..89b89be5 100644 --- a/c-api-legacy/src/capi_compilation_options.rs +++ b/c-api-legacy/src/capi_compilation_options.rs @@ -11,7 +11,7 @@ use multiversx_chain_vm_executor::CompilationOptionsLegacy; /// width across host platforms. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec_compilation_options_t { +pub struct vm_legacy_compilation_options_t { pub gas_limit: u64, pub unmetered_locals: u64, pub max_memory_grow: u64, @@ -21,7 +21,7 @@ pub struct vm_exec_compilation_options_t { pub runtime_breakpoints: bool, } -impl vm_exec_compilation_options_t { +impl vm_legacy_compilation_options_t { pub(crate) fn to_legacy(&self) -> CompilationOptionsLegacy { CompilationOptionsLegacy { gas_limit: self.gas_limit, diff --git a/c-api-legacy/src/capi_executor.rs b/c-api-legacy/src/capi_executor.rs index b5dd313e..1eaf0c6d 100644 --- a/c-api-legacy/src/capi_executor.rs +++ b/c-api-legacy/src/capi_executor.rs @@ -1,8 +1,8 @@ //! Instantiate a module, call functions, and read exports. use crate::{ - capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers, capi_vm_hooks::CapiVMHooks, - service_singleton::with_service, vm_exec_result_t, + capi_vm_hook_pointers::vm_legacy_vm_hook_c_func_pointers, capi_vm_hooks::CapiVMHooks, + service_singleton::with_service, vm_legacy_result_t, }; use libc::c_void; use meta::capi_safe_unwind; @@ -11,7 +11,7 @@ use multiversx_chain_vm_executor_wasmer::force_sighandler_reinstall; #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec_executor_t; +pub struct vm_legacy_executor_t; pub struct CapiExecutor { pub content: Box, @@ -24,11 +24,11 @@ pub struct CapiExecutor { /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_new_executor( - executor: *mut *mut vm_exec_executor_t, - vm_hook_pointers_ptr_ptr: *mut *mut vm_exec_vm_hook_c_func_pointers, -) -> vm_exec_result_t { + executor: *mut *mut vm_legacy_executor_t, + vm_hook_pointers_ptr_ptr: *mut *mut vm_legacy_vm_hook_c_func_pointers, +) -> vm_legacy_result_t { return_if_ptr_null!(vm_hook_pointers_ptr_ptr, "VM hooks ptr is null"); // unpacking the vm hooks object pointer @@ -44,13 +44,13 @@ pub unsafe extern "C" fn vm_exec_new_executor( content: executor_box, }; unsafe { - *executor = Box::into_raw(Box::new(capi_executor)) as *mut vm_exec_executor_t; + *executor = Box::into_raw(Box::new(capi_executor)) as *mut vm_legacy_executor_t; } - vm_exec_result_t::VM_EXEC_OK + vm_legacy_result_t::VM_LEGACY_OK } Err(message) => { with_service(|service| service.update_last_error_str(message.to_string())); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } @@ -79,27 +79,27 @@ pub unsafe extern "C" fn vm_force_sighandler_reinstall() { /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_executor_set_vm_hooks_ptr( - executor_ptr: *mut vm_exec_executor_t, + executor_ptr: *mut vm_legacy_executor_t, vm_hooks_ptr: *mut c_void, -) -> vm_exec_result_t { +) -> vm_legacy_result_t { let result = std::panic::catch_unwind(|| { let capi_executor = cast_input_ptr!(executor_ptr, CapiExecutor, "executor ptr is null"); let result = capi_executor.content.set_vm_hooks_ptr(vm_hooks_ptr); match result { - Ok(()) => vm_exec_result_t::VM_EXEC_OK, + Ok(()) => vm_legacy_result_t::VM_LEGACY_OK, Err(message) => { with_service(|service| service.update_last_error_str(message.to_string())); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } }); match result { Ok(result) => result, - Err(_) => vm_exec_result_t::VM_EXEC_ERROR, + Err(_) => vm_legacy_result_t::VM_LEGACY_ERROR, } } @@ -110,7 +110,7 @@ pub unsafe extern "C" fn vm_exec_executor_set_vm_hooks_ptr( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -pub unsafe extern "C" fn vm_exec_executor_destroy(executor_ptr: *mut vm_exec_executor_t) { +pub unsafe extern "C" fn vm_exec_executor_destroy(executor_ptr: *mut vm_legacy_executor_t) { if !executor_ptr.is_null() { let executor = unsafe { Box::from_raw(executor_ptr as *mut CapiExecutor) }; drop(executor) diff --git a/c-api-legacy/src/capi_instance.rs b/c-api-legacy/src/capi_instance.rs index d38544e7..563158ea 100644 --- a/c-api-legacy/src/capi_instance.rs +++ b/c-api-legacy/src/capi_instance.rs @@ -1,10 +1,10 @@ //! Instantiate a module, call functions, and read exports. use crate::{ - capi_compilation_options::vm_exec_compilation_options_t, - capi_executor::{CapiExecutor, vm_exec_executor_t}, + capi_compilation_options::vm_legacy_compilation_options_t, + capi_executor::{CapiExecutor, vm_legacy_executor_t}, service_singleton::with_service, - string_copy, vm_exec_result_t, + string_copy, vm_legacy_result_t, }; use libc::{c_char, c_int}; use meta::capi_safe_unwind; @@ -18,7 +18,7 @@ use std::{ffi::CStr, slice}; /// the `wasmer_module_instantiate()` function for the most common paths. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec_instance_t; +pub struct vm_legacy_instance_t; pub struct CapiInstance { pub(crate) content: Box, @@ -33,19 +33,19 @@ pub struct CapiInstance { /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_new_instance( - executor_ptr: *mut vm_exec_executor_t, - instance_ptr_ptr: *mut *mut vm_exec_instance_t, + executor_ptr: *mut vm_legacy_executor_t, + instance_ptr_ptr: *mut *mut vm_legacy_instance_t, wasm_bytes_ptr: *mut u8, wasm_bytes_len: u32, - options_ptr: *const vm_exec_compilation_options_t, -) -> vm_exec_result_t { + options_ptr: *const vm_legacy_compilation_options_t, +) -> vm_legacy_result_t { let capi_executor = cast_input_ptr!(executor_ptr, CapiExecutor, "executor ptr is null"); if wasm_bytes_ptr.is_null() { with_service(|service| service.update_last_error_str("wasm bytes ptr is null".to_string())); - return vm_exec_result_t::VM_EXEC_ERROR; + return vm_legacy_result_t::VM_LEGACY_ERROR; } return_if_ptr_null!(options_ptr, "compilation options ptr is null"); @@ -63,13 +63,13 @@ pub unsafe extern "C" fn vm_exec_new_instance( }; unsafe { *instance_ptr_ptr = - Box::into_raw(Box::new(capi_instance)) as *mut vm_exec_instance_t; + Box::into_raw(Box::new(capi_instance)) as *mut vm_legacy_instance_t; } - vm_exec_result_t::VM_EXEC_OK + vm_legacy_result_t::VM_LEGACY_OK } Err(message) => { with_service(|service| service.update_last_error_str(message.to_string())); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } @@ -78,8 +78,8 @@ pub unsafe extern "C" fn vm_exec_new_instance( /// with the provided parameters. The exported function results are /// stored on the provided `results` pointer. /// -/// This function returns `vm_exec_result_t::WASMER_OK` upon success, -/// `vm_exec_result_t::WASMER_ERROR` otherwise. You can use +/// This function returns `vm_legacy_result_t::WASMER_OK` upon success, +/// `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use /// `wasmer_last_error_message()` to get the generated error message. /// /// Potential errors are the following: @@ -93,27 +93,27 @@ pub unsafe extern "C" fn vm_exec_new_instance( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_instance_call( - instance_ptr: *mut vm_exec_instance_t, + instance_ptr: *mut vm_legacy_instance_t, func_name_ptr: *const c_char, -) -> vm_exec_result_t { +) -> vm_legacy_result_t { let capi_instance = cast_input_ptr!(instance_ptr, CapiInstance, "instance ptr is null"); // unpack the function name if func_name_ptr.is_null() { with_service(|service| service.update_last_error_str("name ptr is null".to_string())); - return vm_exec_result_t::VM_EXEC_ERROR; + return vm_legacy_result_t::VM_LEGACY_ERROR; } let func_name_c = unsafe { CStr::from_ptr(func_name_ptr) }; let func_name_r = func_name_c.to_str().unwrap(); let result = capi_instance.content.call(func_name_r); match result { - Ok(()) => vm_exec_result_t::VM_EXEC_OK, + Ok(()) => vm_legacy_result_t::VM_LEGACY_OK, Err(message) => { with_service(|service| service.update_last_error_str(message)); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } @@ -127,20 +127,20 @@ pub unsafe extern "C" fn vm_exec_instance_call( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_check_signatures( - instance_ptr: *mut vm_exec_instance_t, -) -> vm_exec_result_t { + instance_ptr: *mut vm_legacy_instance_t, +) -> vm_legacy_result_t { let capi_instance = cast_input_ptr!(instance_ptr, CapiInstance, "instance ptr is null"); if capi_instance.content.check_signatures() { - vm_exec_result_t::VM_EXEC_OK + vm_legacy_result_t::VM_LEGACY_OK } else { with_service(|service| { service.update_last_error_str( "non-zero number of params or results for at least one endpoint".to_string(), ) }); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } @@ -153,7 +153,7 @@ pub unsafe extern "C" fn vm_check_signatures( #[unsafe(no_mangle)] #[capi_safe_unwind(-1)] pub unsafe extern "C" fn vm_exec_instance_has_function( - instance_ptr: *mut vm_exec_instance_t, + instance_ptr: *mut vm_legacy_instance_t, func_name_ptr: *const c_char, ) -> c_int { let capi_instance = cast_input_ptr!(instance_ptr, CapiInstance, "instance ptr is null", -1); @@ -175,7 +175,7 @@ pub unsafe extern "C" fn vm_exec_instance_has_function( #[unsafe(no_mangle)] #[capi_safe_unwind(-1)] pub unsafe extern "C" fn vm_exec_instance_has_imported_function( - instance_ptr: *mut vm_exec_instance_t, + instance_ptr: *mut vm_legacy_instance_t, func_name_ptr: *const c_char, ) -> c_int { let capi_instance = cast_input_ptr!(instance_ptr, CapiInstance, "instance ptr is null", -1); @@ -197,7 +197,7 @@ pub unsafe extern "C" fn vm_exec_instance_has_imported_function( #[unsafe(no_mangle)] #[capi_safe_unwind(0)] pub unsafe extern "C" fn vm_exported_function_names_length( - instance_ptr: *mut vm_exec_instance_t, + instance_ptr: *mut vm_legacy_instance_t, ) -> c_int { let capi_instance = cast_input_ptr!(instance_ptr, CapiInstance, "instance ptr is null", 0); @@ -225,7 +225,7 @@ pub unsafe extern "C" fn vm_exported_function_names_length( #[unsafe(no_mangle)] #[capi_safe_unwind(0)] pub unsafe extern "C" fn vm_exported_function_names( - instance_ptr: *mut vm_exec_instance_t, + instance_ptr: *mut vm_legacy_instance_t, dest_buffer: *mut c_char, dest_buffer_len: c_int, ) -> c_int { @@ -236,7 +236,7 @@ pub unsafe extern "C" fn vm_exported_function_names( unsafe { string_copy(concat, dest_buffer, dest_buffer_len) } } -/// Frees memory for the given `vm_exec_instance_t`. +/// Frees memory for the given `vm_legacy_instance_t`. /// /// Check the `wasmer_instantiate()` function to get a complete /// example. @@ -248,7 +248,7 @@ pub unsafe extern "C" fn vm_exported_function_names( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -pub unsafe extern "C" fn vm_exec_instance_destroy(instance_ptr: *mut vm_exec_instance_t) { +pub unsafe extern "C" fn vm_exec_instance_destroy(instance_ptr: *mut vm_legacy_instance_t) { if !instance_ptr.is_null() { let instance = unsafe { Box::from_raw(instance_ptr as *mut CapiInstance) }; drop(instance); @@ -262,18 +262,18 @@ pub unsafe extern "C" fn vm_exec_instance_destroy(instance_ptr: *mut vm_exec_ins /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_instance_reset( - instance_ptr: *mut vm_exec_instance_t, -) -> vm_exec_result_t { + instance_ptr: *mut vm_legacy_instance_t, +) -> vm_legacy_result_t { let capi_instance = cast_input_ptr!(instance_ptr, CapiInstance, "instance ptr is null"); let result = capi_instance.content.reset(); match result { - Ok(()) => vm_exec_result_t::VM_EXEC_OK, + Ok(()) => vm_legacy_result_t::VM_LEGACY_OK, Err(message) => { with_service(|service| service.update_last_error_str(message)); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } diff --git a/c-api-legacy/src/capi_instance_cache.rs b/c-api-legacy/src/capi_instance_cache.rs index 58a03802..5e55c7f9 100644 --- a/c-api-legacy/src/capi_instance_cache.rs +++ b/c-api-legacy/src/capi_instance_cache.rs @@ -3,11 +3,11 @@ use std::slice; use meta::capi_safe_unwind; use crate::{ - capi_compilation_options::vm_exec_compilation_options_t, - capi_executor::{CapiExecutor, vm_exec_executor_t}, - capi_instance::{CapiInstance, vm_exec_instance_t}, + capi_compilation_options::vm_legacy_compilation_options_t, + capi_executor::{CapiExecutor, vm_legacy_executor_t}, + capi_instance::{CapiInstance, vm_legacy_instance_t}, service_singleton::with_service, - vm_exec_result_t, + vm_legacy_result_t, }; /// Caches an instance. @@ -17,12 +17,12 @@ use crate::{ /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_instance_cache( - instance_ptr: *const vm_exec_instance_t, + instance_ptr: *const vm_legacy_instance_t, cache_bytes_ptr: *mut *const u8, cache_bytes_len: *mut u32, -) -> vm_exec_result_t { +) -> vm_legacy_result_t { let capi_instance = cast_input_const_ptr!(instance_ptr, CapiInstance, "instance ptr is null"); let result = capi_instance.content.cache(); @@ -33,11 +33,11 @@ pub unsafe extern "C" fn vm_exec_instance_cache( *cache_bytes_len = bytes.len() as u32; } std::mem::forget(bytes); - vm_exec_result_t::VM_EXEC_OK + vm_legacy_result_t::VM_LEGACY_OK } Err(message) => { with_service(|service| service.update_last_error_str(message)); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } @@ -51,21 +51,21 @@ pub unsafe extern "C" fn vm_exec_instance_cache( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment, unused_variables)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_instance_from_cache( - executor_ptr: *mut vm_exec_executor_t, - instance_ptr_ptr: *mut *mut vm_exec_instance_t, + executor_ptr: *mut vm_legacy_executor_t, + instance_ptr_ptr: *mut *mut vm_legacy_instance_t, cache_bytes_ptr: *mut u8, cache_bytes_len: u32, - options_ptr: *const vm_exec_compilation_options_t, -) -> vm_exec_result_t { + options_ptr: *const vm_legacy_compilation_options_t, +) -> vm_legacy_result_t { let capi_executor = cast_input_ptr!(executor_ptr, CapiExecutor, "executor ptr is null"); if cache_bytes_ptr.is_null() { with_service(|service| { service.update_last_error_str("cache bytes ptr is null".to_string()) }); - return vm_exec_result_t::VM_EXEC_ERROR; + return vm_legacy_result_t::VM_LEGACY_ERROR; } return_if_ptr_null!(options_ptr, "compilation options ptr is null"); @@ -83,13 +83,13 @@ pub unsafe extern "C" fn vm_exec_instance_from_cache( }; unsafe { *instance_ptr_ptr = - Box::into_raw(Box::new(capi_instance)) as *mut vm_exec_instance_t; + Box::into_raw(Box::new(capi_instance)) as *mut vm_legacy_instance_t; } - vm_exec_result_t::VM_EXEC_OK + vm_legacy_result_t::VM_LEGACY_OK } Err(message) => { with_service(|service| service.update_last_error_str(message.to_string())); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } diff --git a/c-api-legacy/src/capi_logger.rs b/c-api-legacy/src/capi_logger.rs index e62b9c9d..41c594d5 100644 --- a/c-api-legacy/src/capi_logger.rs +++ b/c-api-legacy/src/capi_logger.rs @@ -3,7 +3,7 @@ use std::sync::Once; use crate::{set_log_level, u64_to_log_level}; use meta::capi_safe_unwind; -use crate::{basic_types::vm_exec_result_t, service_singleton::with_service}; +use crate::{basic_types::vm_legacy_result_t, service_singleton::with_service}; static PANIC_HANDLER: Once = Once::new(); @@ -16,18 +16,18 @@ pub fn set_panic_handler() { /// Sets the log level. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] -pub extern "C" fn vm_exec_set_log_level(value: u64) -> vm_exec_result_t { +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] +pub extern "C" fn vm_exec_set_log_level(value: u64) -> vm_legacy_result_t { let result = u64_to_log_level(value); match result { Ok(level) => { set_log_level(level); - vm_exec_result_t::VM_EXEC_OK + vm_legacy_result_t::VM_LEGACY_OK } Err(message) => { with_service(|service| service.update_last_error_str(message.to_string())); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } diff --git a/c-api-legacy/src/capi_memory.rs b/c-api-legacy/src/capi_memory.rs index 9c1ce183..dbbbaa02 100644 --- a/c-api-legacy/src/capi_memory.rs +++ b/c-api-legacy/src/capi_memory.rs @@ -3,9 +3,9 @@ use meta::capi_safe_unwind; use crate::{ - capi_instance::{CapiInstance, vm_exec_instance_t}, + capi_instance::{CapiInstance, vm_legacy_instance_t}, service_singleton::with_service, - vm_exec_result_t, + vm_legacy_result_t, }; use std::ptr; @@ -20,7 +20,7 @@ use std::ptr; #[unsafe(no_mangle)] #[capi_safe_unwind(0)] pub unsafe extern "C" fn vm_exec_instance_memory_data_length( - instance_ptr: *mut vm_exec_instance_t, + instance_ptr: *mut vm_legacy_instance_t, ) -> u64 { let capi_instance = cast_input_ptr!(instance_ptr, CapiInstance, "instance ptr is null", 0); let result = capi_instance.content.memory_length(); @@ -48,7 +48,7 @@ pub unsafe extern "C" fn vm_exec_instance_memory_data_length( #[unsafe(no_mangle)] #[capi_safe_unwind(ptr::null_mut())] pub unsafe extern "C" fn vm_exec_instance_memory_data( - instance_ptr: *mut vm_exec_instance_t, + instance_ptr: *mut vm_legacy_instance_t, ) -> *mut u8 { let capi_instance = cast_input_ptr!( instance_ptr, @@ -78,19 +78,19 @@ pub unsafe extern "C" fn vm_exec_instance_memory_data( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_instance_memory_grow( - instance_ptr: *mut vm_exec_instance_t, + instance_ptr: *mut vm_legacy_instance_t, by_num_pages: u32, -) -> vm_exec_result_t { +) -> vm_legacy_result_t { let capi_instance = cast_input_ptr!(instance_ptr, CapiInstance, "instance ptr is null"); let grow_result = capi_instance.content.memory_grow(by_num_pages); match grow_result { - Ok(_) => vm_exec_result_t::VM_EXEC_OK, + Ok(_) => vm_legacy_result_t::VM_LEGACY_OK, Err(grow_error) => { with_service(|service| service.update_last_error_str(grow_error.to_string())); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } diff --git a/c-api-legacy/src/capi_metering.rs b/c-api-legacy/src/capi_metering.rs index 350d7976..240e34fe 100644 --- a/c-api-legacy/src/capi_metering.rs +++ b/c-api-legacy/src/capi_metering.rs @@ -1,15 +1,15 @@ -use crate::capi_executor::{CapiExecutor, vm_exec_executor_t}; -use crate::capi_instance::{CapiInstance, vm_exec_instance_t}; -use crate::capi_opcode_cost::vm_exec_opcode_cost_t; +use crate::capi_executor::{CapiExecutor, vm_legacy_executor_t}; +use crate::capi_instance::{CapiInstance, vm_legacy_instance_t}; +use crate::capi_opcode_cost::vm_legacy_opcode_cost_t; use crate::service_singleton::with_service; -use crate::vm_exec_result_t; +use crate::vm_legacy_result_t; use meta::capi_safe_unwind; use multiversx_chain_vm_executor::{OpcodeConfig, OpcodeVersion}; /// Sets the opcode costs for the given executor. /// -/// This function returns `vm_exec_result_t::WASMER_OK` upon success, -/// `vm_exec_result_t::WASMER_ERROR` otherwise. You can use +/// This function returns `vm_legacy_result_t::WASMER_OK` upon success, +/// `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use /// `wasmer_last_error_message()` to get the generated error message. /// /// # Safety @@ -17,12 +17,12 @@ use multiversx_chain_vm_executor::{OpcodeConfig, OpcodeVersion}; /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_set_opcode_config( - executor_ptr: *mut vm_exec_executor_t, + executor_ptr: *mut vm_legacy_executor_t, opcode_version_code: i32, - opcode_cost_ptr: *const vm_exec_opcode_cost_t, -) -> vm_exec_result_t { + opcode_cost_ptr: *const vm_legacy_opcode_cost_t, +) -> vm_legacy_result_t { let capi_executor = cast_input_ptr!(executor_ptr, CapiExecutor, "executor ptr is null"); let Some(opcode_version) = OpcodeVersion::from_i32(opcode_version_code) else { @@ -32,7 +32,7 @@ pub unsafe extern "C" fn vm_exec_set_opcode_config( opcode_version_code )) }); - return vm_exec_result_t::VM_EXEC_ERROR; + return vm_legacy_result_t::VM_LEGACY_ERROR; }; let opcode_cost = unsafe { (*opcode_cost_ptr).to_opcode_cost() }; @@ -43,18 +43,18 @@ pub unsafe extern "C" fn vm_exec_set_opcode_config( let result = capi_executor.content.set_opcode_config(opcode_config); match result { - Ok(()) => vm_exec_result_t::VM_EXEC_OK, + Ok(()) => vm_legacy_result_t::VM_LEGACY_OK, Err(message) => { with_service(|service| service.update_last_error_str(message.to_string())); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } /// Sets the number of points(gas) limit for the given instance. /// -/// This function returns `vm_exec_result_t::WASMER_OK` upon success, -/// `vm_exec_result_t::WASMER_ERROR` otherwise. You can use +/// This function returns `vm_legacy_result_t::WASMER_OK` upon success, +/// `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use /// `wasmer_last_error_message()` to get the generated error message. /// /// # Safety @@ -62,26 +62,26 @@ pub unsafe extern "C" fn vm_exec_set_opcode_config( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_instance_set_points_limit( - instance_ptr: *const vm_exec_instance_t, + instance_ptr: *const vm_legacy_instance_t, limit: u64, -) -> vm_exec_result_t { +) -> vm_legacy_result_t { let capi_instance = cast_input_const_ptr!(instance_ptr, CapiInstance, "instance ptr is null"); let result = capi_instance.content.set_points_limit(limit); match result { - Ok(()) => vm_exec_result_t::VM_EXEC_OK, + Ok(()) => vm_legacy_result_t::VM_LEGACY_OK, Err(message) => { with_service(|service| service.update_last_error_str(message)); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } /// Sets the number of points(gas) for the given instance. /// -/// This function returns `vm_exec_result_t::WASMER_OK` upon success, -/// `vm_exec_result_t::WASMER_ERROR` otherwise. You can use +/// This function returns `vm_legacy_result_t::WASMER_OK` upon success, +/// `vm_legacy_result_t::WASMER_ERROR` otherwise. You can use /// `wasmer_last_error_message()` to get the generated error message. /// /// # Safety @@ -89,18 +89,18 @@ pub unsafe extern "C" fn vm_exec_instance_set_points_limit( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_result_t::VM_EXEC_ERROR)] +#[capi_safe_unwind(vm_legacy_result_t::VM_LEGACY_ERROR)] pub unsafe extern "C" fn vm_exec_instance_set_points_used( - instance_ptr: *const vm_exec_instance_t, + instance_ptr: *const vm_legacy_instance_t, points: u64, -) -> vm_exec_result_t { +) -> vm_legacy_result_t { let capi_instance = cast_input_const_ptr!(instance_ptr, CapiInstance, "instance ptr is null"); let result = capi_instance.content.set_points_used(points); match result { - Ok(()) => vm_exec_result_t::VM_EXEC_OK, + Ok(()) => vm_legacy_result_t::VM_LEGACY_OK, Err(message) => { with_service(|service| service.update_last_error_str(message)); - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR } } } @@ -114,7 +114,7 @@ pub unsafe extern "C" fn vm_exec_instance_set_points_used( #[unsafe(no_mangle)] #[capi_safe_unwind(0)] pub unsafe extern "C" fn vm_exec_instance_get_points_used( - instance_ptr: *const vm_exec_instance_t, + instance_ptr: *const vm_legacy_instance_t, ) -> u64 { let capi_instance = cast_input_const_ptr!(instance_ptr, CapiInstance, "instance ptr is null", 0); diff --git a/c-api-legacy/src/capi_opcode_cost.rs b/c-api-legacy/src/capi_opcode_cost.rs index 26abdb2c..8f0d2b33 100644 --- a/c-api-legacy/src/capi_opcode_cost.rs +++ b/c-api-legacy/src/capi_opcode_cost.rs @@ -13,7 +13,7 @@ use multiversx_chain_vm_executor::OpcodeCost; /// ABI, and vice versa. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec_opcode_cost_t { +pub struct vm_legacy_opcode_cost_t { pub opcode_block: u32, pub opcode_br: u32, pub opcode_brif: u32, @@ -148,7 +148,7 @@ pub struct vm_exec_opcode_cost_t { pub opcode_localallocate: u32, } -impl vm_exec_opcode_cost_t { +impl vm_legacy_opcode_cost_t { pub(crate) fn to_opcode_cost(&self) -> OpcodeCost { OpcodeCost { opcode_block: self.opcode_block, diff --git a/c-api-legacy/src/capi_vm_hook_pointers.rs b/c-api-legacy/src/capi_vm_hook_pointers.rs index 30290003..6fa2013e 100644 --- a/c-api-legacy/src/capi_vm_hook_pointers.rs +++ b/c-api-legacy/src/capi_vm_hook_pointers.rs @@ -10,7 +10,7 @@ use std::ffi::c_void; #[derive(Clone)] #[allow(non_camel_case_types)] #[rustfmt::skip] -pub struct vm_exec_vm_hook_c_func_pointers { +pub struct vm_legacy_vm_hook_c_func_pointers { pub get_gas_left_func_ptr: extern "C" fn(context: *mut c_void) -> i64, pub get_sc_address_func_ptr: extern "C" fn(context: *mut c_void, result_offset: i32), pub get_owner_address_func_ptr: extern "C" fn(context: *mut c_void, result_offset: i32), @@ -290,8 +290,8 @@ pub struct vm_exec_vm_hook_c_func_pointers { pub managed_verify_blsaggregated_signature_func_ptr: extern "C" fn(context: *mut c_void, key_handle: i32, message_handle: i32, sig_handle: i32) -> i32, } -impl std::fmt::Debug for vm_exec_vm_hook_c_func_pointers { +impl std::fmt::Debug for vm_legacy_vm_hook_c_func_pointers { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "vm_exec_vm_hook_c_func_pointers") + write!(f, "vm_legacy_vm_hook_c_func_pointers") } } diff --git a/c-api-legacy/src/capi_vm_hooks.rs b/c-api-legacy/src/capi_vm_hooks.rs index f1a42956..8fe57b1c 100644 --- a/c-api-legacy/src/capi_vm_hooks.rs +++ b/c-api-legacy/src/capi_vm_hooks.rs @@ -8,16 +8,16 @@ use std::ffi::c_void; use multiversx_chain_vm_executor::{MemLength, MemPtr}; -use crate::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; +use crate::capi_vm_hook_pointers::vm_legacy_vm_hook_c_func_pointers; #[derive(Debug)] pub struct CapiVMHooks { pub vm_hooks_ptr: *mut c_void, - pub c_func_pointers_ptr: vm_exec_vm_hook_c_func_pointers, + pub c_func_pointers_ptr: vm_legacy_vm_hook_c_func_pointers, } impl CapiVMHooks { - pub fn new(c_func_pointers_ptr: vm_exec_vm_hook_c_func_pointers) -> Self { + pub fn new(c_func_pointers_ptr: vm_legacy_vm_hook_c_func_pointers) -> Self { Self { vm_hooks_ptr: std::ptr::null_mut(), c_func_pointers_ptr, diff --git a/c-api-legacy/src/macros.rs b/c-api-legacy/src/macros.rs index ab764b3d..de879047 100644 --- a/c-api-legacy/src/macros.rs +++ b/c-api-legacy/src/macros.rs @@ -6,7 +6,7 @@ macro_rules! return_if_ptr_null { } }; ($ptr_var:ident, $err_msg:expr) => { - return_if_ptr_null!($ptr_var, $err_msg, vm_exec_result_t::VM_EXEC_ERROR) + return_if_ptr_null!($ptr_var, $err_msg, vm_legacy_result_t::VM_LEGACY_ERROR) }; } @@ -24,7 +24,7 @@ macro_rules! cast_input_ptr { $ptr_var, $expected_ty, $err_msg, - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR ) }; } @@ -43,7 +43,7 @@ macro_rules! cast_input_const_ptr { $ptr_var, $expected_ty, $err_msg, - vm_exec_result_t::VM_EXEC_ERROR + vm_legacy_result_t::VM_LEGACY_ERROR ) }; } diff --git a/c-api-wasmer6/Cargo.toml b/c-api-wasmer6/Cargo.toml index 598850f6..bb915327 100644 --- a/c-api-wasmer6/Cargo.toml +++ b/c-api-wasmer6/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] meta = { path = "../meta" } multiversx-chain-vm-executor = { path = "../vm-executor" } +multiversx-chain-vm-executor-c-api-common = { path = "../c-api-common" } multiversx-chain-vm-executor-wasmer-experimental = { path = "../vm-executor-experimental" } libc = { version = "^0.2", default-features = false } diff --git a/c-api-wasmer6/cbindgen.toml b/c-api-wasmer6/cbindgen.toml index 09495340..8b67d498 100644 --- a/c-api-wasmer6/cbindgen.toml +++ b/c-api-wasmer6/cbindgen.toml @@ -1,5 +1,17 @@ -# `vm_exec_w6_hook_result_t` is the return type of every generated VM hook pointer, so the Go -# side and the Phase 3 generator both need its typedef — but no function exported from this -# crate mentions it yet, so cbindgen would not emit it on reachability alone. +# The shared vocabulary comes from the common header rather than being repeated here, so that +# a caller linking more than one of these libraries sees one definition of each type. Without +# the exclusion cbindgen would emit its own copy of every type it can reach. +header = "#include \"libvmexeccapi_common.h\"" + [export] -include = ["vm_exec_w6_hook_result_t"] +exclude = [ + "vm_exec_status_t", + "vm_exec_opcode_cost_t", + "vm_exec_compilation_options_t", + "vm_exec_executor_config_t", + "vm_exec_call_result_kind_t", + "vm_exec_call_result_t", + "vm_exec_hook_result_t", + "vm_exec_hook_context_t", + "vm_exec_vm_hook_c_func_pointers", +] diff --git a/c-api-wasmer6/libvmexeccapi_w6.h b/c-api-wasmer6/libvmexeccapi_w6.h index 3b2bca80..48e4aae5 100644 --- a/c-api-wasmer6/libvmexeccapi_w6.h +++ b/c-api-wasmer6/libvmexeccapi_w6.h @@ -1,82 +1,10 @@ +#include "libvmexeccapi_common.h" + #include #include #include #include -/** - * The value of `vm_exec_w6_hook_result_t::early_exit` that means "the hook returned normally". - * - * A dedicated sentinel is needed because `0` is a real early-exit code: the Rust VM uses - * `ASYNC_CALL_EARLY_EXIT_CODE = 0` and the Go VM has the same async-call case, so "code 0" - * and "no code" have to stay distinguishable. - */ -#define VM_EXEC_W6_NO_EARLY_EXIT UINT64_MAX - -/** - * Result of a C API call. - * - * `VM_EXEC_W6_OK` is zero and every failure is negative, so the functions that return a - * count (the memory loads) can share the same code space: a non-negative result is a - * count, a negative one is one of these. - */ -typedef enum vm_exec_w6_status_t { - /** - * The call succeeded. - */ - VM_EXEC_W6_OK = 0, - /** - * The call failed; the error message was written to the provided error buffer. - */ - VM_EXEC_W6_ERROR = -1, - /** - * A pointer argument that must not be null was null. No message is produced, because - * a null pointer may well be the error buffer itself. - */ - VM_EXEC_W6_NULL_POINTER = -2, - /** - * A hook context was used outside the host call it belongs to. See - * [`crate::capi_vm_hook_context`]. - */ - VM_EXEC_W6_STALE_CONTEXT = -3, - /** - * A panic was caught at the boundary. Rust panics must never unwind into C, so every - * entry point traps them and reports this instead. - */ - VM_EXEC_W6_PANIC = -4, -} vm_exec_w6_status_t; - -/** - * How a call ended. - * - * This replaces the legacy arrangement, where a call returned a bare ok/error and the VM - * then polled a wasm global to find out what had actually happened. - */ -typedef enum vm_exec_w6_call_result_kind_t { - /** - * The function returned normally. - */ - VM_EXEC_W6_CALL_OK = 0, - /** - * No exported function by that name. - */ - VM_EXEC_W6_CALL_FUNCTION_NOT_FOUND = 1, - /** - * The instance trapped for a reason that is neither a VM hook's doing nor a - * middleware breakpoint. The message is in the error buffer. - */ - VM_EXEC_W6_CALL_RUNTIME_ERROR = 2, - /** - * A VM hook asked for execution to stop. `early_exit_code` says which; the message, if - * any, is in the error buffer, and is informational — the VM has already recorded its - * own. - */ - VM_EXEC_W6_CALL_EARLY_EXIT = 3, - /** - * A middleware stopped execution. `breakpoint` says which. - */ - VM_EXEC_W6_CALL_BREAKPOINT = 4, -} vm_exec_w6_call_result_kind_t; - /** * Opaque handle to an executor. */ @@ -84,506 +12,6 @@ typedef struct vm_exec_w6_executor_t { } vm_exec_w6_executor_t; -/** - * FFI-safe mirror of `OpcodeCost`. - * - * This is a distinct type rather than a cast of `OpcodeCost` itself, so that - * the general-purpose struct is free to change shape without touching the C - * ABI, and vice versa. - */ -typedef struct vm_exec_opcode_cost_t { - uint32_t opcode_block; - uint32_t opcode_br; - uint32_t opcode_brif; - uint32_t opcode_brtable; - uint32_t opcode_call; - uint32_t opcode_callindirect; - uint32_t opcode_catch; - uint32_t opcode_catchall; - uint32_t opcode_delegate; - uint32_t opcode_drop; - uint32_t opcode_else; - uint32_t opcode_end; - uint32_t opcode_globalget; - uint32_t opcode_globalset; - uint32_t opcode_i32add; - uint32_t opcode_i32and; - uint32_t opcode_i32clz; - uint32_t opcode_i32const; - uint32_t opcode_i32ctz; - uint32_t opcode_i32divs; - uint32_t opcode_i32divu; - uint32_t opcode_i32eq; - uint32_t opcode_i32eqz; - uint32_t opcode_i32extend16s; - uint32_t opcode_i32extend8s; - uint32_t opcode_i32ges; - uint32_t opcode_i32geu; - uint32_t opcode_i32gts; - uint32_t opcode_i32gtu; - uint32_t opcode_i32les; - uint32_t opcode_i32leu; - uint32_t opcode_i32load; - uint32_t opcode_i32load16s; - uint32_t opcode_i32load16u; - uint32_t opcode_i32load8s; - uint32_t opcode_i32load8u; - uint32_t opcode_i32lts; - uint32_t opcode_i32ltu; - uint32_t opcode_i32mul; - uint32_t opcode_i32ne; - uint32_t opcode_i32or; - uint32_t opcode_i32popcnt; - uint32_t opcode_i32rems; - uint32_t opcode_i32remu; - uint32_t opcode_i32rotl; - uint32_t opcode_i32rotr; - uint32_t opcode_i32shl; - uint32_t opcode_i32shrs; - uint32_t opcode_i32shru; - uint32_t opcode_i32store; - uint32_t opcode_i32store16; - uint32_t opcode_i32store8; - uint32_t opcode_i32sub; - uint32_t opcode_i32wrapi64; - uint32_t opcode_i32xor; - uint32_t opcode_i64add; - uint32_t opcode_i64and; - uint32_t opcode_i64clz; - uint32_t opcode_i64const; - uint32_t opcode_i64ctz; - uint32_t opcode_i64divs; - uint32_t opcode_i64divu; - uint32_t opcode_i64eq; - uint32_t opcode_i64eqz; - uint32_t opcode_i64extend16s; - uint32_t opcode_i64extend32s; - uint32_t opcode_i64extend8s; - uint32_t opcode_i64extendi32s; - uint32_t opcode_i64extendi32u; - uint32_t opcode_i64ges; - uint32_t opcode_i64geu; - uint32_t opcode_i64gts; - uint32_t opcode_i64gtu; - uint32_t opcode_i64les; - uint32_t opcode_i64leu; - uint32_t opcode_i64load; - uint32_t opcode_i64load16s; - uint32_t opcode_i64load16u; - uint32_t opcode_i64load32s; - uint32_t opcode_i64load32u; - uint32_t opcode_i64load8s; - uint32_t opcode_i64load8u; - uint32_t opcode_i64lts; - uint32_t opcode_i64ltu; - uint32_t opcode_i64mul; - uint32_t opcode_i64ne; - uint32_t opcode_i64or; - uint32_t opcode_i64popcnt; - uint32_t opcode_i64rems; - uint32_t opcode_i64remu; - uint32_t opcode_i64rotl; - uint32_t opcode_i64rotr; - uint32_t opcode_i64shl; - uint32_t opcode_i64shrs; - uint32_t opcode_i64shru; - uint32_t opcode_i64store; - uint32_t opcode_i64store16; - uint32_t opcode_i64store32; - uint32_t opcode_i64store8; - uint32_t opcode_i64sub; - uint32_t opcode_i64xor; - uint32_t opcode_if; - uint32_t opcode_localget; - uint32_t opcode_localset; - uint32_t opcode_localtee; - uint32_t opcode_loop; - uint32_t opcode_memorygrow; - uint32_t opcode_memorysize; - uint32_t opcode_nop; - uint32_t opcode_reffunc; - uint32_t opcode_refisnull; - uint32_t opcode_refnull; - uint32_t opcode_rethrow; - uint32_t opcode_return; - uint32_t opcode_returncall; - uint32_t opcode_returncallindirect; - uint32_t opcode_select; - uint32_t opcode_tableget; - uint32_t opcode_tablegrow; - uint32_t opcode_tableinit; - uint32_t opcode_tableset; - uint32_t opcode_tablesize; - uint32_t opcode_throw; - uint32_t opcode_try; - uint32_t opcode_typedselect; - uint32_t opcode_unreachable; - uint32_t opcode_unwind; - uint32_t opcode_memorycopy; - uint32_t opcode_memoryfill; - uint32_t opcode_memorycopyperbyte; - uint32_t opcode_memoryfillperbyte; - uint32_t opcode_localallocate; -} vm_exec_opcode_cost_t; - -/** - * Everything an executor is configured with. - * - * The opcode cost table is referenced rather than inlined, because it is large and the - * caller already holds one. It is read during the call and not retained. - */ -typedef struct vm_exec_w6_executor_config_t { - /** - * Which opcode set and whitelist to apply; see `OpcodeVersion` on the Rust side. - */ - int32_t opcode_version; - /** - * The per-opcode gas costs. Must not be null. - */ - const struct vm_exec_opcode_cost_t *opcode_cost; -} vm_exec_w6_executor_config_t; - -/** - * What every VM hook returns. - * - * One 16-byte struct covers every hook shape — `void`, `i32`, `i64` — and is returned in - * registers under both SysV AMD64 (RAX:RDX) and AAPCS64 (X0:X1), so the early-exit channel - * costs no memory traffic. A cgo `//export` function may return a C struct by value, which - * is what makes this shape available to the Go side. - * - * The early-exit *message* does not travel in this struct. A pointer cannot: cgo forbids - * returning an unpinned Go pointer, and a fixed buffer cannot work because the messages are - * unbounded user text. The Go VM does not need it to travel, because the hook has already - * recorded the message in its own output state by the time it returns. - */ -typedef struct vm_exec_w6_hook_result_t { - /** - * The hook's own return value, widened to 64 bits. Ignored for a `void` hook, and for - * any hook that reports an early exit. - */ - int64_t value; - /** - * The early-exit code, or `VM_EXEC_W6_NO_EARLY_EXIT`. - */ - uint64_t early_exit; -} vm_exec_w6_hook_result_t; - -/** - * What a VM hook is handed as its first argument. - * - * Only `go_vm_hooks` is part of the layout the Go side mirrors; the rest is private to - * Rust, and the whole pointer is what travels back into the state functions. - */ -typedef struct vm_exec_w6_hook_context_t { - /** - * The caller's VM hooks handle for this instance. Read by the caller, never by Rust. - * - * An opaque word rather than a pointer, deliberately: Rust only stores it and hands it - * back, and the Go side needs it to be an integer. cgo forbids storing a Go pointer in - * C memory, so what Go puts here is a `runtime/cgo.Handle`, which is a `uintptr` by - * construction. Typing the field as a pointer would force Go to convert it back with - * `unsafe.Pointer(uintptr(...))`, exactly the pattern `go vet` rejects. - */ - uintptr_t go_vm_hooks; - /** - * Erased `&mut ExperimentalInstanceState<'_>`; null while no call is active. - */ - void *state; - /** - * Erased `&RefCell`, for the token check. - */ - const void *pool; - /** - * This activation's call token; `0` while no call is active. - */ - uint64_t token; -} vm_exec_w6_hook_context_t; - -/** - * The Go VM's hook implementations, as a table of C function pointers. - * - * Copied into Rust at executor creation; see CapiVMHookTable. - */ -typedef struct vm_exec_w6_vm_hook_c_func_pointers { - struct vm_exec_w6_hook_result_t (*get_gas_left_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_sc_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*get_owner_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*get_shard_of_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset); - struct vm_exec_w6_hook_result_t (*is_smart_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset); - struct vm_exec_w6_hook_result_t (*signal_error_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t message_offset, int32_t message_length); - struct vm_exec_w6_hook_result_t (*get_external_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*get_block_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t nonce, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*get_esdt_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*get_esdt_nft_name_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); - struct vm_exec_w6_hook_result_t (*get_esdt_nft_attribute_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); - struct vm_exec_w6_hook_result_t (*get_esdt_nft_uri_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce); - struct vm_exec_w6_hook_result_t (*get_esdt_token_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t value_handle, int32_t properties_offset, int32_t hash_offset, int32_t name_offset, int32_t attributes_offset, int32_t creator_offset, int32_t royalties_handle, int32_t uris_offset); - struct vm_exec_w6_hook_result_t (*get_esdt_local_roles_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t token_id_handle); - struct vm_exec_w6_hook_result_t (*validate_token_identifier_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t token_id_handle); - struct vm_exec_w6_hook_result_t (*transfer_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); - struct vm_exec_w6_hook_result_t (*transfer_value_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*transfer_esdt_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*transfer_esdt_nft_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t token_id_offset, int32_t token_id_len, int32_t value_offset, int64_t nonce, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t num_token_transfers, int32_t token_transfers_args_length_offset, int32_t token_transfer_data_offset, int64_t gas_limit, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*create_async_call_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t data_length, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback); - struct vm_exec_w6_hook_result_t (*set_async_context_callback_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t callback, int32_t callback_length, int32_t data, int32_t data_length, int64_t gas); - struct vm_exec_w6_hook_result_t (*upgrade_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*upgrade_from_source_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*delete_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int64_t gas_limit, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*async_call_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_offset, int32_t value_offset, int32_t data_offset, int32_t length); - struct vm_exec_w6_hook_result_t (*get_argument_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id); - struct vm_exec_w6_hook_result_t (*get_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id, int32_t arg_offset); - struct vm_exec_w6_hook_result_t (*get_function_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t function_offset); - struct vm_exec_w6_hook_result_t (*get_num_arguments_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*storage_store_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset, int32_t data_length); - struct vm_exec_w6_hook_result_t (*storage_load_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec_w6_hook_result_t (*storage_load_from_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t key_offset, int32_t key_length, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*storage_load_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*set_storage_lock_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t lock_timestamp); - struct vm_exec_w6_hook_result_t (*get_storage_lock_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec_w6_hook_result_t (*is_storage_locked_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec_w6_hook_result_t (*clear_storage_lock_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec_w6_hook_result_t (*get_caller_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*check_no_payment_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*get_esdt_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*get_esdt_value_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset, int32_t index); - struct vm_exec_w6_hook_result_t (*get_esdt_token_name_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*get_esdt_token_name_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_offset, int32_t index); - struct vm_exec_w6_hook_result_t (*get_esdt_token_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_esdt_token_nonce_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t index); - struct vm_exec_w6_hook_result_t (*get_current_esdt_nft_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len); - struct vm_exec_w6_hook_result_t (*get_esdt_token_type_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_esdt_token_type_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t index); - struct vm_exec_w6_hook_result_t (*get_num_esdt_transfers_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_call_value_token_name_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset); - struct vm_exec_w6_hook_result_t (*get_call_value_token_name_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t call_value_offset, int32_t token_name_offset, int32_t index); - struct vm_exec_w6_hook_result_t (*is_reserved_function_name_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t name_handle); - struct vm_exec_w6_hook_result_t (*write_log_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_pointer, int32_t data_length, int32_t topic_ptr, int32_t num_topics); - struct vm_exec_w6_hook_result_t (*write_event_log_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t num_topics, int32_t topic_lengths_offset, int32_t topic_offset, int32_t data_offset, int32_t data_length); - struct vm_exec_w6_hook_result_t (*get_block_timestamp_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_block_timestamp_ms_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_block_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_block_round_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_block_epoch_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_block_random_seed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t pointer); - struct vm_exec_w6_hook_result_t (*get_state_root_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t pointer); - struct vm_exec_w6_hook_result_t (*get_prev_block_timestamp_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_prev_block_timestamp_ms_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_prev_block_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_prev_block_round_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_prev_block_epoch_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_prev_block_random_seed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t pointer); - struct vm_exec_w6_hook_result_t (*get_block_round_time_ms_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*epoch_start_block_timestamp_ms_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*epoch_start_block_nonce_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*epoch_start_block_round_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*finish_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t pointer, int32_t length); - struct vm_exec_w6_hook_result_t (*execute_on_same_context_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*execute_on_dest_context_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t value_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*execute_read_only_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t address_offset, int32_t function_offset, int32_t function_length, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*create_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t code_offset, int32_t code_metadata_offset, int32_t length, int32_t result_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*deploy_from_source_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas_limit, int32_t value_offset, int32_t source_contract_address_offset, int32_t code_metadata_offset, int32_t result_address_offset, int32_t num_arguments, int32_t arguments_length_offset, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*get_num_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*get_return_data_size_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_id); - struct vm_exec_w6_hook_result_t (*get_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_id, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*clean_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*delete_from_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_id); - struct vm_exec_w6_hook_result_t (*get_original_tx_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*get_current_tx_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*get_prev_tx_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*managed_sc_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*managed_owner_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*managed_caller_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*managed_get_original_caller_addr_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*managed_get_relayer_addr_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*managed_signal_error_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t err_handle); - struct vm_exec_w6_hook_result_t (*managed_write_log_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t topics_handle, int32_t data_handle); - struct vm_exec_w6_hook_result_t (*managed_get_original_tx_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_get_state_root_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_get_block_random_seed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_get_prev_block_random_seed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_get_return_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t result_id, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_get_multi_esdt_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t multi_call_value_handle); - struct vm_exec_w6_hook_result_t (*managed_get_all_transfers_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t transfer_call_values_list_handle); - struct vm_exec_w6_hook_result_t (*managed_get_back_transfers_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t esdt_transfers_value_handle, int32_t egld_value_handle); - struct vm_exec_w6_hook_result_t (*managed_get_esdt_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle); - struct vm_exec_w6_hook_result_t (*managed_get_esdt_token_data_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t value_handle, int32_t properties_handle, int32_t hash_handle, int32_t name_handle, int32_t attributes_handle, int32_t creator_handle, int32_t royalties_handle, int32_t uris_handle); - struct vm_exec_w6_hook_result_t (*managed_get_esdt_token_type_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce, int32_t type_handle); - struct vm_exec_w6_hook_result_t (*managed_async_call_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle); - struct vm_exec_w6_hook_result_t (*managed_create_async_call_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t success_offset, int32_t success_length, int32_t error_offset, int32_t error_length, int64_t gas, int64_t extra_gas_for_callback, int32_t callback_closure_handle); - struct vm_exec_w6_hook_result_t (*managed_get_callback_closure_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t callback_closure_handle); - struct vm_exec_w6_hook_result_t (*managed_upgrade_from_source_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_upgrade_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_delete_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_handle, int64_t gas_limit, int32_t arguments_handle); - struct vm_exec_w6_hook_result_t (*managed_deploy_from_source_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t address_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_create_contract_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t value_handle, int32_t code_handle, int32_t code_metadata_handle, int32_t arguments_handle, int32_t result_address_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_execute_read_only_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_execute_on_same_context_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_execute_on_dest_context_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_execute_on_dest_context_with_error_return_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t gas, int32_t address_handle, int32_t value_handle, int32_t function_handle, int32_t arguments_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*managed_multi_transfer_esdt_nft_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); - struct vm_exec_w6_hook_result_t (*managed_multi_transfer_esdt_nft_execute_with_return_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); - struct vm_exec_w6_hook_result_t (*managed_multi_transfer_esdt_nft_execute_by_user_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t user_handle, int32_t dst_handle, int32_t token_transfers_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); - struct vm_exec_w6_hook_result_t (*managed_transfer_value_execute_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dst_handle, int32_t value_handle, int64_t gas_limit, int32_t function_handle, int32_t arguments_handle); - struct vm_exec_w6_hook_result_t (*managed_is_esdt_frozen_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t token_id_handle, int64_t nonce); - struct vm_exec_w6_hook_result_t (*managed_is_esdt_limited_transfer_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t token_id_handle); - struct vm_exec_w6_hook_result_t (*managed_is_esdt_paused_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t token_id_handle); - struct vm_exec_w6_hook_result_t (*managed_buffer_to_hex_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t source_handle, int32_t dest_handle); - struct vm_exec_w6_hook_result_t (*managed_get_code_metadata_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t response_handle); - struct vm_exec_w6_hook_result_t (*managed_get_code_hash_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t code_hash_handle); - struct vm_exec_w6_hook_result_t (*managed_is_builtin_function_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t function_name_handle); - struct vm_exec_w6_hook_result_t (*big_float_new_from_parts_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t integral_part, int32_t fractional_part, int32_t exponent); - struct vm_exec_w6_hook_result_t (*big_float_new_from_frac_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t numerator, int64_t denominator); - struct vm_exec_w6_hook_result_t (*big_float_new_from_sci_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t significand, int64_t exponent); - struct vm_exec_w6_hook_result_t (*big_float_add_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_float_sub_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_float_mul_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_float_div_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_float_neg_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_float_clone_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_float_cmp_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_float_abs_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_float_sign_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_float_sqrt_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_float_pow_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t exponent); - struct vm_exec_w6_hook_result_t (*big_float_floor_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_float_ceil_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_float_truncate_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t dest_big_int_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_float_set_int64_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int64_t value); - struct vm_exec_w6_hook_result_t (*big_float_is_int_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_float_set_big_int_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t big_int_handle); - struct vm_exec_w6_hook_result_t (*big_float_get_const_pi_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*big_float_get_const_e_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*big_int_get_unsigned_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*big_int_get_signed_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*big_int_storage_store_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t source_handle); - struct vm_exec_w6_hook_result_t (*big_int_storage_load_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*big_int_get_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*big_int_get_esdt_call_value_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination); - struct vm_exec_w6_hook_result_t (*big_int_get_esdt_call_value_by_index_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t index); - struct vm_exec_w6_hook_result_t (*big_int_get_external_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t result); - struct vm_exec_w6_hook_result_t (*big_int_get_esdt_external_balance_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_offset, int32_t token_id_offset, int32_t token_id_len, int64_t nonce, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*big_int_new_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t small_value); - struct vm_exec_w6_hook_result_t (*big_int_unsigned_byte_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle); - struct vm_exec_w6_hook_result_t (*big_int_signed_byte_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle); - struct vm_exec_w6_hook_result_t (*big_int_get_unsigned_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); - struct vm_exec_w6_hook_result_t (*big_int_get_signed_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle, int32_t byte_offset); - struct vm_exec_w6_hook_result_t (*big_int_set_unsigned_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); - struct vm_exec_w6_hook_result_t (*big_int_set_signed_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t byte_offset, int32_t byte_length); - struct vm_exec_w6_hook_result_t (*big_int_is_int64_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*big_int_get_int64_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*big_int_set_int64_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int64_t value); - struct vm_exec_w6_hook_result_t (*big_int_add_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_sub_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_mul_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_tdiv_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_tmod_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_ediv_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_emod_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_sqrt_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_int_pow_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_log2_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op1_handle); - struct vm_exec_w6_hook_result_t (*big_int_abs_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_int_neg_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_int_sign_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_int_cmp_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_not_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle); - struct vm_exec_w6_hook_result_t (*big_int_and_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_or_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_xor_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op1_handle, int32_t op2_handle); - struct vm_exec_w6_hook_result_t (*big_int_shr_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); - struct vm_exec_w6_hook_result_t (*big_int_shl_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t op_handle, int32_t bits); - struct vm_exec_w6_hook_result_t (*big_int_finish_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle); - struct vm_exec_w6_hook_result_t (*big_int_finish_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t reference_handle); - struct vm_exec_w6_hook_result_t (*big_int_to_string_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t big_int_handle, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_new_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*mbuffer_new_from_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t data_length); - struct vm_exec_w6_hook_result_t (*mbuffer_get_length_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_get_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*mbuffer_get_byte_slice_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*mbuffer_copy_byte_slice_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t source_handle, int32_t starting_position, int32_t slice_length, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_eq_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle1, int32_t m_buffer_handle2); - struct vm_exec_w6_hook_result_t (*mbuffer_set_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t data_offset, int32_t data_length); - struct vm_exec_w6_hook_result_t (*mbuffer_set_byte_slice_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t starting_position, int32_t data_length, int32_t data_offset); - struct vm_exec_w6_hook_result_t (*mbuffer_append_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_append_bytes_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t accumulator_handle, int32_t data_offset, int32_t data_length); - struct vm_exec_w6_hook_result_t (*mbuffer_to_big_int_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_to_big_int_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_from_big_int_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_from_big_int_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_int_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_to_small_int_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_to_small_int_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_from_small_int_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); - struct vm_exec_w6_hook_result_t (*mbuffer_from_small_int_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int64_t value); - struct vm_exec_w6_hook_result_t (*mbuffer_to_big_float_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_from_big_float_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_buffer_handle, int32_t big_float_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_storage_store_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t source_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_storage_load_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_storage_load_from_address_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t address_handle, int32_t key_handle, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_get_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id, int32_t destination_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_finish_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t source_handle); - struct vm_exec_w6_hook_result_t (*mbuffer_set_random_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t destination_handle, int32_t length); - struct vm_exec_w6_hook_result_t (*managed_map_new_func_ptr)(struct vm_exec_w6_hook_context_t *ctx); - struct vm_exec_w6_hook_result_t (*managed_map_put_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t value_handle); - struct vm_exec_w6_hook_result_t (*managed_map_get_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); - struct vm_exec_w6_hook_result_t (*managed_map_remove_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle, int32_t out_value_handle); - struct vm_exec_w6_hook_result_t (*managed_map_contains_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t m_map_handle, int32_t key_handle); - struct vm_exec_w6_hook_result_t (*small_int_get_unsigned_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id); - struct vm_exec_w6_hook_result_t (*small_int_get_signed_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id); - struct vm_exec_w6_hook_result_t (*small_int_finish_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t value); - struct vm_exec_w6_hook_result_t (*small_int_finish_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t value); - struct vm_exec_w6_hook_result_t (*small_int_storage_store_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); - struct vm_exec_w6_hook_result_t (*small_int_storage_store_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); - struct vm_exec_w6_hook_result_t (*small_int_storage_load_unsigned_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec_w6_hook_result_t (*small_int_storage_load_signed_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec_w6_hook_result_t (*int64get_argument_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t id); - struct vm_exec_w6_hook_result_t (*int64finish_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int64_t value); - struct vm_exec_w6_hook_result_t (*int64storage_store_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int64_t value); - struct vm_exec_w6_hook_result_t (*int64storage_load_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length); - struct vm_exec_w6_hook_result_t (*sha256_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*managed_sha256_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); - struct vm_exec_w6_hook_result_t (*keccak256_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*managed_keccak256_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); - struct vm_exec_w6_hook_result_t (*ripemd160_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t length, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*managed_ripemd160_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t input_handle, int32_t output_handle); - struct vm_exec_w6_hook_result_t (*verify_bls_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); - struct vm_exec_w6_hook_result_t (*managed_verify_bls_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec_w6_hook_result_t (*verify_ed25519_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t message_offset, int32_t message_length, int32_t sig_offset); - struct vm_exec_w6_hook_result_t (*managed_verify_ed25519_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec_w6_hook_result_t (*verify_custom_secp256k1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset, int32_t hash_type); - struct vm_exec_w6_hook_result_t (*managed_verify_custom_secp256k1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle, int32_t hash_type); - struct vm_exec_w6_hook_result_t (*verify_secp256k1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_offset, int32_t key_length, int32_t message_offset, int32_t message_length, int32_t sig_offset); - struct vm_exec_w6_hook_result_t (*managed_verify_secp256k1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec_w6_hook_result_t (*encode_secp256k1_der_signature_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t r_offset, int32_t r_length, int32_t s_offset, int32_t s_length, int32_t sig_offset); - struct vm_exec_w6_hook_result_t (*managed_encode_secp256k1_der_signature_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t r_handle, int32_t s_handle, int32_t sig_handle); - struct vm_exec_w6_hook_result_t (*add_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t fst_point_xhandle, int32_t fst_point_yhandle, int32_t snd_point_xhandle, int32_t snd_point_yhandle); - struct vm_exec_w6_hook_result_t (*double_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); - struct vm_exec_w6_hook_result_t (*is_on_curve_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle); - struct vm_exec_w6_hook_result_t (*scalar_base_mult_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); - struct vm_exec_w6_hook_result_t (*managed_scalar_base_mult_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); - struct vm_exec_w6_hook_result_t (*scalar_mult_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_offset, int32_t length); - struct vm_exec_w6_hook_result_t (*managed_scalar_mult_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t point_xhandle, int32_t point_yhandle, int32_t data_handle); - struct vm_exec_w6_hook_result_t (*marshal_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*managed_marshal_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*marshal_compressed_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*managed_marshal_compressed_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pair_handle, int32_t y_pair_handle, int32_t ec_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*unmarshal_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); - struct vm_exec_w6_hook_result_t (*managed_unmarshal_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); - struct vm_exec_w6_hook_result_t (*unmarshal_compressed_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_offset, int32_t length); - struct vm_exec_w6_hook_result_t (*managed_unmarshal_compressed_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_result_handle, int32_t y_result_handle, int32_t ec_handle, int32_t data_handle); - struct vm_exec_w6_hook_result_t (*generate_key_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_offset); - struct vm_exec_w6_hook_result_t (*managed_generate_key_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t x_pub_key_handle, int32_t y_pub_key_handle, int32_t ec_handle, int32_t result_handle); - struct vm_exec_w6_hook_result_t (*create_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_offset, int32_t data_length); - struct vm_exec_w6_hook_result_t (*managed_create_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t data_handle); - struct vm_exec_w6_hook_result_t (*get_curve_length_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t ec_handle); - struct vm_exec_w6_hook_result_t (*get_priv_key_byte_length_ec_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t ec_handle); - struct vm_exec_w6_hook_result_t (*elliptic_curve_get_values_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t ec_handle, int32_t field_order_handle, int32_t base_point_order_handle, int32_t eq_constant_handle, int32_t x_base_point_handle, int32_t y_base_point_handle); - struct vm_exec_w6_hook_result_t (*managed_verify_secp256r1_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec_w6_hook_result_t (*managed_verify_blssignature_share_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); - struct vm_exec_w6_hook_result_t (*managed_verify_blsaggregated_signature_func_ptr)(struct vm_exec_w6_hook_context_t *ctx, int32_t key_handle, int32_t message_handle, int32_t sig_handle); -} vm_exec_w6_vm_hook_c_func_pointers; - /** * Opaque handle to an instance. */ @@ -591,58 +19,20 @@ typedef struct vm_exec_w6_instance_t { } vm_exec_w6_instance_t; -/** - * FFI-safe mirror of the new `CompilationOptions`. - * - * Four fields, against the legacy seven: `gas_limit` moved to the call, and metering and - * runtime breakpoints are no longer optional. Fields that are `usize` on the Rust side are - * fixed to `u64` here, since `usize` has no guaranteed width across host platforms. - */ -typedef struct vm_exec_w6_compilation_options_t { - uint64_t unmetered_locals; - uint64_t max_memory_grow; - uint64_t max_memory_grow_delta; - bool opcode_trace; -} vm_exec_w6_compilation_options_t; - -/** - * The out-struct of `vm_exec_w6_instance_call`. - */ -typedef struct vm_exec_w6_call_result_t { - enum vm_exec_w6_call_result_kind_t kind; - /** - * Meaningful only for `VM_EXEC_W6_CALL_EARLY_EXIT`. - */ - uint64_t early_exit_code; - /** - * Meaningful only for `VM_EXEC_W6_CALL_BREAKPOINT`. Values match `BreakpointValue`: - * 4 = out of gas, 5 = memory limit. - */ - uint64_t breakpoint; -} vm_exec_w6_call_result_t; - -/** - * A buffer of this size holds every message the executor currently produces. - * - * Exported so the Go side has one place to take the number from rather than picking its - * own. - */ -extern const int VM_EXEC_W6_ERROR_BUF_RECOMMENDED_LEN; - /** * Creates an executor from one configuration struct and one hook table. * * `hook_table` is copied, so the caller is free to release it as soon as this returns. Every * entry must be populated; see `CapiVMHookTable::copy_from`. * - * On success writes the executor to `executor_out` and returns `VM_EXEC_W6_OK`. The caller + * On success writes the executor to `executor_out` and returns `VM_EXEC_OK`. The caller * owns the result and must release it with `vm_exec_w6_executor_destroy`. * * # Safety * * C API function, works with raw object pointers. */ -enum vm_exec_w6_status_t vm_exec_w6_new_executor(struct vm_exec_w6_executor_t **executor_out, const struct vm_exec_w6_executor_config_t *config, const struct vm_exec_w6_vm_hook_c_func_pointers *hook_table, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w6_new_executor(struct vm_exec_w6_executor_t **executor_out, const vm_exec_executor_config_t *config, const vm_exec_vm_hook_c_func_pointers *hook_table, char *err_buf, int err_buf_len); /** * Destroys an executor. Does nothing if `executor` is null. @@ -668,7 +58,7 @@ void vm_exec_w6_executor_destroy(struct vm_exec_w6_executor_t *executor); * * C API function, works with raw object pointers. */ -enum vm_exec_w6_status_t vm_exec_w6_new_instance(struct vm_exec_w6_executor_t *executor, struct vm_exec_w6_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const struct vm_exec_w6_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w6_new_instance(struct vm_exec_w6_executor_t *executor, struct vm_exec_w6_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const vm_exec_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); /** * Destroys an instance. Does nothing if `instance` is null. @@ -682,7 +72,7 @@ void vm_exec_w6_instance_destroy(struct vm_exec_w6_instance_t *instance); /** * Calls an exported function, with a gas limit for this call. * - * Always writes `result_out` when it returns `VM_EXEC_W6_OK`. A non-`OK` status means the + * Always writes `result_out` when it returns `VM_EXEC_OK`. A non-`OK` status means the * call could not be attempted at all, which is distinct from a call that was attempted and * failed — the latter is reported through `result_out.kind`. * @@ -690,7 +80,7 @@ void vm_exec_w6_instance_destroy(struct vm_exec_w6_instance_t *instance); * * C API function, works with raw object pointers. */ -enum vm_exec_w6_status_t vm_exec_w6_instance_call(struct vm_exec_w6_instance_t *instance, const char *func_name, uint64_t points_limit, struct vm_exec_w6_call_result_t *result_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w6_instance_call(struct vm_exec_w6_instance_t *instance, const char *func_name, uint64_t points_limit, vm_exec_call_result_t *result_out, char *err_buf, int err_buf_len); /** * Returns the number of points (gas) used so far. @@ -699,7 +89,7 @@ enum vm_exec_w6_status_t vm_exec_w6_instance_call(struct vm_exec_w6_instance_t * * * C API function, works with raw object pointers. */ -enum vm_exec_w6_status_t vm_exec_w6_instance_get_points_used(struct vm_exec_w6_instance_t *instance, uint64_t *points_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w6_instance_get_points_used(struct vm_exec_w6_instance_t *instance, uint64_t *points_out, char *err_buf, int err_buf_len); /** * Checks that every exported function takes no arguments and returns nothing. @@ -782,7 +172,7 @@ int vm_exec_w6_instance_imported_function_names(const struct vm_exec_w6_instance * * C API function, works with raw object pointers. */ -int64_t vm_exec_w6_state_mem_load(struct vm_exec_w6_hook_context_t *ctx, int32_t mem_ptr, uint8_t *dest, int32_t dest_len, char *err_buf, int err_buf_len); +int64_t vm_exec_w6_state_mem_load(vm_exec_hook_context_t *ctx, int32_t mem_ptr, uint8_t *dest, int32_t dest_len, char *err_buf, int err_buf_len); /** * Writes to instance memory. The range must fit entirely inside the memory. @@ -791,7 +181,7 @@ int64_t vm_exec_w6_state_mem_load(struct vm_exec_w6_hook_context_t *ctx, int32_t * * C API function, works with raw object pointers. */ -enum vm_exec_w6_status_t vm_exec_w6_state_mem_store(struct vm_exec_w6_hook_context_t *ctx, int32_t mem_ptr, const uint8_t *src, int32_t src_len, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w6_state_mem_store(vm_exec_hook_context_t *ctx, int32_t mem_ptr, const uint8_t *src, int32_t src_len, char *err_buf, int err_buf_len); /** * Returns the size in bytes of the instance memory. @@ -800,7 +190,7 @@ enum vm_exec_w6_status_t vm_exec_w6_state_mem_store(struct vm_exec_w6_hook_conte * * C API function, works with raw object pointers. */ -enum vm_exec_w6_status_t vm_exec_w6_state_mem_length(struct vm_exec_w6_hook_context_t *ctx, uint64_t *length_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w6_state_mem_length(vm_exec_hook_context_t *ctx, uint64_t *length_out, char *err_buf, int err_buf_len); /** * Grows the instance memory by `by_num_pages` pages of 64Kb each. @@ -811,7 +201,7 @@ enum vm_exec_w6_status_t vm_exec_w6_state_mem_length(struct vm_exec_w6_hook_cont * * C API function, works with raw object pointers. */ -enum vm_exec_w6_status_t vm_exec_w6_state_mem_grow(struct vm_exec_w6_hook_context_t *ctx, uint32_t by_num_pages, uint32_t *prev_pages_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w6_state_mem_grow(vm_exec_hook_context_t *ctx, uint32_t by_num_pages, uint32_t *prev_pages_out, char *err_buf, int err_buf_len); /** * Returns the number of points (gas) used by the instance so far. @@ -820,7 +210,7 @@ enum vm_exec_w6_status_t vm_exec_w6_state_mem_grow(struct vm_exec_w6_hook_contex * * C API function, works with raw object pointers. */ -enum vm_exec_w6_status_t vm_exec_w6_state_get_points_used(struct vm_exec_w6_hook_context_t *ctx, uint64_t *points_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w6_state_get_points_used(vm_exec_hook_context_t *ctx, uint64_t *points_out, char *err_buf, int err_buf_len); /** * Sets the number of points (gas) used by the instance. @@ -829,4 +219,4 @@ enum vm_exec_w6_status_t vm_exec_w6_state_get_points_used(struct vm_exec_w6_hook * * C API function, works with raw object pointers. */ -enum vm_exec_w6_status_t vm_exec_w6_state_set_points_used(struct vm_exec_w6_hook_context_t *ctx, uint64_t points, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w6_state_set_points_used(vm_exec_hook_context_t *ctx, uint64_t points, char *err_buf, int err_buf_len); diff --git a/c-api-wasmer6/src/capi_backend.rs b/c-api-wasmer6/src/capi_backend.rs new file mode 100644 index 00000000..6ce07a02 --- /dev/null +++ b/c-api-wasmer6/src/capi_backend.rs @@ -0,0 +1,41 @@ +//! Wiring this crate's backend into the shared hook dispatch. +//! +//! The one place the Wasmer 6 instance state is named. Everything else about hook dispatch — +//! the context, its pool, the table, the generated `VMHooks` implementation — lives in +//! `c-api-common` and knows the state only as an erased pointer. + +use multiversx_chain_vm_executor::VMHooks; +use multiversx_chain_vm_executor_c_api_common::capi_vm_hook_context::CapiVMHooksBuilder; +use multiversx_chain_vm_executor_c_api_common::capi_vm_hook_table::CapiVMHookTable; +use multiversx_chain_vm_executor_wasmer_experimental::{ + ExperimentalInstanceState, ExperimentalVMHooksBuilder, +}; + +/// The instance state type this backend erases into a hook context. +/// +/// `with_instance_state` has to be told the type back, and this is the single definition both +/// sides go through, so the two cannot drift. +pub type BackendInstanceState = ExperimentalInstanceState<'static>; + +/// Implements this backend's builder trait over the shared builder. +/// +/// A newtype rather than an implementation on `CapiVMHooksBuilder` directly, because with the +/// builder in `c-api-common` both the trait and the type are foreign here and the orphan rule +/// forbids it. The wrapper costs nothing and makes the seam explicit. +pub struct BackendVMHooksBuilder(CapiVMHooksBuilder); + +impl BackendVMHooksBuilder { + pub fn new(go_vm_hooks: usize, hook_table: CapiVMHookTable) -> Self { + BackendVMHooksBuilder(CapiVMHooksBuilder::new(go_vm_hooks, hook_table)) + } +} + +impl ExperimentalVMHooksBuilder for BackendVMHooksBuilder { + fn with_vm_hooks( + &self, + instance_state_ref: &mut ExperimentalInstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ) { + self.0.with_vm_hooks_for_state(instance_state_ref, f); + } +} diff --git a/c-api-wasmer6/src/capi_executor.rs b/c-api-wasmer6/src/capi_executor.rs index bbfcc035..d0c222e5 100644 --- a/c-api-wasmer6/src/capi_executor.rs +++ b/c-api-wasmer6/src/capi_executor.rs @@ -19,9 +19,9 @@ use meta::capi_safe_unwind; use multiversx_chain_vm_executor::OpcodeConfig; use std::sync::Arc; -use crate::capi_config::vm_exec_w6_executor_config_t; -use crate::capi_status::{vm_exec_w6_status_t, write_error}; -use crate::capi_vm_hook_pointers::vm_exec_w6_vm_hook_c_func_pointers; +use crate::capi_config::vm_exec_executor_config_t; +use crate::capi_status::{vm_exec_status_t, write_error}; +use crate::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; use crate::capi_vm_hook_table::CapiVMHookTable; /// Opaque handle to an executor. @@ -39,7 +39,7 @@ pub struct CapiExecutor { /// `hook_table` is copied, so the caller is free to release it as soon as this returns. Every /// entry must be populated; see `CapiVMHookTable::copy_from`. /// -/// On success writes the executor to `executor_out` and returns `VM_EXEC_W6_OK`. The caller +/// On success writes the executor to `executor_out` and returns `VM_EXEC_OK`. The caller /// owns the result and must release it with `vm_exec_w6_executor_destroy`. /// /// # Safety @@ -47,16 +47,16 @@ pub struct CapiExecutor { /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] pub unsafe extern "C" fn vm_exec_w6_new_executor( executor_out: *mut *mut vm_exec_w6_executor_t, - config: *const vm_exec_w6_executor_config_t, - hook_table: *const vm_exec_w6_vm_hook_c_func_pointers, + config: *const vm_exec_executor_config_t, + hook_table: *const vm_exec_vm_hook_c_func_pointers, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { +) -> vm_exec_status_t { if executor_out.is_null() || config.is_null() || hook_table.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; + return vm_exec_status_t::VM_EXEC_NULL_POINTER; } let opcode_config = match unsafe { (*config).to_opcode_config() } { @@ -73,7 +73,7 @@ pub unsafe extern "C" fn vm_exec_w6_new_executor( *executor_out = Box::into_raw(Box::new(capi_executor)) as *mut vm_exec_w6_executor_t; } - vm_exec_w6_status_t::VM_EXEC_W6_OK + vm_exec_status_t::VM_EXEC_OK } /// Destroys an executor. Does nothing if `executor` is null. diff --git a/c-api-wasmer6/src/capi_instance.rs b/c-api-wasmer6/src/capi_instance.rs index c1a070c3..a62d34e1 100644 --- a/c-api-wasmer6/src/capi_instance.rs +++ b/c-api-wasmer6/src/capi_instance.rs @@ -8,16 +8,17 @@ use libc::{c_char, c_int}; use meta::capi_safe_unwind; -use multiversx_chain_vm_executor::{BreakpointValue, Instance, InstanceCallResult}; +use multiversx_chain_vm_executor::Instance; use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalInstance; use std::ffi::CStr; use std::slice; -use crate::capi_config::vm_exec_w6_compilation_options_t; +use crate::capi_backend::BackendVMHooksBuilder; +use crate::capi_call_result::vm_exec_call_result_t; +use crate::capi_config::vm_exec_compilation_options_t; use crate::capi_executor::{CapiExecutor, vm_exec_w6_executor_t}; -use crate::capi_status::{vm_exec_w6_status_t, write_error}; +use crate::capi_status::{vm_exec_status_t, write_error}; use crate::capi_strings::{copy_pipe_joined, pipe_joined_length}; -use crate::capi_vm_hook_context::CapiVMHooksBuilder; /// Opaque handle to an instance. #[allow(non_camel_case_types)] @@ -28,58 +29,6 @@ pub struct CapiInstance { pub(crate) content: ExperimentalInstance, } -/// How a call ended. -/// -/// This replaces the legacy arrangement, where a call returned a bare ok/error and the VM -/// then polled a wasm global to find out what had actually happened. -#[allow(non_camel_case_types)] -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum vm_exec_w6_call_result_kind_t { - /// The function returned normally. - VM_EXEC_W6_CALL_OK = 0, - - /// No exported function by that name. - VM_EXEC_W6_CALL_FUNCTION_NOT_FOUND = 1, - - /// The instance trapped for a reason that is neither a VM hook's doing nor a - /// middleware breakpoint. The message is in the error buffer. - VM_EXEC_W6_CALL_RUNTIME_ERROR = 2, - - /// A VM hook asked for execution to stop. `early_exit_code` says which; the message, if - /// any, is in the error buffer, and is informational — the VM has already recorded its - /// own. - VM_EXEC_W6_CALL_EARLY_EXIT = 3, - - /// A middleware stopped execution. `breakpoint` says which. - VM_EXEC_W6_CALL_BREAKPOINT = 4, -} - -/// The out-struct of `vm_exec_w6_instance_call`. -#[allow(non_camel_case_types)] -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct vm_exec_w6_call_result_t { - pub kind: vm_exec_w6_call_result_kind_t, - - /// Meaningful only for `VM_EXEC_W6_CALL_EARLY_EXIT`. - pub early_exit_code: u64, - - /// Meaningful only for `VM_EXEC_W6_CALL_BREAKPOINT`. Values match `BreakpointValue`: - /// 4 = out of gas, 5 = memory limit. - pub breakpoint: u64, -} - -impl vm_exec_w6_call_result_t { - fn of_kind(kind: vm_exec_w6_call_result_kind_t) -> Self { - vm_exec_w6_call_result_t { - kind, - early_exit_code: 0, - breakpoint: 0, - } - } -} - /// Creates an instance. /// /// `go_vm_hooks` is the caller's VM hooks handle for *this* instance; it is handed back to @@ -93,19 +42,19 @@ impl vm_exec_w6_call_result_t { #[allow(clippy::cast_ptr_alignment)] #[allow(clippy::too_many_arguments)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] pub unsafe extern "C" fn vm_exec_w6_new_instance( executor: *mut vm_exec_w6_executor_t, instance_out: *mut *mut vm_exec_w6_instance_t, wasm_bytes: *const u8, wasm_bytes_len: u32, - options: *const vm_exec_w6_compilation_options_t, + options: *const vm_exec_compilation_options_t, go_vm_hooks: usize, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { +) -> vm_exec_status_t { if executor.is_null() || instance_out.is_null() || wasm_bytes.is_null() || options.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; + return vm_exec_status_t::VM_EXEC_NULL_POINTER; } let capi_executor = unsafe { &*(executor as *const CapiExecutor) }; @@ -116,7 +65,7 @@ pub unsafe extern "C" fn vm_exec_w6_new_instance( return write_error(err, err_buf, err_buf_len); } - let builder = CapiVMHooksBuilder::new(go_vm_hooks, capi_executor.hook_table.clone()); + let builder = BackendVMHooksBuilder::new(go_vm_hooks, capi_executor.hook_table.clone()); let instance = match ExperimentalInstance::try_new_instance( Box::new(builder), @@ -133,7 +82,7 @@ pub unsafe extern "C" fn vm_exec_w6_new_instance( as *mut vm_exec_w6_instance_t; } - vm_exec_w6_status_t::VM_EXEC_W6_OK + vm_exec_status_t::VM_EXEC_OK } /// Destroys an instance. Does nothing if `instance` is null. @@ -151,7 +100,7 @@ pub unsafe extern "C" fn vm_exec_w6_instance_destroy(instance: *mut vm_exec_w6_i /// Calls an exported function, with a gas limit for this call. /// -/// Always writes `result_out` when it returns `VM_EXEC_W6_OK`. A non-`OK` status means the +/// Always writes `result_out` when it returns `VM_EXEC_OK`. A non-`OK` status means the /// call could not be attempted at all, which is distinct from a call that was attempted and /// failed — the latter is reported through `result_out.kind`. /// @@ -160,17 +109,17 @@ pub unsafe extern "C" fn vm_exec_w6_instance_destroy(instance: *mut vm_exec_w6_i /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] pub unsafe extern "C" fn vm_exec_w6_instance_call( instance: *mut vm_exec_w6_instance_t, func_name: *const c_char, points_limit: u64, - result_out: *mut vm_exec_w6_call_result_t, + result_out: *mut vm_exec_call_result_t, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { +) -> vm_exec_status_t { if instance.is_null() || func_name.is_null() || result_out.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; + return vm_exec_status_t::VM_EXEC_NULL_POINTER; } let capi_instance = unsafe { &mut *(instance as *mut CapiInstance) }; @@ -179,44 +128,19 @@ pub unsafe extern "C" fn vm_exec_w6_instance_call( return write_error("function name is not valid UTF-8", err_buf, err_buf_len); }; - let result = match capi_instance.content.call(func_name, points_limit) { - InstanceCallResult::Ok => { - vm_exec_w6_call_result_t::of_kind(vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_OK) - } - InstanceCallResult::FunctionNotFound => vm_exec_w6_call_result_t::of_kind( - vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_FUNCTION_NOT_FOUND, - ), - InstanceCallResult::RuntimeError(err) => { - write_error(err, err_buf, err_buf_len); - vm_exec_w6_call_result_t::of_kind( - vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_RUNTIME_ERROR, - ) - } - InstanceCallResult::VMHooksEarlyExit(early_exit) => { - // Informational: the hook has already recorded its own message on the VM side. - write_error(&early_exit.message, err_buf, err_buf_len); - vm_exec_w6_call_result_t { - kind: vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_EARLY_EXIT, - early_exit_code: early_exit.code, - breakpoint: 0, - } - } - InstanceCallResult::Breakpoint(breakpoint) => vm_exec_w6_call_result_t { - kind: vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_BREAKPOINT, - early_exit_code: 0, - breakpoint: breakpoint_as_u64(breakpoint), - }, + let result = unsafe { + vm_exec_call_result_t::from_call_result( + capi_instance.content.call(func_name, points_limit), + err_buf, + err_buf_len, + ) }; unsafe { *result_out = result; } - vm_exec_w6_status_t::VM_EXEC_W6_OK -} - -fn breakpoint_as_u64(breakpoint: BreakpointValue) -> u64 { - breakpoint.as_u64() + vm_exec_status_t::VM_EXEC_OK } /// Returns the number of points (gas) used so far. @@ -226,15 +150,15 @@ fn breakpoint_as_u64(breakpoint: BreakpointValue) -> u64 { /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] pub unsafe extern "C" fn vm_exec_w6_instance_get_points_used( instance: *mut vm_exec_w6_instance_t, points_out: *mut u64, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { +) -> vm_exec_status_t { if instance.is_null() || points_out.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; + return vm_exec_status_t::VM_EXEC_NULL_POINTER; } let capi_instance = unsafe { &mut *(instance as *mut CapiInstance) }; @@ -243,7 +167,7 @@ pub unsafe extern "C" fn vm_exec_w6_instance_get_points_used( unsafe { *points_out = points; } - vm_exec_w6_status_t::VM_EXEC_W6_OK + vm_exec_status_t::VM_EXEC_OK } Err(err) => write_error(err, err_buf, err_buf_len), } @@ -258,12 +182,12 @@ pub unsafe extern "C" fn vm_exec_w6_instance_get_points_used( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] pub unsafe extern "C" fn vm_exec_w6_instance_check_signatures( instance: *const vm_exec_w6_instance_t, ) -> c_int { if instance.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; @@ -279,18 +203,18 @@ pub unsafe extern "C" fn vm_exec_w6_instance_check_signatures( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] pub unsafe extern "C" fn vm_exec_w6_instance_has_function( instance: *const vm_exec_w6_instance_t, func_name: *const c_char, ) -> c_int { if instance.is_null() || func_name.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; let Ok(func_name) = unsafe { CStr::from_ptr(func_name) }.to_str() else { - return vm_exec_w6_status_t::VM_EXEC_W6_ERROR.as_c_int(); + return vm_exec_status_t::VM_EXEC_ERROR.as_c_int(); }; c_int::from(capi_instance.content.has_function(func_name)) @@ -303,12 +227,12 @@ pub unsafe extern "C" fn vm_exec_w6_instance_has_function( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names_length( instance: *const vm_exec_w6_instance_t, ) -> c_int { if instance.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; @@ -325,14 +249,14 @@ pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names_length( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names( instance: *const vm_exec_w6_instance_t, dest: *mut c_char, dest_len: c_int, ) -> c_int { if instance.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; @@ -352,12 +276,12 @@ pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names_length( instance: *const vm_exec_w6_instance_t, ) -> c_int { if instance.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; @@ -374,14 +298,14 @@ pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names_length( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_c_int())] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names( instance: *const vm_exec_w6_instance_t, dest: *mut c_char, dest_len: c_int, ) -> c_int { if instance.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int(); + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); } let capi_instance = unsafe { &*(instance as *const CapiInstance) }; diff --git a/c-api-wasmer6/src/capi_instance_state.rs b/c-api-wasmer6/src/capi_instance_state.rs index 83b45be3..37f5409e 100644 --- a/c-api-wasmer6/src/capi_instance_state.rs +++ b/c-api-wasmer6/src/capi_instance_state.rs @@ -2,7 +2,7 @@ //! //! Every function here takes a hook context and is valid only while the hook that was handed //! that context is still running. See `capi_vm_hook_context` for how that is enforced; a stale -//! context yields `VM_EXEC_W6_STALE_CONTEXT` rather than a dereference of a dead borrow. +//! context yields `VM_EXEC_STALE_CONTEXT` rather than a dereference of a dead borrow. //! //! Bounds checking lives on this side of the boundary, not in Go. The executor is the only //! side that knows the real extent of the instance memory, and the rules are observable by @@ -16,17 +16,9 @@ use multiversx_chain_vm_executor::{ }; use std::slice; -use crate::capi_status::{vm_exec_w6_status_t, write_error, write_error_count}; -use crate::capi_vm_hook_context::{ContextError, vm_exec_w6_hook_context_t, with_instance_state}; - -impl ContextError { - fn as_status(self) -> vm_exec_w6_status_t { - match self { - ContextError::NullContext => vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER, - ContextError::Stale => vm_exec_w6_status_t::VM_EXEC_W6_STALE_CONTEXT, - } - } -} +use crate::capi_backend::BackendInstanceState; +use crate::capi_status::{vm_exec_status_t, write_error, write_error_count}; +use crate::capi_vm_hook_context::{vm_exec_hook_context_t, with_instance_state}; /// Loads from instance memory into a caller-provided buffer, **truncating** at the end of /// the memory. @@ -44,9 +36,9 @@ impl ContextError { /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC.as_i64())] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_i64())] pub unsafe extern "C" fn vm_exec_w6_state_mem_load( - ctx: *mut vm_exec_w6_hook_context_t, + ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, dest: *mut u8, dest_len: i32, @@ -54,22 +46,25 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_load( err_buf_len: c_int, ) -> i64 { let loaded = unsafe { - with_instance_state(ctx, |state| -> Result { - let memory_length = state.memory_length()?; - let to_load = - clamped_load_len(mem_ptr as MemPtr, dest_len as MemLength, memory_length)?; - if to_load == 0 { - return Ok(0); - } - - if dest.is_null() { - return Err(Box::new(ServiceError::new("destination buffer is null"))); - } - - let dest = slice::from_raw_parts_mut(dest, to_load); - state.memory_load_to_slice(mem_ptr as MemPtr, dest)?; - Ok(to_load) - }) + with_instance_state::( + ctx, + |state| -> Result { + let memory_length = state.memory_length()?; + let to_load = + clamped_load_len(mem_ptr as MemPtr, dest_len as MemLength, memory_length)?; + if to_load == 0 { + return Ok(0); + } + + if dest.is_null() { + return Err(Box::new(ServiceError::new("destination buffer is null"))); + } + + let dest = slice::from_raw_parts_mut(dest, to_load); + state.memory_load_to_slice(mem_ptr as MemPtr, dest)?; + Ok(to_load) + }, + ) }; match loaded { @@ -85,24 +80,24 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_load( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] pub unsafe extern "C" fn vm_exec_w6_state_mem_store( - ctx: *mut vm_exec_w6_hook_context_t, + ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, src: *const u8, src_len: i32, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { +) -> vm_exec_status_t { if src_len < 0 { return write_error("negative length", err_buf, err_buf_len); } if src.is_null() && src_len > 0 { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; + return vm_exec_status_t::VM_EXEC_NULL_POINTER; } let stored = unsafe { - with_instance_state(ctx, |state| { + with_instance_state::(ctx, |state| { let data = if src_len == 0 { &[][..] } else { @@ -115,7 +110,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_store( match stored { Err(context_err) => context_err.as_status(), Ok(Err(err)) => write_error(err, err_buf, err_buf_len), - Ok(Ok(())) => vm_exec_w6_status_t::VM_EXEC_W6_OK, + Ok(Ok(())) => vm_exec_status_t::VM_EXEC_OK, } } @@ -125,18 +120,20 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_store( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] pub unsafe extern "C" fn vm_exec_w6_state_mem_length( - ctx: *mut vm_exec_w6_hook_context_t, + ctx: *mut vm_exec_hook_context_t, length_out: *mut u64, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { +) -> vm_exec_status_t { if length_out.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; + return vm_exec_status_t::VM_EXEC_NULL_POINTER; } - let length = unsafe { with_instance_state(ctx, |state| state.memory_length()) }; + let length = unsafe { + with_instance_state::(ctx, |state| state.memory_length()) + }; match length { Err(context_err) => context_err.as_status(), @@ -145,7 +142,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_length( unsafe { *length_out = length; } - vm_exec_w6_status_t::VM_EXEC_W6_OK + vm_exec_status_t::VM_EXEC_OK } } } @@ -158,19 +155,21 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_length( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] pub unsafe extern "C" fn vm_exec_w6_state_mem_grow( - ctx: *mut vm_exec_w6_hook_context_t, + ctx: *mut vm_exec_hook_context_t, by_num_pages: u32, prev_pages_out: *mut u32, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { +) -> vm_exec_status_t { if prev_pages_out.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; + return vm_exec_status_t::VM_EXEC_NULL_POINTER; } - let grown = unsafe { with_instance_state(ctx, |state| state.memory_grow(by_num_pages)) }; + let grown = unsafe { + with_instance_state::(ctx, |state| state.memory_grow(by_num_pages)) + }; match grown { Err(context_err) => context_err.as_status(), @@ -179,7 +178,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_grow( unsafe { *prev_pages_out = prev_pages; } - vm_exec_w6_status_t::VM_EXEC_W6_OK + vm_exec_status_t::VM_EXEC_OK } } } @@ -190,18 +189,20 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_grow( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] pub unsafe extern "C" fn vm_exec_w6_state_get_points_used( - ctx: *mut vm_exec_w6_hook_context_t, + ctx: *mut vm_exec_hook_context_t, points_out: *mut u64, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { +) -> vm_exec_status_t { if points_out.is_null() { - return vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER; + return vm_exec_status_t::VM_EXEC_NULL_POINTER; } - let points = unsafe { with_instance_state(ctx, |state| state.get_points_used()) }; + let points = unsafe { + with_instance_state::(ctx, |state| state.get_points_used()) + }; match points { Err(context_err) => context_err.as_status(), @@ -210,7 +211,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_get_points_used( unsafe { *points_out = points; } - vm_exec_w6_status_t::VM_EXEC_W6_OK + vm_exec_status_t::VM_EXEC_OK } } } @@ -221,18 +222,20 @@ pub unsafe extern "C" fn vm_exec_w6_state_get_points_used( /// /// C API function, works with raw object pointers. #[unsafe(no_mangle)] -#[capi_safe_unwind(vm_exec_w6_status_t::VM_EXEC_W6_PANIC)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] pub unsafe extern "C" fn vm_exec_w6_state_set_points_used( - ctx: *mut vm_exec_w6_hook_context_t, + ctx: *mut vm_exec_hook_context_t, points: u64, err_buf: *mut c_char, err_buf_len: c_int, -) -> vm_exec_w6_status_t { - let set = unsafe { with_instance_state(ctx, |state| state.set_points_used(points)) }; +) -> vm_exec_status_t { + let set = unsafe { + with_instance_state::(ctx, |state| state.set_points_used(points)) + }; match set { Err(context_err) => context_err.as_status(), Ok(Err(err)) => write_error(err, err_buf, err_buf_len), - Ok(Ok(())) => vm_exec_w6_status_t::VM_EXEC_W6_OK, + Ok(Ok(())) => vm_exec_status_t::VM_EXEC_OK, } } diff --git a/c-api-wasmer6/src/capi_logger.rs b/c-api-wasmer6/src/capi_logger.rs index 33365a8b..6bfc90ff 100644 --- a/c-api-wasmer6/src/capi_logger.rs +++ b/c-api-wasmer6/src/capi_logger.rs @@ -1,20 +1,6 @@ -//! Panic suppression at the C boundary. +//! Satisfies the path `meta::capi_safe_unwind` expands to. //! -//! The module is named `capi_logger` because that is the path the shared -//! `meta::capi_safe_unwind` attribute expands to. It deliberately does not carry the legacy -//! crate's logging FFI: log configuration belongs to the process, and the legacy `c-api` -//! already owns those symbols. - -use std::sync::Once; - -static PANIC_HANDLER: Once = Once::new(); - -/// Installs a no-op panic hook, so a caught panic does not also print a backtrace. -/// -/// A panic must never unwind across `extern "C"`; the `capi_safe_unwind` attribute catches -/// it and turns it into `VM_EXEC_W6_PANIC`. This only silences the default hook's output. -pub fn set_panic_handler() { - PANIC_HANDLER.call_once(|| { - std::panic::set_hook(Box::new(|_| {})); - }); -} +//! The attribute emits `crate::capi_logger::set_panic_handler()`, so every crate that uses it +//! needs a module of that name at its own root. The implementation is shared; only the path +//! has to be local. +pub use multiversx_chain_vm_executor_c_api_common::set_panic_handler; diff --git a/c-api-wasmer6/src/capi_vm_hook_pointers.rs b/c-api-wasmer6/src/capi_vm_hook_pointers.rs deleted file mode 100644 index 3c12fef9..00000000 --- a/c-api-wasmer6/src/capi_vm_hook_pointers.rs +++ /dev/null @@ -1,300 +0,0 @@ -// Code generated by vmhooks generator. DO NOT EDIT. - -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -use crate::capi_vm_hook_context::{vm_exec_w6_hook_context_t, vm_exec_w6_hook_result_t}; - -/// The Go VM's hook implementations, as a table of C function pointers. -/// -/// Copied into Rust at executor creation; see CapiVMHookTable. -#[repr(C)] -#[derive(Clone)] -#[allow(non_camel_case_types)] -#[rustfmt::skip] -pub struct vm_exec_w6_vm_hook_c_func_pointers { - pub get_gas_left_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_shard_of_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32) -> vm_exec_w6_hook_result_t, - pub is_smart_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32) -> vm_exec_w6_hook_result_t, - pub signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, message_offset: i32, message_length: i32) -> vm_exec_w6_hook_result_t, - pub get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_block_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, nonce: i64, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_esdt_nft_name_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec_w6_hook_result_t, - pub get_esdt_nft_attribute_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec_w6_hook_result_t, - pub get_esdt_nft_uri_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64) -> vm_exec_w6_hook_result_t, - pub get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, value_handle: i32, properties_offset: i32, hash_offset: i32, name_offset: i32, attributes_offset: i32, creator_offset: i32, royalties_handle: i32, uris_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_esdt_local_roles_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, token_id_handle: i32) -> vm_exec_w6_hook_result_t, - pub validate_token_identifier_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, token_id_handle: i32) -> vm_exec_w6_hook_result_t, - pub transfer_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, - pub transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub transfer_esdt_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, token_id_offset: i32, token_id_len: i32, value_offset: i32, nonce: i64, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, num_token_transfers: i32, token_transfers_args_length_offset: i32, token_transfer_data_offset: i32, gas_limit: i64, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, data_length: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64) -> vm_exec_w6_hook_result_t, - pub set_async_context_callback_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, callback: i32, callback_length: i32, data: i32, data_length: i32, gas: i64) -> vm_exec_w6_hook_result_t, - pub upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, gas_limit: i64, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_offset: i32, value_offset: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, - pub get_argument_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32) -> vm_exec_w6_hook_result_t, - pub get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32, arg_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_function_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, function_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_num_arguments_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, - pub storage_load_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, - pub storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub set_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, lock_timestamp: i64) -> vm_exec_w6_hook_result_t, - pub get_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, - pub is_storage_locked_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, - pub clear_storage_lock_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, - pub get_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub check_no_payment_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_esdt_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_esdt_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32, index: i32) -> vm_exec_w6_hook_result_t, - pub get_esdt_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_esdt_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_offset: i32, index: i32) -> vm_exec_w6_hook_result_t, - pub get_esdt_token_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_esdt_token_nonce_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, index: i32) -> vm_exec_w6_hook_result_t, - pub get_current_esdt_nft_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32) -> vm_exec_w6_hook_result_t, - pub get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_esdt_token_type_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, index: i32) -> vm_exec_w6_hook_result_t, - pub get_num_esdt_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_call_value_token_name_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, call_value_offset: i32, token_name_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_call_value_token_name_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, call_value_offset: i32, token_name_offset: i32, index: i32) -> vm_exec_w6_hook_result_t, - pub is_reserved_function_name_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, name_handle: i32) -> vm_exec_w6_hook_result_t, - pub write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_pointer: i32, data_length: i32, topic_ptr: i32, num_topics: i32) -> vm_exec_w6_hook_result_t, - pub write_event_log_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, num_topics: i32, topic_lengths_offset: i32, topic_offset: i32, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, - pub get_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, pointer: i32) -> vm_exec_w6_hook_result_t, - pub get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, pointer: i32) -> vm_exec_w6_hook_result_t, - pub get_prev_block_timestamp_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_prev_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_prev_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_prev_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_prev_block_epoch_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, pointer: i32) -> vm_exec_w6_hook_result_t, - pub get_block_round_time_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub epoch_start_block_timestamp_ms_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub epoch_start_block_nonce_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub epoch_start_block_round_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub finish_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, pointer: i32, length: i32) -> vm_exec_w6_hook_result_t, - pub execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, address_offset: i32, value_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, address_offset: i32, function_offset: i32, function_length: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, value_offset: i32, code_offset: i32, code_metadata_offset: i32, length: i32, result_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas_limit: i64, value_offset: i32, source_contract_address_offset: i32, code_metadata_offset: i32, result_address_offset: i32, num_arguments: i32, arguments_length_offset: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_num_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub get_return_data_size_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_id: i32) -> vm_exec_w6_hook_result_t, - pub get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_id: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub clean_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub delete_from_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_id: i32) -> vm_exec_w6_hook_result_t, - pub get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_current_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub get_prev_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_sc_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_owner_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_caller_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_original_caller_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_relayer_addr_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_signal_error_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, err_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_write_log_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, topics_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_original_tx_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_state_root_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_prev_block_random_seed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_return_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, result_id: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_multi_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, multi_call_value_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_all_transfers_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, transfer_call_values_list_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_back_transfers_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, esdt_transfers_value_handle: i32, egld_value_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_esdt_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_esdt_token_data_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, value_handle: i32, properties_handle: i32, hash_handle: i32, name_handle: i32, attributes_handle: i32, creator_handle: i32, royalties_handle: i32, uris_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_esdt_token_type_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64, type_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_create_async_call_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, success_offset: i32, success_length: i32, error_offset: i32, error_length: i32, gas: i64, extra_gas_for_callback: i64, callback_closure_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_callback_closure_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, callback_closure_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_upgrade_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_upgrade_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_delete_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_handle: i32, gas_limit: i64, arguments_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_deploy_from_source_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, value_handle: i32, address_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_create_contract_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, value_handle: i32, code_handle: i32, code_metadata_handle: i32, arguments_handle: i32, result_address_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_execute_read_only_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, address_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_execute_on_same_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_execute_on_dest_context_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_execute_on_dest_context_with_error_return_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, gas: i64, address_handle: i32, value_handle: i32, function_handle: i32, arguments_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_multi_transfer_esdt_nft_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_multi_transfer_esdt_nft_execute_with_return_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_multi_transfer_esdt_nft_execute_by_user_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, user_handle: i32, dst_handle: i32, token_transfers_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_transfer_value_execute_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dst_handle: i32, value_handle: i32, gas_limit: i64, function_handle: i32, arguments_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_is_esdt_frozen_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, token_id_handle: i32, nonce: i64) -> vm_exec_w6_hook_result_t, - pub managed_is_esdt_limited_transfer_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, token_id_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_is_esdt_paused_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, token_id_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_buffer_to_hex_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, source_handle: i32, dest_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_code_metadata_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, response_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_get_code_hash_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, code_hash_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_is_builtin_function_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, function_name_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_new_from_parts_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, integral_part: i32, fractional_part: i32, exponent: i32) -> vm_exec_w6_hook_result_t, - pub big_float_new_from_frac_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, numerator: i64, denominator: i64) -> vm_exec_w6_hook_result_t, - pub big_float_new_from_sci_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, significand: i64, exponent: i64) -> vm_exec_w6_hook_result_t, - pub big_float_add_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_div_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_clone_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32, exponent: i32) -> vm_exec_w6_hook_result_t, - pub big_float_floor_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_ceil_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_truncate_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, dest_big_int_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, value: i64) -> vm_exec_w6_hook_result_t, - pub big_float_is_int_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_set_big_int_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_get_const_pi_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_float_get_const_e_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, source_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_esdt_call_value_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_esdt_call_value_by_index_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, index: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, result: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_esdt_external_balance_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_offset: i32, token_id_offset: i32, token_id_len: i32, nonce: i64, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_new_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, small_value: i64) -> vm_exec_w6_hook_result_t, - pub big_int_unsigned_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_signed_byte_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32, byte_offset: i32) -> vm_exec_w6_hook_result_t, - pub big_int_set_unsigned_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec_w6_hook_result_t, - pub big_int_set_signed_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, byte_offset: i32, byte_length: i32) -> vm_exec_w6_hook_result_t, - pub big_int_is_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_get_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_set_int64_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, value: i64) -> vm_exec_w6_hook_result_t, - pub big_int_add_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_sub_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_mul_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_tdiv_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_tmod_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_ediv_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_emod_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_sqrt_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_pow_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_log2_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op1_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_abs_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_neg_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_sign_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_cmp_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_not_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_and_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_or_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_xor_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op1_handle: i32, op2_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_shr_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec_w6_hook_result_t, - pub big_int_shl_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, op_handle: i32, bits: i32) -> vm_exec_w6_hook_result_t, - pub big_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, reference_handle: i32) -> vm_exec_w6_hook_result_t, - pub big_int_to_string_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, big_int_handle: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_new_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub mbuffer_new_from_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_get_length_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_get_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_get_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_copy_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, source_handle: i32, starting_position: i32, slice_length: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_eq_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle1: i32, m_buffer_handle2: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_set_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_set_byte_slice_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, starting_position: i32, data_length: i32, data_offset: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_append_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, accumulator_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_append_bytes_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, accumulator_handle: i32, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_to_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_to_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_from_big_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_from_big_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_int_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_to_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_to_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_from_small_int_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec_w6_hook_result_t, - pub mbuffer_from_small_int_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, value: i64) -> vm_exec_w6_hook_result_t, - pub mbuffer_to_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_from_big_float_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_buffer_handle: i32, big_float_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, source_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_storage_load_from_address_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, address_handle: i32, key_handle: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32, destination_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_finish_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, source_handle: i32) -> vm_exec_w6_hook_result_t, - pub mbuffer_set_random_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, destination_handle: i32, length: i32) -> vm_exec_w6_hook_result_t, - pub managed_map_new_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t, - pub managed_map_put_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_map_handle: i32, key_handle: i32, value_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_map_get_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_map_remove_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_map_handle: i32, key_handle: i32, out_value_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_map_contains_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, m_map_handle: i32, key_handle: i32) -> vm_exec_w6_hook_result_t, - pub small_int_get_unsigned_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32) -> vm_exec_w6_hook_result_t, - pub small_int_get_signed_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32) -> vm_exec_w6_hook_result_t, - pub small_int_finish_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, value: i64) -> vm_exec_w6_hook_result_t, - pub small_int_finish_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, value: i64) -> vm_exec_w6_hook_result_t, - pub small_int_storage_store_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec_w6_hook_result_t, - pub small_int_storage_store_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec_w6_hook_result_t, - pub small_int_storage_load_unsigned_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, - pub small_int_storage_load_signed_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, - pub int64get_argument_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, id: i32) -> vm_exec_w6_hook_result_t, - pub int64finish_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, value: i64) -> vm_exec_w6_hook_result_t, - pub int64storage_store_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, value: i64) -> vm_exec_w6_hook_result_t, - pub int64storage_load_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32) -> vm_exec_w6_hook_result_t, - pub sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_sha256_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec_w6_hook_result_t, - pub keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_keccak256_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec_w6_hook_result_t, - pub ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, length: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_ripemd160_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, input_handle: i32, output_handle: i32) -> vm_exec_w6_hook_result_t, - pub verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_verify_bls_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, - pub verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_verify_ed25519_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, - pub verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32, hash_type: i32) -> vm_exec_w6_hook_result_t, - pub managed_verify_custom_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32, hash_type: i32) -> vm_exec_w6_hook_result_t, - pub verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_offset: i32, key_length: i32, message_offset: i32, message_length: i32, sig_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_verify_secp256k1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, - pub encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, r_offset: i32, r_length: i32, s_offset: i32, s_length: i32, sig_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_encode_secp256k1_der_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, r_handle: i32, s_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, - pub add_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, fst_point_xhandle: i32, fst_point_yhandle: i32, snd_point_xhandle: i32, snd_point_yhandle: i32) -> vm_exec_w6_hook_result_t, - pub double_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec_w6_hook_result_t, - pub is_on_curve_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, ec_handle: i32, point_xhandle: i32, point_yhandle: i32) -> vm_exec_w6_hook_result_t, - pub scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, - pub managed_scalar_base_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, - pub scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, - pub managed_scalar_mult_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, point_xhandle: i32, point_yhandle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, - pub marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_marshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_marshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pair_handle: i32, y_pair_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, - pub managed_unmarshal_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, - pub unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_offset: i32, length: i32) -> vm_exec_w6_hook_result_t, - pub managed_unmarshal_compressed_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_result_handle: i32, y_result_handle: i32, ec_handle: i32, data_handle: i32) -> vm_exec_w6_hook_result_t, - pub generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_offset: i32) -> vm_exec_w6_hook_result_t, - pub managed_generate_key_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, x_pub_key_handle: i32, y_pub_key_handle: i32, ec_handle: i32, result_handle: i32) -> vm_exec_w6_hook_result_t, - pub create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_offset: i32, data_length: i32) -> vm_exec_w6_hook_result_t, - pub managed_create_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, data_handle: i32) -> vm_exec_w6_hook_result_t, - pub get_curve_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, ec_handle: i32) -> vm_exec_w6_hook_result_t, - pub get_priv_key_byte_length_ec_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, ec_handle: i32) -> vm_exec_w6_hook_result_t, - pub elliptic_curve_get_values_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, ec_handle: i32, field_order_handle: i32, base_point_order_handle: i32, eq_constant_handle: i32, x_base_point_handle: i32, y_base_point_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_verify_secp256r1_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_verify_blssignature_share_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, - pub managed_verify_blsaggregated_signature_func_ptr: extern "C" fn(ctx: *mut vm_exec_w6_hook_context_t, key_handle: i32, message_handle: i32, sig_handle: i32) -> vm_exec_w6_hook_result_t, -} - -impl std::fmt::Debug for vm_exec_w6_vm_hook_c_func_pointers { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "vm_exec_w6_vm_hook_c_func_pointers") - } -} diff --git a/c-api-wasmer6/src/lib.rs b/c-api-wasmer6/src/lib.rs index 4bdeebd2..9af3bd12 100644 --- a/c-api-wasmer6/src/lib.rs +++ b/c-api-wasmer6/src/lib.rs @@ -11,7 +11,7 @@ //! - **Configuration is passed once**, at the point the thing it configures is created. //! There are no post-construction setters. //! - **A call reports what happened in its result**, instead of leaving the caller to poll a -//! wasm global afterwards. See `vm_exec_w6_call_result_t`. +//! wasm global afterwards. See `vm_exec_call_result_t`. //! //! The hook dispatch — the typed function-pointer table (`capi_vm_hook_pointers`) and the //! `VMHooks` implementation that calls into it (`capi_vm_hooks`) — is generated from the Go @@ -21,14 +21,14 @@ mod capi_logger; -pub mod capi_config; +pub mod capi_backend; pub mod capi_executor; pub mod capi_instance; pub mod capi_instance_state; -pub mod capi_opcode_cost; -pub mod capi_status; -pub(crate) mod capi_strings; -pub mod capi_vm_hook_context; -pub mod capi_vm_hook_pointers; -pub mod capi_vm_hook_table; -pub mod capi_vm_hooks; + +// The shared C vocabulary, re-exported so that this crate's modules — and the cbindgen run +// that produces its header — reach the same definitions the common header publishes. +pub use multiversx_chain_vm_executor_c_api_common::{ + capi_call_result, capi_config, capi_opcode_cost, capi_status, capi_strings, + capi_vm_hook_context, capi_vm_hook_pointers, capi_vm_hook_table, capi_vm_hooks, +}; diff --git a/c-api-wasmer6/tests/ffi_boundary.rs b/c-api-wasmer6/tests/ffi_boundary.rs index 3e1a1ac1..6f83b9f9 100644 --- a/c-api-wasmer6/tests/ffi_boundary.rs +++ b/c-api-wasmer6/tests/ffi_boundary.rs @@ -15,8 +15,11 @@ use std::ffi::{CString, c_char, c_int, c_void}; use std::sync::{Arc, Mutex, OnceLock}; use multiversx_chain_vm_executor::{VMHooks, VMHooksDefault}; +use multiversx_chain_vm_executor_c_api_wasmer6::capi_call_result::{ + vm_exec_call_result_kind_t, vm_exec_call_result_t, +}; use multiversx_chain_vm_executor_c_api_wasmer6::capi_config::{ - vm_exec_w6_compilation_options_t, vm_exec_w6_executor_config_t, + vm_exec_compilation_options_t, vm_exec_executor_config_t, }; use multiversx_chain_vm_executor_c_api_wasmer6::capi_executor::{ vm_exec_w6_executor_destroy, vm_exec_w6_executor_t, vm_exec_w6_new_executor, @@ -24,11 +27,11 @@ use multiversx_chain_vm_executor_c_api_wasmer6::capi_executor::{ use multiversx_chain_vm_executor_c_api_wasmer6::capi_instance::*; use multiversx_chain_vm_executor_c_api_wasmer6::capi_instance_state::*; use multiversx_chain_vm_executor_c_api_wasmer6::capi_opcode_cost::vm_exec_opcode_cost_t; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_status::vm_exec_w6_status_t; +use multiversx_chain_vm_executor_c_api_wasmer6::capi_status::vm_exec_status_t; use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_context::{ - CapiVMHooksBuilder, vm_exec_w6_hook_context_t, vm_exec_w6_hook_result_t, + CapiVMHooksBuilder, vm_exec_hook_context_t, vm_exec_hook_result_t, }; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_pointers::vm_exec_w6_vm_hook_c_func_pointers; +use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_table::CapiVMHookTable; use multiversx_chain_vm_executor_wasmer_experimental::{ ExperimentalInstance, ExperimentalInstanceState, ExperimentalVMHooksBuilder, @@ -83,8 +86,8 @@ fn opcode_cost() -> vm_exec_opcode_cost_t { unsafe { std::mem::zeroed() } } -fn compilation_options() -> vm_exec_w6_compilation_options_t { - vm_exec_w6_compilation_options_t { +fn compilation_options() -> vm_exec_compilation_options_t { + vm_exec_compilation_options_t { unmetered_locals: 0, max_memory_grow: 10, max_memory_grow_delta: 10, @@ -100,8 +103,8 @@ fn compilation_options() -> vm_exec_w6_compilation_options_t { // `extern "C" fn` is a non-nullable type. Calling one of the stubs would be a bug in the test // contract, and it reports a code the assertions would catch. -extern "C" fn stub_hook(_ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t { - vm_exec_w6_hook_result_t::early_exit(UNEXPECTED_HOOK_CODE) +extern "C" fn stub_hook(_ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t { + vm_exec_hook_result_t::early_exit(UNEXPECTED_HOOK_CODE) } /// What the mock hooks record, for assertions after the call has returned. @@ -126,7 +129,7 @@ fn hook_log() -> &'static Mutex { /// Loads from the contract's memory through the hook context it was handed. fn load_through_context( - ctx: *mut vm_exec_w6_hook_context_t, + ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, len: i32, ) -> Result, i64> { @@ -141,7 +144,7 @@ fn load_through_context( Ok(buf) } -extern "C" fn mock_get_gas_left(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_w6_hook_result_t { +extern "C" fn mock_get_gas_left(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t { let mut log = hook_log().lock().unwrap(); log.get_gas_left_calls += 1; @@ -156,19 +159,19 @@ extern "C" fn mock_get_gas_left(ctx: *mut vm_exec_w6_hook_context_t) -> vm_exec_ let mut length = 0u64; let status = unsafe { vm_exec_w6_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; - if status != vm_exec_w6_status_t::VM_EXEC_W6_OK || length != 65536 { + if status != vm_exec_status_t::VM_EXEC_OK || length != 65536 { log.failures .push(format!("mem_length in getGasLeft: {status:?}, {length}")); } - vm_exec_w6_hook_result_t::ok(4242) + vm_exec_hook_result_t::ok(4242) } extern "C" fn mock_signal_error( - ctx: *mut vm_exec_w6_hook_context_t, + ctx: *mut vm_exec_hook_context_t, message_offset: i32, message_length: i32, -) -> vm_exec_w6_hook_result_t { +) -> vm_exec_hook_result_t { let mut log = hook_log().lock().unwrap(); log.signal_error_calls += 1; @@ -181,23 +184,22 @@ extern "C" fn mock_signal_error( // A hook that wants execution to stop says so in its result. Nothing is written to a // breakpoint global on the way out. - vm_exec_w6_hook_result_t::early_exit(USER_ERROR_CODE) + vm_exec_hook_result_t::early_exit(USER_ERROR_CODE) } /// Builds a table whose every entry is a valid function pointer. -fn mock_hook_table() -> &'static vm_exec_w6_vm_hook_c_func_pointers { +fn mock_hook_table() -> &'static vm_exec_vm_hook_c_func_pointers { const WORD: usize = std::mem::size_of::(); - let words = std::mem::size_of::() / WORD; + let words = std::mem::size_of::() / WORD; assert_eq!( - std::mem::size_of::() % WORD, + std::mem::size_of::() % WORD, 0, "the hook table should be a flat block of function pointers" ); let filled = vec![stub_hook as *const () as usize; words].into_boxed_slice(); - let table = unsafe { - &mut *(Box::leak(filled).as_mut_ptr() as *mut vm_exec_w6_vm_hook_c_func_pointers) - }; + let table = + unsafe { &mut *(Box::leak(filled).as_mut_ptr() as *mut vm_exec_vm_hook_c_func_pointers) }; table.get_gas_left_func_ptr = mock_get_gas_left; table.signal_error_func_ptr = mock_signal_error; @@ -208,7 +210,7 @@ fn mock_hook_table() -> &'static vm_exec_w6_vm_hook_c_func_pointers { fn new_executor(err: &mut ErrBuf) -> *mut vm_exec_w6_executor_t { let cost = opcode_cost(); - let config = vm_exec_w6_executor_config_t { + let config = vm_exec_executor_config_t { opcode_version: 1, opcode_cost: &cost, }; @@ -226,7 +228,7 @@ fn new_executor(err: &mut ErrBuf) -> *mut vm_exec_w6_executor_t { assert_eq!( status, - vm_exec_w6_status_t::VM_EXEC_W6_OK, + vm_exec_status_t::VM_EXEC_OK, "new_executor failed: {}", err.message() ); @@ -258,7 +260,7 @@ fn new_instance( assert_eq!( status, - vm_exec_w6_status_t::VM_EXEC_W6_OK, + vm_exec_status_t::VM_EXEC_OK, "new_instance failed: {}", err.message() ); @@ -270,10 +272,10 @@ fn call( instance: *mut vm_exec_w6_instance_t, func_name: &str, err: &mut ErrBuf, -) -> vm_exec_w6_call_result_t { +) -> vm_exec_call_result_t { let name = CString::new(func_name).unwrap(); - let mut result = vm_exec_w6_call_result_t { - kind: vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_RUNTIME_ERROR, + let mut result = vm_exec_call_result_t { + kind: vm_exec_call_result_kind_t::VM_EXEC_CALL_RUNTIME_ERROR, early_exit_code: u64::MAX, breakpoint: u64::MAX, }; @@ -288,7 +290,7 @@ fn call( ERR_BUF_LEN, ) }; - assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_OK); + assert_eq!(status, vm_exec_status_t::VM_EXEC_OK); result } @@ -325,23 +327,20 @@ fn lifecycle() { let instance = new_instance(executor, &mut err); let result = call(instance, "doNothing", &mut err); - assert_eq!( - result.kind, - vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_OK - ); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); // A missing endpoint is reported in the result, not as a failure of the call itself. let result = call(instance, "noSuchEndpoint", &mut err); assert_eq!( result.kind, - vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_FUNCTION_NOT_FOUND + vm_exec_call_result_kind_t::VM_EXEC_CALL_FUNCTION_NOT_FOUND ); let mut points = u64::MAX; let status = unsafe { vm_exec_w6_instance_get_points_used(instance, &mut points, err.ptr(), ERR_BUF_LEN) }; - assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_OK); + assert_eq!(status, vm_exec_status_t::VM_EXEC_OK); assert_ne!(points, u64::MAX, "points_used was not written"); assert_eq!(unsafe { vm_exec_w6_instance_check_signatures(instance) }, 1); @@ -396,7 +395,7 @@ fn hook_dispatch() { let result = call(instance, "callGetGasLeft", &mut err); assert_eq!( result.kind, - vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_OK, + vm_exec_call_result_kind_t::VM_EXEC_CALL_OK, "calling a hook that returns normally should not stop execution" ); @@ -405,7 +404,7 @@ fn hook_dispatch() { let result = call(instance, "callSignalError", &mut err); assert_eq!( result.kind, - vm_exec_w6_call_result_kind_t::VM_EXEC_W6_CALL_EARLY_EXIT + vm_exec_call_result_kind_t::VM_EXEC_CALL_EARLY_EXIT ); assert_eq!(result.early_exit_code, USER_ERROR_CODE); @@ -445,12 +444,12 @@ fn null_arguments_are_refused_without_a_message() { ERR_BUF_LEN, ) }; - assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER); + assert_eq!(status, vm_exec_status_t::VM_EXEC_NULL_POINTER); assert!(executor.is_null()); assert_eq!( unsafe { vm_exec_w6_instance_check_signatures(std::ptr::null()) }, - vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER.as_c_int() + vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int() ); } @@ -458,7 +457,7 @@ fn null_arguments_are_refused_without_a_message() { fn an_invalid_opcode_version_is_reported_through_the_buffer() { let mut err = ErrBuf::new(); let cost = opcode_cost(); - let config = vm_exec_w6_executor_config_t { + let config = vm_exec_executor_config_t { opcode_version: 99, opcode_cost: &cost, }; @@ -474,7 +473,7 @@ fn an_invalid_opcode_version_is_reported_through_the_buffer() { ) }; - assert_eq!(status, vm_exec_w6_status_t::VM_EXEC_W6_ERROR); + assert_eq!(status, vm_exec_status_t::VM_EXEC_ERROR); assert_eq!(err.message(), "invalid opcode version code: 99"); assert!(executor.is_null()); } @@ -487,7 +486,7 @@ fn a_context_is_rejected_when_no_call_is_active() { unsafe { vm_exec_w6_state_mem_length(std::ptr::null_mut(), &mut 0u64, err.ptr(), ERR_BUF_LEN) }, - vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER + vm_exec_status_t::VM_EXEC_NULL_POINTER ); let mut points = 0u64; @@ -500,7 +499,7 @@ fn a_context_is_rejected_when_no_call_is_active() { ERR_BUF_LEN, ) }, - vm_exec_w6_status_t::VM_EXEC_W6_NULL_POINTER + vm_exec_status_t::VM_EXEC_NULL_POINTER ); } @@ -551,7 +550,7 @@ impl ExperimentalVMHooksBuilder for ProbingHooksBuilder { } } -fn mem_load(ctx: *mut vm_exec_w6_hook_context_t, mem_ptr: i32, dest: &mut [u8], len: i32) -> i64 { +fn mem_load(ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, dest: &mut [u8], len: i32) -> i64 { unsafe { vm_exec_w6_state_mem_load( ctx, @@ -564,7 +563,7 @@ fn mem_load(ctx: *mut vm_exec_w6_hook_context_t, mem_ptr: i32, dest: &mut [u8], } } -fn mem_store(ctx: *mut vm_exec_w6_hook_context_t, mem_ptr: i32, src: &[u8]) -> vm_exec_w6_status_t { +fn mem_store(ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, src: &[u8]) -> vm_exec_status_t { unsafe { vm_exec_w6_state_mem_store( ctx, @@ -577,14 +576,14 @@ fn mem_store(ctx: *mut vm_exec_w6_hook_context_t, mem_ptr: i32, src: &[u8]) -> v } } -fn probe_state_ops(ctx: *mut vm_exec_w6_hook_context_t, outcome: &mut Outcome) { +fn probe_state_ops(ctx: *mut vm_exec_hook_context_t, outcome: &mut Outcome) { const PAGE: u64 = 65536; let mut length = 0u64; let status = unsafe { vm_exec_w6_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; outcome.check( "mem_length", - status == vm_exec_w6_status_t::VM_EXEC_W6_OK && length == PAGE, + status == vm_exec_status_t::VM_EXEC_OK && length == PAGE, format!("{status:?}, length {length}"), ); @@ -618,21 +617,21 @@ fn probe_state_ops(ctx: *mut vm_exec_w6_hook_context_t, outcome: &mut Outcome) { let loaded = mem_load(ctx, (PAGE + 1) as i32, &mut buf, 1); outcome.check( "mem_load rejects an offset past the end", - loaded == vm_exec_w6_status_t::VM_EXEC_W6_ERROR.as_i64(), + loaded == vm_exec_status_t::VM_EXEC_ERROR.as_i64(), format!("loaded {loaded}"), ); let loaded = mem_load(ctx, -1, &mut buf, 1); outcome.check( "mem_load rejects a negative offset", - loaded == vm_exec_w6_status_t::VM_EXEC_W6_ERROR.as_i64(), + loaded == vm_exec_status_t::VM_EXEC_ERROR.as_i64(), format!("loaded {loaded}"), ); let loaded = mem_load(ctx, 0, &mut buf, -1); outcome.check( "mem_load rejects a negative length", - loaded == vm_exec_w6_status_t::VM_EXEC_W6_ERROR.as_i64(), + loaded == vm_exec_status_t::VM_EXEC_ERROR.as_i64(), format!("loaded {loaded}"), ); @@ -642,7 +641,7 @@ fn probe_state_ops(ctx: *mut vm_exec_w6_hook_context_t, outcome: &mut Outcome) { let loaded = mem_load(ctx, 16, &mut buf, 3); outcome.check( "mem_store round-trips", - status == vm_exec_w6_status_t::VM_EXEC_W6_OK && loaded == 3 && &buf == b"ABC", + status == vm_exec_status_t::VM_EXEC_OK && loaded == 3 && &buf == b"ABC", format!( "{status:?}, loaded {loaded}, {:?}", std::str::from_utf8(&buf) @@ -653,7 +652,7 @@ fn probe_state_ops(ctx: *mut vm_exec_w6_hook_context_t, outcome: &mut Outcome) { let status = mem_store(ctx, (PAGE - 1) as i32, b"ABC"); outcome.check( "mem_store rejects a range past the end", - status == vm_exec_w6_status_t::VM_EXEC_W6_ERROR, + status == vm_exec_status_t::VM_EXEC_ERROR, format!("{status:?}"), ); @@ -664,8 +663,8 @@ fn probe_state_ops(ctx: *mut vm_exec_w6_hook_context_t, outcome: &mut Outcome) { unsafe { vm_exec_w6_state_get_points_used(ctx, &mut points, std::ptr::null_mut(), 0) }; outcome.check( "points used round-trip", - status == vm_exec_w6_status_t::VM_EXEC_W6_OK - && get_status == vm_exec_w6_status_t::VM_EXEC_W6_OK + status == vm_exec_status_t::VM_EXEC_OK + && get_status == vm_exec_status_t::VM_EXEC_OK && points == 42, format!("{status:?}, {get_status:?}, points {points}"), ); @@ -678,7 +677,7 @@ fn probe_state_ops(ctx: *mut vm_exec_w6_hook_context_t, outcome: &mut Outcome) { unsafe { vm_exec_w6_state_mem_length(ctx, &mut length_after, std::ptr::null_mut(), 0) }; outcome.check( "mem_grow", - status == vm_exec_w6_status_t::VM_EXEC_W6_OK && prev_pages == 1 && length_after == 2 * PAGE, + status == vm_exec_status_t::VM_EXEC_OK && prev_pages == 1 && length_after == 2 * PAGE, format!("{status:?}, prev_pages {prev_pages}, length {length_after}"), ); } @@ -687,7 +686,7 @@ fn probe_state_ops(ctx: *mut vm_exec_w6_hook_context_t, outcome: &mut Outcome) { fn probe_nesting( builder: &CapiVMHooksBuilder, state_ptr: *mut c_void, - outer_ctx: *mut vm_exec_w6_hook_context_t, + outer_ctx: *mut vm_exec_hook_context_t, outcome: &mut Outcome, ) { unsafe { @@ -703,7 +702,7 @@ fn probe_nesting( vm_exec_w6_state_mem_length(inner_ctx, &mut length, std::ptr::null_mut(), 0); outcome.check( "the innermost context works", - inner_status == vm_exec_w6_status_t::VM_EXEC_W6_OK, + inner_status == vm_exec_status_t::VM_EXEC_OK, format!("{inner_status:?}"), ); @@ -711,7 +710,7 @@ fn probe_nesting( vm_exec_w6_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); outcome.check( "an outer context is rejected while a nested call runs", - outer_status == vm_exec_w6_status_t::VM_EXEC_W6_STALE_CONTEXT, + outer_status == vm_exec_status_t::VM_EXEC_STALE_CONTEXT, format!("{outer_status:?}"), ); }); @@ -721,7 +720,7 @@ fn probe_nesting( let status = vm_exec_w6_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); outcome.check( "the outer context works again after the nested call", - status == vm_exec_w6_status_t::VM_EXEC_W6_OK, + status == vm_exec_status_t::VM_EXEC_OK, format!("{status:?}"), ); } @@ -772,13 +771,13 @@ fn state_ops() { // The context that escaped the host call must now be refused. Contexts are pooled for the // instance's lifetime, so this reads live memory and fails the token check, rather than // being undefined behaviour. - let escaped = outcome.escaped_context as *mut vm_exec_w6_hook_context_t; + let escaped = outcome.escaped_context as *mut vm_exec_hook_context_t; let mut length = 0u64; let status = unsafe { vm_exec_w6_state_mem_length(escaped, &mut length, std::ptr::null_mut(), 0) }; assert_eq!( status, - vm_exec_w6_status_t::VM_EXEC_W6_STALE_CONTEXT, + vm_exec_status_t::VM_EXEC_STALE_CONTEXT, "a context used after its call must be rejected" ); } From 68b614fcde94879b43cc81dd050cca661ee90657 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 6 Oct 2026 09:39:36 +0300 Subject: [PATCH 15/26] new FFI plan update: converge interfaces --- PLAN-go-ffi.md | 104 +++++++---- PLAN-rust-vm-caching.md | 375 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 450 insertions(+), 29 deletions(-) create mode 100644 PLAN-rust-vm-caching.md diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index a402a6c5..90410e45 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -159,10 +159,11 @@ is what flips. | # | Decision | Why it blocks | | --- | --- | --- | -| **G1** | Do the hooks hold the state, or take it as a parameter? | The C ABI passes the context as the first argument of every hook — that *is* the parameter form. If the Rust trait keeps the state bundled, the generator must emit and reconcile two shapes. Deciding after the ABI is frozen turns a refactor into a two-repo migration. The Wasmer 6 builder currently returns `Box`, so a per-hook allocation also rides on this. **Phase 2 found this already answered by the committed Wasmer 6 code**: `ExperimentalVMHooksBuilder::with_vm_hooks` hands the builder the state and lends the hooks back through a closure, so the state is *bundled* in Rust and *passed as a parameter* in C — the two are compatible, and the generator emits one shape, not two. The allocation question is also already resolved: the builder lends `&mut dyn VMHooks` on its own stack frame. | +| **G1** | Do the hooks hold the state, or take it as a parameter? | The C ABI passes the context as the first argument of every hook — that *is* the parameter form. If the Rust trait keeps the state bundled, the generator must emit and reconcile two shapes. Deciding after the ABI is frozen turns a refactor into a two-repo migration. The Wasmer 6 builder currently returns `Box`, so a per-hook allocation also rides on this. **Phase 2 found this already answered by the committed Wasmer 6 code**: `ExperimentalVMHooksBuilder::with_vm_hooks` hands the builder the state and lends the hooks back through a closure, so the state is *bundled* in Rust and *passed as a parameter* in C — the two are compatible, and the generator emits one shape, not two. The allocation question is also already resolved: the builder lends `&mut dyn VMHooks` on its own stack frame. **Superseded in scope by G6**, which decides where that builder is attached. | | **G2** | Does the new executor keep the Go VM's truncating load? | **Settled in Phase 2: yes, it clamps.** `vm_exec2_state_mem_load` writes into the caller's buffer and returns `min(requested, available)`, so the truncation is visible in the return value. Chosen to preserve behaviour contracts have already observed; the strict load stays available on the trait for a caller that wants it. The rule itself moved into `clamped_load_len` in `vm-executor/src/new_traits/memory.rs`, so the FFI and `InstanceState::memory_load_clamped` cannot drift apart. | -| **G4** | Which `OpcodeVersion` does each host declare, and does the config stay mutable? | **Resolved in Phase 4b: immutable, with the executor rebuilt on change.** Phase 2 had already taken the immutable branch for the new C API — version and costs arrive in `vm_exec2_executor_config_t`, with no `set_opcode_config`. The obstacle was `GasScheduleChange` (`vmhost/hostCore/host.go:375`), which re-applies both to the *live* executor at an epoch. It is reconcilable because that function already calls `ClearWarmInstanceCache()`, so no instance survives the change: recreating the executor is behaviour-preserving, and is what lets `SetOpcodeConfig` leave the `Executor` interface. Phase 1.6 still has to reconcile the 2.2 backend's `Arc>` if that path is ever built. | +| **G4** | Which `OpcodeVersion` does each host declare, and does the config stay mutable? | **Resolved in Phase 4b: immutable, with the executor rebuilt on change.** Phase 2 had already taken the immutable branch for the new C API — version and costs arrive in `vm_exec2_executor_config_t`, with no `set_opcode_config`. The obstacle was `GasScheduleChange` (`vmhost/hostCore/host.go:375`), which re-applies both to the *live* executor at an epoch. It is reconcilable because that function already calls `ClearWarmInstanceCache()`, so no instance survives the change: recreating the executor is behaviour-preserving, and is what lets `SetOpcodeConfig` leave the `Executor` interface. Phase 1.6 still has to reconcile the 2.2 backend's `Arc>` if that path is ever built. **Extended by G6's companion decision**: the opcode config is not executor state at all — it folds into `CompilationOptions`, passed per instance, which removes the setter rather than replacing it with a rebuild. | | **G5** | Does one C library carry both backends, selected by a flag? | **Answered: no — one library per backend, link the one you need.** That is already the pattern: a node links four wasmer runtimes today (v1.2/v1.3/v1.4 share one byte-identical `libwasmer`, v1.5 brings `libvmexeccapi`) with zero shared symbols between them. Bundling would make the 32 duplicate `wasmer_vm_*` libcalls a *static* collision the linker silently collapses, breaking one backend's `vmctx` layout; two libraries make it a *dynamic* one that `-Bsymbolic-functions` fixes. Duplication is near zero: one Rust crate with a backend feature, one Go bridge with a build tag. What remains is **verifying `-Bsymbolic-functions` on Linux, before anything links both** — see "Serving both backends" below. | +| **G6** | Are the VM hooks attached per executor or per instance, and when is the execution context resolved? | **Closed: per executor, resolved at call time.** The two are independent axes, and only the second decides whether a cached instance is safe — an instance keeps its hooks object for life (`reset()` never rebuilds the import object), so reuse is sound iff the hooks find the current execution when called rather than when created. Attaching them per executor makes that structural: instances carry no host identity, so there is nowhere for a stale context to hide. It is also what the Go VM already does (`vmHooksProvider { host }`), which is why *its* warm cache is sound and the Rust VM's capture-at-creation design could never have one. The per-instance alternative buys stateless instantiation at the price of foreclosing instance reuse, and needs an `Any` downcast for non-wasm executors that this option does not. Full reasoning, with measurements, in `PLAN-rust-vm-caching.md`. | ## Phase 1 — Close the trait gaps @@ -174,8 +175,8 @@ is what flips. | 3 | Bounds-check `WasmerInstance::memory_load`/`memory_store`, which sliced with an unchecked, overflow-prone `mem_ptr + mem_length` | **done** | | 4 | Remove every panic from `Result`-returning paths: instance init in both executors (needs `rc_new_cyclic_fallible` for the 2.2 backend), `new_instance_from_cache`, `reset`, the breakpoint read in `call`, the prod state setters, `get_memory_view` | **done** | | 5 | `BreakpointValue` discriminants aligned to the wire values 4/5 so `as_u64` and `TryFrom` are inverses | **done** (separate commit) | -| 6 | Converge the two `RuntimeRef` traits into one config plus one hooks-provider trait, dropping the `Mutex` | gated on **G1**, **G4** | -| 7 | Apply G1: if the state becomes a hook parameter, the builder trait disappears and `VMHooksWrapper` holds one `Box` per instance | gated on **G1** | +| 6 | Converge the two `RuntimeRef` traits into one `ExecutorRuntime`, dropping the `Mutex` and folding `OpcodeConfig` into `CompilationOptions` | **decided**, see G6; ready to build | +| 7 | Create instances through the `Executor` trait rather than around it | **decided**, see G6; the per-instance handle it was waiting for turns out not to exist — the runtime reference is per executor | The bounds rules are deliberately shared by all backends — they are observable by contracts, so there is one implementation, not one per backend. Ten tests pin the edge cases, including @@ -224,6 +225,14 @@ takes `go_vm_hooks` and builds the instance directly against the Wasmer 6 backen 1.6's converged hooks-provider trait should take a per-instance host handle**, otherwise the `Executor` trait cannot serve the C API on either backend. +*Resolved by G6, and not the way this predicted.* There is no per-instance host handle, +because there is no per-instance host state: Go's provider holds nothing but the host, and the +per-instance `cgo.Handle` carries only `activeState` (a Phase 4b item 1 workaround) and an +error buffer. So the handle becomes **per executor**, passed once to +`vm_exec_w6_new_executor`, and `vm_exec_w6_new_instance` loses its `go_vm_hooks` argument +entirely. That is what lets this file go back through the `Executor` trait and serve both +backends — once 4b item 1 has removed `activeState`. + **The hook context is pooled, not stack-allocated.** The original sketch called for a stack-allocated context per invocation. That is unsafe as specified: a Go side holding the pointer after the hook returns would be reading a reused stack frame, so even *reading the @@ -864,25 +873,54 @@ against `wasmer6` is Wasmer's doing and not the new FFI's. - Stale-handle test: keep a `ctx` past the hook return and confirm the token check rejects it rather than faulting. **Done in Phase 2** (`state_ops`). -## Phase 6 — Deferred - -- `reset()` on the Wasmer 6 instance — prerequisite for Go's warm-instance tracker. -- `new_instance_from_cache` and the `cache` path. -- Only then wire warm instances into wasmer3. - -## Cross-repo note - -`mx-sdk-rs` pins `multiversx-chain-vm-executor* = "=0.5.1"` from crates.io, so it picks up -none of this until a version bump. At that bump, three accumulated breaks need resolving -together: - -- `ExperimentalExecutorRuntimeRef::opcode_cost` → `opcode_config` (carries the G4 decision); -- `VMHooksDispatcher` is missing five newer hooks (`activate_unsafe_mode`, - `deactivate_unsafe_mode`, `managed_get_num_errors`, `managed_get_error_with_index`, - `managed_get_last_error`) — the `VMHooks` trait has 282 required methods and no defaults, - so every addition is a hard break; -- `WasmerProdInstanceState::set_breakpoint_value_legacy` / `set_early_exit` now return - `Result`, so `InstanceStateSetEarlyExit` in `chain/wasmer-prod` must handle them. +## Phase 6 — Caching — **re-scoped, see `PLAN-rust-vm-caching.md`** + +Was: `reset()` on the Wasmer 6 instance, as a prerequisite for Go's warm-instance tracker, +then `new_instance_from_cache`, then warm instances for wasmer6. + +**`reset()` is off the list.** Measured on a 55 KB contract: compiling costs ~6 ms and +instantiating from a module already in memory ~30–50 µs, so reusing a live instance saves +under 1% over instantiating a cached module afresh. And it is not cheap to get — Wasmer 6 +*and* 7.4 have no instance reset (`Memory::reset` only zeroes and shrinks memory; no +data-segment or global restore), so it would mean forking Wasmer 6, which is consumed +unmodified from crates.io unlike the 2.2 fork. Re-instantiating is behaviourally identical to +resetting. + +So for this backend the warm instance cache **collapses into the module cache**. The cheapest +route for the Go VM is for `Wasmer6Executor` to hold compiled modules in memory and let +`Reset()` keep returning false, which the instance tracker already handles by evicting and +recreating. `NewInstanceFromCompiledCodeWithOptions` is already the Wasmer-6-compatible form +of reuse. + +Remaining here: the serialized-artifact path. Note that artifacts carry an ABI version that +`deserialize` rejects on mismatch — it moved from 11 (Wasmer 6.1) to 24 (Wasmer 7.4) — so a +persisted cache needs the Wasmer version in its key or a purge on upgrade. + +## Cross-repo note — `mx-sdk-rs` + +**The working copy is `/Users/andreim/multiversx/rs/mx-sdk-rs-w1`, and its `Cargo.toml` now +patches the three executor crates to this checkout** — both `[patch.crates-io]` and +`[patch."https://github.com/multiversx/mx-vm-executor-rs"]`, because `chain/wasmer-prod` +reaches `multiversx-chain-vm-executor` through crates.io by design while the rest of the +workspace reaches it by git rev. Patching one table and not the other leaves cargo with two +incompatible copies in the graph. A trait change and its only Rust consumer can therefore be +compiled together, with no push-and-bump round trip. Both tables come out, and the `rev =` +pins move to the pushed commit, before publishing. + +Accumulated breaks, with status: + +- `ExperimentalExecutorRuntimeRef::opcode_cost` → `opcode_config` — superseded again by G6: + the opcode config leaves the runtime reference altogether and folds into + `CompilationOptions`; +- `VMHooksDispatcher` carried five methods the `VMHooks` trait no longer declares + (`activate_unsafe_mode`, `deactivate_unsafe_mode`, `managed_get_num_errors`, + `managed_get_error_with_index`, `managed_get_last_error`) — stale stubs that only + `panic!`ed. **Removed**; this was what stopped the workspace compiling against the local + checkout at all. Note `vh_dispatcher.rs` is hand-maintained, not generated, so nothing was + going to fix it for us; +- `WasmerProdInstanceState::set_breakpoint_value_legacy` / `set_early_exit` returning + `Result` — to be handled when the 2.2 backend is reworked, if it is: that integration is + kept only to validate that the refactor preserved behaviour, and is then deleted. ## Critical path @@ -900,12 +938,20 @@ rest. The struct-return ABI works end to end, and the backends coexist. Phase 4b depended on G5 for the interface *shape* — that was already determined by the Rust side — only for how the backend gets chosen, so G5's answer does not change it. -**Phase 1.7 is on the critical path; Phase 1.6 came off it.** Phase 4b needs the hooks -provider to take a per-instance handle, which is 1.7, and G4 has to be settled for that. Phase -1.6 — converging the two `RuntimeRef` traits — was only needed in order to serve wasmer 2.2 -through the *new* FFI, and G5's answer removed that need: the differential run uses `wasmer2` -over the legacy library, presenting the new Go interfaces. 1.6 is now cleanup for when the -legacy interface set is retired. +**G6 reordered 1.6/1.7 against 4b.** The earlier reading was that Phase 4b needed the hooks +provider to take a per-instance handle (1.7). G6 found there is no per-instance host state to +hand it — so the dependency runs the other way: **4b item 1 is now a prerequisite for the FFI +half of 1.6/1.7**, because moving the runtime handle to per-executor leaves Go's per-instance +`activeState` slot with nowhere to live, and that slot exists only because the VM still +reaches wasm memory through `executor.Instance` instead of through the state it was handed. + +The Rust half of 1.6/1.7 — one `ExecutorRuntime` on the executor, `OpcodeConfig` folded into +`CompilationOptions`, instances created through the trait — is **not** blocked by 4b and can +land first. Only the C ABI change waits. + +Phase 1.6 also stopped being optional: it was cleanup while the plan was to serve wasmer 2.2 +through the legacy library, but G6's shape is what both backends now share, and it is the +precondition for the caching work in `PLAN-rust-vm-caching.md`. **What took its place:** verifying `-Wl,-Bsymbolic-functions` on Linux. Any binary linking both `libvmexeccapi` and `libvmexeccapi2` — the Phase 5 harness, and later a node that keeps the old diff --git a/PLAN-rust-vm-caching.md b/PLAN-rust-vm-caching.md new file mode 100644 index 00000000..0224a641 --- /dev/null +++ b/PLAN-rust-vm-caching.md @@ -0,0 +1,375 @@ +# Instance caching in the Rust VM, and what it means for the executor traits + +Goal as set: bring the Rust VM up to the Go VM's level — contract code compiled once and +reused — and implement the executor side that this needs. Two halves: the experimental +(Wasmer 6) executor, and the Rust VM itself. + +The first thing done was to measure what reuse is worth, because that determines the shape of +both halves. It turned out to change the plan substantially, so it comes first. + +## The measurement + +A real contract, `test/delegation/v0_4_genesis` (55 KB of wasm), 50 iterations, release build, +Wasmer 6.1 Singlepass on an arm64 Mac. Numbers vary ±20% between runs; the *ratios* do not. + +| Operation | Cost | Share of a cold instance | +| --- | --- | --- | +| Compile + instantiate — the path taken today, with all four middlewares | ~6 ms | 100% | +| Compile only, **without** middlewares (a lower bound on the compile share) | ~1.7 ms | — | +| **Instantiate from a `Module` already in memory** | **~30–50 µs** | **under 1%** | +| Deserialize a serialized artifact (427 KB) | ~45 µs | under 1% | +| Hash the wasm bytes, for a cache key | ~14 µs | 0.2% | + +The middlewares roughly triple compile time, which is why the full path costs ~6 ms where a +bare Singlepass compile costs ~1.7 ms. + +**Compilation is essentially the entire cost; instantiation is noise.** Everything below +follows from that. + +### Consequence 1: cache modules, not instances + +The Go VM keeps a *warm instance* cache and reuses live instances via `Instance.Reset()`. On +Wasmer 6 that model buys ~30–50 µs on a ~6 ms operation — under 1% — over simply instantiating +a cached module afresh. It is not worth its complexity, and the complexity is considerable: + +- **Wasmer 6 has no instance reset, and adding one means forking it.** `Memory::reset` exists + but, per its own doc, "resets the memory back to zero length" — it does not re-apply data + segments and does not touch globals. The Wasmer 2.2 `reset()` the Go VM relies on lives in + the **MultiversX fork** (`handle.reset(data_initializers)`, reaching into `wasmer-vm`). + Wasmer 6 is consumed from **crates.io, unmodified** — which is a property worth keeping. +- Re-instantiating is **behaviourally identical** to resetting: both yield fresh globals and + memory initialised from the module's data segments. Nothing observable to a contract + distinguishes them. + +So: `reset()` stays unimplemented, deliberately, with the measurement recorded as the reason. + +### Consequence 2: instance caching stays possible, so the traits must allow it + +Module caching is what pays, but instance caching must not be *designed out*, because the +C ABI is expensive to change later. The constraint it imposes comes from the fact above: an +instance keeps its hooks object for life, so + +> an instance is safely reusable **iff** its hooks resolve the execution context when they are +> called, not when the instance is created. + +That is one axis. Where the hooks are *attached* — per executor or per instance — is a second, +independent one. Only the first decides cache safety. + +**Decided: hooks per executor, context resolved at call time.** The executor is given a +runtime reference once, at construction, and asks it for hooks on every host call. Instances +then carry no host identity at all, which makes caching them safe by construction rather than +by discipline. It is also what the Go VM already does (`vmHooksProvider { host }`), so the two +VMs converge. + +The alternative that was considered and rejected — passing a per-instance hooks provider to +`new_instance` — gives stateless instantiation without a context stack, but binds one +execution into the instance and so forecloses instance reuse. Two further variants (per +instance + resolve-at-call; capture + an explicit `Instance::reuse(hooks)` rebind) each pay +this option's cost without its guarantee, or add an entry point across the FFI for a sub-1% +win. + +Three things follow, and two of them are savings: + +- The Rust VM's `set_executor_context(Some)/(None)` slot is replaced by a **context stack** + held for the whole execution, not an arming window around instantiation. That removes the + current unenforced invariant ("nobody touches the slot between `set` and `new_instance`"). +- **No `Any` downcast anywhere.** The escape hatch the per-instance design needed for + `ContractDebugExecutor` is unnecessary: that executor already holds a `RuntimeWeakRef`, so + it reads the current context from the stack directly. +- `Executor::new_instance` takes only the wasm and the options. + +## The runtime interface + +The one thing the executor needs from its host, and therefore the only method on the trait: + +```rust +/// The host behind an executor. One per executor, for the executor's whole life. +pub trait ExecutorRuntime: Send + Sync { + /// Builds the VM hooks servicing one host call, around the instance state for that call, + /// and lends them to `f` — which is called exactly once. + /// + /// Built per call, not per instance: under Wasmer 6 the instance state is only reachable + /// for the duration of a call, so hooks that hold it cannot outlive it. Lending rather + /// than returning avoids a `Box` allocation on every hook invocation. + /// + /// Which execution the hooks serve is resolved *here*, by the host, from its own call + /// stack. That is what makes a cached instance safe to reuse: the instance contributes + /// the state, never the identity. + fn with_vm_hooks( + &self, + instance_state: &mut dyn InstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ); +} +``` + +Object-safe, so an executor stores `Arc`. No opcode config and no +compilation options on it — those are arguments to `new_instance`. + +### Over the FFI + +The runtime reference becomes a per-**executor** handle, where today it is per-instance: + +```c +vm_exec_status_t vm_exec_w6_new_executor( + vm_exec_w6_executor_t **executor_out, + uintptr_t runtime_ref, /* NEW: the host's runtime handle */ + const vm_exec_vm_hook_c_func_pointers *hook_table, + char *err_buf, int err_buf_len); + +vm_exec_status_t vm_exec_w6_new_instance( + vm_exec_w6_executor_t *executor, + vm_exec_w6_instance_t **instance_out, + const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, + const vm_exec_compilation_options_t *options, /* now carries the opcode config */ + char *err_buf, int err_buf_len); /* go_vm_hooks is gone */ +``` + +`uintptr_t` for the same reason as before: from Go it is a `runtime/cgo.Handle`, and cgo +forbids storing a Go pointer in C memory. `vm_exec_hook_context_t.go_vm_hooks` is renamed to +`runtime_ref` and now carries the executor's handle rather than the instance's. + +**The hook context pool becomes thread-local.** It is per-instance today. It cannot become +per-executor behind a `RefCell`, because `vmHost.RunSmartContractCall` takes only an `RLock` +— concurrent executions against one host are permitted, so a shared pool would race. A call +stack is per-thread anyway, and cgo keeps a goroutine on its OS thread for the duration of a +call, which the C API already states as an invariant. Thread-local is also strictly safer than +today: the pool outlives every instance, closing the window in which a context pointer leaked +past instance destruction would dangle rather than merely fail its token check. + +## Recursion: the same contract twice on one stack + +A contract that calls itself — directly, or round a cycle — needs two live instances of the +same code at once. An instance cache must not hand the inner frame the instance the outer +frame is running, because preparing it for reuse resets memory and globals, which would +destroy the outer frame mid-execution. + +**What the Go VM does today**, three guards, all keyed on the stack: + +1. *Do not take a warm instance* when the code hash or the SC address is already on the stack + — `useWarmInstanceIfExists` → `isContractOrCodeHashOnTheStack` (`runtime.go:297`). The + recursive call falls through to the compiled-code path, and then to bytecode. +2. *Do not save as warm* when the code hash is on the stack — `saveWarmInstance` → + `IsCodeHashOnTheStack` (`runtime.go:366`). The duplicate never enters the cache. +3. *Destroy the duplicate on the way out* — `PopSetActiveState` cleans the popped instance + when it is not the one on top of the stack and its code hash is on the stack + (`instanceTracker.go:97`). + +Bounded by `maxInstanceStackSize`, checked at the top of `StartWasmerInstance`. + +So the answer to "two copies from the same cache bytes" is **yes, deliberately**: the inner +frame deserializes the artifact again and gets its own memory and globals. Correct, but it +pays a deserialize (~45 µs) per recursion level. + +**Under a module cache the problem does not arise.** What is cached is the compiled module, +which is immutable: two frames each call `Instance::new` on the same `Module` and get separate +memories and globals, sharing nothing mutable. No on-stack check is needed for correctness — +guard 1 becomes vacuous because there is no warm instance cache, and guards 2 and 3 have +nothing to protect. It is also *faster* than the current recursion path: one compile, then +~30–50 µs per level instead of a deserialize per level. + +The one constraint to respect is the one already stated in 1.1: an `Engine` may be reused to +**instantiate** the module it compiled, but never to **compile** a second module, because the +metering middleware keeps `global_indexes` populated at compile time. Instantiation does not +run middlewares, so recursion is unaffected. + +**On the Go side today this is all vacuous anyway**: `Wasmer6Instance.Reset()` returns false, +and both `Cache()` and `NewInstanceFromCompiledCodeWithOptions` return `ErrCacheUnsupported`, +so every execution compiles from bytecode and no instance is ever shared. Recursion is safe +and slow. That is what the module cache fixes. + +**Hooks under recursion** need no extra mechanism either. With hooks attached per executor and +the execution resolved at call time (G6), the inner frame's hooks resolve to the inner +context and the outer frame's to the outer, through the host's context stack — which is what +that stack is for. On the C side the hook context pool is already a stack: the inner +activation is innermost, the outer context is refused while it runs and valid again +afterwards, which `probe_nesting` in `c-api-wasmer6/tests/ffi_boundary.rs` already pins. + +## Wasmer 7.4, checked alongside — and one reason to move + +Everything above was re-verified against Wasmer 7.4, by source diff and by running a real +serialize → deserialize → instantiate round trip (49 exports intact). + +- **`serialize`/`deserialize` are unchanged**, in signature and in behaviour. Deserialize + ~43 µs, instantiate ~31 µs, artifact 436 KB — the same profile as 6.1. The caching design + does not depend on which of the two we are on. +- **The `deserialize` quirk persists**: it still delegates to `deserialize_unchecked`, so the + "checked" name still validates nothing. Artifacts must come from a trusted store. +- **Still no instance reset.** `Memory::reset` carries the same "resets the memory back to + zero length" semantics, with no data-segment or global restore. +- Artifact ABI version 11 → 24, as noted in 1.3. + +**The reason to move is unrelated to caching.** +`vm-executor-experimental/src/middlewares/we_metering.rs` has `feed_local_count` commented out +under a TODO listing three options ("find alternative / PR to Wasmer / fork Wasmer"), because +Wasmer 6 has no hook for it. The Wasmer 2.2 backend has that function **live**. So the two +backends currently disagree on gas: **Wasmer 6 does not charge the unmetered-locals cost that +Wasmer 2.2 charges.** + +Wasmer 7.4 adds `FunctionMiddleware::locals_info(&mut self, locals: &[ValType])` — option (b) +of that TODO, landed upstream. It is fed from `read_local_decl`, which expands each +declaration group, so `locals.len()` is exactly the old `count`, delivered once before the +first operator. One wrinkle: it returns `()`, so `check_local_count_exceeded`'s error has to be +stashed and reported from the first `feed`. + +The cost of the upgrade is that `FunctionMiddleware` gained a lifetime parameter +(`FunctionMiddleware<'a>`, `generate_function_middleware<'a>() -> Box + 'a>`, +`feed` losing its own `'a`), which is mechanical but touches all five middlewares. The rest of +the API we use — `FunctionEnvMut`, `MemoryView`, `Store`, the `Global` helpers, the Singlepass +config, trap and error types — has not been audited; that needs a compile, which is a contained +experiment. + +Worth pointing the hook-sequence differential at contracts with many locals either way, since +that is where the divergence would show. + +## Side 1 — the experimental executor + +### 1.1 Separate compilation from instantiation + +`ExperimentalInstance::try_new_instance` currently does `Store::new(compiler)` → `Module::new` +→ instantiate in one call. Split it: + +```rust +/// A contract compiled and ready to instantiate. Holds no host state. +pub trait CompiledModule: Send + Sync { + fn serialize(&self) -> Result, ExecutorError>; +} + +pub trait Executor { + fn compile(&self, wasm: &[u8], options: &CompilationOptions) + -> Result, ExecutorError>; + + fn instantiate(&self, module: &dyn CompiledModule) + -> Result, ExecutorError>; + + /// Convenience, and the cache's entry point: compile-or-reuse, then instantiate. + fn new_instance(&self, wasm: &[u8], options: &CompilationOptions) + -> Result, ExecutorError>; +} +``` + +The Wasmer 6 implementation holds `{ engine: Engine, module: Module }`. The `Engine` has to +travel with the `Module`, because instantiation needs a `Store` built on the same engine +(`Store::new(engine.clone())` — the clone is an `Arc` bump). + +**One engine per compiled module, not one shared engine.** The metering middleware keeps +`global_indexes: Mutex>`, populated by `transform_module_info` +during compilation. Compiling a second contract through the same engine would overwrite the +first one's indexes. Instantiation does not run middlewares, so *reusing* an engine to +instantiate the module it compiled is safe; reusing it to compile is not. + +### 1.2 The module cache + +In the executor, not in the VM — it is pure memoization with no host semantics, so every host +gets it without writing any cache code. + +- **Key: `(hash(wasm_bytes), hash(options))`.** Including the options makes the cache + self-invalidating: a gas-schedule change simply misses, with no "remember to clear the + cache" discipline anywhere. That is strictly better than the Go VM's model, where + `GasScheduleChange` clears the warm instance cache but the persisted compiled-code cache is + cleared only at flagged activation epochs. +- **Bound it with an LRU.** Artifacts are ~8× the wasm size (427 KB for 55 KB of wasm), so an + entry count alone is a poor bound; prefer a size-aware cap. +- Hashing costs ~14 µs on 55 KB with a non-cryptographic hasher — a third of an + instantiation. Acceptable. Use a fast hash, not a cryptographic one: this key never leaves + the process and is not consensus-relevant. + +*Decision deferred:* the Go VM already has a code hash and would be paying that 14 µs twice. +If it shows up, add an optional caller-supplied key. Not worth the extra parameter up front. + +### 1.3 The serialized-artifact path + +`cache()` already works (`module.serialize()`). Finish the other half: `new_instance_from_cache` +should do `Module::deserialize(&engine, bytes)` against an engine built from the same options. + +**Security note to carry into the doc comment:** in Wasmer 6.1, `Module::deserialize` delegates +to `deserialize_unchecked` — the "checked" variant performs no validation. Artifacts must come +from a trusted store. That is true of our own cache, but it must be stated, because the name +suggests otherwise. + +**Artifacts are not portable across Wasmer versions.** The serialized header carries an ABI +version which `deserialize` rejects on mismatch, and it moved from **11** (Wasmer 6.1) to +**24** (Wasmer 7.4). Any persistent cache therefore needs the Wasmer version in its key, or an +explicit purge on upgrade. Irrelevant to the in-memory module cache, which never serializes. + +This tier matters less for the Rust VM (in-process, so the module cache covers it) than for +persistence across process restarts. Lower priority than 1.1 and 1.2. + +### 1.4 What is explicitly not built + +`reset()` stays `Err(ResetUnsupported)`, with the measurement and the fork argument recorded in +the doc comment so the question is not reopened without new evidence. + +## Side 2 — the Rust VM + +Smaller than expected, because the cache lives in the executor. Mostly replacement and removal. + +### 2.1 A context stack in place of the arming slot + +`Runtime::context_cell: RwLock>` is set before `new_instance` and cleared +straight after, so the hooks capture the transaction while the window is open. Replace it with +a stack pushed for the whole of `RuntimeRef::execute` and popped at the end: + +- `push_context(tx_context_ref)` / `pop_context()`, with `current_context()` reading the top. +- Nesting works the way it already does in the Go VM: a hook that re-enters the VM pushes its + own context and pops it on the way out, and the outer context is on top again afterwards. +- The unenforced "nobody touches the slot between `set` and `new_instance`" invariant goes + away, because nothing depends on a window any more. + +`impl ExecutorRuntime for RuntimeWeakRef` then has one method, which resolves the current +context from that stack and builds the dispatcher around the state it was handed. One +implementation serves both backends — the Wasmer 2.2 and Wasmer 6 executors are configured +identically. + +### 2.2 `ContractDebugExecutor` needs nothing special + +It runs Rust closures rather than wasm and needs the `TxContextRef` itself. It already holds a +`RuntimeWeakRef`, so it reads the current context from the stack. No downcast, no `Any` +supertrait, no escape hatch — which was the weak point of the rejected per-instance design. + +### 2.3 Wiring and measurement + +- `CompositeExecutor` forwards only the options; there is no provider to thread through. +- No VM-side cache code, no instance tracker, no instance stack. +- **Measure on a real scenario suite** before and after. The expected win is the compile share + of every repeated execution — ~6 ms per call collapsing to ~50 µs for every call after the + first with the same code. If the observed win is far from that, the cache is not being hit + and the key is wrong. + +## Sequencing + +| Stage | Work | Depends on | +| --- | --- | --- | +| 0 | `ExecutorRuntime` on the executor; `OpcodeConfig` folded into `CompilationOptions`; `new_instance(wasm, options)` | — | +| 1 | Executor: split compile from instantiate; `CompiledModule` | 0 | +| 2 | Executor: module cache, keyed on wasm + options | 1 | +| 3 | Rust VM: context stack, `impl ExecutorRuntime for RuntimeWeakRef`, delete the slot | 0 | +| 4 | Measure on a scenario suite | 2, 3 | +| 5 | Serialized-artifact path (`new_instance_from_cache`), keyed including the Wasmer version | 1 | + +Stages 1–2 and 3 are independent once 0 lands. + +**One cross-repo constraint on the C API.** Moving the runtime handle from per-instance to +per-executor requires **`PLAN-go-ffi.md` Phase 4b item 1** to land first. Go's per-instance +`cgo.Handle` resolves to `instanceHooks`, which holds `activeState`; that slot's own comment +says the nesting argument depends on it being per instance ("a nested call runs on a different +instance"). It exists only because the VM still reaches wasm memory through +`executor.Instance` rather than through the state it was handed, which is exactly what 4b item +1 removes. Until then the C API keeps its per-instance handle and the capi keeps bypassing the +`Executor` trait. The Rust-side stages above are not blocked by this; only the FFI change is. + +## What this means for the Go VM + +Not in scope here, but the measurement applies to Go's Wasmer 6 backend too, and it bears on +`PLAN-go-ffi.md` Phase 6 ("`reset()` on the Wasmer 6 instance — prerequisite for Go's +warm-instance tracker"). + +It is not a prerequisite, because it is not worth building. The Go VM's existing +*compiled-code* path (`NewInstanceFromCompiledCodeWithOptions`) is already the +Wasmer-6-compatible form of reuse: for this backend the warm instance cache collapses into the +module cache. The cheapest route is for `Wasmer6Executor` to keep compiled modules in memory +and let `Reset()` keep returning false, which the instance tracker already handles by evicting +and recreating. + +Worth confirming separately, because it is the one place where the Go VM's reuse model and +Wasmer 6's capabilities genuinely disagree. From a7c8568f957919b1d89cbf48e47fc43b61d6b5d1 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 6 Oct 2026 12:17:48 +0300 Subject: [PATCH 16/26] new FFI plan update: converge interfaces --- PLAN-go-ffi.md | 77 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 90410e45..3f2ba2e2 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -722,19 +722,74 @@ The test is skipped unless `VMEXECUTOR_DIFF=1`, since it cannot pass until Phase original purpose: wasmer2, wasmer2, wasmer3, wasmer3, wasmer2 in one process, which passes and exercises backend coexistence through the real VM rather than through a trap-handler probe. +#### Done: the instance interface shrunk, and the call reports its own outcome + +`executor.Instance` went from 24 methods to 19, and `CallFunction` now returns a result +instead of a bare error. + +**Four methods removed as provably dead**, not merely unused: + +- `HasMemory()` — every implementation answered an unconditional `true`, so + `verifyMemoryDeclaration` could not fire. It and `ErrMemoryDeclarationMissing` are gone with + it; the executors already reject a contract without exactly one memory when they instantiate + it, which happens first. +- `MemDump()` — exactly `MemLoad(0, MemLength())`, which is how the wasmer6 adapter already + implemented it. The two tests that used it say so directly now. +- `SetVMHooksPtr` / `GetVMHooksPtr` — no VM caller at all, and every implementation of the + getter returned zero. That made `TestExecution_MultipleInstances_SameVMHooks` vacuous (it + asserted a one-element set of zeros) and `TestExecution_MultipleVMs_OverlappingDifferentVMHooks` + unrevivable (it asserted those zeros were *distinct*, which is why it carried a `t.Skip()`). + The first keeps its execution loop without the dead assertion; the second is deleted. Under + G6 the property they were reaching for holds by construction: the hooks belong to the + executor, so every instance it creates shares them. + +**`CallFunction(name) InstanceCallResult`.** The new type lives in +`executor/executorInstanceCallResult.go` and carries `Kind`, the original `Err`, the +early-exit code and the breakpoint. `Err` is the error itself rather than its text, so +`errors.Is` and `errors.As` keep working across the boundary — the thing a value-typed result +would otherwise have quietly broken. + +The translation moved **up**, into `runtimeContext.CallSCFunction`, where it is written once +for both backends instead of once per adapter: + +- wasmer6 reports what actually happened and no longer stashes the breakpoint itself; +- wasmer2legacy can only say ok-or-failed — its C API cannot tell a trap from a breakpoint, + and its middlewares set the global themselves — so it never reports `CallBreakpoint`, and + the VM's existing readers are undisturbed; +- `CallSCFunction` records a reported breakpoint where those readers look, then returns + `result.Err`. + +This is the hard prerequisite `PLAN-go-breakpoint-removal.md` was waiting on. + +**Verified.** The full suite passes on the default backend. Under `$VMEXECUTOR=wasmer6` the +failure set is **byte-identical before and after** (25 failures, compared by stashing the +change and re-running), so nothing regressed. The hook-sequence differential on `erc20-c` +reproduces its recorded baseline exactly: **9,135 lines, 96 differing, every one of them +instance lifecycle** — `Reset`/`Clean` and the `SetPointsUsed`/`SetGasLimit`/`SetBreakpointValue` +triple that follows a warm reuse. Every VM hook call and every gas value still agree. + #### Still to do -1. Shrink `executor.Instance` to the outside view, pushing the legacy-only methods down into - wasmer2's own types: `SetBreakpointValue`/`GetBreakpointValue` (the new executor reports - breakpoints in its call result), `SetVMHooksPtr`/`GetVMHooksPtr` (no VM caller at all), - `SetGasLimit`, `MemDump`, `HasMemory`. -2. `CallFunction` returns an `InstanceCallResult` rather than a bare `error` — the hard - prerequisite for `PLAN-go-breakpoint-removal.md`. -3. `MemLoadFromMemory` / `MemStoreToMemory` bypassed for wasmer3, where bounds checking lives - in Rust, and kept for wasmer2. -4. `SetOpcodeConfig` removal — see the G4 resolution below. -5. ~~`wasmer3` implements `executor.Executor` and `executor.Instance`~~ — **done**, as a - transitional adapter; it is item 1 that lets the adapter go away. +1. `SetGasLimit` off the interface. It was on the original list for this tranche and came off + on inspection: the limit is sampled once, at `StartWasmerInstance`, from + `metering.GetGasForExecution()`, and **must not be re-sampled** at call time or the gas + semantics change. For wasmer 2.2 it then lives in a per-instance wasm global, which nesting + restores for free. Folding it into `CallFunction` therefore means carrying the sampled + value on the instance tracker alongside the instance — a small change, but a + gas-sensitive one that deserves its own step rather than riding along with the call-result + conversion. +2. `SetBreakpointValue` / `GetBreakpointValue` off the interface. Blocked on + `PLAN-go-breakpoint-removal.md`, which item 2 above has now unblocked: while the VM still + polls the breakpoint slot, the methods have to stay. +3. `MemLoad` / `MemStore` off the interface, which is what finally removes + `instanceHooks.activeState` and lets the C API's runtime handle move to per-executor + (G6). The census says this is nearly done already: **the only non-test caller in VM code is + `hostInstanceState`** in `vmhost/vmhooks/vmHooksImpl.go`, which is the Wasmer 2.2 path + resolving the instance through the host stack. What remains is the eight + `vmhost/contexts` tests that address memory through `Instance` from outside a host call. +4. `SetOpcodeConfig` removal — now folded into the G6 companion decision: the opcode config + leaves the executor entirely and becomes part of `CompilationOptions`, passed per instance. + Blocked on the Rust-side C ABI change. #### G4 resolved: the config can be immutable, if the executor is rebuilt From ec5f6a089cfbacec932cd061e267da21b72752e8 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 8 Oct 2026 11:53:00 +0300 Subject: [PATCH 17/26] new FFI - ExecutorRuntime trait, opcode config in compilation options --- PLAN-go-ffi.md | 55 ++++++++------- PLAN-rust-vm-caching.md | 53 +++++++++++++-- c-api-common/libvmexeccapi_common.h | 8 +++ c-api-common/src/capi_config.rs | 16 ++++- c-api-common/src/capi_vm_hook_context.rs | 54 +++++++++++---- c-api-common/src/capi_vm_hook_table.rs | 2 +- c-api-wasmer6/src/capi_backend.rs | 41 ----------- c-api-wasmer6/src/capi_executor.rs | 18 ++--- c-api-wasmer6/src/capi_instance.rs | 39 ++++++----- c-api-wasmer6/src/capi_instance_state.rs | 17 +++-- c-api-wasmer6/src/lib.rs | 1 - c-api-wasmer6/tests/ffi_boundary.rs | 44 ++++++------ vm-executor-experimental/src/lib.rs | 2 - vm-executor-experimental/src/we_executor.rs | 37 +++------- vm-executor-experimental/src/we_instance.rs | 25 +++---- .../src/we_instance_state.rs | 2 +- vm-executor-experimental/src/we_vm_hooks.rs | 26 +++---- .../src/we_vm_hooks_builder.rs | 33 --------- .../tests/bulk_memory_metering_test.rs | 29 +++----- .../tests/common/test_instance.rs | 25 ++----- .../src/new_traits/wasmer_prod_executor.rs | 28 ++++++-- .../tests/opcode_version_test.rs | 11 +-- vm-executor/src/new_traits.rs | 2 + .../src/new_traits/compilation_options_new.rs | 23 ++++++- vm-executor/src/new_traits/executor_new.rs | 10 +++ .../src/new_traits/executor_runtime.rs | 68 +++++++++++++++++++ vm-executor/src/new_traits/instance_state.rs | 53 +++++++++++++++ 27 files changed, 435 insertions(+), 287 deletions(-) delete mode 100644 c-api-wasmer6/src/capi_backend.rs delete mode 100644 vm-executor-experimental/src/we_vm_hooks_builder.rs create mode 100644 vm-executor/src/new_traits/executor_runtime.rs diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 3f2ba2e2..28f1ef3d 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -227,11 +227,12 @@ takes `go_vm_hooks` and builds the instance directly against the Wasmer 6 backen *Resolved by G6, and not the way this predicted.* There is no per-instance host handle, because there is no per-instance host state: Go's provider holds nothing but the host, and the -per-instance `cgo.Handle` carries only `activeState` (a Phase 4b item 1 workaround) and an +per-instance `cgo.Handle` carries only `activeState` (a workaround for the VM still reaching +wasm memory through `executor.Instance`) and an error buffer. So the handle becomes **per executor**, passed once to `vm_exec_w6_new_executor`, and `vm_exec_w6_new_instance` loses its `go_vm_hooks` argument entirely. That is what lets this file go back through the `Executor` trait and serve both -backends — once 4b item 1 has removed `activeState`. +backends — once 4b's `MemLoad`/`MemStore` removal has removed `activeState`. **The hook context is pooled, not stack-allocated.** The original sketch called for a stack-allocated context per invocation. That is unsafe as specified: a Go side holding the @@ -407,15 +408,17 @@ one definition, and the context-taking ones then reinterpret `vmctx` under the w Fixing that means renaming symbols in one version's rlib or patching `wasmer-vm`. Two libraries make it a *dynamic* one, which is a link flag — see below. -**What the duplication actually costs: almost nothing.** `c-api-new` touches its backend in -**three type names across two of thirteen files** — `ExperimentalInstance` in -`capi_instance.rs`, `ExperimentalInstanceState` and `ExperimentalVMHooksBuilder` in -`capi_vm_hook_context.rs`. Status codes, strings, config, opcode cost, the hook context -machinery and the 908-line generated hooks implementation are all backend-agnostic. So the -shape is **one crate with a Cargo feature selecting a backend alias module, built twice**, not -two crates. The one snag is that the aliases are not drop-in: `ExperimentalInstanceState<'a>` -is a borrow of the host-call frame while `WasmerProdInstanceState` is owned, so the lifetime -shapes have to agree first — which is Phase 1.6's job regardless. +**What the duplication actually costs: almost nothing.** This was first measured as *three* +backend type names across two of thirteen files. **Stage 0 cut it to one**: with the hook +dispatch behind `ExecutorRuntime` and `&mut dyn InstanceState`, the only name left is +`ExperimentalExecutor` in `capi_instance.rs`. Status codes, strings, config, opcode cost, the +whole hook context machinery and the generated hooks implementation are backend-agnostic. So +the shape is **one crate with a Cargo feature selecting a backend alias, built twice**, not two +crates. + +The snag recorded here — that `ExperimentalInstanceState<'a>` borrows the host-call frame while +`WasmerProdInstanceState` is owned, so the alias was not drop-in — **no longer applies**: +neither type is named at the boundary, and both reach it through the same `dyn InstanceState`. Likewise on the Go side: if both libraries keep the same `vm_exec2_*` ABI, there is **one** bridge, with a build tag choosing between the `LDFLAGS` lines, and the 3,631-line generated @@ -545,8 +548,9 @@ Two risks the phase closed on its own account: Two things were decided here that the plan had left open: - **The hooks are built per call, not per instance** (`VMHooksProvider.VMHooksForCall`), - mirroring the Rust side exactly: `ExperimentalVMHooksBuilder::with_vm_hooks` builds its - hooks around the state for the duration of one call and lends them out. + mirroring the Rust side exactly: `ExecutorRuntime::with_vm_hooks` (then named + `ExperimentalVMHooksBuilder`) builds its hooks around the state for the duration of one call + and lends them out. The alternative — one hooks object per instance with a mutable "current state" slot — is *workable*, and it is worth being straight about why, because the obvious objection to it @@ -864,15 +868,19 @@ because one output fed both the legacy crate and the new one. The hook dispatch moved because the context carries the instance state as an **erased pointer**: nothing in the pool, the table or the 900-odd generated lines ever names a backend type. Making `with_instance_state` and `with_hook_context` generic over `S` was enough, and the backend crate -now names its state type in exactly one place — `capi_backend.rs`, which defines -`BackendInstanceState` and wraps the shared builder to implement its own builder trait. So -`c-api-wasmer2` will not duplicate the generated hooks; it gets a `capi_backend.rs` of its own. - -Two things the compiler forced, both worth keeping: - -- **The orphan rule.** With the builder in `c-api-common`, both it and `ExperimentalVMHooksBuilder` - are foreign to the backend crate, so the trait cannot be implemented directly. A newtype - (`BackendVMHooksBuilder`) fixes it and makes the seam explicit rather than incidental. +named its state type in exactly one place — `capi_backend.rs`. + +**Stage 0 removed even that.** With one `ExecutorRuntime` taking `&mut dyn InstanceState`, the +erased pointer is a thin pointer *to the fat pointer*, so the vtable travels with the value +instead of being recovered by naming a concrete type. `capi_backend.rs` is deleted, +`CapiVMHooksBuilder` implements `ExecutorRuntime` directly in `c-api-common`, and the one +type alias left — `ErasedInstanceState` — is backend-independent. `c-api-wasmer2` therefore +needs *no* backend file at all, not merely a small one. + +- **The orphan rule stopped applying.** It forced a `BackendVMHooksBuilder` newtype while the + builder trait was the backend's own. Now the trait is foreign but the type + (`CapiVMHooksBuilder`) is local to `c-api-common`, so the impl is legal where it belongs and + the newtype is gone. - **`capi_logger` has to exist in every crate**, because `meta::capi_safe_unwind` expands to `crate::capi_logger::set_panic_handler()`. Each backend crate carries a one-line re-export. @@ -995,7 +1003,8 @@ for how the backend gets chosen, so G5's answer does not change it. **G6 reordered 1.6/1.7 against 4b.** The earlier reading was that Phase 4b needed the hooks provider to take a per-instance handle (1.7). G6 found there is no per-instance host state to -hand it — so the dependency runs the other way: **4b item 1 is now a prerequisite for the FFI +hand it — so the dependency runs the other way: **taking `MemLoad`/`MemStore` off +`executor.Instance` (4b, "Still to do") is now a prerequisite for the FFI half of 1.6/1.7**, because moving the runtime handle to per-executor leaves Go's per-instance `activeState` slot with nowhere to live, and that slot exists only because the VM still reaches wasm memory through `executor.Instance` instead of through the state it was handed. diff --git a/PLAN-rust-vm-caching.md b/PLAN-rust-vm-caching.md index 0224a641..35e7312d 100644 --- a/PLAN-rust-vm-caching.md +++ b/PLAN-rust-vm-caching.md @@ -340,7 +340,7 @@ supertrait, no escape hatch — which was the weak point of the rejected per-ins | Stage | Work | Depends on | | --- | --- | --- | -| 0 | `ExecutorRuntime` on the executor; `OpcodeConfig` folded into `CompilationOptions`; `new_instance(wasm, options)` | — | +| 0 | ~~`ExecutorRuntime` on the executor; `OpcodeConfig` folded into `CompilationOptions`; `new_instance(wasm, options)`~~ — **done**, see below | — | | 1 | Executor: split compile from instantiate; `CompiledModule` | 0 | | 2 | Executor: module cache, keyed on wasm + options | 1 | | 3 | Rust VM: context stack, `impl ExecutorRuntime for RuntimeWeakRef`, delete the slot | 0 | @@ -349,13 +349,58 @@ supertrait, no escape hatch — which was the weak point of the rejected per-ins Stages 1–2 and 3 are independent once 0 lands. +### Stage 0 as built + +`ExecutorRuntime` landed as drafted above, with one method. What the implementation added to +the sketch: + +- **`impl InstanceState for &mut T`.** Without it a host that is + generic over `S: InstanceState` cannot accept the `&mut dyn InstanceState` the runtime lends + it, and every host needs its own forwarding wrapper — which puts the backend's concrete + state type back into the host, the one thing the erased state exists to prevent. The Rust + VM's `WasmerProdInstanceStateAdapter` is exactly that wrapper, written out by hand. + `ExperimentalInstanceState` now implements `InstanceState` on the struct rather than on + `&mut` itself, so the blanket impl covers the reference. + +- **The C API stopped naming a backend entirely.** `capi_backend.rs` is deleted and + `CapiVMHooksBuilder` implements `ExecutorRuntime` directly; see `PLAN-go-ffi.md` Phase 4c. + +- **The arming slot had to become a stack, early.** This was Stage 3's item, but Stage 0 + forced its core: resolving the execution per hook call means the slot must stay armed for + the whole call, where it used to be cleared as soon as the instance was built. Widening the + window naively breaks nesting — a nested `execute` would clear its caller's slot on the way + out — so `RuntimeRef::execute` now saves the previous value and restores it on `Drop`. The + saved values form the stack, held on the Rust call stack. What remains for Stage 3 is + deleting the slot in favour of reading the stack directly. + +- **The C ABI is unchanged, deliberately.** The opcode config has left the executor in Rust, + but `vm_exec_executor_config_t` stays and the executor merges it into every instance's + options. Moving it across the ABI is a coordinated change with the Go VM, and it rides with + the runtime-handle move rather than going separately. + +**Verification.** Executor workspace: 19 test binaries green, clippy clean, the C header diff +comment-only. Rust VM: the gas tests pass on both backends with their exact gas assertions +(`gas_used == 2045`), and 250 `basic-features` tests pass against compiled wasm through the +experimental executor. Go VM: the rebuilt dylib drops in with no Go changes and the +`VMEXECUTOR=wasmer6` failure set is identical before and after — 33 failures, same set, +compared by stashing the dylib. + +One check worth recording, because it nearly produced a false pass: the composability suite +(332 tests, contract-to-contract and recursive callers) runs on the **Debugger** by default, +not the experimental executor, so it stays green even with the context guard deliberately +broken. The suites that actually exercise this path are the ones calling +`ExecutorConfig::full_suite()` or `::Experimental`, and then only with `compiled-sc-tests` +active. Dropping the guard early makes `scenario-tester` fail with *no executor context +configured*, which is what establishes the guard is load-bearing. + **One cross-repo constraint on the C API.** Moving the runtime handle from per-instance to -per-executor requires **`PLAN-go-ffi.md` Phase 4b item 1** to land first. Go's per-instance +per-executor requires **`PLAN-go-ffi.md` Phase 4b's `MemLoad`/`MemStore` removal** to land +first. Go's per-instance `cgo.Handle` resolves to `instanceHooks`, which holds `activeState`; that slot's own comment says the nesting argument depends on it being per instance ("a nested call runs on a different instance"). It exists only because the VM still reaches wasm memory through -`executor.Instance` rather than through the state it was handed, which is exactly what 4b item -1 removes. Until then the C API keeps its per-instance handle and the capi keeps bypassing the +`executor.Instance` rather than through the state it was handed, which is exactly what that +item removes. Until then the C API keeps its per-instance handle and the capi keeps bypassing the `Executor` trait. The Rust-side stages above are not blocked by this; only the FFI change is. ## What this means for the Go VM diff --git a/c-api-common/libvmexeccapi_common.h b/c-api-common/libvmexeccapi_common.h index fc73793f..f5ce4971 100644 --- a/c-api-common/libvmexeccapi_common.h +++ b/c-api-common/libvmexeccapi_common.h @@ -241,6 +241,14 @@ typedef struct vm_exec_compilation_options_t { * * The opcode cost table is referenced rather than inlined, because it is large and the * caller already holds one. It is read during the call and not retained. + * + * On the Rust side the opcode config has moved off the executor and into + * `CompilationOptions`, where it belongs: it is execution logic, and it is half of what + * determines a compiled module. This struct survives because moving it across the ABI is a + * coordinated change with the Go VM, gated on the same step that makes the runtime handle + * per-executor. Until then the executor holds the config it is given and merges it into the + * options of every instance it creates, which is behaviour-identical — the Go VM already + * rebuilds the executor rather than mutating it when the gas schedule changes. */ typedef struct vm_exec_executor_config_t { /** diff --git a/c-api-common/src/capi_config.rs b/c-api-common/src/capi_config.rs index 8e4da713..fea1c5ba 100644 --- a/c-api-common/src/capi_config.rs +++ b/c-api-common/src/capi_config.rs @@ -5,6 +5,8 @@ //! `vm_exec_executor_set_vm_hooks_ptr` / `vm_exec_set_opcode_config` pair does not come //! along. +use std::sync::Arc; + use multiversx_chain_vm_executor::{CompilationOptions, OpcodeConfig, OpcodeVersion}; use crate::capi_opcode_cost::vm_exec_opcode_cost_t; @@ -13,6 +15,14 @@ use crate::capi_opcode_cost::vm_exec_opcode_cost_t; /// /// The opcode cost table is referenced rather than inlined, because it is large and the /// caller already holds one. It is read during the call and not retained. +/// +/// On the Rust side the opcode config has moved off the executor and into +/// `CompilationOptions`, where it belongs: it is execution logic, and it is half of what +/// determines a compiled module. This struct survives because moving it across the ABI is a +/// coordinated change with the Go VM, gated on the same step that makes the runtime handle +/// per-executor. Until then the executor holds the config it is given and merges it into the +/// options of every instance it creates, which is behaviour-identical — the Go VM already +/// rebuilds the executor rather than mutating it when the gas schedule changes. #[allow(non_camel_case_types)] #[repr(C)] pub struct vm_exec_executor_config_t { @@ -63,12 +73,16 @@ pub struct vm_exec_compilation_options_t { } impl vm_exec_compilation_options_t { - pub fn to_compilation_options(&self) -> CompilationOptions { + /// Completes the options with the opcode config the executor was created with. + /// + /// The config is not in the C struct yet; see `vm_exec_executor_config_t`. + pub fn to_compilation_options(&self, opcode_config: Arc) -> CompilationOptions { CompilationOptions { unmetered_locals: self.unmetered_locals as usize, max_memory_grow: self.max_memory_grow as usize, max_memory_grow_delta: self.max_memory_grow_delta as usize, opcode_trace: self.opcode_trace, + opcode_config, } } } diff --git a/c-api-common/src/capi_vm_hook_context.rs b/c-api-common/src/capi_vm_hook_context.rs index 09788ad4..cb3fac3f 100644 --- a/c-api-common/src/capi_vm_hook_context.rs +++ b/c-api-common/src/capi_vm_hook_context.rs @@ -34,7 +34,7 @@ use std::cell::RefCell; use std::ffi::c_void; -use multiversx_chain_vm_executor::VMHooks; +use multiversx_chain_vm_executor::{ExecutorRuntime, InstanceState, VMHooks}; use crate::capi_status::vm_exec_status_t; @@ -135,6 +135,18 @@ impl vm_exec_hook_context_t { } } +/// What the instance state is erased to, and reconstituted as. +/// +/// `&mut dyn InstanceState` is a fat pointer; what travels through the context is a thin +/// pointer *to* it, so the vtable goes along with the value instead of having to be recovered +/// by naming a backend's concrete state type. That is what lets this crate, and every state +/// function built on it, name no backend at all. +/// +/// The `'static` is a deliberate lie — the real state borrows the host-call frame and is +/// valid for exactly one hook invocation. Nothing may rely on it and nothing does: the token +/// check in [`with_instance_state`] is what establishes that the state is still alive. +pub type ErasedInstanceState = &'static mut (dyn InstanceState + 'static); + /// Borrows the instance state out of a context, after checking that the context is live. /// /// This is the one place the erased pointer is reconstituted, so it is the one place the @@ -169,8 +181,8 @@ pub unsafe fn with_instance_state( } // The caller names `S`, and is responsible for naming the same type the matching - // `with_hook_context` erased. Within one backend crate there is only one such type, which - // is what makes that obligation checkable by reading a single file. + // `with_hook_context` erased. There is exactly one such type — `ErasedInstanceState` — + // and it is backend-independent, so the obligation is discharged in one place. let mut state: &mut S = unsafe { &mut *(ctx.state as *mut S) }; Ok(f(&mut state)) } @@ -219,17 +231,26 @@ pub struct CapiVMHookContextPool { active_token: u64, } -/// Builds the VM hooks for one host call, and owns the context lifecycle. +/// The executor runtime behind the C API: builds the VM hooks for one host call, and owns +/// the context lifecycle. /// -/// Backend-independent: the state pointer it stores is erased, so nothing here names the -/// backend's state type. Each backend crate supplies the `with_vm_hooks` implementation that -/// its builder trait requires, naming its own state type at that one point. +/// Fully backend-independent. The state pointer it stores is erased and the caller hands it +/// in as `&mut dyn InstanceState`, so no backend type is named here or anywhere downstream. pub struct CapiVMHooksBuilder { go_vm_hooks: usize, hook_table: CapiVMHookTable, pool: RefCell, } +// Safety: `ExecutorRuntime` is `Send + Sync` because an executor is, and the only non-`Sync` +// field is the context pool. The pool is touched exactly where guarantee 3 at the top of this +// module already requires single-threaded access — inside a hook invocation, on the thread +// cgo has pinned the calling goroutine to — and a goroutine spawned inside a hook is +// forbidden from touching the context at all. This mirrors the same unsafe impl the Wasmer 6 +// backend puts on its own hooks wrapper, for the same reason. +unsafe impl Send for CapiVMHooksBuilder {} +unsafe impl Sync for CapiVMHooksBuilder {} + impl CapiVMHooksBuilder { pub fn new(go_vm_hooks: usize, hook_table: CapiVMHookTable) -> Self { CapiVMHooksBuilder { @@ -325,19 +346,22 @@ impl CapiVMHooksBuilder { } } -impl CapiVMHooksBuilder { +impl ExecutorRuntime for CapiVMHooksBuilder { /// Activates a context around `instance_state` and lends the generated hooks to `f`. /// - /// This is the whole of hook dispatch, and it names no backend: the state is erased on the - /// way in and reconstituted by `with_instance_state` on the way back. A backend crate - /// wraps this in the builder trait its executor expects, which is the one place its state - /// type appears. - pub fn with_vm_hooks_for_state( + /// This is the whole of hook dispatch. The state is erased on the way in and + /// reconstituted by [`with_instance_state`] on the way back; neither direction names a + /// backend. + fn with_vm_hooks( &self, - instance_state: &mut S, + instance_state: &mut dyn InstanceState, f: &mut dyn FnMut(&mut dyn VMHooks), ) { - self.with_hook_context(instance_state, |ctx| { + // Reborrowed into a local so that what gets erased is a thin pointer to the fat + // pointer, which is what `ErasedInstanceState` names on the way back out. + let mut erased: &mut dyn InstanceState = instance_state; + + self.with_hook_context(&mut erased, |ctx| { // The hooks borrow the context, so they live exactly as long as this activation // — which is also exactly as long as the instance state they reach through it. f(&mut CapiVMHooks::new(ctx, self.hook_table.func_pointers())); diff --git a/c-api-common/src/capi_vm_hook_table.rs b/c-api-common/src/capi_vm_hook_table.rs index d15fe3ab..17d700f9 100644 --- a/c-api-common/src/capi_vm_hook_table.rs +++ b/c-api-common/src/capi_vm_hook_table.rs @@ -18,7 +18,7 @@ pub struct CapiVMHookTable { } // The table is a block of plain function pointers with no interior mutability, and is only -// ever read. The `ExperimentalExecutorRuntimeRef` it ends up behind is `Send + Sync`, so this +// ever read. The `ExecutorRuntime` it ends up behind is `Send + Sync`, so this // has to be too. unsafe impl Send for CapiVMHookTable {} unsafe impl Sync for CapiVMHookTable {} diff --git a/c-api-wasmer6/src/capi_backend.rs b/c-api-wasmer6/src/capi_backend.rs deleted file mode 100644 index 6ce07a02..00000000 --- a/c-api-wasmer6/src/capi_backend.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Wiring this crate's backend into the shared hook dispatch. -//! -//! The one place the Wasmer 6 instance state is named. Everything else about hook dispatch — -//! the context, its pool, the table, the generated `VMHooks` implementation — lives in -//! `c-api-common` and knows the state only as an erased pointer. - -use multiversx_chain_vm_executor::VMHooks; -use multiversx_chain_vm_executor_c_api_common::capi_vm_hook_context::CapiVMHooksBuilder; -use multiversx_chain_vm_executor_c_api_common::capi_vm_hook_table::CapiVMHookTable; -use multiversx_chain_vm_executor_wasmer_experimental::{ - ExperimentalInstanceState, ExperimentalVMHooksBuilder, -}; - -/// The instance state type this backend erases into a hook context. -/// -/// `with_instance_state` has to be told the type back, and this is the single definition both -/// sides go through, so the two cannot drift. -pub type BackendInstanceState = ExperimentalInstanceState<'static>; - -/// Implements this backend's builder trait over the shared builder. -/// -/// A newtype rather than an implementation on `CapiVMHooksBuilder` directly, because with the -/// builder in `c-api-common` both the trait and the type are foreign here and the orphan rule -/// forbids it. The wrapper costs nothing and makes the seam explicit. -pub struct BackendVMHooksBuilder(CapiVMHooksBuilder); - -impl BackendVMHooksBuilder { - pub fn new(go_vm_hooks: usize, hook_table: CapiVMHookTable) -> Self { - BackendVMHooksBuilder(CapiVMHooksBuilder::new(go_vm_hooks, hook_table)) - } -} - -impl ExperimentalVMHooksBuilder for BackendVMHooksBuilder { - fn with_vm_hooks( - &self, - instance_state_ref: &mut ExperimentalInstanceState, - f: &mut dyn FnMut(&mut dyn VMHooks), - ) { - self.0.with_vm_hooks_for_state(instance_state_ref, f); - } -} diff --git a/c-api-wasmer6/src/capi_executor.rs b/c-api-wasmer6/src/capi_executor.rs index d0c222e5..aad10231 100644 --- a/c-api-wasmer6/src/capi_executor.rs +++ b/c-api-wasmer6/src/capi_executor.rs @@ -5,14 +5,16 @@ //! `vm_exec_set_opcode_config` and `vm_exec_executor_set_vm_hooks_ptr` setters have no //! counterpart. //! -//! Note that it does not wrap a `dyn Executor`. The new `Executor` trait's `new_instance` -//! takes only the wasm bytes and the compilation options, with the hooks coming from the -//! executor's own runtime reference — but this API needs one Go hooks object *per instance*, -//! because that is where the Go side keeps the per-instance early-exit slot. So instances are -//! built directly against the Wasmer 6 backend, and the per-instance pointer is supplied at -//! `vm_exec_w6_new_instance`. Converging the two backends' runtime-reference traits (Phase 1.6) -//! should give the hooks provider that per-instance argument, at which point this can go back -//! through the trait and serve both backends. +//! Note that it does not yet wrap a `dyn Executor`. An executor is built around one runtime +//! reference, for its whole life, and this API's runtime handle is still supplied per +//! instance at `vm_exec_w6_new_instance` — so the executor is built there, per instance, and +//! this struct holds the configuration it will be built from. The handle becomes +//! per-executor, and the `dyn Executor` moves here, once the Go VM stops reaching wasm memory +//! through `executor.Instance`; see `PLAN-go-ffi.md`. +//! +//! The opcode config is held here for the same reason. On the Rust side it has moved into +//! `CompilationOptions`, and this API merges the two at instance creation rather than +//! changing the C struct ahead of the Go VM. use libc::{c_char, c_int}; use meta::capi_safe_unwind; diff --git a/c-api-wasmer6/src/capi_instance.rs b/c-api-wasmer6/src/capi_instance.rs index a62d34e1..fed894bc 100644 --- a/c-api-wasmer6/src/capi_instance.rs +++ b/c-api-wasmer6/src/capi_instance.rs @@ -8,17 +8,18 @@ use libc::{c_char, c_int}; use meta::capi_safe_unwind; -use multiversx_chain_vm_executor::Instance; -use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalInstance; +use multiversx_chain_vm_executor::{Executor, Instance}; +use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; use std::ffi::CStr; use std::slice; +use std::sync::Arc; -use crate::capi_backend::BackendVMHooksBuilder; use crate::capi_call_result::vm_exec_call_result_t; use crate::capi_config::vm_exec_compilation_options_t; use crate::capi_executor::{CapiExecutor, vm_exec_w6_executor_t}; use crate::capi_status::{vm_exec_status_t, write_error}; use crate::capi_strings::{copy_pipe_joined, pipe_joined_length}; +use crate::capi_vm_hook_context::CapiVMHooksBuilder; /// Opaque handle to an instance. #[allow(non_camel_case_types)] @@ -26,7 +27,7 @@ use crate::capi_strings::{copy_pipe_joined, pipe_joined_length}; pub struct vm_exec_w6_instance_t; pub struct CapiInstance { - pub(crate) content: ExperimentalInstance, + pub(crate) content: Box, } /// Creates an instance. @@ -59,20 +60,22 @@ pub unsafe extern "C" fn vm_exec_w6_new_instance( let capi_executor = unsafe { &*(executor as *const CapiExecutor) }; let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_len as usize) }; - let compilation_options = unsafe { (*options).to_compilation_options() }; - - if let Err(err) = multiversx_chain_vm_executor::check_missing_wasm(wasm_bytes) { - return write_error(err, err_buf, err_buf_len); - } - - let builder = BackendVMHooksBuilder::new(go_vm_hooks, capi_executor.hook_table.clone()); - - let instance = match ExperimentalInstance::try_new_instance( - Box::new(builder), - capi_executor.opcode_config.clone(), - wasm_bytes, - &compilation_options, - ) { + let compilation_options = + unsafe { (*options).to_compilation_options(capi_executor.opcode_config.clone()) }; + + // One executor per instance, for as long as the runtime handle is per instance: the + // handle is what the executor is built around, so it cannot outlive the instance that + // owns it. Building it here costs nothing — an executor holds only the runtime — and it + // means this file goes through the `Executor` trait rather than around it. When the + // handle moves to `vm_exec_w6_new_executor`, the executor moves to `CapiExecutor` with + // it and this becomes a plain `new_instance` call. + let runtime = Arc::new(CapiVMHooksBuilder::new( + go_vm_hooks, + capi_executor.hook_table.clone(), + )); + let backend_executor = ExperimentalExecutor::new(runtime); + + let instance = match backend_executor.new_instance(wasm_bytes, &compilation_options) { Ok(instance) => instance, Err(err) => return write_error(err, err_buf, err_buf_len), }; diff --git a/c-api-wasmer6/src/capi_instance_state.rs b/c-api-wasmer6/src/capi_instance_state.rs index 37f5409e..321dab59 100644 --- a/c-api-wasmer6/src/capi_instance_state.rs +++ b/c-api-wasmer6/src/capi_instance_state.rs @@ -12,13 +12,12 @@ use libc::{c_char, c_int}; use meta::capi_safe_unwind; use multiversx_chain_vm_executor::{ - ExecutorError, InstanceState, MemLength, MemPtr, ServiceError, clamped_load_len, + ExecutorError, MemLength, MemPtr, ServiceError, clamped_load_len, }; use std::slice; -use crate::capi_backend::BackendInstanceState; use crate::capi_status::{vm_exec_status_t, write_error, write_error_count}; -use crate::capi_vm_hook_context::{vm_exec_hook_context_t, with_instance_state}; +use crate::capi_vm_hook_context::{ErasedInstanceState, vm_exec_hook_context_t, with_instance_state}; /// Loads from instance memory into a caller-provided buffer, **truncating** at the end of /// the memory. @@ -46,7 +45,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_load( err_buf_len: c_int, ) -> i64 { let loaded = unsafe { - with_instance_state::( + with_instance_state::( ctx, |state| -> Result { let memory_length = state.memory_length()?; @@ -97,7 +96,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_store( } let stored = unsafe { - with_instance_state::(ctx, |state| { + with_instance_state::(ctx, |state| { let data = if src_len == 0 { &[][..] } else { @@ -132,7 +131,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_length( } let length = unsafe { - with_instance_state::(ctx, |state| state.memory_length()) + with_instance_state::(ctx, |state| state.memory_length()) }; match length { @@ -168,7 +167,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_grow( } let grown = unsafe { - with_instance_state::(ctx, |state| state.memory_grow(by_num_pages)) + with_instance_state::(ctx, |state| state.memory_grow(by_num_pages)) }; match grown { @@ -201,7 +200,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_get_points_used( } let points = unsafe { - with_instance_state::(ctx, |state| state.get_points_used()) + with_instance_state::(ctx, |state| state.get_points_used()) }; match points { @@ -230,7 +229,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_set_points_used( err_buf_len: c_int, ) -> vm_exec_status_t { let set = unsafe { - with_instance_state::(ctx, |state| state.set_points_used(points)) + with_instance_state::(ctx, |state| state.set_points_used(points)) }; match set { diff --git a/c-api-wasmer6/src/lib.rs b/c-api-wasmer6/src/lib.rs index 9af3bd12..4ac5dd5a 100644 --- a/c-api-wasmer6/src/lib.rs +++ b/c-api-wasmer6/src/lib.rs @@ -21,7 +21,6 @@ mod capi_logger; -pub mod capi_backend; pub mod capi_executor; pub mod capi_instance; pub mod capi_instance_state; diff --git a/c-api-wasmer6/tests/ffi_boundary.rs b/c-api-wasmer6/tests/ffi_boundary.rs index 6f83b9f9..4637ae21 100644 --- a/c-api-wasmer6/tests/ffi_boundary.rs +++ b/c-api-wasmer6/tests/ffi_boundary.rs @@ -14,7 +14,9 @@ use std::ffi::{CString, c_char, c_int, c_void}; use std::sync::{Arc, Mutex, OnceLock}; -use multiversx_chain_vm_executor::{VMHooks, VMHooksDefault}; +use multiversx_chain_vm_executor::{ + Executor, ExecutorRuntime, InstanceState, VMHooks, VMHooksDefault, +}; use multiversx_chain_vm_executor_c_api_wasmer6::capi_call_result::{ vm_exec_call_result_kind_t, vm_exec_call_result_t, }; @@ -33,9 +35,7 @@ use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_context::{ }; use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_table::CapiVMHookTable; -use multiversx_chain_vm_executor_wasmer_experimental::{ - ExperimentalInstance, ExperimentalInstanceState, ExperimentalVMHooksBuilder, -}; +use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; /// One memory page, five known bytes at offset 0, and endpoints that call VM hooks. const CONTRACT_WAT: &str = r#" @@ -527,13 +527,18 @@ struct ProbingHooksBuilder { outcome: Arc>, } -impl ExperimentalVMHooksBuilder for ProbingHooksBuilder { +impl ExecutorRuntime for ProbingHooksBuilder { fn with_vm_hooks( &self, - instance_state_ref: &mut ExperimentalInstanceState, + instance_state: &mut dyn InstanceState, f: &mut dyn FnMut(&mut dyn VMHooks), ) { - let state_ptr = (instance_state_ref as *mut ExperimentalInstanceState<'_>).cast::(); + // Erased exactly as `CapiVMHooksBuilder` erases it — a thin pointer to the fat + // pointer — since the state functions under test reconstitute it as + // `ErasedInstanceState`. + let mut erased: &mut dyn InstanceState = instance_state; + let state_ptr = (&mut erased as *mut &mut dyn InstanceState).cast::(); + let mut outcome = self.outcome.lock().unwrap(); outcome.hook_calls += 1; @@ -736,21 +741,18 @@ fn state_ops() { outcome: outcome.clone(), }; - let mut instance = ExperimentalInstance::try_new_instance( - Box::new(builder), - Arc::new(Default::default()), - CONTRACT_WAT.as_bytes(), - // The same options as the C path, built directly: the FFI mirror is crate-private. - &multiversx_chain_vm_executor::CompilationOptions { - unmetered_locals: 0, - max_memory_grow: 10, - max_memory_grow_delta: 10, - opcode_trace: false, - }, - ) - .expect("instance creation failed"); + let mut instance = ExperimentalExecutor::new(Arc::new(builder)) + .new_instance( + CONTRACT_WAT.as_bytes(), + // The same options as the C path, built directly: the FFI mirror is crate-private. + &multiversx_chain_vm_executor::CompilationOptions { + max_memory_grow: 10, + max_memory_grow_delta: 10, + ..Default::default() + }, + ) + .expect("instance creation failed"); - use multiversx_chain_vm_executor::Instance; let result = instance.call("callGetGasLeft", 1_000_000); assert!( matches!(result, multiversx_chain_vm_executor::InstanceCallResult::Ok), diff --git a/vm-executor-experimental/src/lib.rs b/vm-executor-experimental/src/lib.rs index 788bf2d7..46d1b3e6 100644 --- a/vm-executor-experimental/src/lib.rs +++ b/vm-executor-experimental/src/lib.rs @@ -7,10 +7,8 @@ mod we_instance_state; mod we_executor; mod we_vm_hooks; -mod we_vm_hooks_builder; pub use we_error::ExperimentalError; pub use we_executor::*; pub use we_instance::*; pub use we_instance_state::*; -pub use we_vm_hooks_builder::*; diff --git a/vm-executor-experimental/src/we_executor.rs b/vm-executor-experimental/src/we_executor.rs index a81c3289..e37354dc 100644 --- a/vm-executor-experimental/src/we_executor.rs +++ b/vm-executor-experimental/src/we_executor.rs @@ -1,18 +1,12 @@ -use crate::{ExperimentalError, ExperimentalInstance, ExperimentalVMHooksBuilder}; +use crate::{ExperimentalError, ExperimentalInstance}; use multiversx_chain_vm_executor::{ - CompilationOptions, Executor, ExecutorError, Instance, OpcodeConfig, check_missing_wasm, + CompilationOptions, Executor, ExecutorError, ExecutorRuntime, Instance, check_missing_wasm, }; use std::{fmt, sync::Arc}; -pub trait ExperimentalExecutorRuntimeRef: Send + Sync { - fn vm_hooks_builder(&self) -> Box; - - fn opcode_config(&self) -> Arc; -} - /// Executor implementation that produces wasmer instances with correctly injected VM hooks from runtime. pub struct ExperimentalExecutor { - runtime_ref: Box, + runtime: Arc, } impl fmt::Debug for ExperimentalExecutor { @@ -22,36 +16,27 @@ impl fmt::Debug for ExperimentalExecutor { } impl ExperimentalExecutor { - pub fn new(runtime_ref: Box) -> Self { - ExperimentalExecutor { runtime_ref } + pub fn new(runtime: Arc) -> Self { + ExperimentalExecutor { runtime } } +} - fn new_instance_from_bytes( +impl Executor for ExperimentalExecutor { + fn new_instance( &self, wasm_bytes: &[u8], compilation_options: &CompilationOptions, ) -> Result, ExecutorError> { + check_missing_wasm(wasm_bytes)?; + let instance = ExperimentalInstance::try_new_instance( - self.runtime_ref.vm_hooks_builder(), - self.runtime_ref.opcode_config(), + self.runtime.clone(), wasm_bytes, compilation_options, )?; Ok(Box::new(instance)) } -} - -impl Executor for ExperimentalExecutor { - fn new_instance( - &self, - wasm_bytes: &[u8], - compilation_options: &CompilationOptions, - ) -> Result, ExecutorError> { - check_missing_wasm(wasm_bytes)?; - - self.new_instance_from_bytes(wasm_bytes, compilation_options) - } fn new_instance_from_cache( &self, diff --git a/vm-executor-experimental/src/we_instance.rs b/vm-executor-experimental/src/we_instance.rs index 4fe35430..a61ae100 100644 --- a/vm-executor-experimental/src/we_instance.rs +++ b/vm-executor-experimental/src/we_instance.rs @@ -2,12 +2,12 @@ use crate::middlewares::{ Breakpoints, Metering, OpcodeControl, OpcodeTracer, ProtectedGlobals, get_breakpoint_value, get_points_used, set_points_limit, }; -use crate::{ExperimentalError, ExperimentalVMHooksBuilder}; +use crate::ExperimentalError; use crate::{we_imports::generate_import_object, we_vm_hooks::VMHooksWrapper}; use log::trace; use multiversx_chain_vm_executor::{ - BreakpointValue, CompilationOptions, ExecutorError, Instance, InstanceCallResult, OpcodeConfig, - ServiceError, VMHooksEarlyExit, + BreakpointValue, CompilationOptions, ExecutorError, ExecutorRuntime, Instance, + InstanceCallResult, ServiceError, VMHooksEarlyExit, }; use rc_new_cyclic_fallible::rc_new_cyclic_fallible; use std::{rc::Rc, rc::Weak, sync::Arc}; @@ -37,7 +37,7 @@ impl ExperimentalInstanceInner { } fn prepare_wasmer_instance_inner( - vm_hooks_builder: Box, + runtime: Arc, module: &Module, store: &mut Store, weak: &Weak, @@ -45,7 +45,7 @@ fn prepare_wasmer_instance_inner( // Create an empty import object. trace!("Generating imports ..."); let vm_hooks_wrapper = VMHooksWrapper { - vm_hooks_builder, + runtime, wasmer_inner: weak.clone(), }; let import_object = generate_import_object(store, vm_hooks_wrapper); @@ -73,8 +73,7 @@ fn prepare_wasmer_instance_inner( impl ExperimentalInstance { pub fn try_new_instance( - vm_hooks_builder: Box, - opcode_config: Arc, + runtime: Arc, wasm_bytes: &[u8], compilation_options: &CompilationOptions, ) -> Result { @@ -82,7 +81,7 @@ impl ExperimentalInstance { let mut compiler = Singlepass::default(); // Push middlewares - push_middlewares(&mut compiler, compilation_options, opcode_config); + push_middlewares(&mut compiler, compilation_options); // Create the store let mut store: Store = Store::new(compiler); @@ -91,7 +90,7 @@ impl ExperimentalInstance { let module = Module::new(&store, wasm_bytes)?; let inner = rc_new_cyclic_fallible(|weak| { - prepare_wasmer_instance_inner(vm_hooks_builder, &module, &mut store, weak) + prepare_wasmer_instance_inner(runtime, &module, &mut store, weak) })?; Ok(ExperimentalInstance { @@ -148,11 +147,7 @@ fn validate_memory(memory: &wasmer::Memory, store: &wasmer::Store) -> Result<(), Ok(()) } -fn push_middlewares( - compiler: &mut Singlepass, - compilation_options: &CompilationOptions, - opcode_config: Arc, -) { +fn push_middlewares(compiler: &mut Singlepass, compilation_options: &CompilationOptions) { // Create breakpoints middleware let breakpoints_middleware = Arc::new(Breakpoints::new()); @@ -167,7 +162,7 @@ fn push_middlewares( // Create metering middleware let metering_middleware = Arc::new(Metering::new( compilation_options.unmetered_locals, - opcode_config, + compilation_options.opcode_config.clone(), breakpoints_middleware.clone(), )); diff --git a/vm-executor-experimental/src/we_instance_state.rs b/vm-executor-experimental/src/we_instance_state.rs index 3adc27be..6af062f2 100644 --- a/vm-executor-experimental/src/we_instance_state.rs +++ b/vm-executor-experimental/src/we_instance_state.rs @@ -36,7 +36,7 @@ impl ExperimentalInstanceState<'_> { } } -impl InstanceState for &'_ mut ExperimentalInstanceState<'_> { +impl InstanceState for ExperimentalInstanceState<'_> { fn get_points_used(&mut self) -> Result { let wasmer_inner = self.get_wasmer_inner()?; let points_used = get_points_used(&wasmer_inner.wasmer_instance, &mut self.store_mut)?; diff --git a/vm-executor-experimental/src/we_vm_hooks.rs b/vm-executor-experimental/src/we_vm_hooks.rs index 21339096..6c27dc0f 100644 --- a/vm-executor-experimental/src/we_vm_hooks.rs +++ b/vm-executor-experimental/src/we_vm_hooks.rs @@ -1,12 +1,14 @@ -use std::rc::Weak; +use std::{rc::Weak, sync::Arc}; -use multiversx_chain_vm_executor::{MemLength, MemPtr, VMHooks, VMHooksEarlyExit}; +use multiversx_chain_vm_executor::{ + ExecutorRuntime, MemLength, MemPtr, VMHooks, VMHooksEarlyExit, with_vm_hooks_once, +}; use wasmer::FunctionEnvMut; -use crate::{ExperimentalInstanceInner, ExperimentalInstanceState, ExperimentalVMHooksBuilder}; +use crate::{ExperimentalInstanceInner, ExperimentalInstanceState}; pub struct VMHooksWrapper { - pub vm_hooks_builder: Box, + pub runtime: Arc, pub wasmer_inner: Weak, } @@ -36,16 +38,8 @@ where store_mut: &mut store_mut, }; - // The builder lends the hooks through an `FnMut`, while `f` is an `FnOnce`, - // so it gets moved out on the (single) call. - let mut f = Some(f); - let mut result = None; - - data.vm_hooks_builder - .with_vm_hooks(&mut instance_state, &mut |vm_hooks| { - let f = f.take().expect("VM hooks builder called the closure twice"); - result = Some(f(vm_hooks)); - }); - - result.expect("VM hooks builder did not call the closure") + with_vm_hooks_once( + |lend| data.runtime.with_vm_hooks(&mut instance_state, lend), + f, + ) } diff --git a/vm-executor-experimental/src/we_vm_hooks_builder.rs b/vm-executor-experimental/src/we_vm_hooks_builder.rs deleted file mode 100644 index 8a5f705f..00000000 --- a/vm-executor-experimental/src/we_vm_hooks_builder.rs +++ /dev/null @@ -1,33 +0,0 @@ -use multiversx_chain_vm_executor::{VMHooks, VMHooksDefault}; - -use crate::ExperimentalInstanceState; - -/// Creates the VM hooks used to service one host function call. -/// -/// The hooks borrow the instance state, which is only reachable for the duration of the -/// call, so they cannot outlive it and cannot be returned to the caller. Rather than -/// heap-allocating a `Box` on every single hook invocation, implementations -/// build the hooks on their own stack frame and lend them to the given closure. -pub trait ExperimentalVMHooksBuilder { - /// Builds the VM hooks around the given instance state and passes them to `f`. - /// - /// Implementations must call `f` exactly once. - fn with_vm_hooks( - &self, - instance_state_ref: &mut ExperimentalInstanceState, - f: &mut dyn FnMut(&mut dyn VMHooks), - ); -} - -#[derive(Debug)] -pub struct ExperimentalVMHooksBuilderDefault; - -impl ExperimentalVMHooksBuilder for ExperimentalVMHooksBuilderDefault { - fn with_vm_hooks( - &self, - _instance_state_ref: &mut ExperimentalInstanceState, - f: &mut dyn FnMut(&mut dyn VMHooks), - ) { - f(&mut VMHooksDefault); - } -} diff --git a/vm-executor-experimental/tests/bulk_memory_metering_test.rs b/vm-executor-experimental/tests/bulk_memory_metering_test.rs index b163c21c..6380edd7 100644 --- a/vm-executor-experimental/tests/bulk_memory_metering_test.rs +++ b/vm-executor-experimental/tests/bulk_memory_metering_test.rs @@ -1,11 +1,10 @@ use std::sync::Arc; use multiversx_chain_vm_executor::{ - CompilationOptions, Instance, InstanceCallResult, OpcodeConfig, OpcodeCost, OpcodeVersion, -}; -use multiversx_chain_vm_executor_wasmer_experimental::{ - ExperimentalInstance, ExperimentalVMHooksBuilderDefault, + CompilationOptions, Executor, ExecutorRuntimeDefault, InstanceCallResult, OpcodeConfig, + OpcodeCost, OpcodeVersion, }; +use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; use wasmer::wat2wasm; const BULK_MEMORY_WAT: &[u8] = include_bytes!("bulk-memory.wat"); @@ -16,13 +15,6 @@ const MEMORY_COPY_PER_BYTE_COST: u32 = 7; const MEMORY_FILL_COST: u32 = 400; const MEMORY_FILL_PER_BYTE_COST: u32 = 11; -const COMPILATION_OPTIONS: CompilationOptions = CompilationOptions { - unmetered_locals: 0, - max_memory_grow: 0, - max_memory_grow_delta: 0, - opcode_trace: false, -}; - /// All opcodes are free, except the bulk memory ones, so that `points_used` after a call /// is exactly the cost of the single `memory.copy`/`memory.fill` in the called function. /// @@ -42,13 +34,14 @@ fn points_used_by(func_name: &str) -> u64 { }, }; - let mut instance = ExperimentalInstance::try_new_instance( - Box::new(ExperimentalVMHooksBuilderDefault), - Arc::new(opcode_config), - &wasm_bytes, - &COMPILATION_OPTIONS, - ) - .unwrap(); + let compilation_options = CompilationOptions { + opcode_config: Arc::new(opcode_config), + ..Default::default() + }; + + let mut instance = ExperimentalExecutor::new(Arc::new(ExecutorRuntimeDefault)) + .new_instance(&wasm_bytes, &compilation_options) + .unwrap(); assert!(matches!( instance.call(func_name, POINTS_LIMIT), diff --git a/vm-executor-experimental/tests/common/test_instance.rs b/vm-executor-experimental/tests/common/test_instance.rs index 559c2faa..5ba60d94 100644 --- a/vm-executor-experimental/tests/common/test_instance.rs +++ b/vm-executor-experimental/tests/common/test_instance.rs @@ -1,28 +1,15 @@ use std::sync::Arc; -use multiversx_chain_vm_executor::{CompilationOptions, Instance, OpcodeConfig}; -use multiversx_chain_vm_executor_wasmer_experimental::{ - ExperimentalInstance, ExperimentalVMHooksBuilderDefault, +use multiversx_chain_vm_executor::{ + CompilationOptions, Executor, ExecutorRuntimeDefault, Instance, }; +use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; use wasmer::wat2wasm; -const DUMMY_COMPILATION_OPTIONS: CompilationOptions = CompilationOptions { - unmetered_locals: 0, - max_memory_grow: 0, - max_memory_grow_delta: 0, - opcode_trace: false, -}; - pub fn test_instance(wat: &str) -> Box { let wasm_bytes = wat2wasm(wat.as_bytes()).unwrap(); - Box::new( - ExperimentalInstance::try_new_instance( - Box::new(ExperimentalVMHooksBuilderDefault), - Arc::new(OpcodeConfig::default()), - &wasm_bytes, - &DUMMY_COMPILATION_OPTIONS, - ) - .unwrap(), - ) + ExperimentalExecutor::new(Arc::new(ExecutorRuntimeDefault)) + .new_instance(&wasm_bytes, &CompilationOptions::default()) + .unwrap() } diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs index 2160c891..55f9641b 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs @@ -1,5 +1,3 @@ -use multiversx_chain_vm_executor::OpcodeConfig; - use crate::WasmerInstance; use crate::executor_interface::{ CompilationOptions, Executor, ExecutorError, Instance, VMHooksLegacy, check_missing_wasm, @@ -13,10 +11,24 @@ use std::{ use super::{WasmerExecutorError, WasmerProdInstance, WasmerProdInstanceState}; +/// The host behind a Wasmer 2.2 executor. +/// +/// This is deliberately **not** [`multiversx_chain_vm_executor::ExecutorRuntime`], and the +/// difference is in the shape of the legacy hooks rather than in the design. `ExecutorRuntime` +/// *lends* hooks per host call; the legacy `VMHooksLegacy` is an **object**, built once and +/// installed in the import object for the instance's life, because its methods return plain +/// values and have nowhere to report an early exit — the hooks must therefore own a place to +/// stash one. So the runtime hands over a built object here, and the execution it serves is +/// whatever was current when the instance was created. +/// +/// The practical consequence is that instances from this backend **cannot be reused across +/// executions**, which is exactly what Wasmer 6 is being converged for and why that backend +/// gets the lending trait. Closing the gap would mean regenerating `VMHooksLegacyAdapter` +/// around a lender and giving the early-exit slot a home outside the hooks; it is not worth +/// doing to a backend that is scheduled for deletion, and whose value until then is being an +/// unchanged reference for the differential tests. pub trait WasmerProdRuntimeRef: Send + Sync { fn vm_hooks(&self, instance_state: WasmerProdInstanceState) -> Box; - - fn opcode_config(&self) -> Arc>; } /// Executor implementation that produces wasmer instances with correctly injected VM hooks from runtime. @@ -40,13 +52,19 @@ impl WasmerProdExecutor { wasm_bytes: &[u8], compilation_options: &CompilationOptions, ) -> Result, ExecutorError> { + // The 2.2 middleware wants the config behind a `Mutex`, which was how the gas + // schedule used to be swapped under a live executor. Nothing mutates it any more: + // the config now arrives per instance in the compilation options, so this is a + // private copy that no one else holds a handle to. + let opcode_config = Arc::new(Mutex::new((*compilation_options.opcode_config).clone())); + let inner_instance_ref = rc_new_cyclic_fallible(|weak| { let instance_state = WasmerProdInstanceState::new(weak.clone()); let vm_hooks = self.runtime_ref.vm_hooks(instance_state); WasmerInstance::try_new_instance( Rc::from(vm_hooks), - self.runtime_ref.opcode_config(), + opcode_config.clone(), wasm_bytes, &compilation_options.to_legacy(), ) diff --git a/vm-executor-wasmer/tests/opcode_version_test.rs b/vm-executor-wasmer/tests/opcode_version_test.rs index c9b453e5..a3b44630 100644 --- a/vm-executor-wasmer/tests/opcode_version_test.rs +++ b/vm-executor-wasmer/tests/opcode_version_test.rs @@ -7,19 +7,12 @@ use wasmer::wat2wasm; const MEMORY_COPY_WAT: &[u8] = include_bytes!("memory-copy.wat"); -const DUMMY_COMPILATION_OPTIONS: CompilationOptions = CompilationOptions { - unmetered_locals: 0, - max_memory_grow: 0, - max_memory_grow_delta: 0, - opcode_trace: false, -}; - #[test] fn opcode_version_test() { let wasm_bytes = wat2wasm(MEMORY_COPY_WAT).unwrap(); let mut executor = WasmerExecutor::new(Box::new(VMHooksLegacyDefault)); - let result = executor.new_instance(&wasm_bytes, &DUMMY_COMPILATION_OPTIONS.to_legacy()); + let result = executor.new_instance(&wasm_bytes, &CompilationOptions::default().to_legacy()); assert!(result.is_err()); executor @@ -29,6 +22,6 @@ fn opcode_version_test() { }) .unwrap(); - let result = executor.new_instance(&wasm_bytes, &DUMMY_COMPILATION_OPTIONS.to_legacy()); + let result = executor.new_instance(&wasm_bytes, &CompilationOptions::default().to_legacy()); assert!(result.is_ok()); } diff --git a/vm-executor/src/new_traits.rs b/vm-executor/src/new_traits.rs index 531c4859..3d8958a6 100644 --- a/vm-executor/src/new_traits.rs +++ b/vm-executor/src/new_traits.rs @@ -1,6 +1,7 @@ mod breakpoint_value_new; mod compilation_options_new; mod executor_new; +mod executor_runtime; mod instance_new; mod instance_state; mod memory; @@ -11,6 +12,7 @@ mod vm_hooks_new; pub use breakpoint_value_new::{BreakpointValue, UnknownBreakpointValueError}; pub use compilation_options_new::CompilationOptions; pub use executor_new::Executor; +pub use executor_runtime::{ExecutorRuntime, ExecutorRuntimeDefault, with_vm_hooks_once}; pub use instance_new::{Instance, InstanceCallResult}; pub use instance_state::InstanceState; pub use memory::{MemoryError, checked_load_range, checked_store_range, clamped_load_len}; diff --git a/vm-executor/src/new_traits/compilation_options_new.rs b/vm-executor/src/new_traits/compilation_options_new.rs index 207daf8f..8072144b 100644 --- a/vm-executor/src/new_traits/compilation_options_new.rs +++ b/vm-executor/src/new_traits/compilation_options_new.rs @@ -1,10 +1,31 @@ -use crate::CompilationOptionsLegacy; +use std::sync::Arc; +use crate::{CompilationOptionsLegacy, OpcodeConfig}; + +/// Everything that determines how a wasm module is compiled. +/// +/// Passed to [`crate::Executor::new_instance`] on every instance, rather than configured on +/// the executor: these are essential execution logic, identical for every instance the host +/// cares about, and a compiled module is only interchangeable with another compiled from the +/// same options. Keeping them here means the pair (wasm, options) fully determines the +/// result, which is what lets a module cache be keyed on it. +/// +/// That includes the opcode costs, which used to sit on the executor behind a mutable setter. +/// A gas schedule change is therefore a new set of options — and, for a host that caches, +/// a different cache key — rather than a reconfigured executor. +#[derive(Clone, Debug, Default)] pub struct CompilationOptions { pub unmetered_locals: usize, pub max_memory_grow: usize, pub max_memory_grow_delta: usize, pub opcode_trace: bool, + + /// The opcode set and the per-opcode gas costs the metering middleware is built with. + /// + /// Shared rather than owned: the cost table is some hundreds of bytes and every instance + /// under one gas schedule uses the same one, so the host builds it once and the + /// middleware keeps this handle. + pub opcode_config: Arc, } impl CompilationOptions { diff --git a/vm-executor/src/new_traits/executor_new.rs b/vm-executor/src/new_traits/executor_new.rs index f2bd2c3a..5cd846ab 100644 --- a/vm-executor/src/new_traits/executor_new.rs +++ b/vm-executor/src/new_traits/executor_new.rs @@ -1,5 +1,15 @@ use crate::{CompilationOptions, ExecutorError, Instance}; +/// Creates instances, and nothing else. +/// +/// An executor is a factory and an instance-type selector. It holds only what is constant +/// across every instance it will ever make: the [`crate::ExecutorRuntime`] behind it, and +/// whatever plumbing the backend itself needs, such as logging. Everything that is execution +/// logic — the opcode costs, the memory limits — arrives per instance, in +/// [`CompilationOptions`]. +/// +/// There are no setters. A host that needs different compilation options passes different +/// options; a host that needs a different runtime builds a different executor. pub trait Executor { /// Creates a new VM executor instance. fn new_instance( diff --git a/vm-executor/src/new_traits/executor_runtime.rs b/vm-executor/src/new_traits/executor_runtime.rs new file mode 100644 index 00000000..77acdd2e --- /dev/null +++ b/vm-executor/src/new_traits/executor_runtime.rs @@ -0,0 +1,68 @@ +use crate::{InstanceState, VMHooks, VMHooksDefault}; + +/// The host behind an executor: one reference, for the executor's whole life. +/// +/// This is the only thing an executor needs from its host that is not in the wasm and not in +/// the compilation options. It is supplied once, at executor construction, and never +/// replaced — which is what lets an executor be configured at once and then treated as +/// immutable. +pub trait ExecutorRuntime: Send + Sync { + /// Builds the VM hooks servicing one host call, around the instance state for that call, + /// and lends them to `f` — which implementations must call exactly once. + /// + /// **Built per call, not per instance.** Under Wasmer 6 the instance state is only + /// reachable for the duration of a call — memory and globals live behind the `StoreMut` + /// the host function is handed — so hooks that hold it cannot outlive the call and + /// cannot be returned to the caller. Lending rather than returning also avoids a + /// `Box` allocation on every single hook invocation. + /// + /// **Which execution the hooks serve is resolved here**, by the host, from its own call + /// stack — not captured when the instance was created. That is the property that makes a + /// cached instance safe to reuse across transactions: the instance contributes the + /// state, never the identity. + /// + /// The state arrives as `&mut dyn InstanceState` so that one runtime serves every + /// backend. + fn with_vm_hooks( + &self, + instance_state: &mut dyn InstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ); +} + +/// Bridges a `with_vm_hooks`-style lender to a caller that has an `FnOnce` and wants its +/// result back. +/// +/// The lending side takes an `FnMut`, because [`ExecutorRuntime`] has to stay object-safe, +/// while servicing a single hook is naturally an `FnOnce` returning a value. Every caller of +/// a runtime needs the same bridge, so it lives here rather than in each of them. +/// +/// Panics if the lender breaks the contract of calling the closure exactly once. +pub fn with_vm_hooks_once( + lend: impl FnOnce(&mut dyn FnMut(&mut dyn VMHooks)), + f: impl FnOnce(&mut dyn VMHooks) -> R, +) -> R { + let mut f = Some(f); + let mut result = None; + + lend(&mut |vm_hooks| { + let f = f.take().expect("executor runtime called the closure twice"); + result = Some(f(vm_hooks)); + }); + + result.expect("executor runtime did not call the closure") +} + +/// A runtime that yields hooks doing nothing, for instances that never call into the host. +#[derive(Debug)] +pub struct ExecutorRuntimeDefault; + +impl ExecutorRuntime for ExecutorRuntimeDefault { + fn with_vm_hooks( + &self, + _instance_state: &mut dyn InstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ) { + f(&mut VMHooksDefault); + } +} diff --git a/vm-executor/src/new_traits/instance_state.rs b/vm-executor/src/new_traits/instance_state.rs index 26a62102..5157ca38 100644 --- a/vm-executor/src/new_traits/instance_state.rs +++ b/vm-executor/src/new_traits/instance_state.rs @@ -64,3 +64,56 @@ pub trait InstanceState { self.memory_load_owned(mem_ptr, to_load as MemLength) } } + +/// A mutable reference to an instance state is itself an instance state. +/// +/// This is what lets [`crate::ExecutorRuntime::with_vm_hooks`] hand the state over as +/// `&mut dyn InstanceState` and have the host pass it straight on to code that is generic +/// over `S: InstanceState`. Without it, every host would need its own forwarding wrapper, and +/// the backend's concrete state type would leak back into the host — which is the thing the +/// erased state exists to prevent. +/// +/// `?Sized` so that `&mut dyn InstanceState` is covered, not merely `&mut ConcreteState`. +impl InstanceState for &mut T { + fn get_points_used(&mut self) -> Result { + (**self).get_points_used() + } + + fn set_points_used(&mut self, points: u64) -> Result<(), ExecutorError> { + (**self).set_points_used(points) + } + + fn memory_length(&self) -> Result { + (**self).memory_length() + } + + fn memory_grow(&mut self, by_num_pages: u32) -> Result { + (**self).memory_grow(by_num_pages) + } + + fn memory_load_to_slice(&self, mem_ptr: MemPtr, dest: &mut [u8]) -> Result<(), ExecutorError> { + (**self).memory_load_to_slice(mem_ptr, dest) + } + + fn memory_load_owned( + &self, + mem_ptr: MemPtr, + mem_length: MemLength, + ) -> Result, ExecutorError> { + (**self).memory_load_owned(mem_ptr, mem_length) + } + + fn memory_store(&self, mem_ptr: MemPtr, data: &[u8]) -> Result<(), ExecutorError> { + (**self).memory_store(mem_ptr, data) + } + + /// Forwarded rather than left to the default, so that a state which overrides the + /// clamping rules keeps its own behaviour when reached through a reference. + fn memory_load_clamped( + &self, + mem_ptr: MemPtr, + mem_length: MemLength, + ) -> Result, ExecutorError> { + (**self).memory_load_clamped(mem_ptr, mem_length) + } +} From abd1b07b9012c18bd002051df42e2e995861a1a2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 8 Oct 2026 17:59:35 +0300 Subject: [PATCH 18/26] new FFI - VMHooksLegacyAdapter simplified --- PLAN-go-ffi.md | 69 ++++++++++++------ PLAN-rust-vm-caching.md | 18 ++++- vm-executor-wasmer/src/new_traits.rs | 2 +- .../src/new_traits/wasmer_prod_executor.rs | 42 ++++------- .../new_traits/wasmer_prod_instance_state.rs | 33 +++++---- vm-executor/src/new_traits.rs | 2 +- .../src/new_traits/executor_runtime.rs | 46 +++++++++++- vm-executor/src/new_traits/instance_state.rs | 42 ++++++++++- .../src/new_traits/vm_hooks_legacy_adapter.rs | 70 +++++++++++++------ 9 files changed, 236 insertions(+), 88 deletions(-) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 28f1ef3d..25ebf5fd 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -460,22 +460,44 @@ The `-Bsymbolic-functions` work is what takes its place on the critical path. ### What the wasmer 2.2 path needs -`WasmerProdExecutor` already implements the new `Executor` trait, so the gap is in the hooks, -not the instance: - -- `WasmerProdRuntimeRef::vm_hooks` returns `Box` and `opcode_config()` - returns `Arc>`, where the Wasmer 6 side returns a hooks *builder* and a - plain `Arc`. Converging the two (**Phase 1.6**) is therefore a prerequisite for this path, - not an optional tidy-up. -- The generated `CapiVMHooks` is new-style and borrows a per-call context. Wasmer 2.2 asks for - the hooks once per instance. Rather than generating a second 277-method implementation, the - capi should reuse the one it has behind `VMHooksLegacyAdapter`: that adapter already wraps - every hook call, so a capi-specific `VMHooksSetEarlyExit` can **activate a hook context - around each call** and build `CapiVMHooks` there. One generated implementation, two - activation paths, and the Go side cannot tell which backend it is talking to. -- `set_early_exit` on that path must do what `mx-sdk-rs/chain/wasmer-prod/src/wasmer_prod.rs` - does: store the exit *and* trip the breakpoint global, because under 2.2 the global is still - the mechanical halt. See `PLAN-go-breakpoint-removal.md` §3. +**Resolved by Stage 0.** `WasmerProdExecutor` already implemented the new `Executor` trait; +the gap was in the hooks, and it is now closed. For the record, what it took: + +- `WasmerProdRuntimeRef` is **deleted**. Both backends take `Arc`. Its + two divergences went separately: `opcode_config()` left with the fold into + `CompilationOptions`, and `vm_hooks()` with the adapter below. +- The generated `CapiVMHooks` is new-style and borrows a per-call context, while Wasmer 2.2 + asks for the hooks once per instance. Rather than generate a second 277-method + implementation, `VMHooksLegacyAdapter` was **regenerated over the two interfaces that + already exist**: it holds an `R: ExecutorRuntime` and an `S: InstanceState`, and activates a + hook context around *each* call instead of holding one hooks object for the instance. Only + the generator's header template changed — all 277 generated bodies are byte-identical. One + generated implementation, two activation paths, and the Go side cannot tell which backend + it is talking to. + + An intermediate `VMHooksLegacyLender` trait was tried first and **removed**: it described a + capability the backend already had, so every backend owed a newtype implementing it. Taking + the runtime and the state directly costs two blanket impls instead — `Arc` is an + `ExecutorRuntime`, `&mut T` is an `InstanceState` — which are reusable, and `WasmerProdLender` + vanished with it. +- Early exit on that path stores the exit *and* trips the breakpoint global, because under + 2.2 the global is still the mechanical halt. Both halves are owned by the instance, so they + are one defaulted method on `InstanceState` — `handle_early_exit_legacy` — whose default is + a failure, since for any other backend reaching it means an early exit is about to be + dropped. The **stash moved into the executor**; only the mapping stayed with the host, as + the defaulted `ExecutorRuntime::early_exit_breakpoint`, because the code in a + `VMHooksEarlyExit` is the + host VM's own `ReturnCode`. Go implements nothing — its hooks set the breakpoint themselves. + See `PLAN-go-breakpoint-removal.md` §3. + +**One finding worth carrying forward.** The breakpoint *value* on this path is currently +unobservable: `WasmerProdInstance::call` checks `take_early_exit()` before it ever reads the +global, the breakpoints middleware traps on `!= None` without inspecting the value, and the +legacy capi never goes through the adapter at all. Verified by deliberately flattening the +mapping to `ExecutionFailed` — the prod gas tests, including the OutOfGas and UserError cases, +still pass. The mapping was kept anyway, because 2.2's remaining job is to be an *unchanged* +reference and the differential that would validate the change does not exist yet. Deleting +`early_exit_breakpoint` is a safe follow-up once 2.2-vs-2.2 is green. ## Phase 4 — Go VM @@ -902,12 +924,15 @@ opaque handles and functions → `vm_exec_w6_*`.** `capi_instance_state.rs`, ~700 lines) are still per-backend, because each needs its own symbol prefix. `c-api-wasmer2` will duplicate them unless they are generated from a macro parameterised by prefix — worth deciding when that crate is written, not before. -2. **`c-api-wasmer2`.** Wasmer 2.2 through the new interfaces. This is what puts **Phase 1.6 - back on the critical path**: `WasmerProdRuntimeRef::vm_hooks` returns - `Box` and `Arc>` where the Wasmer 6 side returns a - builder and a plain `Arc`, and the two have to converge before one crate body can serve - both. The `VMHooksLegacyAdapter` activation path described under "What the wasmer 2.2 path - needs" is the other half. +2. **`c-api-wasmer2`.** Wasmer 2.2 through the new interfaces. **No longer blocked** — Stage 0 + closed both halves: `WasmerProdRuntimeRef` is gone and `VMHooksLegacyAdapter` is + built directly on `ExecutorRuntime` + `InstanceState`, so one crate body can serve both + backends. What is left is mechanical: the + three `#[no_mangle]` entry-point files under a `vm_exec_w2_` prefix plus the one-line + `capi_logger` re-export. `capi_instance_state.rs` is now *character-identical* to the + Wasmer 6 copy apart from the prefix, since neither names a backend any more — which is what + makes generating the three from a prefix-parameterised macro worth deciding now rather than + duplicating a third time. 3. **`wasmercommon` in Go**, wrapping the common header: the error buffer, the status mapping, the call result and the hook result, so `wasmer2` and `wasmer6` share them instead of each carrying a copy. Worth doing *after* `c-api-common`, since the Go types mirror it. diff --git a/PLAN-rust-vm-caching.md b/PLAN-rust-vm-caching.md index 35e7312d..3979d822 100644 --- a/PLAN-rust-vm-caching.md +++ b/PLAN-rust-vm-caching.md @@ -365,6 +365,21 @@ the sketch: - **The C API stopped naming a backend entirely.** `capi_backend.rs` is deleted and `CapiVMHooksBuilder` implements `ExecutorRuntime` directly; see `PLAN-go-ffi.md` Phase 4c. +- **Both backends converged, not just Wasmer 6.** `WasmerProdRuntimeRef` is deleted; + `WasmerProdExecutor` takes the same `Arc`. The obstacle was that legacy + `VMHooksLegacy` is an *object* built once per instance, with nowhere to report an early exit, + while the runtime *lends* hooks per call. Resolved by regenerating `VMHooksLegacyAdapter` + over `R: ExecutorRuntime` + `S: InstanceState` — header template only, all 277 bodies + byte-identical — so 2.2 now resolves the execution when the hook runs, exactly as Wasmer 6 + does. The stash-and-halt half is a defaulted `InstanceState::handle_early_exit_legacy`, + which fails for every backend that does not implement it; a probe removing the 2.2 override + fails exactly the OutOfGas and UserError tests and leaves the no-early-exit one passing. This was not optional tidying: + `c-api-common`'s shared runtime *is* an `ExecutorRuntime`, so a 2.2 capi built on it could + not have existed otherwise. In the Rust VM it deleted `RuntimeRefAdapter`, + `WasmerProdInstanceStateAdapter` and the whole `InstanceStateSetEarlyExit` / + `VMHooksSetEarlyExit` pair, and `impl ExecutorRuntime for RuntimeWeakRef` moved out of the + `wasmer-experimental` feature gate to serve both. + - **The arming slot had to become a stack, early.** This was Stage 3's item, but Stage 0 forced its core: resolving the execution per hook call means the slot must stay armed for the whole call, where it used to be cleared as soon as the instance was built. Widening the @@ -379,7 +394,8 @@ the sketch: the runtime-handle move rather than going separately. **Verification.** Executor workspace: 19 test binaries green, clippy clean, the C header diff -comment-only. Rust VM: the gas tests pass on both backends with their exact gas assertions +comment-only. The 2.2 backend passes its gas tests unchanged, including the OutOfGas and +UserError early-exit cases that exercise the new adapter path. Rust VM: the gas tests pass on both backends with their exact gas assertions (`gas_used == 2045`), and 250 `basic-features` tests pass against compiled wasm through the experimental executor. Go VM: the rebuilt dylib drops in with no Go changes and the `VMEXECUTOR=wasmer6` failure set is identical before and after — 33 failures, same set, diff --git a/vm-executor-wasmer/src/new_traits.rs b/vm-executor-wasmer/src/new_traits.rs index b17a4da6..64d0c171 100644 --- a/vm-executor-wasmer/src/new_traits.rs +++ b/vm-executor-wasmer/src/new_traits.rs @@ -4,6 +4,6 @@ mod wasmer_prod_instance; mod wasmer_prod_instance_state; pub use wasmer_error::WasmerExecutorError; -pub use wasmer_prod_executor::{WasmerProdExecutor, WasmerProdRuntimeRef}; +pub use wasmer_prod_executor::WasmerProdExecutor; pub use wasmer_prod_instance::WasmerProdInstance; pub use wasmer_prod_instance_state::WasmerProdInstanceState; diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs index 55f9641b..f61ef0dc 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs @@ -1,6 +1,7 @@ use crate::WasmerInstance; use crate::executor_interface::{ - CompilationOptions, Executor, ExecutorError, Instance, VMHooksLegacy, check_missing_wasm, + CompilationOptions, Executor, ExecutorError, ExecutorRuntime, Instance, VMHooksLegacyAdapter, + check_missing_wasm, }; use rc_new_cyclic_fallible::rc_new_cyclic_fallible; use std::{ @@ -11,29 +12,9 @@ use std::{ use super::{WasmerExecutorError, WasmerProdInstance, WasmerProdInstanceState}; -/// The host behind a Wasmer 2.2 executor. -/// -/// This is deliberately **not** [`multiversx_chain_vm_executor::ExecutorRuntime`], and the -/// difference is in the shape of the legacy hooks rather than in the design. `ExecutorRuntime` -/// *lends* hooks per host call; the legacy `VMHooksLegacy` is an **object**, built once and -/// installed in the import object for the instance's life, because its methods return plain -/// values and have nowhere to report an early exit — the hooks must therefore own a place to -/// stash one. So the runtime hands over a built object here, and the execution it serves is -/// whatever was current when the instance was created. -/// -/// The practical consequence is that instances from this backend **cannot be reused across -/// executions**, which is exactly what Wasmer 6 is being converged for and why that backend -/// gets the lending trait. Closing the gap would mean regenerating `VMHooksLegacyAdapter` -/// around a lender and giving the early-exit slot a home outside the hooks; it is not worth -/// doing to a backend that is scheduled for deletion, and whose value until then is being an -/// unchanged reference for the differential tests. -pub trait WasmerProdRuntimeRef: Send + Sync { - fn vm_hooks(&self, instance_state: WasmerProdInstanceState) -> Box; -} - /// Executor implementation that produces wasmer instances with correctly injected VM hooks from runtime. pub struct WasmerProdExecutor { - runtime_ref: Box, + runtime: Arc, } impl fmt::Debug for WasmerProdExecutor { @@ -43,8 +24,8 @@ impl fmt::Debug for WasmerProdExecutor { } impl WasmerProdExecutor { - pub fn new(runtime_ref: Box) -> Self { - WasmerProdExecutor { runtime_ref } + pub fn new(runtime: Arc) -> Self { + WasmerProdExecutor { runtime } } fn new_instance_from_bytes( @@ -59,11 +40,18 @@ impl WasmerProdExecutor { let opcode_config = Arc::new(Mutex::new((*compilation_options.opcode_config).clone())); let inner_instance_ref = rc_new_cyclic_fallible(|weak| { - let instance_state = WasmerProdInstanceState::new(weak.clone()); - let vm_hooks = self.runtime_ref.vm_hooks(instance_state); + // The legacy import object wants a hooks object for the instance's life, while + // the runtime lends hooks per call; the adapter is the join. The hooks an + // instance calls are therefore built fresh each time, around this instance's + // state, so they serve whichever execution the host has current — never the one + // that happened to be current when the instance was created. + let vm_hooks = VMHooksLegacyAdapter::new( + self.runtime.clone(), + WasmerProdInstanceState::new(weak.clone()), + ); WasmerInstance::try_new_instance( - Rc::from(vm_hooks), + Rc::new(vm_hooks), opcode_config.clone(), wasm_bytes, &compilation_options.to_legacy(), diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs index 412a9d65..563047e4 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs @@ -24,19 +24,6 @@ impl WasmerProdInstanceState { .map_or_else(|| Err(WasmerExecutorError::BadInstancePointer.into()), Ok) } - pub fn set_breakpoint_value_legacy( - &self, - value: BreakpointValueLegacy, - ) -> Result<(), ExecutorError> { - self.instance_rc()? - .set_breakpoint_value(value) - .map_err(|err| WasmerExecutorError::WrappedInstance(err).into()) - } - - pub fn set_early_exit(&self, early_exit: VMHooksEarlyExit) -> Result<(), ExecutorError> { - self.instance_rc()?.set_early_exit(early_exit); - Ok(()) - } } impl InstanceState for WasmerProdInstanceState { @@ -82,4 +69,24 @@ impl InstanceState for WasmerProdInstanceState { fn memory_store(&self, mem_ptr: MemPtr, data: &[u8]) -> Result<(), ExecutorError> { self.instance_rc()?.memory_store(mem_ptr, data) } + + /// The only backend that implements this: 2.2 is where legacy hooks run. + fn handle_early_exit_legacy( + &mut self, + early_exit: VMHooksEarlyExit, + breakpoint: BreakpointValueLegacy, + ) -> Result<(), ExecutorError> { + let instance_rc = self.instance_rc()?; + + // Trip the breakpoint global first: under 2.2 that is the mechanical halt, and it is + // what stops the contract after the current instruction. + instance_rc + .set_breakpoint_value(breakpoint) + .map_err(WasmerExecutorError::WrappedInstance)?; + + // Then stash the exit, for the error handler to pick up once the call unwinds. + instance_rc.set_early_exit(early_exit); + + Ok(()) + } } diff --git a/vm-executor/src/new_traits.rs b/vm-executor/src/new_traits.rs index 3d8958a6..d4000f3a 100644 --- a/vm-executor/src/new_traits.rs +++ b/vm-executor/src/new_traits.rs @@ -17,5 +17,5 @@ pub use instance_new::{Instance, InstanceCallResult}; pub use instance_state::InstanceState; pub use memory::{MemoryError, checked_load_range, checked_store_range, clamped_load_len}; pub use vm_hooks_early_exit::VMHooksEarlyExit; -pub use vm_hooks_legacy_adapter::{VMHooksLegacyAdapter, VMHooksSetEarlyExit}; +pub use vm_hooks_legacy_adapter::VMHooksLegacyAdapter; pub use vm_hooks_new::{VMHooks, VMHooksDefault}; diff --git a/vm-executor/src/new_traits/executor_runtime.rs b/vm-executor/src/new_traits/executor_runtime.rs index 77acdd2e..d8df77fa 100644 --- a/vm-executor/src/new_traits/executor_runtime.rs +++ b/vm-executor/src/new_traits/executor_runtime.rs @@ -1,4 +1,6 @@ -use crate::{InstanceState, VMHooks, VMHooksDefault}; +use std::sync::Arc; + +use crate::{BreakpointValueLegacy, InstanceState, VMHooks, VMHooksDefault, VMHooksEarlyExit}; /// The host behind an executor: one reference, for the executor's whole life. /// @@ -28,6 +30,48 @@ pub trait ExecutorRuntime: Send + Sync { instance_state: &mut dyn InstanceState, f: &mut dyn FnMut(&mut dyn VMHooks), ); + + /// Which breakpoint should halt the instance when a hook signals an early exit. + /// + /// **Only the legacy Wasmer 2.2 backend asks.** Its hooks return plain values, with no + /// channel for an early exit, so the executor has to stash the exit and trip the + /// instance's breakpoint global to stop it. Wasmer 6 hooks return the early exit directly + /// and never call this. + /// + /// Stashing is the executor's job; only the mapping is the host's, and it has to be, + /// because the code inside a [`VMHooksEarlyExit`] is the host VM's own return code and + /// means nothing to the executor. A host whose hooks already set the breakpoint + /// themselves — the Go VM does — wants the default and implements nothing. + /// + /// Defaulted rather than split into a second, legacy-only trait so that one `impl` serves + /// both backends. A separate trait would be foreign to `c-api-common`, where the shared + /// runtime lives, and the orphan rule would force a newtype back into every backend crate. + fn early_exit_breakpoint(&self, _early_exit: &VMHooksEarlyExit) -> BreakpointValueLegacy { + BreakpointValueLegacy::ExecutionFailed + } +} + +/// A shared handle to a runtime is itself a runtime. +/// +/// Every executor holds its runtime as `Arc`, because the host supplies +/// it once and several instances share it. This impl is what lets that handle be passed on +/// to code generic over `R: ExecutorRuntime` — [`crate::VMHooksLegacyAdapter`] is the one +/// that needs it — without each backend writing a forwarding newtype. +/// +/// `?Sized` so that the trait-object form, which is the only form anyone actually holds, is +/// covered. +impl ExecutorRuntime for Arc { + fn with_vm_hooks( + &self, + instance_state: &mut dyn InstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ) { + (**self).with_vm_hooks(instance_state, f) + } + + fn early_exit_breakpoint(&self, early_exit: &VMHooksEarlyExit) -> BreakpointValueLegacy { + (**self).early_exit_breakpoint(early_exit) + } } /// Bridges a `with_vm_hooks`-style lender to a caller that has an `FnOnce` and wants its diff --git a/vm-executor/src/new_traits/instance_state.rs b/vm-executor/src/new_traits/instance_state.rs index 5157ca38..5c854802 100644 --- a/vm-executor/src/new_traits/instance_state.rs +++ b/vm-executor/src/new_traits/instance_state.rs @@ -1,4 +1,7 @@ -use crate::{ExecutorError, MemLength, MemPtr, clamped_load_len}; +use crate::{ + BreakpointValueLegacy, ExecutorError, MemLength, MemPtr, ServiceError, VMHooksEarlyExit, + clamped_load_len, +}; /// The interface through which VM hooks update the instance state. /// @@ -63,6 +66,33 @@ pub trait InstanceState { self.memory_load_owned(mem_ptr, to_load as MemLength) } + + /// Halts the instance on an early exit raised by a *legacy* hook, and stashes the exit + /// where the caller will find it once the call unwinds. + /// + /// **Only the Wasmer 2.2 backend implements this**, and only because its hooks return + /// plain values: a [`crate::VMHooksLegacy`] method has no channel for an early exit, so + /// the exit has to be put somewhere and the instance stopped by tripping its breakpoint + /// global. Wasmer 6 hooks return the early exit directly and never come through here. + /// + /// Both halves belong to the instance — it owns the stash and it owns the global — which + /// is why they are one method on the state rather than two capabilities a backend has to + /// surface separately. Which breakpoint, though, is the host's call: the code carried by + /// an early exit is the host VM's own return code and means nothing to the executor, so + /// it arrives already mapped, from + /// [`crate::ExecutorRuntime::early_exit_breakpoint`]. + /// + /// The default is a failure, because for every other backend reaching this point means + /// the early exit is about to be silently dropped. + fn handle_early_exit_legacy( + &mut self, + _early_exit: VMHooksEarlyExit, + _breakpoint: BreakpointValueLegacy, + ) -> Result<(), ExecutorError> { + Err(Box::new(ServiceError::new( + "early exit via breakpoint is only supported by the legacy Wasmer 2.2 backend", + ))) + } } /// A mutable reference to an instance state is itself an instance state. @@ -116,4 +146,14 @@ impl InstanceState for &mut T { ) -> Result, ExecutorError> { (**self).memory_load_clamped(mem_ptr, mem_length) } + + /// Forwarded for the same reason: a backend that handles early exits must keep doing so + /// when reached through a reference, rather than fall back to the failing default. + fn handle_early_exit_legacy( + &mut self, + early_exit: VMHooksEarlyExit, + breakpoint: BreakpointValueLegacy, + ) -> Result<(), ExecutorError> { + (**self).handle_early_exit_legacy(early_exit, breakpoint) + } } diff --git a/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs b/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs index 16375549..bfeaf5e2 100644 --- a/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs +++ b/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs @@ -4,46 +4,74 @@ // !!!!!!!!!!!!!!!!!!!!!! AUTO-GENERATED FILE !!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -use std::{cell::RefCell, ffi::c_void}; +use std::{ffi::c_void, fmt}; -use crate::{MemLength, MemPtr, VMHooks, VMHooksEarlyExit, VMHooksLegacy}; - -/// Allows VM hooks handler to define an early exit method to be used by the VMHooksLegacyAdapter. -pub trait VMHooksSetEarlyExit: VMHooks { - fn set_early_exit(&self, early_exit: VMHooksEarlyExit); -} +use crate::{ + ExecutorRuntime, InstanceState, MemLength, MemPtr, VMHooks, VMHooksEarlyExit, VMHooksLegacy, + with_vm_hooks_once, +}; /// Allow conversion from the new VMHooks to the old. /// /// Will eventually be removed, once everything gets migrated. -#[derive(Debug)] -pub struct VMHooksLegacyAdapter { - inner_cell: RefCell, +/// +/// The legacy import object wants a hooks *object*, built once and kept for the instance's +/// life, while a runtime lends hooks per call. This is the join: it holds the runtime and the +/// state of one instance, and on every hook call asks the runtime for hooks around that +/// state. So the execution a hook serves is resolved when the hook runs, exactly as on +/// Wasmer 6, and not captured when the instance was created. +/// +/// The state is cloned per call because the legacy hooks are reached through a shared +/// reference. Backends make it a handle to the instance, so the clone is cheap. +pub struct VMHooksLegacyAdapter { + runtime: R, + instance_state: S, } -impl VMHooksLegacyAdapter { - pub fn new(inner: VH) -> Self { +// VMHooksLegacy requires Debug. Implemented by hand rather than derived, so that neither the +// runtime nor the instance state has to be Debug just to be held here. +impl fmt::Debug for VMHooksLegacyAdapter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("VMHooksLegacyAdapter").finish_non_exhaustive() + } +} + +impl VMHooksLegacyAdapter { + pub fn new(runtime: R, instance_state: S) -> Self { VMHooksLegacyAdapter { - inner_cell: RefCell::new(inner), + runtime, + instance_state, } } - fn adapt_vm_hooks(&self, f: F) -> R + fn adapt_vm_hooks(&self, f: F) -> T where - R: Default, - F: FnOnce(&mut dyn VMHooks) -> Result, + T: Default, + F: FnOnce(&mut dyn VMHooks) -> Result, { - let mut vm_hooks = self.inner_cell.borrow_mut(); - let result = f(&mut *vm_hooks); + let mut instance_state = self.instance_state.clone(); + + let result = with_vm_hooks_once( + |lend| self.runtime.with_vm_hooks(&mut instance_state, lend), + f, + ); + result.unwrap_or_else(|early_exit| { - vm_hooks.set_early_exit(early_exit); - R::default() + // A legacy hook returns a plain value and has nowhere to report an early exit, + // so the instance has to be stopped and the exit stashed for the caller to pick + // up once the call unwinds. Which breakpoint stops it is the host's call. + let breakpoint = self.runtime.early_exit_breakpoint(&early_exit); + instance_state + .handle_early_exit_legacy(early_exit, breakpoint) + .expect("could not record early exit"); + + T::default() }) } } #[rustfmt::skip] -impl VMHooksLegacy for VMHooksLegacyAdapter { +impl VMHooksLegacy for VMHooksLegacyAdapter { fn set_vm_hooks_ptr(&mut self, _vm_hooks_ptr: *mut c_void) { } From 70b3be4013240b0b15fc5cd4e5b467fc4ea7afb4 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 8 Oct 2026 18:00:39 +0300 Subject: [PATCH 19/26] new FFI - legacy breakpoint code cleanup --- PLAN-go-ffi.md | 48 ++++++++++---- PLAN-rust-vm-caching.md | 5 +- .../new_traits/wasmer_prod_instance_state.rs | 7 +- .../src/new_traits/executor_runtime.rs | 25 +------ vm-executor/src/new_traits/instance_state.rs | 15 ++--- .../src/new_traits/vm_hooks_early_exit.rs | 66 +++++++++++++++++++ .../src/new_traits/vm_hooks_legacy_adapter.rs | 8 +-- 7 files changed, 115 insertions(+), 59 deletions(-) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 25ebf5fd..8fa8b3b3 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -484,20 +484,40 @@ the gap was in the hooks, and it is now closed. For the record, what it took: 2.2 the global is still the mechanical halt. Both halves are owned by the instance, so they are one defaulted method on `InstanceState` — `handle_early_exit_legacy` — whose default is a failure, since for any other backend reaching it means an early exit is about to be - dropped. The **stash moved into the executor**; only the mapping stayed with the host, as - the defaulted `ExecutorRuntime::early_exit_breakpoint`, because the code in a - `VMHooksEarlyExit` is the - host VM's own `ReturnCode`. Go implements nothing — its hooks set the breakpoint themselves. - See `PLAN-go-breakpoint-removal.md` §3. - -**One finding worth carrying forward.** The breakpoint *value* on this path is currently -unobservable: `WasmerProdInstance::call` checks `take_early_exit()` before it ever reads the -global, the breakpoints middleware traps on `!= None` without inspecting the value, and the -legacy capi never goes through the adapter at all. Verified by deliberately flattening the -mapping to `ExecutionFailed` — the prod gas tests, including the OutOfGas and UserError cases, -still pass. The mapping was kept anyway, because 2.2's remaining job is to be an *unchanged* -reference and the differential that would validate the change does not exist yet. Deleting -`early_exit_breakpoint` is a safe follow-up once 2.2-vs-2.2 is green. + dropped. The whole thing **moved into the executor**, mapping included. +- The mapping was briefly a host responsibility (`ExecutorRuntime::early_exit_breakpoint`), + on the reasoning that the code in a `VMHooksEarlyExit` is the host VM's own `ReturnCode` + and so only a host could read it. **That was wrong, and it is now + `VMHooksEarlyExit::breakpoint_legacy`** — a plain method, no trait, no host implementing + anything. The two VMs do not merely agree, they are the same switch: compare `applyEarlyExit` + in `vmhost/vmhooks/earlyExit.go` with what the Rust VM had, on the same numbering + (`UserError = 4`, `OutOfGas = 5`, breakpoints `0..5` in the same order), and code 0 is + reserved for the legacy async call on both sides of the ABI — which is why the C hook result + carries a separate "no early exit" sentinel rather than using 0. The Rust VM's version was + also *incomplete*: it mapped code 0 to `ExecutionFailed` instead of `AsyncCall`, which no + test could catch for the reason below. + +**Two findings worth carrying forward.** + +1. The breakpoint *value* on this path is currently unobservable: + `WasmerProdInstance::call` checks `take_early_exit()` before it ever reads the global, the + breakpoints middleware traps on `!= None` without inspecting the value, and the legacy capi + never goes through the adapter at all. Verified by deliberately flattening the mapping to + `ExecutionFailed` — the prod gas tests, including the OutOfGas and UserError cases, still + pass. It becomes observable the moment the Go VM runs 2.2 through the new C API, because + there `handleBreakpointIfAny` turns the value back into the error the VM returns. So the + mapping is pinned by a unit test rather than left to a suite that cannot fail. + +2. **Who sets the breakpoint on the Go 2.2-new path is still open.** Go's hook sets it itself + (`HandleHookError` → `applyEarlyExit`) and *then* returns the code, so the executor's + `handle_early_exit_legacy` would set it a second time. Agreeing on the value makes the + second write harmless in every case but one: `FailExecution` upgrades to `BreakpointOutOfGas` + when the cause wraps `ErrNotEnoughGas` while the code stays `ExecutionFailed`, and the cause + does not cross the ABI. The known gas sites already route through `ExitOutOfGas()` (code 5), + so this is a narrow residual, not systematic — but it is a decision for `c-api-wasmer2`: + either the Go hook stops setting the breakpoint (which is where + `PLAN-go-breakpoint-removal.md` is heading anyway) or the executor skips the write when one + is already set. ## Phase 4 — Go VM diff --git a/PLAN-rust-vm-caching.md b/PLAN-rust-vm-caching.md index 3979d822..3678ac19 100644 --- a/PLAN-rust-vm-caching.md +++ b/PLAN-rust-vm-caching.md @@ -373,7 +373,10 @@ the sketch: byte-identical — so 2.2 now resolves the execution when the hook runs, exactly as Wasmer 6 does. The stash-and-halt half is a defaulted `InstanceState::handle_early_exit_legacy`, which fails for every backend that does not implement it; a probe removing the 2.2 override - fails exactly the OutOfGas and UserError tests and leaves the no-early-exit one passing. This was not optional tidying: + fails exactly the OutOfGas and UserError tests and leaves the no-early-exit one passing. + The code-to-breakpoint mapping it needs turned out to be universal rather than host-specific + — the Go VM's `applyEarlyExit` is the same switch on the same numbering — so it is + `VMHooksEarlyExit::breakpoint_legacy`, and no host implements anything. This was not optional tidying: `c-api-common`'s shared runtime *is* an `ExecutorRuntime`, so a 2.2 capi built on it could not have existed otherwise. In the Rust VM it deleted `RuntimeRefAdapter`, `WasmerProdInstanceStateAdapter` and the whole `InstanceStateSetEarlyExit` / diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs index 563047e4..edfaf105 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance_state.rs @@ -1,7 +1,6 @@ use crate::WasmerInstance; use crate::executor_interface::{ - BreakpointValueLegacy, ExecutorError, InstanceLegacy, InstanceState, MemLength, MemPtr, - VMHooksEarlyExit, + ExecutorError, InstanceLegacy, InstanceState, MemLength, MemPtr, VMHooksEarlyExit, }; use std::rc::{Rc, Weak}; @@ -23,7 +22,6 @@ impl WasmerProdInstanceState { .upgrade() .map_or_else(|| Err(WasmerExecutorError::BadInstancePointer.into()), Ok) } - } impl InstanceState for WasmerProdInstanceState { @@ -74,14 +72,13 @@ impl InstanceState for WasmerProdInstanceState { fn handle_early_exit_legacy( &mut self, early_exit: VMHooksEarlyExit, - breakpoint: BreakpointValueLegacy, ) -> Result<(), ExecutorError> { let instance_rc = self.instance_rc()?; // Trip the breakpoint global first: under 2.2 that is the mechanical halt, and it is // what stops the contract after the current instruction. instance_rc - .set_breakpoint_value(breakpoint) + .set_breakpoint_value(early_exit.breakpoint_legacy()) .map_err(WasmerExecutorError::WrappedInstance)?; // Then stash the exit, for the error handler to pick up once the call unwinds. diff --git a/vm-executor/src/new_traits/executor_runtime.rs b/vm-executor/src/new_traits/executor_runtime.rs index d8df77fa..2fea5327 100644 --- a/vm-executor/src/new_traits/executor_runtime.rs +++ b/vm-executor/src/new_traits/executor_runtime.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{BreakpointValueLegacy, InstanceState, VMHooks, VMHooksDefault, VMHooksEarlyExit}; +use crate::{InstanceState, VMHooks, VMHooksDefault}; /// The host behind an executor: one reference, for the executor's whole life. /// @@ -30,25 +30,6 @@ pub trait ExecutorRuntime: Send + Sync { instance_state: &mut dyn InstanceState, f: &mut dyn FnMut(&mut dyn VMHooks), ); - - /// Which breakpoint should halt the instance when a hook signals an early exit. - /// - /// **Only the legacy Wasmer 2.2 backend asks.** Its hooks return plain values, with no - /// channel for an early exit, so the executor has to stash the exit and trip the - /// instance's breakpoint global to stop it. Wasmer 6 hooks return the early exit directly - /// and never call this. - /// - /// Stashing is the executor's job; only the mapping is the host's, and it has to be, - /// because the code inside a [`VMHooksEarlyExit`] is the host VM's own return code and - /// means nothing to the executor. A host whose hooks already set the breakpoint - /// themselves — the Go VM does — wants the default and implements nothing. - /// - /// Defaulted rather than split into a second, legacy-only trait so that one `impl` serves - /// both backends. A separate trait would be foreign to `c-api-common`, where the shared - /// runtime lives, and the orphan rule would force a newtype back into every backend crate. - fn early_exit_breakpoint(&self, _early_exit: &VMHooksEarlyExit) -> BreakpointValueLegacy { - BreakpointValueLegacy::ExecutionFailed - } } /// A shared handle to a runtime is itself a runtime. @@ -68,10 +49,6 @@ impl ExecutorRuntime for Arc { ) { (**self).with_vm_hooks(instance_state, f) } - - fn early_exit_breakpoint(&self, early_exit: &VMHooksEarlyExit) -> BreakpointValueLegacy { - (**self).early_exit_breakpoint(early_exit) - } } /// Bridges a `with_vm_hooks`-style lender to a caller that has an `FnOnce` and wants its diff --git a/vm-executor/src/new_traits/instance_state.rs b/vm-executor/src/new_traits/instance_state.rs index 5c854802..2e3762dd 100644 --- a/vm-executor/src/new_traits/instance_state.rs +++ b/vm-executor/src/new_traits/instance_state.rs @@ -1,7 +1,4 @@ -use crate::{ - BreakpointValueLegacy, ExecutorError, MemLength, MemPtr, ServiceError, VMHooksEarlyExit, - clamped_load_len, -}; +use crate::{ExecutorError, MemLength, MemPtr, ServiceError, VMHooksEarlyExit, clamped_load_len}; /// The interface through which VM hooks update the instance state. /// @@ -77,17 +74,14 @@ pub trait InstanceState { /// /// Both halves belong to the instance — it owns the stash and it owns the global — which /// is why they are one method on the state rather than two capabilities a backend has to - /// surface separately. Which breakpoint, though, is the host's call: the code carried by - /// an early exit is the host VM's own return code and means nothing to the executor, so - /// it arrives already mapped, from - /// [`crate::ExecutorRuntime::early_exit_breakpoint`]. + /// surface separately. Which breakpoint to trip comes from + /// [`VMHooksEarlyExit::breakpoint_legacy`], the same for every host. /// /// The default is a failure, because for every other backend reaching this point means /// the early exit is about to be silently dropped. fn handle_early_exit_legacy( &mut self, _early_exit: VMHooksEarlyExit, - _breakpoint: BreakpointValueLegacy, ) -> Result<(), ExecutorError> { Err(Box::new(ServiceError::new( "early exit via breakpoint is only supported by the legacy Wasmer 2.2 backend", @@ -152,8 +146,7 @@ impl InstanceState for &mut T { fn handle_early_exit_legacy( &mut self, early_exit: VMHooksEarlyExit, - breakpoint: BreakpointValueLegacy, ) -> Result<(), ExecutorError> { - (**self).handle_early_exit_legacy(early_exit, breakpoint) + (**self).handle_early_exit_legacy(early_exit) } } diff --git a/vm-executor/src/new_traits/vm_hooks_early_exit.rs b/vm-executor/src/new_traits/vm_hooks_early_exit.rs index ed290be5..e527a008 100644 --- a/vm-executor/src/new_traits/vm_hooks_early_exit.rs +++ b/vm-executor/src/new_traits/vm_hooks_early_exit.rs @@ -1,5 +1,7 @@ use std::{borrow::Cow, fmt}; +use crate::BreakpointValueLegacy; + /// Contains details regarding an early exit triggered by a VM hook. /// /// It doesn't have to be an error, for instance in the case of legacy async calls @@ -10,6 +12,16 @@ pub struct VMHooksEarlyExit { pub message: Cow<'static, str>, } +/// Legacy async call. Code 0 is reserved for it on both sides of the ABI, which is why the +/// C hook result carries a separate "no early exit" sentinel instead of using 0 for it. +const CODE_ASYNC_CALL: u64 = 0; + +/// An error signalled by the contract itself. +const CODE_USER_ERROR: u64 = 4; + +/// Gas exhausted. +const CODE_OUT_OF_GAS: u64 = 5; + impl VMHooksEarlyExit { pub fn new(code: u64) -> Self { VMHooksEarlyExit { @@ -27,6 +39,29 @@ impl VMHooksEarlyExit { self.message = Cow::Owned(message); self } + + /// Which breakpoint halts a Wasmer 2.2 instance for this exit. + /// + /// **Only the legacy backend asks.** Its hooks return plain values, with no channel for + /// an early exit, so the instance has to be stopped by tripping its breakpoint global. + /// Wasmer 6 hooks return the early exit directly and never come here. + /// + /// The codes are the high-level VM's own return codes, and for a while this looked like + /// something only a host could decide. It is not: the Go VM and the Rust VM use the same + /// numbering and reach the same three answers — compare `applyEarlyExit` in the Go VM's + /// `vmhost/vmhooks/earlyExit.go`, which is this same switch. So it lives on the exit + /// itself, and no host implements anything. + /// + /// Everything else is a failure, which is also what an unrecognised code has to be: the + /// instance must stop either way, and stopping it as a failure is the safe reading. + pub fn breakpoint_legacy(&self) -> BreakpointValueLegacy { + match self.code { + CODE_ASYNC_CALL => BreakpointValueLegacy::AsyncCall, + CODE_USER_ERROR => BreakpointValueLegacy::SignalError, + CODE_OUT_OF_GAS => BreakpointValueLegacy::OutOfGas, + _ => BreakpointValueLegacy::ExecutionFailed, + } + } } impl fmt::Display for VMHooksEarlyExit { @@ -39,3 +74,34 @@ impl fmt::Display for VMHooksEarlyExit { } impl std::error::Error for VMHooksEarlyExit {} + +#[cfg(test)] +mod tests { + use super::*; + + /// The mapping has no runtime observer yet: the Rust VM reads the stashed exit before it + /// reads the breakpoint, and the Go VM's hooks set the breakpoint themselves. It becomes + /// observable the moment the Go VM runs Wasmer 2.2 through the new C API, so it is pinned + /// here instead of being left to a test that cannot fail. + #[test] + fn breakpoint_legacy_matches_both_high_level_vms() { + let bp = |code: u64| VMHooksEarlyExit::new(code).breakpoint_legacy(); + + // Go: BreakpointAsyncCall, set directly by the async hook. Rust VM: code 0. + assert_eq!(bp(CODE_ASYNC_CALL), BreakpointValueLegacy::AsyncCall); + + // Go: SignalUserError -> BreakpointSignalError. + assert_eq!(bp(CODE_USER_ERROR), BreakpointValueLegacy::SignalError); + + // Go: SetRuntimeBreakpointValue(BreakpointOutOfGas). + assert_eq!(bp(CODE_OUT_OF_GAS), BreakpointValueLegacy::OutOfGas); + + // Go: FailExecution -> BreakpointExecutionFailed. 10 is vmcommon.ExecutionFailed, + // which the Go VM sends as the default for any hook error that does not describe + // its own exit. + assert_eq!(bp(10), BreakpointValueLegacy::ExecutionFailed); + + // A code neither VM uses still has to stop the instance. + assert_eq!(bp(u64::MAX), BreakpointValueLegacy::ExecutionFailed); + } +} diff --git a/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs b/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs index bfeaf5e2..319ff7fb 100644 --- a/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs +++ b/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs @@ -32,7 +32,8 @@ pub struct VMHooksLegacyAdapter { // runtime nor the instance state has to be Debug just to be held here. impl fmt::Debug for VMHooksLegacyAdapter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("VMHooksLegacyAdapter").finish_non_exhaustive() + f.debug_struct("VMHooksLegacyAdapter") + .finish_non_exhaustive() } } @@ -59,10 +60,9 @@ impl VMHooksLegacyAdapter { result.unwrap_or_else(|early_exit| { // A legacy hook returns a plain value and has nowhere to report an early exit, // so the instance has to be stopped and the exit stashed for the caller to pick - // up once the call unwinds. Which breakpoint stops it is the host's call. - let breakpoint = self.runtime.early_exit_breakpoint(&early_exit); + // up once the call unwinds. instance_state - .handle_early_exit_legacy(early_exit, breakpoint) + .handle_early_exit_legacy(early_exit) .expect("could not record early exit"); T::default() From a5da41cce3eb89917cdc0c05471deac583630890 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 9 Oct 2026 11:16:34 +0300 Subject: [PATCH 20/26] new FFI - wasmer 2 new integration, common code moved to c-api-common --- Cargo.toml | 1 + Makefile | 36 ++ PLAN-go-ffi.md | 133 +++- PLAN-rust-vm-caching.md | 11 + c-api-common/src/capi_executor_ops.rs | 60 ++ c-api-common/src/capi_handles.rs | 41 ++ c-api-common/src/capi_instance_ops.rs | 257 ++++++++ c-api-common/src/capi_state_ops.rs | 224 +++++++ c-api-common/src/lib.rs | 14 +- c-api-wasmer2/Cargo.toml | 21 + c-api-wasmer2/build.rs | 39 ++ c-api-wasmer2/cbindgen.toml | 17 + c-api-wasmer2/libvmexeccapi_w2.h | 222 +++++++ c-api-wasmer2/src/capi_backend.rs | 28 + c-api-wasmer2/src/capi_executor.rs | 80 +++ c-api-wasmer2/src/capi_instance.rs | 246 +++++++ c-api-wasmer2/src/capi_instance_state.rs | 136 ++++ c-api-wasmer2/src/capi_logger.rs | 6 + c-api-wasmer2/src/lib.rs | 41 ++ c-api-wasmer2/tests/ffi_boundary.rs | 785 +++++++++++++++++++++++ c-api-wasmer2/tests/prefix_parity.rs | 53 ++ c-api-wasmer6/src/capi_backend.rs | 23 + c-api-wasmer6/src/capi_executor.rs | 43 +- c-api-wasmer6/src/capi_instance.rs | 160 ++--- c-api-wasmer6/src/capi_instance_state.rs | 132 +--- c-api-wasmer6/src/lib.rs | 6 +- 26 files changed, 2524 insertions(+), 291 deletions(-) create mode 100644 c-api-common/src/capi_executor_ops.rs create mode 100644 c-api-common/src/capi_handles.rs create mode 100644 c-api-common/src/capi_instance_ops.rs create mode 100644 c-api-common/src/capi_state_ops.rs create mode 100644 c-api-wasmer2/Cargo.toml create mode 100644 c-api-wasmer2/build.rs create mode 100644 c-api-wasmer2/cbindgen.toml create mode 100644 c-api-wasmer2/libvmexeccapi_w2.h create mode 100644 c-api-wasmer2/src/capi_backend.rs create mode 100644 c-api-wasmer2/src/capi_executor.rs create mode 100644 c-api-wasmer2/src/capi_instance.rs create mode 100644 c-api-wasmer2/src/capi_instance_state.rs create mode 100644 c-api-wasmer2/src/capi_logger.rs create mode 100644 c-api-wasmer2/src/lib.rs create mode 100644 c-api-wasmer2/tests/ffi_boundary.rs create mode 100644 c-api-wasmer2/tests/prefix_parity.rs create mode 100644 c-api-wasmer6/src/capi_backend.rs diff --git a/Cargo.toml b/Cargo.toml index 1231c4ae..e29542e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "meta", "c-api-common", "c-api-legacy", + "c-api-wasmer2", "c-api-wasmer6", "vm-executor", "vm-executor-experimental", diff --git a/Makefile b/Makefile index 1d56cba2..abe7f51a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: capi capi-linux-amd64 capi-linux-arm capi-osx-amd64 capi-osx-arm \ capi-osx-amd64-cross capi-linux-amd64-docker capi-linux-arm64-docker \ capi-w6 capi-w6-linux-amd64 capi-w6-linux-arm capi-w6-osx-amd64 capi-w6-osx-arm \ + capi-w2 capi-w2-linux-amd64 capi-w2-linux-arm capi-w2-osx-amd64 capi-w2-osx-arm \ ci-local clean check-macos # cargo and docker already parallelise internally, and a parallel make would @@ -68,6 +69,36 @@ capi-w6-osx-arm: check-macos capi-w6 mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer6.dylib target/release/libvmexeccapi_w6_arm.dylib install_name_tool -id @rpath/libvmexeccapi_w6_arm.dylib target/release/libvmexeccapi_w6_arm.dylib +# --- The new C API, Wasmer 2.2 ------------------------------------------------ +# The same interface over the executor currently in production. Built alongside the +# Wasmer 6 library rather than instead of it: running the *same* runtime through the +# *new* boundary is what isolates the boundary as the variable, so a difference against +# the legacy `libvmexeccapi` is attributable to the refactor and not to a runtime change. +# +# Three libraries can therefore be linked at once — `vm_exec_*`, `vm_exec_w2_*` and +# `vm_exec_w6_*` share no exported symbols. On Linux that needs +# `-Wl,-Bsymbolic-functions`, because the two Wasmer versions do collide on their +# internal `wasmer_vm_*` symbols; macOS's two-level namespace handles it without help. + +capi-w2: + cargo build -p multiversx-chain-vm-executor-c-api-wasmer2 --release + +capi-w2-linux-amd64: capi-w2 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer2.so target/release/libvmexeccapi_w2.so + patchelf --set-soname libvmexeccapi_w2.so target/release/libvmexeccapi_w2.so + +capi-w2-linux-arm: capi-w2 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer2.so target/release/libvmexeccapi_w2_arm.so + patchelf --set-soname libvmexeccapi_w2_arm.so target/release/libvmexeccapi_w2_arm.so + +capi-w2-osx-amd64: check-macos capi-w2 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer2.dylib target/release/libvmexeccapi_w2.dylib + install_name_tool -id @rpath/libvmexeccapi_w2.dylib target/release/libvmexeccapi_w2.dylib + +capi-w2-osx-arm: check-macos capi-w2 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer2.dylib target/release/libvmexeccapi_w2_arm.dylib + install_name_tool -id @rpath/libvmexeccapi_w2_arm.dylib target/release/libvmexeccapi_w2_arm.dylib + # --- Local reproduction of the CI build matrix ------------------------------- # The four targets above are what CI runs natively, one per runner. The targets # below drive the same builds from a single host: the two Linux libraries build @@ -132,5 +163,10 @@ clean: rm -f target/release/libvmexeccapi_w6.dylib rm -f target/release/libvmexeccapi_w6_arm.dylib rm -f c-api-wasmer6/libvmexeccapi_w6.h + rm -f target/release/libvmexeccapi_w2.so + rm -f target/release/libvmexeccapi_w2_arm.so + rm -f target/release/libvmexeccapi_w2.dylib + rm -f target/release/libvmexeccapi_w2_arm.dylib + rm -f c-api-wasmer2/libvmexeccapi_w2.h rm -f target/x86_64-apple-darwin/release/libvmexeccapi.dylib rm -rf $(CI_LOCAL_DIR) diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 8fa8b3b3..08958f76 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -458,6 +458,16 @@ That makes **Phase 1.6 and the `VMHooksLegacyAdapter` activation trick cleanup r migration**: worth doing when the legacy interface set is retired, not before wasmer 6 is live. The `-Bsymbolic-functions` work is what takes its place on the critical path. +**Superseded — it was needed, for a reason this section missed.** The argument above is sound +about the *differential*: `wasmer2` over the legacy library would indeed have isolated the +interface migration. What it misses is that the Go-side interfaces are not the only thing being +migrated. The legacy library reaches 2.2 through the legacy *Rust* traits, so a `wasmer2` +built on it would have left the new Rust traits, the C ABI and the hook-dispatch path +exercised by one backend only. Building `c-api-wasmer2` put the same runtime behind all of +that, and the first thing it found was a bug in the new Rust path that no amount of +Go-side comparison could have reached: the breakpoint global was never cleared, making a 2.2 +instance single-use after an early exit. See Phase 4c. + ### What the wasmer 2.2 path needs **Resolved by Stage 0.** `WasmerProdExecutor` already implemented the new `Executor` trait; @@ -936,33 +946,110 @@ Shared types are published under the neutral `vm_exec_*` prefix. The rule that f worth stating, because it is what the next crate follows: **types shared → `vm_exec_*`; opaque handles and functions → `vm_exec_w6_*`.** +### Done: `c-api-wasmer2`, and how the duplication was resolved + +Both libraries now exist, and the open question from item 1 below is answered: **not a macro**. +cbindgen reads the source syntactically, so an entry point produced by `macro_rules!` yields no +header entry at all — the `#[no_mangle]` functions have to be written out literally. What does +*not* have to be duplicated is their bodies, so those moved into `c-api-common` +(`capi_executor_ops`, `capi_instance_ops`, `capi_state_ops`, over the shared `capi_handles`), +and each backend crate keeps a file of thin wrappers whose documentation is the header. + +The one point of variation is `capi_handles::CapiBackend`, a trait with a single function: +which `Executor` an instance is built from. Because the entry points reach it through a name +that is the same in both crates (`capi_backend::Backend`), **all three entry-point files are +character-identical apart from the prefix** — `c-api-wasmer2/tests/prefix_parity.rs` asserts +exactly that, and the two generated headers are identical under the same substitution. Adding a +backend now costs one `CapiBackend` impl plus three derived files. + +Sizes: the three files went from ~700 lines of logic per backend to ~450 lines of wrappers, +most of it the C contract, over one shared body. + +### Done: the Go `wasmer2` package + +A copy of `wasmer6` with the prefix changed — it has to be, because a cgo `//export` name is a +C symbol and two packages cannot export the same one; that is also the entire reason the +prefixes exist. The generated shims come from the generator (item 4 below, done: +`WriteWasmer2NewCgo`, prefix `w2_`). `testexecutor.ExecWasmer2` is wired up, so +`VMEXECUTOR=wasmer2` runs the whole suite through it. + +**One real bug, found by the ported boundary test.** Under 2.2 an early exit trips the +breakpoint global to halt the contract, and nothing cleared it afterwards — so an instance was +single-use after any early exit, and the next call trapped immediately on `unreachable`. Under +the legacy interface clearing it was the *host's* job (`runtimeContext.UseWarmInstance` → +`SetRuntimeBreakpointValue(BreakpointNone)`), and the new C API deliberately does not expose +the global, so the only side that can still do it is the executor. +`WasmerProdInstance::call` now clears it alongside setting the points limit. Wasmer 6 never had +the bug because it has no such global to leave behind — which is exactly the class of +divergence running 2.2 through the new boundary exists to catch. + ### Still to do -1. ~~**`c-api-common`.**~~ **Done** — see above. What remains of the original estimate: The extraction is small: of the thirteen modules in `c-api-wasmer6`, - **ten already reference no backend type at all**. Only `capi_vm_hook_context.rs` (10 - the three `#[no_mangle]` entry-point files (`capi_executor.rs`, `capi_instance.rs`, - `capi_instance_state.rs`, ~700 lines) are still per-backend, because each needs its own - symbol prefix. `c-api-wasmer2` will duplicate them unless they are generated from a macro - parameterised by prefix — worth deciding when that crate is written, not before. -2. **`c-api-wasmer2`.** Wasmer 2.2 through the new interfaces. **No longer blocked** — Stage 0 - closed both halves: `WasmerProdRuntimeRef` is gone and `VMHooksLegacyAdapter` is - built directly on `ExecutorRuntime` + `InstanceState`, so one crate body can serve both - backends. What is left is mechanical: the - three `#[no_mangle]` entry-point files under a `vm_exec_w2_` prefix plus the one-line - `capi_logger` re-export. `capi_instance_state.rs` is now *character-identical* to the - Wasmer 6 copy apart from the prefix, since neither names a backend any more — which is what - makes generating the three from a prefix-parameterised macro worth deciding now rather than - duplicating a third time. +1. ~~**`c-api-common`.**~~ **Done.** ~~**The prefix-parameterised macro question.**~~ **Answered + above: shared bodies, literal wrappers, parity enforced by test.** +2. ~~**`c-api-wasmer2`.**~~ **Done** — see above. 3. **`wasmercommon` in Go**, wrapping the common header: the error buffer, the status mapping, the call result and the hook result, so `wasmer2` and `wasmer6` share them instead of each - carrying a copy. Worth doing *after* `c-api-common`, since the Go types mirror it. -4. A third cgo shim prefix for the new `wasmer2` package, since `w2_` belongs to the legacy one - for as long as both exist. - -The payoff is the comparison the whole structure exists for: **wasmer 2.2 against wasmer 2.2**, -one through each interface set, under the hook-sequence differential. That isolates the -interface migration from the runtime upgrade — if those two agree, any remaining difference -against `wasmer6` is Wasmer's doing and not the new FFI's. + carrying a copy. Still outstanding, and deliberately so: it is a refactor of two packages, + and doing it while the second was being brought up would have made any failure ambiguous. + Now that the differential is green except for the known gap, it is safe to take. + Note the constraint it has to work around: cgo types are per-package, so anything whose + signature mentions `C.vm_exec_*` cannot move. What can is the error sentinels, the buffer + and its message decoding (over a plain `int32` status), the call-result enum, and + `readNames`. +4. ~~A third cgo shim prefix for the new `wasmer2` package.~~ **Done.** + +### What the differential now says + +The comparison the whole structure exists for is running: `TestMigrationPreservesHookSequence` +holds the wasm runtime constant and varies only the boundary. + +| scenario folder | hook-sequence lines | differences | +|---|---|---| +| `adder` | 183 | instance lifecycle only | +| `crowdfunding-esdt` (16) | 8,896 | instance lifecycle only | +| `erc20-rust` (57) | 14,441 | instance lifecycle only | + +"Instance lifecycle only" means literally this, and nothing else in any of them: + +``` +< Reset: true > Reset: false +< SetPointsUsed: 0 > Clean: true +< SetGasLimit: ... +< SetBreakpointValue: 0 +``` + +Every hook call, in order, with every argument and every gas value, is identical. The +divergence is the warm-instance path the new C API does not have yet (no `reset`, no module +cache — Phase 6), and it is the same shape already seen against `wasmer6`. + +Two things it has also established: + +- **The whole-suite failure sets now separate the two variables.** `wasmer2legacy` 0 failures, + `wasmer2` 28, `wasmer6` 33 — and the 28 are a strict *subset* of the 33. So 28 failures + belong to the boundary (all of them the `MemLoad`/`MemStore`-outside-a-hook and + `Reset`/`Cache` gaps already on the 4b and Phase 6 lists) and exactly 5 belong to the runtime + change: `TestExecution_Call_GasConsumptionOnLocals`, + `TestExecution_DeployWASM_MoreThanMaximumLocals`, `TestRustBasicFeaturesBarnard`, + `TestRustBasicFeaturesLatest`, `TestRustComposability`. Before this library existed there was + no way to make that attribution. +- **`delegation/v0_5_latest` fails 3 of 27 scenarios with "not enough gas", under *both* new + backends.** Previously invisible: `TestDelegation_v0_5_latest` is skipped on arm64, so the + differential was the first thing to run it here. Consistent with the cold-instance path, and + it is the sharpest symptom yet of why Phase 6 matters — but it is attributed, not diagnosed. + +### Three libraries in one binary, on macOS + +A `VMEXECUTOR=wasmer2` test binary links all three: `libvmexeccapi` (2.2), `libvmexeccapi_w2` +(2.2 again) and `libvmexeccapi_w6` (6). Two independent copies of Wasmer 2.2 in one process, +and the whole suite runs. `wasmer6/dualbackend` — which had gone stale against the +`InstanceCallResult` change and could not compile — is repaired and both trap-handler +orderings pass again. + +This does **not** carry to Linux. The colliding internal `wasmer_vm_*` symbols are still +unverified there, and `-Wl,-Bsymbolic-functions` remains the open item; see +"The symbol problem does not go away". + ## Phase 5 — Bring-up and validation diff --git a/PLAN-rust-vm-caching.md b/PLAN-rust-vm-caching.md index 3678ac19..2ec880b8 100644 --- a/PLAN-rust-vm-caching.md +++ b/PLAN-rust-vm-caching.md @@ -383,6 +383,17 @@ the sketch: `VMHooksSetEarlyExit` pair, and `impl ExecutorRuntime for RuntimeWeakRef` moved out of the `wasmer-experimental` feature gate to serve both. +- **A prerequisite for instance reuse turned up on its own.** Under Wasmer 2.2 an early exit + trips the breakpoint global to halt the contract, and nothing cleared it afterwards: the + host used to, and the new interface does not expose it. So a 2.2 instance was single-use + after any early exit — which is precisely a reuse bug, found while building `c-api-wasmer2` + rather than while building the cache. `WasmerProdInstance::call` now clears it alongside + setting the points limit, so an instance is armed for each call rather than still halted + from the last one. Wasmer 6 never had the problem, having no such global; the general point + stands for stages 1–3 either way, that anything a call leaves behind in an instance has to + be reset by the executor, because after caching nobody else knows an instance is being + reused. + - **The arming slot had to become a stack, early.** This was Stage 3's item, but Stage 0 forced its core: resolving the execution per hook call means the slot must stay armed for the whole call, where it used to be cleared as soon as the instance was built. Widening the diff --git a/c-api-common/src/capi_executor_ops.rs b/c-api-common/src/capi_executor_ops.rs new file mode 100644 index 00000000..eab982dc --- /dev/null +++ b/c-api-common/src/capi_executor_ops.rs @@ -0,0 +1,60 @@ +//! Executor lifecycle, shared by every backend's prefixed entry points. +//! +//! The functions here are the bodies; the `#[no_mangle]` wrappers live in the backend +//! crates, because cbindgen reads the source syntactically and only sees an entry point that +//! is written out literally. That is the whole reason for the split: the wrappers exist to be +//! *seen*, and carry the C contract as their documentation, while the logic is compiled once. + +use libc::{c_char, c_int}; +use std::sync::Arc; + +use crate::capi_config::vm_exec_executor_config_t; +use crate::capi_handles::CapiExecutor; +use crate::capi_status::{vm_exec_status_t, write_error}; +use crate::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; +use crate::capi_vm_hook_table::CapiVMHookTable; + +/// Creates an executor from one configuration struct and one hook table. +/// +/// # Safety +/// +/// `config` and `hook_table` must be valid for the duration of the call, and `executor_out` +/// must be writable. The hook table is copied, so the caller may release it on return. +pub unsafe fn new_executor( + executor_out: *mut *mut CapiExecutor, + config: *const vm_exec_executor_config_t, + hook_table: *const vm_exec_vm_hook_c_func_pointers, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if executor_out.is_null() || config.is_null() || hook_table.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let opcode_config = match unsafe { (*config).to_opcode_config() } { + Ok(opcode_config) => opcode_config, + Err(message) => return write_error(message, err_buf, err_buf_len), + }; + + let capi_executor = CapiExecutor { + opcode_config: Arc::new(opcode_config), + hook_table: unsafe { CapiVMHookTable::copy_from(hook_table) }, + }; + + unsafe { + *executor_out = Box::into_raw(Box::new(capi_executor)); + } + + vm_exec_status_t::VM_EXEC_OK +} + +/// Destroys an executor. Does nothing if `executor` is null. +/// +/// # Safety +/// +/// `executor` must be null, or a pointer this module produced and has not already destroyed. +pub unsafe fn executor_destroy(executor: *mut CapiExecutor) { + if !executor.is_null() { + drop(unsafe { Box::from_raw(executor) }); + } +} diff --git a/c-api-common/src/capi_handles.rs b/c-api-common/src/capi_handles.rs new file mode 100644 index 00000000..50f35a5f --- /dev/null +++ b/c-api-common/src/capi_handles.rs @@ -0,0 +1,41 @@ +//! What the opaque C handles point to, and the one thing the two C APIs do not share. +//! +//! Both backends' handles hold exactly the same thing, because neither names a backend: an +//! executor is a configuration plus a hook table, and an instance is a `Box`. +//! The single point of variation is [`CapiBackend`] — which `Executor` implementation an +//! instance is built from — and it is a trait rather than a parameter so that the shared +//! operations stay plain functions a prefixed entry point can call directly. + +use std::sync::Arc; + +use multiversx_chain_vm_executor::{Executor, Instance, OpcodeConfig}; + +use crate::capi_vm_hook_context::CapiVMHooksBuilder; +use crate::capi_vm_hook_table::CapiVMHookTable; + +/// What a `vm_exec__executor_t` points to. +/// +/// The shared, immutable configuration: passed once at creation and never mutated. It does +/// not yet hold a `dyn Executor`, because an executor is built around one runtime reference +/// for its whole life and this API's runtime handle still arrives per instance. See +/// `capi_instance_ops::new_instance`. +pub struct CapiExecutor { + pub opcode_config: Arc, + pub hook_table: CapiVMHookTable, +} + +/// What a `vm_exec__instance_t` points to. +pub struct CapiInstance { + pub content: Box, +} + +/// Which wasm runtime a C API is for. +/// +/// This is the entire difference between the two libraries. Everything else — the handles, +/// the operations, the hook dispatch, the error convention — is compiled once and shared, +/// which is why adding a backend costs one implementation of this plus a file of prefixed +/// entry points. +pub trait CapiBackend { + /// Builds the backend's executor around the runtime the C API supplies. + fn new_executor(runtime: Arc) -> Box; +} diff --git a/c-api-common/src/capi_instance_ops.rs b/c-api-common/src/capi_instance_ops.rs new file mode 100644 index 00000000..3d2e6c46 --- /dev/null +++ b/c-api-common/src/capi_instance_ops.rs @@ -0,0 +1,257 @@ +//! Instance lifecycle and the outer instance operations, shared by every backend. +//! +//! "Outer" is the side of the trait split that the VM sees from outside a host call: +//! calling, gas accounting, and module introspection. Everything a *hook* needs is in +//! `capi_state_ops`, reached through a hook context rather than through this handle. +//! +//! Only [`new_instance`] is parameterised by backend, and only to pick which `Executor` to +//! build. Everything after that goes through `dyn Instance`. + +use libc::{c_char, c_int}; +use std::ffi::CStr; +use std::slice; +use std::sync::Arc; + +use crate::capi_call_result::vm_exec_call_result_t; +use crate::capi_config::vm_exec_compilation_options_t; +use crate::capi_handles::{CapiBackend, CapiExecutor, CapiInstance}; +use crate::capi_status::{vm_exec_status_t, write_error}; +use crate::capi_strings::{copy_pipe_joined, pipe_joined_length}; +use crate::capi_vm_hook_context::CapiVMHooksBuilder; + +/// Creates an instance of `B`'s backend. +/// +/// # Safety +/// +/// All pointer arguments must be valid for the duration of the call, and whatever +/// `go_vm_hooks` identifies must outlive the instance. +#[allow(clippy::too_many_arguments)] +pub unsafe fn new_instance( + executor: *mut CapiExecutor, + instance_out: *mut *mut CapiInstance, + wasm_bytes: *const u8, + wasm_bytes_len: u32, + options: *const vm_exec_compilation_options_t, + go_vm_hooks: usize, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if executor.is_null() || instance_out.is_null() || wasm_bytes.is_null() || options.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let capi_executor = unsafe { &*executor }; + let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_len as usize) }; + let compilation_options = + unsafe { (*options).to_compilation_options(capi_executor.opcode_config.clone()) }; + + // One executor per instance, for as long as the runtime handle is per instance: the + // handle is what the executor is built around, so it cannot outlive the instance that + // owns it. Building it here costs nothing — an executor holds only the runtime — and it + // means this goes through the `Executor` trait rather than around it. When the handle + // moves to `new_executor`, the executor moves to `CapiExecutor` with it and this becomes + // a plain `new_instance` call. + let runtime = Arc::new(CapiVMHooksBuilder::new( + go_vm_hooks, + capi_executor.hook_table.clone(), + )); + let backend_executor = B::new_executor(runtime); + + let instance = match backend_executor.new_instance(wasm_bytes, &compilation_options) { + Ok(instance) => instance, + Err(err) => return write_error(err, err_buf, err_buf_len), + }; + + unsafe { + *instance_out = Box::into_raw(Box::new(CapiInstance { content: instance })); + } + + vm_exec_status_t::VM_EXEC_OK +} + +/// Destroys an instance. Does nothing if `instance` is null. +/// +/// # Safety +/// +/// `instance` must be null, or a pointer this module produced and has not already destroyed. +pub unsafe fn instance_destroy(instance: *mut CapiInstance) { + if !instance.is_null() { + drop(unsafe { Box::from_raw(instance) }); + } +} + +/// Calls an exported function, with a gas limit for this call. +/// +/// # Safety +/// +/// All pointer arguments must be valid for the duration of the call. +pub unsafe fn instance_call( + instance: *mut CapiInstance, + func_name: *const c_char, + points_limit: u64, + result_out: *mut vm_exec_call_result_t, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if instance.is_null() || func_name.is_null() || result_out.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let capi_instance = unsafe { &mut *instance }; + + let Ok(func_name) = unsafe { CStr::from_ptr(func_name) }.to_str() else { + return write_error("function name is not valid UTF-8", err_buf, err_buf_len); + }; + + let result = unsafe { + vm_exec_call_result_t::from_call_result( + capi_instance.content.call(func_name, points_limit), + err_buf, + err_buf_len, + ) + }; + + unsafe { + *result_out = result; + } + + vm_exec_status_t::VM_EXEC_OK +} + +/// Returns the number of points (gas) used so far. +/// +/// # Safety +/// +/// All pointer arguments must be valid for the duration of the call. +pub unsafe fn instance_get_points_used( + instance: *mut CapiInstance, + points_out: *mut u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if instance.is_null() || points_out.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let capi_instance = unsafe { &mut *instance }; + match capi_instance.content.get_points_used() { + Ok(points) => { + unsafe { + *points_out = points; + } + vm_exec_status_t::VM_EXEC_OK + } + Err(err) => write_error(err, err_buf, err_buf_len), + } +} + +/// Checks that every exported function takes no arguments and returns nothing. +/// +/// # Safety +/// +/// `instance` must be null or valid for the duration of the call. +pub unsafe fn instance_check_signatures(instance: *const CapiInstance) -> c_int { + if instance.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*instance }; + c_int::from(capi_instance.content.check_signatures()) +} + +/// Checks whether the contract exports a function with the given name. +/// +/// # Safety +/// +/// Both pointers must be null or valid for the duration of the call. +pub unsafe fn instance_has_function( + instance: *const CapiInstance, + func_name: *const c_char, +) -> c_int { + if instance.is_null() || func_name.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*instance }; + let Ok(func_name) = unsafe { CStr::from_ptr(func_name) }.to_str() else { + return vm_exec_status_t::VM_EXEC_ERROR.as_c_int(); + }; + + c_int::from(capi_instance.content.has_function(func_name)) +} + +/// Buffer length needed by [`instance_exported_function_names`]. +/// +/// # Safety +/// +/// `instance` must be null or valid for the duration of the call. +pub unsafe fn instance_exported_function_names_length(instance: *const CapiInstance) -> c_int { + if instance.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*instance }; + pipe_joined_length(&capi_instance.content.get_exported_function_names()) +} + +/// Writes the exported function names, pipe-separated. +/// +/// # Safety +/// +/// `instance` must be null or valid, and `dest` must be writable for `dest_len` bytes. +pub unsafe fn instance_exported_function_names( + instance: *const CapiInstance, + dest: *mut c_char, + dest_len: c_int, +) -> c_int { + if instance.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*instance }; + unsafe { + copy_pipe_joined( + &capi_instance.content.get_exported_function_names(), + dest, + dest_len, + ) + } +} + +/// Buffer length needed by [`instance_imported_function_names`]. +/// +/// # Safety +/// +/// `instance` must be null or valid for the duration of the call. +pub unsafe fn instance_imported_function_names_length(instance: *const CapiInstance) -> c_int { + if instance.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*instance }; + pipe_joined_length(&capi_instance.content.get_imported_function_names()) +} + +/// Writes the names of the VM hooks the contract imports, pipe-separated. +/// +/// # Safety +/// +/// `instance` must be null or valid, and `dest` must be writable for `dest_len` bytes. +pub unsafe fn instance_imported_function_names( + instance: *const CapiInstance, + dest: *mut c_char, + dest_len: c_int, +) -> c_int { + if instance.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); + } + + let capi_instance = unsafe { &*instance }; + unsafe { + copy_pipe_joined( + &capi_instance.content.get_imported_function_names(), + dest, + dest_len, + ) + } +} diff --git a/c-api-common/src/capi_state_ops.rs b/c-api-common/src/capi_state_ops.rs new file mode 100644 index 00000000..857fdc6e --- /dev/null +++ b/c-api-common/src/capi_state_ops.rs @@ -0,0 +1,224 @@ +//! The state operations: what a VM hook may do to the instance it is running inside. +//! +//! Every function here takes a hook context and is valid only while the hook that was handed +//! that context is still running. See `capi_vm_hook_context` for how that is enforced; a +//! stale context yields `VM_EXEC_STALE_CONTEXT` rather than a dereference of a dead borrow. +//! +//! Bounds checking lives on this side of the boundary, not in Go. The executor is the only +//! side that knows the real extent of the instance memory, and the rules are observable by +//! contracts, so there is one implementation of them — shared with the other backends in +//! `vm-executor/src/new_traits/memory.rs` — rather than one per caller. +//! +//! Nothing here mentions a backend, and nothing here needs to: the state arrives through the +//! context as an erased `&mut dyn InstanceState`. That is why these bodies are shared whole, +//! and each backend crate contributes only the prefixed symbols. + +use libc::{c_char, c_int}; +use multiversx_chain_vm_executor::{ + ExecutorError, MemLength, MemPtr, ServiceError, clamped_load_len, +}; +use std::slice; + +use crate::capi_status::{vm_exec_status_t, write_error, write_error_count}; +use crate::capi_vm_hook_context::{ + ErasedInstanceState, vm_exec_hook_context_t, with_instance_state, +}; + +/// Loads from instance memory into a caller-provided buffer, truncating at the end of the +/// memory. Returns the number of bytes written, or a negative status. +/// +/// # Safety +/// +/// `ctx` must be null or a context this crate activated; `dest` must be writable for +/// `dest_len` bytes. +pub unsafe fn state_mem_load( + ctx: *mut vm_exec_hook_context_t, + mem_ptr: i32, + dest: *mut u8, + dest_len: i32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> i64 { + let loaded = unsafe { + with_instance_state::( + ctx, + |state| -> Result { + let memory_length = state.memory_length()?; + let to_load = + clamped_load_len(mem_ptr as MemPtr, dest_len as MemLength, memory_length)?; + if to_load == 0 { + return Ok(0); + } + + if dest.is_null() { + return Err(Box::new(ServiceError::new("destination buffer is null"))); + } + + let dest = slice::from_raw_parts_mut(dest, to_load); + state.memory_load_to_slice(mem_ptr as MemPtr, dest)?; + Ok(to_load) + }, + ) + }; + + match loaded { + Err(context_err) => context_err.as_status().as_i64(), + Ok(Err(err)) => write_error_count(err, err_buf, err_buf_len), + Ok(Ok(to_load)) => to_load as i64, + } +} + +/// Writes to instance memory. The range must fit entirely inside the memory. +/// +/// # Safety +/// +/// `ctx` must be null or a context this crate activated; `src` must be readable for +/// `src_len` bytes. +pub unsafe fn state_mem_store( + ctx: *mut vm_exec_hook_context_t, + mem_ptr: i32, + src: *const u8, + src_len: i32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if src_len < 0 { + return write_error("negative length", err_buf, err_buf_len); + } + if src.is_null() && src_len > 0 { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let stored = unsafe { + with_instance_state::(ctx, |state| { + let data = if src_len == 0 { + &[][..] + } else { + slice::from_raw_parts(src, src_len as usize) + }; + state.memory_store(mem_ptr as MemPtr, data) + }) + }; + + match stored { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(())) => vm_exec_status_t::VM_EXEC_OK, + } +} + +/// Returns the size in bytes of the instance memory. +/// +/// # Safety +/// +/// `ctx` must be null or a context this crate activated; `length_out` must be writable. +pub unsafe fn state_mem_length( + ctx: *mut vm_exec_hook_context_t, + length_out: *mut u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if length_out.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let length = unsafe { + with_instance_state::(ctx, |state| state.memory_length()) + }; + + match length { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(length)) => { + unsafe { + *length_out = length; + } + vm_exec_status_t::VM_EXEC_OK + } + } +} + +/// Grows the instance memory by `by_num_pages` pages of 64Kb each. +/// +/// # Safety +/// +/// `ctx` must be null or a context this crate activated; `prev_pages_out` must be writable. +pub unsafe fn state_mem_grow( + ctx: *mut vm_exec_hook_context_t, + by_num_pages: u32, + prev_pages_out: *mut u32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if prev_pages_out.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let grown = unsafe { + with_instance_state::(ctx, |state| state.memory_grow(by_num_pages)) + }; + + match grown { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(prev_pages)) => { + unsafe { + *prev_pages_out = prev_pages; + } + vm_exec_status_t::VM_EXEC_OK + } + } +} + +/// Returns the number of points (gas) used by the instance so far. +/// +/// # Safety +/// +/// `ctx` must be null or a context this crate activated; `points_out` must be writable. +pub unsafe fn state_get_points_used( + ctx: *mut vm_exec_hook_context_t, + points_out: *mut u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if points_out.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let points = unsafe { + with_instance_state::(ctx, |state| state.get_points_used()) + }; + + match points { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(points)) => { + unsafe { + *points_out = points; + } + vm_exec_status_t::VM_EXEC_OK + } + } +} + +/// Sets the number of points (gas) used by the instance. +/// +/// # Safety +/// +/// `ctx` must be null or a context this crate activated. +pub unsafe fn state_set_points_used( + ctx: *mut vm_exec_hook_context_t, + points: u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + let set = unsafe { + with_instance_state::(ctx, |state| state.set_points_used(points)) + }; + + match set { + Err(context_err) => context_err.as_status(), + Ok(Err(err)) => write_error(err, err_buf, err_buf_len), + Ok(Ok(())) => vm_exec_status_t::VM_EXEC_OK, + } +} diff --git a/c-api-common/src/lib.rs b/c-api-common/src/lib.rs index dbc41f21..a35b5041 100644 --- a/c-api-common/src/lib.rs +++ b/c-api-common/src/lib.rs @@ -20,7 +20,15 @@ //! of it names a backend, because the context carries the instance state as an erased //! pointer and only the backend's own builder implementation ever names its type. //! -//! The backend crates keep only their opaque handles and their prefixed entry points. The +//! - **The operations themselves**: executor and instance lifecycle, the outer instance +//! calls, and the state calls a hook makes. Only one of them is parameterised by backend — +//! which `Executor` an instance is built from, named by `capi_handles::CapiBackend`. +//! +//! The backend crates keep only their opaque handles and their prefixed entry points, which +//! are thin wrappers over the operations here. They have to be written out literally rather +//! than expanded from a macro, because cbindgen parses the source syntactically and generates +//! a header only for the entry points it can see; that is also why the C contract is +//! documented on the wrappers, where it becomes the header, and the rationale here. The //! legacy C API is not part of this: its types were renamed to `vm_legacy_*` precisely so //! that it can coexist with these without colliding, and it is on its way out. @@ -28,7 +36,11 @@ mod capi_logger; pub mod capi_call_result; pub mod capi_config; +pub mod capi_executor_ops; +pub mod capi_handles; +pub mod capi_instance_ops; pub mod capi_opcode_cost; +pub mod capi_state_ops; pub mod capi_status; pub mod capi_strings; pub mod capi_vm_hook_context; diff --git a/c-api-wasmer2/Cargo.toml b/c-api-wasmer2/Cargo.toml new file mode 100644 index 00000000..9364c42e --- /dev/null +++ b/c-api-wasmer2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "multiversx-chain-vm-executor-c-api-wasmer2" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +# `cdylib` is what the Go VM links against; `rlib` is what the integration tests under +# `tests/` need in order to call the exported functions from Rust. +crate-type = ["cdylib", "rlib"] + +[dependencies] +meta = { path = "../meta" } +multiversx-chain-vm-executor = { path = "../vm-executor" } +multiversx-chain-vm-executor-c-api-common = { path = "../c-api-common" } +multiversx-chain-vm-executor-wasmer = { path = "../vm-executor-wasmer" } + +libc = { version = "^0.2", default-features = false } + +[build-dependencies] +cbindgen = "0.29" diff --git a/c-api-wasmer2/build.rs b/c-api-wasmer2/build.rs new file mode 100644 index 00000000..df5369ed --- /dev/null +++ b/c-api-wasmer2/build.rs @@ -0,0 +1,39 @@ +extern crate cbindgen; + +use cbindgen::{Builder, Config, Language, Style}; +use std::{env, fs, path::PathBuf}; + +const HEADER_FILE_NAME: &str = "libvmexeccapi_w2"; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut crate_header_file = PathBuf::from(&crate_dir); + crate_header_file.push(HEADER_FILE_NAME); + + let out_dir = env::var("OUT_DIR").unwrap(); + let mut out_header_file = PathBuf::from(&out_dir); + out_header_file.push(HEADER_FILE_NAME); + + // Generate the C bindings in the `OUT_DIR`. + out_header_file.set_extension("h"); + // Reads `cbindgen.toml`, which force-exports the types no exported function mentions. + let config = Config::from_root_or_default(&crate_dir); + + Builder::new() + .with_config(config) + .with_crate(&crate_dir) + .with_language(Language::C) + // Same formatting choices as the legacy `c-api` header, so the two are easy to + // diff against each other. + .with_line_length(usize::MAX) + .with_style(Style::Both) + .generate() + .expect("Unable to generate C bindings") + .write_to_file(out_header_file.as_path()); + + // Copy the generated C bindings from `OUT_DIR` to `CARGO_MANIFEST_DIR`. + crate_header_file.set_extension("h"); + out_header_file.set_extension("h"); + fs::copy(out_header_file.as_path(), crate_header_file.as_path()) + .expect("Unable to copy the generated C bindings"); +} diff --git a/c-api-wasmer2/cbindgen.toml b/c-api-wasmer2/cbindgen.toml new file mode 100644 index 00000000..8b67d498 --- /dev/null +++ b/c-api-wasmer2/cbindgen.toml @@ -0,0 +1,17 @@ +# The shared vocabulary comes from the common header rather than being repeated here, so that +# a caller linking more than one of these libraries sees one definition of each type. Without +# the exclusion cbindgen would emit its own copy of every type it can reach. +header = "#include \"libvmexeccapi_common.h\"" + +[export] +exclude = [ + "vm_exec_status_t", + "vm_exec_opcode_cost_t", + "vm_exec_compilation_options_t", + "vm_exec_executor_config_t", + "vm_exec_call_result_kind_t", + "vm_exec_call_result_t", + "vm_exec_hook_result_t", + "vm_exec_hook_context_t", + "vm_exec_vm_hook_c_func_pointers", +] diff --git a/c-api-wasmer2/libvmexeccapi_w2.h b/c-api-wasmer2/libvmexeccapi_w2.h new file mode 100644 index 00000000..d83e71b8 --- /dev/null +++ b/c-api-wasmer2/libvmexeccapi_w2.h @@ -0,0 +1,222 @@ +#include "libvmexeccapi_common.h" + +#include +#include +#include +#include + +/** + * Opaque handle to an executor. + */ +typedef struct vm_exec_w2_executor_t { + +} vm_exec_w2_executor_t; + +/** + * Opaque handle to an instance. + */ +typedef struct vm_exec_w2_instance_t { + +} vm_exec_w2_instance_t; + +/** + * Creates an executor from one configuration struct and one hook table. + * + * `hook_table` is copied, so the caller is free to release it as soon as this returns. Every + * entry must be populated; see `CapiVMHookTable::copy_from`. + * + * On success writes the executor to `executor_out` and returns `VM_EXEC_OK`. The caller + * owns the result and must release it with `vm_exec_w2_executor_destroy`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_new_executor(struct vm_exec_w2_executor_t **executor_out, const vm_exec_executor_config_t *config, const vm_exec_vm_hook_c_func_pointers *hook_table, char *err_buf, int err_buf_len); + +/** + * Destroys an executor. Does nothing if `executor` is null. + * + * Instances created from an executor do not borrow it, so the order of destruction between + * the two does not matter. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +void vm_exec_w2_executor_destroy(struct vm_exec_w2_executor_t *executor); + +/** + * Creates an instance. + * + * `go_vm_hooks` is the caller's VM hooks handle for *this* instance; it is handed back to + * every VM hook as field 0 of the hook context, and is otherwise opaque here. Whatever it + * refers to must stay alive for the instance's lifetime. From Go it is a `cgo.Handle`, + * which is why it is an integer and not a pointer. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_new_instance(struct vm_exec_w2_executor_t *executor, struct vm_exec_w2_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const vm_exec_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); + +/** + * Destroys an instance. Does nothing if `instance` is null. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +void vm_exec_w2_instance_destroy(struct vm_exec_w2_instance_t *instance); + +/** + * Calls an exported function, with a gas limit for this call. + * + * Always writes `result_out` when it returns `VM_EXEC_OK`. A non-`OK` status means the + * call could not be attempted at all, which is distinct from a call that was attempted and + * failed — the latter is reported through `result_out.kind`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_instance_call(struct vm_exec_w2_instance_t *instance, const char *func_name, uint64_t points_limit, vm_exec_call_result_t *result_out, char *err_buf, int err_buf_len); + +/** + * Returns the number of points (gas) used so far. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_instance_get_points_used(struct vm_exec_w2_instance_t *instance, uint64_t *points_out, char *err_buf, int err_buf_len); + +/** + * Checks that every exported function takes no arguments and returns nothing. + * + * Returns 1 if so, 0 if not, or a negative status. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w2_instance_check_signatures(const struct vm_exec_w2_instance_t *instance); + +/** + * Checks whether the contract exports a function with the given name. + * + * Returns 1 if so, 0 if not, or a negative status. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w2_instance_has_function(const struct vm_exec_w2_instance_t *instance, const char *func_name); + +/** + * Buffer length needed by `vm_exec_w2_instance_exported_function_names`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w2_instance_exported_function_names_length(const struct vm_exec_w2_instance_t *instance); + +/** + * Writes the exported function names, pipe-separated, e.g. `"init|endpoint1|endpoint2"`. + * + * No ordering is guaranteed. Returns the number of bytes written including the terminator, + * or `-1` if the buffer is absent or too small. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w2_instance_exported_function_names(const struct vm_exec_w2_instance_t *instance, char *dest, int dest_len); + +/** + * Buffer length needed by `vm_exec_w2_instance_imported_function_names`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w2_instance_imported_function_names_length(const struct vm_exec_w2_instance_t *instance); + +/** + * Writes the names of the VM hooks the contract imports, pipe-separated. + * + * All of them at once, rather than a per-name query, because the Go contract validator + * checks the module against whole sets of allowed names. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int vm_exec_w2_instance_imported_function_names(const struct vm_exec_w2_instance_t *instance, char *dest, int dest_len); + +/** + * Loads from instance memory into a caller-provided buffer, **truncating** at the end of + * the memory. + * + * Returns the number of bytes actually written, which may be fewer than `dest_len`, or a + * negative status. The truncation is where the Go VM's historical load semantics live: a + * load whose offset is valid but whose end lies past the end of the memory yields the bytes + * that are available and no error. Contracts have observed that, so the executor keeps + * offering it. An offset outside the memory, or a negative length, is still an error. + * + * `dest_len` doubles as the requested length, which is what makes the clamping visible to + * the caller: Go allocates `length` bytes and gets back `min(length, available)`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +int64_t vm_exec_w2_state_mem_load(vm_exec_hook_context_t *ctx, int32_t mem_ptr, uint8_t *dest, int32_t dest_len, char *err_buf, int err_buf_len); + +/** + * Writes to instance memory. The range must fit entirely inside the memory. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_state_mem_store(vm_exec_hook_context_t *ctx, int32_t mem_ptr, const uint8_t *src, int32_t src_len, char *err_buf, int err_buf_len); + +/** + * Returns the size in bytes of the instance memory. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_state_mem_length(vm_exec_hook_context_t *ctx, uint64_t *length_out, char *err_buf, int err_buf_len); + +/** + * Grows the instance memory by `by_num_pages` pages of 64Kb each. + * + * Writes the number of pages the memory had *before* growing to `prev_pages_out`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_state_mem_grow(vm_exec_hook_context_t *ctx, uint32_t by_num_pages, uint32_t *prev_pages_out, char *err_buf, int err_buf_len); + +/** + * Returns the number of points (gas) used by the instance so far. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_state_get_points_used(vm_exec_hook_context_t *ctx, uint64_t *points_out, char *err_buf, int err_buf_len); + +/** + * Sets the number of points (gas) used by the instance. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_state_set_points_used(vm_exec_hook_context_t *ctx, uint64_t points, char *err_buf, int err_buf_len); diff --git a/c-api-wasmer2/src/capi_backend.rs b/c-api-wasmer2/src/capi_backend.rs new file mode 100644 index 00000000..30d84507 --- /dev/null +++ b/c-api-wasmer2/src/capi_backend.rs @@ -0,0 +1,28 @@ +//! Which wasm runtime this library is for. +//! +//! The one file in this crate that names a backend, and the reason the three entry-point +//! files do not: they reach the backend only through this type, under a name that is the +//! same in every backend crate. That is what keeps them identical to the Wasmer 6 copy +//! apart from the symbol prefix — a property an integration test checks, so the two cannot +//! drift. + +use multiversx_chain_vm_executor::Executor; +use multiversx_chain_vm_executor_wasmer::new_traits::WasmerProdExecutor; +use std::sync::Arc; + +use crate::capi_handles::CapiBackend; +use crate::capi_vm_hook_context::CapiVMHooksBuilder; + +/// Wasmer 2.2, through the production backend. +/// +/// Its hooks are the legacy value-returning kind, with no channel for an early exit, so the +/// backend wraps the hooks this C API builds in `VMHooksLegacyAdapter`. Nothing about that is +/// visible here or to the caller: the adapter asks the runtime for hooks per call, exactly as +/// Wasmer 6 does, so both libraries resolve the current execution when a hook runs. +pub struct Backend; + +impl CapiBackend for Backend { + fn new_executor(runtime: Arc) -> Box { + Box::new(WasmerProdExecutor::new(runtime)) + } +} diff --git a/c-api-wasmer2/src/capi_executor.rs b/c-api-wasmer2/src/capi_executor.rs new file mode 100644 index 00000000..18cb2894 --- /dev/null +++ b/c-api-wasmer2/src/capi_executor.rs @@ -0,0 +1,80 @@ +//! Executor lifecycle. +//! +//! An executor here is the shared, immutable configuration: the opcode config and the Go +//! hook table. It is passed once, at creation, and never mutated — the legacy +//! `vm_exec_set_opcode_config` and `vm_exec_executor_set_vm_hooks_ptr` setters have no +//! counterpart. +//! +//! Note that it does not yet wrap a `dyn Executor`. An executor is built around one runtime +//! reference, for its whole life, and this API's runtime handle is still supplied per +//! instance at `vm_exec_w2_new_instance` — so the executor is built there, per instance, and +//! this struct holds the configuration it will be built from. The handle becomes +//! per-executor, and the `dyn Executor` moves here, once the Go VM stops reaching wasm memory +//! through `executor.Instance`; see `PLAN-go-ffi.md`. +//! +//! The opcode config is held here for the same reason. On the Rust side it has moved into +//! `CompilationOptions`, and this API merges the two at instance creation rather than +//! changing the C struct ahead of the Go VM. +//! +//! The bodies are in `c-api-common`, shared with the Wasmer 2.2 library. What is here is the +//! prefixed symbol and the C contract. + +use libc::{c_char, c_int}; +use meta::capi_safe_unwind; + +use crate::capi_config::vm_exec_executor_config_t; +use crate::capi_executor_ops; +use crate::capi_handles::CapiExecutor; +use crate::capi_status::vm_exec_status_t; +use crate::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; + +/// Opaque handle to an executor. +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct vm_exec_w2_executor_t; + +/// Creates an executor from one configuration struct and one hook table. +/// +/// `hook_table` is copied, so the caller is free to release it as soon as this returns. Every +/// entry must be populated; see `CapiVMHookTable::copy_from`. +/// +/// On success writes the executor to `executor_out` and returns `VM_EXEC_OK`. The caller +/// owns the result and must release it with `vm_exec_w2_executor_destroy`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_new_executor( + executor_out: *mut *mut vm_exec_w2_executor_t, + config: *const vm_exec_executor_config_t, + hook_table: *const vm_exec_vm_hook_c_func_pointers, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_executor_ops::new_executor( + executor_out as *mut *mut CapiExecutor, + config, + hook_table, + err_buf, + err_buf_len, + ) + } +} + +/// Destroys an executor. Does nothing if `executor` is null. +/// +/// Instances created from an executor do not borrow it, so the order of destruction between +/// the two does not matter. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn vm_exec_w2_executor_destroy(executor: *mut vm_exec_w2_executor_t) { + unsafe { capi_executor_ops::executor_destroy(executor as *mut CapiExecutor) } +} diff --git a/c-api-wasmer2/src/capi_instance.rs b/c-api-wasmer2/src/capi_instance.rs new file mode 100644 index 00000000..6f730f8b --- /dev/null +++ b/c-api-wasmer2/src/capi_instance.rs @@ -0,0 +1,246 @@ +//! Instance lifecycle and the outer instance operations. +//! +//! "Outer" is the side of the trait split that the VM sees from outside a host call: +//! calling, gas accounting, and module introspection. Everything a *hook* needs is in +//! `capi_instance_state`, reached through a hook context rather than through this handle. +//! Keeping the split is what makes the Go side match the Rust trait pair instead of merely +//! wrapping it. +//! +//! The bodies are in `c-api-common`, shared with the other backend's library; `capi_backend` +//! is the only thing that differs. What is here is the prefixed symbols and the C contract. + +use libc::{c_char, c_int}; +use meta::capi_safe_unwind; + +use crate::capi_backend::Backend; +use crate::capi_call_result::vm_exec_call_result_t; +use crate::capi_config::vm_exec_compilation_options_t; +use crate::capi_executor::vm_exec_w2_executor_t; +use crate::capi_handles::{CapiExecutor, CapiInstance}; +use crate::capi_instance_ops; +use crate::capi_status::vm_exec_status_t; + +/// Opaque handle to an instance. +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct vm_exec_w2_instance_t; + +/// Creates an instance. +/// +/// `go_vm_hooks` is the caller's VM hooks handle for *this* instance; it is handed back to +/// every VM hook as field 0 of the hook context, and is otherwise opaque here. Whatever it +/// refers to must stay alive for the instance's lifetime. From Go it is a `cgo.Handle`, +/// which is why it is an integer and not a pointer. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[allow(clippy::too_many_arguments)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_new_instance( + executor: *mut vm_exec_w2_executor_t, + instance_out: *mut *mut vm_exec_w2_instance_t, + wasm_bytes: *const u8, + wasm_bytes_len: u32, + options: *const vm_exec_compilation_options_t, + go_vm_hooks: usize, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::new_instance::( + executor as *mut CapiExecutor, + instance_out as *mut *mut CapiInstance, + wasm_bytes, + wasm_bytes_len, + options, + go_vm_hooks, + err_buf, + err_buf_len, + ) + } +} + +/// Destroys an instance. Does nothing if `instance` is null. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn vm_exec_w2_instance_destroy(instance: *mut vm_exec_w2_instance_t) { + unsafe { capi_instance_ops::instance_destroy(instance as *mut CapiInstance) } +} + +/// Calls an exported function, with a gas limit for this call. +/// +/// Always writes `result_out` when it returns `VM_EXEC_OK`. A non-`OK` status means the +/// call could not be attempted at all, which is distinct from a call that was attempted and +/// failed — the latter is reported through `result_out.kind`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_instance_call( + instance: *mut vm_exec_w2_instance_t, + func_name: *const c_char, + points_limit: u64, + result_out: *mut vm_exec_call_result_t, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::instance_call( + instance as *mut CapiInstance, + func_name, + points_limit, + result_out, + err_buf, + err_buf_len, + ) + } +} + +/// Returns the number of points (gas) used so far. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_instance_get_points_used( + instance: *mut vm_exec_w2_instance_t, + points_out: *mut u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::instance_get_points_used( + instance as *mut CapiInstance, + points_out, + err_buf, + err_buf_len, + ) + } +} + +/// Checks that every exported function takes no arguments and returns nothing. +/// +/// Returns 1 if so, 0 if not, or a negative status. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w2_instance_check_signatures( + instance: *const vm_exec_w2_instance_t, +) -> c_int { + unsafe { capi_instance_ops::instance_check_signatures(instance as *const CapiInstance) } +} + +/// Checks whether the contract exports a function with the given name. +/// +/// Returns 1 if so, 0 if not, or a negative status. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w2_instance_has_function( + instance: *const vm_exec_w2_instance_t, + func_name: *const c_char, +) -> c_int { + unsafe { capi_instance_ops::instance_has_function(instance as *const CapiInstance, func_name) } +} + +/// Buffer length needed by `vm_exec_w2_instance_exported_function_names`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w2_instance_exported_function_names_length( + instance: *const vm_exec_w2_instance_t, +) -> c_int { + unsafe { + capi_instance_ops::instance_exported_function_names_length(instance as *const CapiInstance) + } +} + +/// Writes the exported function names, pipe-separated, e.g. `"init|endpoint1|endpoint2"`. +/// +/// No ordering is guaranteed. Returns the number of bytes written including the terminator, +/// or `-1` if the buffer is absent or too small. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w2_instance_exported_function_names( + instance: *const vm_exec_w2_instance_t, + dest: *mut c_char, + dest_len: c_int, +) -> c_int { + unsafe { + capi_instance_ops::instance_exported_function_names( + instance as *const CapiInstance, + dest, + dest_len, + ) + } +} + +/// Buffer length needed by `vm_exec_w2_instance_imported_function_names`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w2_instance_imported_function_names_length( + instance: *const vm_exec_w2_instance_t, +) -> c_int { + unsafe { + capi_instance_ops::instance_imported_function_names_length(instance as *const CapiInstance) + } +} + +/// Writes the names of the VM hooks the contract imports, pipe-separated. +/// +/// All of them at once, rather than a per-name query, because the Go contract validator +/// checks the module against whole sets of allowed names. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] +pub unsafe extern "C" fn vm_exec_w2_instance_imported_function_names( + instance: *const vm_exec_w2_instance_t, + dest: *mut c_char, + dest_len: c_int, +) -> c_int { + unsafe { + capi_instance_ops::instance_imported_function_names( + instance as *const CapiInstance, + dest, + dest_len, + ) + } +} diff --git a/c-api-wasmer2/src/capi_instance_state.rs b/c-api-wasmer2/src/capi_instance_state.rs new file mode 100644 index 00000000..dc6322ec --- /dev/null +++ b/c-api-wasmer2/src/capi_instance_state.rs @@ -0,0 +1,136 @@ +//! The state operations: what a VM hook may do to the instance it is running inside. +//! +//! Every function here takes a hook context and is valid only while the hook that was handed +//! that context is still running. See `capi_vm_hook_context` for how that is enforced; a stale +//! context yields `VM_EXEC_STALE_CONTEXT` rather than a dereference of a dead borrow. +//! +//! Bounds checking lives on this side of the boundary, not in Go. The executor is the only +//! side that knows the real extent of the instance memory, and the rules are observable by +//! contracts, so there is one implementation of them — shared with the other backends in +//! `vm-executor/src/new_traits/memory.rs` — rather than one per caller. +//! +//! The bodies are in `c-api-common`, shared verbatim with the other backend's library: the +//! state arrives through the context as an erased `&mut dyn InstanceState`, so none of this +//! names a backend. What is here is the prefixed symbols and the C contract. + +use libc::{c_char, c_int}; +use meta::capi_safe_unwind; + +use crate::capi_state_ops; +use crate::capi_status::vm_exec_status_t; +use crate::capi_vm_hook_context::vm_exec_hook_context_t; + +/// Loads from instance memory into a caller-provided buffer, **truncating** at the end of +/// the memory. +/// +/// Returns the number of bytes actually written, which may be fewer than `dest_len`, or a +/// negative status. The truncation is where the Go VM's historical load semantics live: a +/// load whose offset is valid but whose end lies past the end of the memory yields the bytes +/// that are available and no error. Contracts have observed that, so the executor keeps +/// offering it. An offset outside the memory, or a negative length, is still an error. +/// +/// `dest_len` doubles as the requested length, which is what makes the clamping visible to +/// the caller: Go allocates `length` bytes and gets back `min(length, available)`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_i64())] +pub unsafe extern "C" fn vm_exec_w2_state_mem_load( + ctx: *mut vm_exec_hook_context_t, + mem_ptr: i32, + dest: *mut u8, + dest_len: i32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> i64 { + unsafe { capi_state_ops::state_mem_load(ctx, mem_ptr, dest, dest_len, err_buf, err_buf_len) } +} + +/// Writes to instance memory. The range must fit entirely inside the memory. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_state_mem_store( + ctx: *mut vm_exec_hook_context_t, + mem_ptr: i32, + src: *const u8, + src_len: i32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { capi_state_ops::state_mem_store(ctx, mem_ptr, src, src_len, err_buf, err_buf_len) } +} + +/// Returns the size in bytes of the instance memory. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_state_mem_length( + ctx: *mut vm_exec_hook_context_t, + length_out: *mut u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { capi_state_ops::state_mem_length(ctx, length_out, err_buf, err_buf_len) } +} + +/// Grows the instance memory by `by_num_pages` pages of 64Kb each. +/// +/// Writes the number of pages the memory had *before* growing to `prev_pages_out`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_state_mem_grow( + ctx: *mut vm_exec_hook_context_t, + by_num_pages: u32, + prev_pages_out: *mut u32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_state_ops::state_mem_grow(ctx, by_num_pages, prev_pages_out, err_buf, err_buf_len) + } +} + +/// Returns the number of points (gas) used by the instance so far. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_state_get_points_used( + ctx: *mut vm_exec_hook_context_t, + points_out: *mut u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { capi_state_ops::state_get_points_used(ctx, points_out, err_buf, err_buf_len) } +} + +/// Sets the number of points (gas) used by the instance. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_state_set_points_used( + ctx: *mut vm_exec_hook_context_t, + points: u64, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { capi_state_ops::state_set_points_used(ctx, points, err_buf, err_buf_len) } +} diff --git a/c-api-wasmer2/src/capi_logger.rs b/c-api-wasmer2/src/capi_logger.rs new file mode 100644 index 00000000..6bfc90ff --- /dev/null +++ b/c-api-wasmer2/src/capi_logger.rs @@ -0,0 +1,6 @@ +//! Satisfies the path `meta::capi_safe_unwind` expands to. +//! +//! The attribute emits `crate::capi_logger::set_panic_handler()`, so every crate that uses it +//! needs a module of that name at its own root. The implementation is shared; only the path +//! has to be local. +pub use multiversx_chain_vm_executor_c_api_common::set_panic_handler; diff --git a/c-api-wasmer2/src/lib.rs b/c-api-wasmer2/src/lib.rs new file mode 100644 index 00000000..15079bbc --- /dev/null +++ b/c-api-wasmer2/src/lib.rs @@ -0,0 +1,41 @@ +//! C API for the new executor interface, over the **Wasmer 2.2** backend. +//! +//! The sibling of `c-api-wasmer6`, and deliberately as close to it as two libraries can be: +//! the three entry-point files are character-identical to that crate's apart from the symbol +//! prefix, and `tests/prefix_parity.rs` fails if they ever stop being. Everything behind them +//! — the operations, the hook dispatch, the error convention — is compiled once in +//! `c-api-common`. The only thing this crate decides is `capi_backend`. +//! +//! **Why it exists.** Wasmer 2.2 is the executor currently in production, and it is the +//! reference the new interfaces have to be measured against. Running the *same* runtime +//! through the *new* boundary is what isolates the boundary as the variable: a difference +//! between this library and `libvmexeccapi` is a difference the refactor introduced, with no +//! wasm runtime change to confound it. Comparing Wasmer 6 against Wasmer 2.2 cannot say that, +//! because it changes both at once. +//! +//! - **Exported symbols are prefixed `vm_exec_w2_`**, so this library, `libvmexeccapi_w6` and +//! the legacy `libvmexeccapi` can all be linked into one binary. +//! - **Types are shared, not duplicated**: the vocabulary comes from `libvmexeccapi_common.h`, +//! which both new headers include, so a caller linking both sees one definition of each. +//! +//! One thing this library does *not* expose, although the backend supports it: there is no +//! instance `reset`, and no `cache`. Those are the warm-instance path the legacy API has and +//! Wasmer 6 does not, and adding them here would make this library's shape depend on which +//! backend is underneath — which is the thing it exists to hold constant. A differential +//! against the legacy library therefore compares a warm cache against cold instances; the +//! hook sequence and the gas accounting, which are what it is measuring, are unaffected. + +mod capi_logger; + +pub mod capi_backend; +pub mod capi_executor; +pub mod capi_instance; +pub mod capi_instance_state; + +// The shared C vocabulary, re-exported so that this crate's modules — and the cbindgen run +// that produces its header — reach the same definitions the common header publishes. +pub use multiversx_chain_vm_executor_c_api_common::{ + capi_call_result, capi_config, capi_executor_ops, capi_handles, capi_instance_ops, + capi_opcode_cost, capi_state_ops, capi_status, capi_strings, capi_vm_hook_context, + capi_vm_hook_pointers, capi_vm_hook_table, capi_vm_hooks, +}; diff --git a/c-api-wasmer2/tests/ffi_boundary.rs b/c-api-wasmer2/tests/ffi_boundary.rs new file mode 100644 index 00000000..0c4f0ecf --- /dev/null +++ b/c-api-wasmer2/tests/ffi_boundary.rs @@ -0,0 +1,785 @@ +//! Drives the C API from Rust, so the boundary is testable before any Go exists. +//! +//! Three halves: +//! +//! - [`lifecycle`] goes through the exported functions exactly as the Go side will. +//! - [`hook_dispatch`] runs a contract that calls VM hooks, with a mock hook table standing +//! in for the generated Go shims. This is the full round trip: wasm into Rust, out through +//! a C function pointer, back into Rust through the state functions, and — for an early +//! exit — out again as an `InstanceCallResult`. +//! - [`state_ops`] installs a mock hooks *provider* instead, to reach the context lifecycle +//! directly: a context that outlives its call, and an outer context while a nested call is +//! running, must both be refused. + +use std::ffi::{CString, c_char, c_int, c_void}; +use std::sync::{Arc, Mutex, OnceLock}; + +use multiversx_chain_vm_executor::{ + Executor, ExecutorRuntime, InstanceState, VMHooks, VMHooksDefault, +}; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_call_result::{ + vm_exec_call_result_kind_t, vm_exec_call_result_t, +}; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_config::{ + vm_exec_compilation_options_t, vm_exec_executor_config_t, +}; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_executor::{ + vm_exec_w2_executor_destroy, vm_exec_w2_executor_t, vm_exec_w2_new_executor, +}; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_instance::*; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_instance_state::*; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_opcode_cost::vm_exec_opcode_cost_t; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_status::vm_exec_status_t; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_vm_hook_context::{ + CapiVMHooksBuilder, vm_exec_hook_context_t, vm_exec_hook_result_t, +}; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; +use multiversx_chain_vm_executor_c_api_wasmer2::capi_vm_hook_table::CapiVMHookTable; +use multiversx_chain_vm_executor_wasmer::new_traits::WasmerProdExecutor; + +/// One memory page, five known bytes at offset 0, and endpoints that call VM hooks. +const CONTRACT_WAT: &str = r#" +(module + (import "env" "getGasLeft" (func $getGasLeft (result i64))) + (import "env" "signalError" (func $signalError (param i32 i32))) + (memory (export "memory") 1) + (data (i32.const 0) "hello") + (func (export "doNothing")) + (func (export "callGetGasLeft") (drop (call $getGasLeft))) + (func (export "callSignalError") (call $signalError (i32.const 0) (i32.const 5))) +) +"#; + +const ERR_BUF_LEN: c_int = 1024; + +/// The code the mock `signalError` reports; `vmcommon.UserError` on the Go side. +const USER_ERROR_CODE: u64 = 4; + +/// Reported by a hook the test contract was not supposed to call. +const UNEXPECTED_HOOK_CODE: u64 = 9999; + +struct ErrBuf(Vec); + +impl ErrBuf { + fn new() -> Self { + ErrBuf(vec![0; ERR_BUF_LEN as usize]) + } + + fn ptr(&mut self) -> *mut c_char { + self.0.as_mut_ptr() + } + + fn message(&self) -> String { + let bytes: Vec = self + .0 + .iter() + .take_while(|b| **b != 0) + .map(|b| *b as u8) + .collect(); + String::from_utf8(bytes).expect("error message is not valid UTF-8") + } +} + +fn opcode_cost() -> vm_exec_opcode_cost_t { + // A `#[repr(C)]` struct of `u32`s, so an all-zero cost table is a valid value. Zero costs + // keep the tests away from the metering limit, which is not what they are about. + unsafe { std::mem::zeroed() } +} + +fn compilation_options() -> vm_exec_compilation_options_t { + vm_exec_compilation_options_t { + unmetered_locals: 0, + max_memory_grow: 10, + max_memory_grow_delta: 10, + opcode_trace: false, + } +} + +// --- The mock hook table ---------------------------------------------------------------- +// +// Only the two hooks the test contract imports get a correctly typed implementation. The rest +// of the table is filled with a stub of the same shape, so that copying the table reads valid +// function pointers throughout — a `mem::zeroed` table would be immediately invalid, since +// `extern "C" fn` is a non-nullable type. Calling one of the stubs would be a bug in the test +// contract, and it reports a code the assertions would catch. + +extern "C" fn stub_hook(_ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t { + vm_exec_hook_result_t::early_exit(UNEXPECTED_HOOK_CODE) +} + +/// What the mock hooks record, for assertions after the call has returned. +/// +/// They cannot assert directly: a panic must not unwind out of an `extern "C"` function. +#[derive(Default)] +struct HookLog { + get_gas_left_calls: usize, + signal_error_calls: usize, + /// What `getGasLeft` read out of the contract's memory through its context. + memory_seen: Option>, + /// The message `signalError` was pointed at. + signalled_message: Option>, + /// Non-empty if a state operation misbehaved. + failures: Vec, +} + +fn hook_log() -> &'static Mutex { + static LOG: OnceLock> = OnceLock::new(); + LOG.get_or_init(|| Mutex::new(HookLog::default())) +} + +/// Loads from the contract's memory through the hook context it was handed. +fn load_through_context( + ctx: *mut vm_exec_hook_context_t, + mem_ptr: i32, + len: i32, +) -> Result, i64> { + let mut buf = vec![0u8; len.max(0) as usize]; + let loaded = unsafe { + vm_exec_w2_state_mem_load(ctx, mem_ptr, buf.as_mut_ptr(), len, std::ptr::null_mut(), 0) + }; + if loaded < 0 { + return Err(loaded); + } + buf.truncate(loaded as usize); + Ok(buf) +} + +extern "C" fn mock_get_gas_left(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hook_result_t { + let mut log = hook_log().lock().unwrap(); + log.get_gas_left_calls += 1; + + // The point of the exercise: a hook reaching back into the instance it is running inside, + // through the context the executor handed it. + match load_through_context(ctx, 0, 5) { + Ok(bytes) => log.memory_seen = Some(bytes), + Err(status) => log + .failures + .push(format!("mem_load in getGasLeft: {status}")), + } + + let mut length = 0u64; + let status = unsafe { vm_exec_w2_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; + if status != vm_exec_status_t::VM_EXEC_OK || length != 65536 { + log.failures + .push(format!("mem_length in getGasLeft: {status:?}, {length}")); + } + + vm_exec_hook_result_t::ok(4242) +} + +extern "C" fn mock_signal_error( + ctx: *mut vm_exec_hook_context_t, + message_offset: i32, + message_length: i32, +) -> vm_exec_hook_result_t { + let mut log = hook_log().lock().unwrap(); + log.signal_error_calls += 1; + + match load_through_context(ctx, message_offset, message_length) { + Ok(bytes) => log.signalled_message = Some(bytes), + Err(status) => log + .failures + .push(format!("mem_load in signalError: {status}")), + } + + // A hook that wants execution to stop says so in its result. Nothing is written to a + // breakpoint global on the way out. + vm_exec_hook_result_t::early_exit(USER_ERROR_CODE) +} + +/// Builds a table whose every entry is a valid function pointer. +fn mock_hook_table() -> &'static vm_exec_vm_hook_c_func_pointers { + const WORD: usize = std::mem::size_of::(); + let words = std::mem::size_of::() / WORD; + assert_eq!( + std::mem::size_of::() % WORD, + 0, + "the hook table should be a flat block of function pointers" + ); + + let filled = vec![stub_hook as *const () as usize; words].into_boxed_slice(); + let table = + unsafe { &mut *(Box::leak(filled).as_mut_ptr() as *mut vm_exec_vm_hook_c_func_pointers) }; + + table.get_gas_left_func_ptr = mock_get_gas_left; + table.signal_error_func_ptr = mock_signal_error; + table +} + +// --- Driving the exported functions ------------------------------------------------------- + +fn new_executor(err: &mut ErrBuf) -> *mut vm_exec_w2_executor_t { + let cost = opcode_cost(); + let config = vm_exec_executor_config_t { + opcode_version: 1, + opcode_cost: &cost, + }; + + let mut executor: *mut vm_exec_w2_executor_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec_w2_new_executor( + &mut executor, + &config, + mock_hook_table(), + err.ptr(), + ERR_BUF_LEN, + ) + }; + + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "new_executor failed: {}", + err.message() + ); + assert!(!executor.is_null()); + executor +} + +fn new_instance( + executor: *mut vm_exec_w2_executor_t, + err: &mut ErrBuf, +) -> *mut vm_exec_w2_instance_t { + let options = compilation_options(); + let wasm = CONTRACT_WAT.as_bytes(); + let go_vm_hooks = 0xBEEFusize; + + let mut instance: *mut vm_exec_w2_instance_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec_w2_new_instance( + executor, + &mut instance, + wasm.as_ptr(), + wasm.len() as u32, + &options, + go_vm_hooks, + err.ptr(), + ERR_BUF_LEN, + ) + }; + + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "new_instance failed: {}", + err.message() + ); + assert!(!instance.is_null()); + instance +} + +fn call( + instance: *mut vm_exec_w2_instance_t, + func_name: &str, + err: &mut ErrBuf, +) -> vm_exec_call_result_t { + let name = CString::new(func_name).unwrap(); + let mut result = vm_exec_call_result_t { + kind: vm_exec_call_result_kind_t::VM_EXEC_CALL_RUNTIME_ERROR, + early_exit_code: u64::MAX, + breakpoint: u64::MAX, + }; + + let status = unsafe { + vm_exec_w2_instance_call( + instance, + name.as_ptr(), + 1_000_000, + &mut result, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_OK); + result +} + +fn names( + instance: *mut vm_exec_w2_instance_t, + length: unsafe extern "C" fn(*const vm_exec_w2_instance_t) -> c_int, + write: unsafe extern "C" fn(*const vm_exec_w2_instance_t, *mut c_char, c_int) -> c_int, +) -> Vec { + let needed = unsafe { length(instance) }; + assert!(needed > 0, "expected a non-empty name list"); + + let mut buf = vec![0 as c_char; needed as usize]; + let written = unsafe { write(instance, buf.as_mut_ptr(), needed) }; + assert_eq!(written, needed, "two-step length and write must agree"); + + let bytes: Vec = buf + .iter() + .take_while(|b| **b != 0) + .map(|b| *b as u8) + .collect(); + let mut names: Vec = String::from_utf8(bytes) + .unwrap() + .split('|') + .map(str::to_string) + .collect(); + names.sort(); + names +} + +#[test] +fn lifecycle() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + let result = call(instance, "doNothing", &mut err); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); + + // A missing endpoint is reported in the result, not as a failure of the call itself. + let result = call(instance, "noSuchEndpoint", &mut err); + assert_eq!( + result.kind, + vm_exec_call_result_kind_t::VM_EXEC_CALL_FUNCTION_NOT_FOUND + ); + + let mut points = u64::MAX; + let status = unsafe { + vm_exec_w2_instance_get_points_used(instance, &mut points, err.ptr(), ERR_BUF_LEN) + }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_OK); + assert_ne!(points, u64::MAX, "points_used was not written"); + + assert_eq!(unsafe { vm_exec_w2_instance_check_signatures(instance) }, 1); + + let present = CString::new("doNothing").unwrap(); + let absent = CString::new("noSuchEndpoint").unwrap(); + assert_eq!( + unsafe { vm_exec_w2_instance_has_function(instance, present.as_ptr()) }, + 1 + ); + assert_eq!( + unsafe { vm_exec_w2_instance_has_function(instance, absent.as_ptr()) }, + 0 + ); + + assert_eq!( + names( + instance, + vm_exec_w2_instance_exported_function_names_length, + vm_exec_w2_instance_exported_function_names, + ), + vec![ + "callGetGasLeft".to_string(), + "callSignalError".to_string(), + "doNothing".to_string() + ] + ); + + // All of the hooks the module imports, in one call. + assert_eq!( + names( + instance, + vm_exec_w2_instance_imported_function_names_length, + vm_exec_w2_instance_imported_function_names, + ), + vec!["getGasLeft".to_string(), "signalError".to_string()] + ); + + unsafe { + vm_exec_w2_instance_destroy(instance); + vm_exec_w2_executor_destroy(executor); + } +} + +#[test] +fn hook_dispatch() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + // A hook that returns normally: execution continues, and the contract gets the value. + let result = call(instance, "callGetGasLeft", &mut err); + assert_eq!( + result.kind, + vm_exec_call_result_kind_t::VM_EXEC_CALL_OK, + "calling a hook that returns normally should not stop execution" + ); + + // A hook that asks to stop: the code travels out in the call result, and the sentinel + // keeps it distinct from "no early exit". + let result = call(instance, "callSignalError", &mut err); + assert_eq!( + result.kind, + vm_exec_call_result_kind_t::VM_EXEC_CALL_EARLY_EXIT + ); + assert_eq!(result.early_exit_code, USER_ERROR_CODE); + + let log = hook_log().lock().unwrap(); + assert!( + log.failures.is_empty(), + "state op failures inside hooks:\n {}", + log.failures.join("\n ") + ); + assert_eq!(log.get_gas_left_calls, 1); + assert_eq!(log.signal_error_calls, 1); + + // Both hooks reached the contract's memory through the context they were handed. + assert_eq!(log.memory_seen.as_deref(), Some(&b"hello"[..])); + assert_eq!(log.signalled_message.as_deref(), Some(&b"hello"[..])); + + drop(log); + unsafe { + vm_exec_w2_instance_destroy(instance); + vm_exec_w2_executor_destroy(executor); + } +} + +#[test] +fn null_arguments_are_refused_without_a_message() { + let mut err = ErrBuf::new(); + let mut executor: *mut vm_exec_w2_executor_t = std::ptr::null_mut(); + + // A null config cannot be reported through the error buffer, because the buffer itself may + // be the thing that is null, so these return a dedicated status instead. + let status = unsafe { + vm_exec_w2_new_executor( + &mut executor, + std::ptr::null(), + std::ptr::null(), + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_NULL_POINTER); + assert!(executor.is_null()); + + assert_eq!( + unsafe { vm_exec_w2_instance_check_signatures(std::ptr::null()) }, + vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int() + ); +} + +#[test] +fn an_invalid_opcode_version_is_reported_through_the_buffer() { + let mut err = ErrBuf::new(); + let cost = opcode_cost(); + let config = vm_exec_executor_config_t { + opcode_version: 99, + opcode_cost: &cost, + }; + + let mut executor: *mut vm_exec_w2_executor_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec_w2_new_executor( + &mut executor, + &config, + mock_hook_table(), + err.ptr(), + ERR_BUF_LEN, + ) + }; + + assert_eq!(status, vm_exec_status_t::VM_EXEC_ERROR); + assert_eq!(err.message(), "invalid opcode version code: 99"); + assert!(executor.is_null()); +} + +#[test] +fn a_context_is_rejected_when_no_call_is_active() { + let mut err = ErrBuf::new(); + + assert_eq!( + unsafe { + vm_exec_w2_state_mem_length(std::ptr::null_mut(), &mut 0u64, err.ptr(), ERR_BUF_LEN) + }, + vm_exec_status_t::VM_EXEC_NULL_POINTER + ); + + let mut points = 0u64; + assert_eq!( + unsafe { + vm_exec_w2_state_get_points_used( + std::ptr::null_mut(), + &mut points, + err.ptr(), + ERR_BUF_LEN, + ) + }, + vm_exec_status_t::VM_EXEC_NULL_POINTER + ); +} + +// --- The context lifecycle, reached through a mock hooks provider -------------------------- + +/// What `state_ops` records from inside the host call, to be asserted after it returns. +#[derive(Default)] +struct Outcome { + failures: Vec, + hook_calls: usize, + escaped_context: usize, +} + +impl Outcome { + fn check(&mut self, what: &str, condition: bool, detail: String) { + if !condition { + self.failures.push(format!("{what}: {detail}")); + } + } +} + +/// A mock hooks provider that exercises the state FFI from inside a real host call. +struct ProbingHooksBuilder { + inner: CapiVMHooksBuilder, + outcome: Arc>, +} + +impl ExecutorRuntime for ProbingHooksBuilder { + fn with_vm_hooks( + &self, + instance_state: &mut dyn InstanceState, + f: &mut dyn FnMut(&mut dyn VMHooks), + ) { + // Erased exactly as `CapiVMHooksBuilder` erases it — a thin pointer to the fat + // pointer — since the state functions under test reconstitute it as + // `ErasedInstanceState`. + let mut erased: &mut dyn InstanceState = instance_state; + let state_ptr = (&mut erased as *mut &mut dyn InstanceState).cast::(); + + let mut outcome = self.outcome.lock().unwrap(); + outcome.hook_calls += 1; + + unsafe { + self.inner.with_hook_context_raw(state_ptr, |ctx| { + probe_state_ops(ctx, &mut outcome); + probe_nesting(&self.inner, state_ptr, ctx, &mut outcome); + outcome.escaped_context = ctx as usize; + }); + } + + drop(outcome); + f(&mut VMHooksDefault); + } +} + +fn mem_load(ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, dest: &mut [u8], len: i32) -> i64 { + unsafe { + vm_exec_w2_state_mem_load( + ctx, + mem_ptr, + dest.as_mut_ptr(), + len, + std::ptr::null_mut(), + 0, + ) + } +} + +fn mem_store(ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, src: &[u8]) -> vm_exec_status_t { + unsafe { + vm_exec_w2_state_mem_store( + ctx, + mem_ptr, + src.as_ptr(), + src.len() as i32, + std::ptr::null_mut(), + 0, + ) + } +} + +fn probe_state_ops(ctx: *mut vm_exec_hook_context_t, outcome: &mut Outcome) { + const PAGE: u64 = 65536; + + let mut length = 0u64; + let status = unsafe { vm_exec_w2_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; + outcome.check( + "mem_length", + status == vm_exec_status_t::VM_EXEC_OK && length == PAGE, + format!("{status:?}, length {length}"), + ); + + // A load entirely inside the memory returns what the data segment put there. + let mut buf = [0u8; 5]; + let loaded = mem_load(ctx, 0, &mut buf, 5); + outcome.check( + "mem_load reads the data segment", + loaded == 5 && &buf == b"hello", + format!("loaded {loaded}, {:?}", std::str::from_utf8(&buf)), + ); + + // A load that runs past the end is truncated, not refused: the Go VM's semantics. + let mut buf = [0u8; 10]; + let loaded = mem_load(ctx, (PAGE - 2) as i32, &mut buf, 10); + outcome.check( + "mem_load clamps at the end of memory", + loaded == 2, + format!("loaded {loaded}"), + ); + + // An offset exactly at the end is a valid offset that yields nothing. + let loaded = mem_load(ctx, PAGE as i32, &mut buf, 1); + outcome.check( + "mem_load at the end of memory yields nothing", + loaded == 0, + format!("loaded {loaded}"), + ); + + // An offset past the end is an error, even though a too-long range is not. + let loaded = mem_load(ctx, (PAGE + 1) as i32, &mut buf, 1); + outcome.check( + "mem_load rejects an offset past the end", + loaded == vm_exec_status_t::VM_EXEC_ERROR.as_i64(), + format!("loaded {loaded}"), + ); + + let loaded = mem_load(ctx, -1, &mut buf, 1); + outcome.check( + "mem_load rejects a negative offset", + loaded == vm_exec_status_t::VM_EXEC_ERROR.as_i64(), + format!("loaded {loaded}"), + ); + + let loaded = mem_load(ctx, 0, &mut buf, -1); + outcome.check( + "mem_load rejects a negative length", + loaded == vm_exec_status_t::VM_EXEC_ERROR.as_i64(), + format!("loaded {loaded}"), + ); + + // A store, read back through a load. + let status = mem_store(ctx, 16, b"ABC"); + let mut buf = [0u8; 3]; + let loaded = mem_load(ctx, 16, &mut buf, 3); + outcome.check( + "mem_store round-trips", + status == vm_exec_status_t::VM_EXEC_OK && loaded == 3 && &buf == b"ABC", + format!( + "{status:?}, loaded {loaded}, {:?}", + std::str::from_utf8(&buf) + ), + ); + + // Unlike a load, a store is never truncated. + let status = mem_store(ctx, (PAGE - 1) as i32, b"ABC"); + outcome.check( + "mem_store rejects a range past the end", + status == vm_exec_status_t::VM_EXEC_ERROR, + format!("{status:?}"), + ); + + // Gas, through the state handle rather than the instance handle. + let status = unsafe { vm_exec_w2_state_set_points_used(ctx, 42, std::ptr::null_mut(), 0) }; + let mut points = 0u64; + let get_status = + unsafe { vm_exec_w2_state_get_points_used(ctx, &mut points, std::ptr::null_mut(), 0) }; + outcome.check( + "points used round-trip", + status == vm_exec_status_t::VM_EXEC_OK + && get_status == vm_exec_status_t::VM_EXEC_OK + && points == 42, + format!("{status:?}, {get_status:?}, points {points}"), + ); + + // Growing reports the page count from before the growth. + let mut prev_pages = 0u32; + let status = + unsafe { vm_exec_w2_state_mem_grow(ctx, 1, &mut prev_pages, std::ptr::null_mut(), 0) }; + let mut length_after = 0u64; + unsafe { vm_exec_w2_state_mem_length(ctx, &mut length_after, std::ptr::null_mut(), 0) }; + outcome.check( + "mem_grow", + status == vm_exec_status_t::VM_EXEC_OK && prev_pages == 1 && length_after == 2 * PAGE, + format!("{status:?}, prev_pages {prev_pages}, length {length_after}"), + ); +} + +/// A nested host call must not let the outer call's context stay usable. +fn probe_nesting( + builder: &CapiVMHooksBuilder, + state_ptr: *mut c_void, + outer_ctx: *mut vm_exec_hook_context_t, + outcome: &mut Outcome, +) { + unsafe { + builder.with_hook_context_raw(state_ptr, |inner_ctx| { + outcome.check( + "nesting uses a distinct context", + inner_ctx != outer_ctx, + format!("inner {inner_ctx:?} outer {outer_ctx:?}"), + ); + + let mut length = 0u64; + let inner_status = + vm_exec_w2_state_mem_length(inner_ctx, &mut length, std::ptr::null_mut(), 0); + outcome.check( + "the innermost context works", + inner_status == vm_exec_status_t::VM_EXEC_OK, + format!("{inner_status:?}"), + ); + + let outer_status = + vm_exec_w2_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); + outcome.check( + "an outer context is rejected while a nested call runs", + outer_status == vm_exec_status_t::VM_EXEC_STALE_CONTEXT, + format!("{outer_status:?}"), + ); + }); + + // ... and is usable again once the nested call has returned. + let mut length = 0u64; + let status = vm_exec_w2_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); + outcome.check( + "the outer context works again after the nested call", + status == vm_exec_status_t::VM_EXEC_OK, + format!("{status:?}"), + ); + } +} + +#[test] +fn state_ops() { + let outcome = Arc::new(Mutex::new(Outcome::default())); + let builder = ProbingHooksBuilder { + inner: CapiVMHooksBuilder::new(0xBEEFusize, unsafe { + CapiVMHookTable::copy_from(mock_hook_table()) + }), + outcome: outcome.clone(), + }; + + let mut instance = WasmerProdExecutor::new(Arc::new(builder)) + .new_instance( + CONTRACT_WAT.as_bytes(), + // The same options as the C path, built directly: the FFI mirror is crate-private. + &multiversx_chain_vm_executor::CompilationOptions { + max_memory_grow: 10, + max_memory_grow_delta: 10, + ..Default::default() + }, + ) + .expect("instance creation failed"); + + let result = instance.call("callGetGasLeft", 1_000_000); + assert!( + matches!(result, multiversx_chain_vm_executor::InstanceCallResult::Ok), + "the probing call did not succeed" + ); + + let outcome = outcome.lock().unwrap(); + assert_eq!( + outcome.hook_calls, 1, + "the hook was not called exactly once" + ); + assert!( + outcome.failures.is_empty(), + "state op failures:\n {}", + outcome.failures.join("\n ") + ); + + // The context that escaped the host call must now be refused. Contexts are pooled for the + // instance's lifetime, so this reads live memory and fails the token check, rather than + // being undefined behaviour. + let escaped = outcome.escaped_context as *mut vm_exec_hook_context_t; + let mut length = 0u64; + let status = + unsafe { vm_exec_w2_state_mem_length(escaped, &mut length, std::ptr::null_mut(), 0) }; + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_STALE_CONTEXT, + "a context used after its call must be rejected" + ); +} diff --git a/c-api-wasmer2/tests/prefix_parity.rs b/c-api-wasmer2/tests/prefix_parity.rs new file mode 100644 index 00000000..b110f5e2 --- /dev/null +++ b/c-api-wasmer2/tests/prefix_parity.rs @@ -0,0 +1,53 @@ +//! The two backend C APIs must stay identical apart from their symbol prefix. +//! +//! This is what makes the duplication honest. The entry-point files have to be written out +//! literally — cbindgen parses the source syntactically, so a macro-generated `#[no_mangle]` +//! function produces no header entry — but nothing about them is allowed to *differ*. Their +//! bodies all forward into `c-api-common`, and the one real difference between the libraries +//! is each crate's `capi_backend`, which these files reach through a name that is the same on +//! both sides. +//! +//! A failure here means someone changed one copy and not the other. The fix is to re-derive +//! the file rather than to patch it twice. + +/// `w6` and `w2` are the same length, so the substitution cannot change alignment or layout — +/// the comparison is a plain string equality, not a normalisation. +fn to_neutral_prefix(source: &str, prefix: &str) -> String { + source.replace(prefix, "wN") +} + +fn assert_parity(file_name: &str, wasmer2: &str, wasmer6: &str) { + assert_eq!( + to_neutral_prefix(wasmer2, "w2"), + to_neutral_prefix(wasmer6, "w6"), + "{file_name} has diverged between c-api-wasmer2 and c-api-wasmer6; \ + re-derive it instead of editing both", + ); +} + +#[test] +fn executor_entry_points_match() { + assert_parity( + "capi_executor.rs", + include_str!("../src/capi_executor.rs"), + include_str!("../../c-api-wasmer6/src/capi_executor.rs"), + ); +} + +#[test] +fn instance_entry_points_match() { + assert_parity( + "capi_instance.rs", + include_str!("../src/capi_instance.rs"), + include_str!("../../c-api-wasmer6/src/capi_instance.rs"), + ); +} + +#[test] +fn instance_state_entry_points_match() { + assert_parity( + "capi_instance_state.rs", + include_str!("../src/capi_instance_state.rs"), + include_str!("../../c-api-wasmer6/src/capi_instance_state.rs"), + ); +} diff --git a/c-api-wasmer6/src/capi_backend.rs b/c-api-wasmer6/src/capi_backend.rs new file mode 100644 index 00000000..02a137ab --- /dev/null +++ b/c-api-wasmer6/src/capi_backend.rs @@ -0,0 +1,23 @@ +//! Which wasm runtime this library is for. +//! +//! The one file in this crate that names a backend, and the reason the three entry-point +//! files do not: they reach the backend only through this type, under a name that is the +//! same in every backend crate. That is what keeps them identical to the Wasmer 2.2 copy +//! apart from the symbol prefix — a property an integration test checks, so the two cannot +//! drift. + +use multiversx_chain_vm_executor::Executor; +use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; +use std::sync::Arc; + +use crate::capi_handles::CapiBackend; +use crate::capi_vm_hook_context::CapiVMHooksBuilder; + +/// Wasmer 6, through the experimental backend. +pub struct Backend; + +impl CapiBackend for Backend { + fn new_executor(runtime: Arc) -> Box { + Box::new(ExperimentalExecutor::new(runtime)) + } +} diff --git a/c-api-wasmer6/src/capi_executor.rs b/c-api-wasmer6/src/capi_executor.rs index aad10231..a0877b85 100644 --- a/c-api-wasmer6/src/capi_executor.rs +++ b/c-api-wasmer6/src/capi_executor.rs @@ -15,27 +15,24 @@ //! The opcode config is held here for the same reason. On the Rust side it has moved into //! `CompilationOptions`, and this API merges the two at instance creation rather than //! changing the C struct ahead of the Go VM. +//! +//! The bodies are in `c-api-common`, shared with the Wasmer 2.2 library. What is here is the +//! prefixed symbol and the C contract. use libc::{c_char, c_int}; use meta::capi_safe_unwind; -use multiversx_chain_vm_executor::OpcodeConfig; -use std::sync::Arc; use crate::capi_config::vm_exec_executor_config_t; -use crate::capi_status::{vm_exec_status_t, write_error}; +use crate::capi_executor_ops; +use crate::capi_handles::CapiExecutor; +use crate::capi_status::vm_exec_status_t; use crate::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; -use crate::capi_vm_hook_table::CapiVMHookTable; /// Opaque handle to an executor. #[allow(non_camel_case_types)] #[repr(C)] pub struct vm_exec_w6_executor_t; -pub struct CapiExecutor { - pub(crate) opcode_config: Arc, - pub(crate) hook_table: CapiVMHookTable, -} - /// Creates an executor from one configuration struct and one hook table. /// /// `hook_table` is copied, so the caller is free to release it as soon as this returns. Every @@ -57,25 +54,15 @@ pub unsafe extern "C" fn vm_exec_w6_new_executor( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - if executor_out.is_null() || config.is_null() || hook_table.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER; - } - - let opcode_config = match unsafe { (*config).to_opcode_config() } { - Ok(opcode_config) => opcode_config, - Err(message) => return write_error(message, err_buf, err_buf_len), - }; - - let capi_executor = CapiExecutor { - opcode_config: Arc::new(opcode_config), - hook_table: unsafe { CapiVMHookTable::copy_from(hook_table) }, - }; - unsafe { - *executor_out = Box::into_raw(Box::new(capi_executor)) as *mut vm_exec_w6_executor_t; + capi_executor_ops::new_executor( + executor_out as *mut *mut CapiExecutor, + config, + hook_table, + err_buf, + err_buf_len, + ) } - - vm_exec_status_t::VM_EXEC_OK } /// Destroys an executor. Does nothing if `executor` is null. @@ -89,7 +76,5 @@ pub unsafe extern "C" fn vm_exec_w6_new_executor( #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] pub unsafe extern "C" fn vm_exec_w6_executor_destroy(executor: *mut vm_exec_w6_executor_t) { - if !executor.is_null() { - drop(unsafe { Box::from_raw(executor as *mut CapiExecutor) }); - } + unsafe { capi_executor_ops::executor_destroy(executor as *mut CapiExecutor) } } diff --git a/c-api-wasmer6/src/capi_instance.rs b/c-api-wasmer6/src/capi_instance.rs index fed894bc..951ae9da 100644 --- a/c-api-wasmer6/src/capi_instance.rs +++ b/c-api-wasmer6/src/capi_instance.rs @@ -5,31 +5,26 @@ //! `capi_instance_state`, reached through a hook context rather than through this handle. //! Keeping the split is what makes the Go side match the Rust trait pair instead of merely //! wrapping it. +//! +//! The bodies are in `c-api-common`, shared with the other backend's library; `capi_backend` +//! is the only thing that differs. What is here is the prefixed symbols and the C contract. use libc::{c_char, c_int}; use meta::capi_safe_unwind; -use multiversx_chain_vm_executor::{Executor, Instance}; -use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; -use std::ffi::CStr; -use std::slice; -use std::sync::Arc; +use crate::capi_backend::Backend; use crate::capi_call_result::vm_exec_call_result_t; use crate::capi_config::vm_exec_compilation_options_t; -use crate::capi_executor::{CapiExecutor, vm_exec_w6_executor_t}; -use crate::capi_status::{vm_exec_status_t, write_error}; -use crate::capi_strings::{copy_pipe_joined, pipe_joined_length}; -use crate::capi_vm_hook_context::CapiVMHooksBuilder; +use crate::capi_executor::vm_exec_w6_executor_t; +use crate::capi_handles::{CapiExecutor, CapiInstance}; +use crate::capi_instance_ops; +use crate::capi_status::vm_exec_status_t; /// Opaque handle to an instance. #[allow(non_camel_case_types)] #[repr(C)] pub struct vm_exec_w6_instance_t; -pub struct CapiInstance { - pub(crate) content: Box, -} - /// Creates an instance. /// /// `go_vm_hooks` is the caller's VM hooks handle for *this* instance; it is handed back to @@ -54,38 +49,18 @@ pub unsafe extern "C" fn vm_exec_w6_new_instance( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - if executor.is_null() || instance_out.is_null() || wasm_bytes.is_null() || options.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER; - } - - let capi_executor = unsafe { &*(executor as *const CapiExecutor) }; - let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_len as usize) }; - let compilation_options = - unsafe { (*options).to_compilation_options(capi_executor.opcode_config.clone()) }; - - // One executor per instance, for as long as the runtime handle is per instance: the - // handle is what the executor is built around, so it cannot outlive the instance that - // owns it. Building it here costs nothing — an executor holds only the runtime — and it - // means this file goes through the `Executor` trait rather than around it. When the - // handle moves to `vm_exec_w6_new_executor`, the executor moves to `CapiExecutor` with - // it and this becomes a plain `new_instance` call. - let runtime = Arc::new(CapiVMHooksBuilder::new( - go_vm_hooks, - capi_executor.hook_table.clone(), - )); - let backend_executor = ExperimentalExecutor::new(runtime); - - let instance = match backend_executor.new_instance(wasm_bytes, &compilation_options) { - Ok(instance) => instance, - Err(err) => return write_error(err, err_buf, err_buf_len), - }; - unsafe { - *instance_out = Box::into_raw(Box::new(CapiInstance { content: instance })) - as *mut vm_exec_w6_instance_t; + capi_instance_ops::new_instance::( + executor as *mut CapiExecutor, + instance_out as *mut *mut CapiInstance, + wasm_bytes, + wasm_bytes_len, + options, + go_vm_hooks, + err_buf, + err_buf_len, + ) } - - vm_exec_status_t::VM_EXEC_OK } /// Destroys an instance. Does nothing if `instance` is null. @@ -96,9 +71,7 @@ pub unsafe extern "C" fn vm_exec_w6_new_instance( #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] pub unsafe extern "C" fn vm_exec_w6_instance_destroy(instance: *mut vm_exec_w6_instance_t) { - if !instance.is_null() { - drop(unsafe { Box::from_raw(instance as *mut CapiInstance) }); - } + unsafe { capi_instance_ops::instance_destroy(instance as *mut CapiInstance) } } /// Calls an exported function, with a gas limit for this call. @@ -121,29 +94,16 @@ pub unsafe extern "C" fn vm_exec_w6_instance_call( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - if instance.is_null() || func_name.is_null() || result_out.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER; - } - - let capi_instance = unsafe { &mut *(instance as *mut CapiInstance) }; - - let Ok(func_name) = unsafe { CStr::from_ptr(func_name) }.to_str() else { - return write_error("function name is not valid UTF-8", err_buf, err_buf_len); - }; - - let result = unsafe { - vm_exec_call_result_t::from_call_result( - capi_instance.content.call(func_name, points_limit), + unsafe { + capi_instance_ops::instance_call( + instance as *mut CapiInstance, + func_name, + points_limit, + result_out, err_buf, err_buf_len, ) - }; - - unsafe { - *result_out = result; } - - vm_exec_status_t::VM_EXEC_OK } /// Returns the number of points (gas) used so far. @@ -160,19 +120,13 @@ pub unsafe extern "C" fn vm_exec_w6_instance_get_points_used( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - if instance.is_null() || points_out.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER; - } - - let capi_instance = unsafe { &mut *(instance as *mut CapiInstance) }; - match capi_instance.content.get_points_used() { - Ok(points) => { - unsafe { - *points_out = points; - } - vm_exec_status_t::VM_EXEC_OK - } - Err(err) => write_error(err, err_buf, err_buf_len), + unsafe { + capi_instance_ops::instance_get_points_used( + instance as *mut CapiInstance, + points_out, + err_buf, + err_buf_len, + ) } } @@ -189,12 +143,7 @@ pub unsafe extern "C" fn vm_exec_w6_instance_get_points_used( pub unsafe extern "C" fn vm_exec_w6_instance_check_signatures( instance: *const vm_exec_w6_instance_t, ) -> c_int { - if instance.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); - } - - let capi_instance = unsafe { &*(instance as *const CapiInstance) }; - c_int::from(capi_instance.content.check_signatures()) + unsafe { capi_instance_ops::instance_check_signatures(instance as *const CapiInstance) } } /// Checks whether the contract exports a function with the given name. @@ -211,16 +160,7 @@ pub unsafe extern "C" fn vm_exec_w6_instance_has_function( instance: *const vm_exec_w6_instance_t, func_name: *const c_char, ) -> c_int { - if instance.is_null() || func_name.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); - } - - let capi_instance = unsafe { &*(instance as *const CapiInstance) }; - let Ok(func_name) = unsafe { CStr::from_ptr(func_name) }.to_str() else { - return vm_exec_status_t::VM_EXEC_ERROR.as_c_int(); - }; - - c_int::from(capi_instance.content.has_function(func_name)) + unsafe { capi_instance_ops::instance_has_function(instance as *const CapiInstance, func_name) } } /// Buffer length needed by `vm_exec_w6_instance_exported_function_names`. @@ -234,12 +174,9 @@ pub unsafe extern "C" fn vm_exec_w6_instance_has_function( pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names_length( instance: *const vm_exec_w6_instance_t, ) -> c_int { - if instance.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); + unsafe { + capi_instance_ops::instance_exported_function_names_length(instance as *const CapiInstance) } - - let capi_instance = unsafe { &*(instance as *const CapiInstance) }; - pipe_joined_length(&capi_instance.content.get_exported_function_names()) } /// Writes the exported function names, pipe-separated, e.g. `"init|endpoint1|endpoint2"`. @@ -258,14 +195,9 @@ pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names( dest: *mut c_char, dest_len: c_int, ) -> c_int { - if instance.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); - } - - let capi_instance = unsafe { &*(instance as *const CapiInstance) }; unsafe { - copy_pipe_joined( - &capi_instance.content.get_exported_function_names(), + capi_instance_ops::instance_exported_function_names( + instance as *const CapiInstance, dest, dest_len, ) @@ -283,12 +215,9 @@ pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names( pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names_length( instance: *const vm_exec_w6_instance_t, ) -> c_int { - if instance.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); + unsafe { + capi_instance_ops::instance_imported_function_names_length(instance as *const CapiInstance) } - - let capi_instance = unsafe { &*(instance as *const CapiInstance) }; - pipe_joined_length(&capi_instance.content.get_imported_function_names()) } /// Writes the names of the VM hooks the contract imports, pipe-separated. @@ -307,14 +236,9 @@ pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names( dest: *mut c_char, dest_len: c_int, ) -> c_int { - if instance.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int(); - } - - let capi_instance = unsafe { &*(instance as *const CapiInstance) }; unsafe { - copy_pipe_joined( - &capi_instance.content.get_imported_function_names(), + capi_instance_ops::instance_imported_function_names( + instance as *const CapiInstance, dest, dest_len, ) diff --git a/c-api-wasmer6/src/capi_instance_state.rs b/c-api-wasmer6/src/capi_instance_state.rs index 321dab59..cc1309fd 100644 --- a/c-api-wasmer6/src/capi_instance_state.rs +++ b/c-api-wasmer6/src/capi_instance_state.rs @@ -8,16 +8,17 @@ //! side that knows the real extent of the instance memory, and the rules are observable by //! contracts, so there is one implementation of them — shared with the other backends in //! `vm-executor/src/new_traits/memory.rs` — rather than one per caller. +//! +//! The bodies are in `c-api-common`, shared verbatim with the other backend's library: the +//! state arrives through the context as an erased `&mut dyn InstanceState`, so none of this +//! names a backend. What is here is the prefixed symbols and the C contract. use libc::{c_char, c_int}; use meta::capi_safe_unwind; -use multiversx_chain_vm_executor::{ - ExecutorError, MemLength, MemPtr, ServiceError, clamped_load_len, -}; -use std::slice; -use crate::capi_status::{vm_exec_status_t, write_error, write_error_count}; -use crate::capi_vm_hook_context::{ErasedInstanceState, vm_exec_hook_context_t, with_instance_state}; +use crate::capi_state_ops; +use crate::capi_status::vm_exec_status_t; +use crate::capi_vm_hook_context::vm_exec_hook_context_t; /// Loads from instance memory into a caller-provided buffer, **truncating** at the end of /// the memory. @@ -44,33 +45,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_load( err_buf: *mut c_char, err_buf_len: c_int, ) -> i64 { - let loaded = unsafe { - with_instance_state::( - ctx, - |state| -> Result { - let memory_length = state.memory_length()?; - let to_load = - clamped_load_len(mem_ptr as MemPtr, dest_len as MemLength, memory_length)?; - if to_load == 0 { - return Ok(0); - } - - if dest.is_null() { - return Err(Box::new(ServiceError::new("destination buffer is null"))); - } - - let dest = slice::from_raw_parts_mut(dest, to_load); - state.memory_load_to_slice(mem_ptr as MemPtr, dest)?; - Ok(to_load) - }, - ) - }; - - match loaded { - Err(context_err) => context_err.as_status().as_i64(), - Ok(Err(err)) => write_error_count(err, err_buf, err_buf_len), - Ok(Ok(to_load)) => to_load as i64, - } + unsafe { capi_state_ops::state_mem_load(ctx, mem_ptr, dest, dest_len, err_buf, err_buf_len) } } /// Writes to instance memory. The range must fit entirely inside the memory. @@ -88,29 +63,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_store( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - if src_len < 0 { - return write_error("negative length", err_buf, err_buf_len); - } - if src.is_null() && src_len > 0 { - return vm_exec_status_t::VM_EXEC_NULL_POINTER; - } - - let stored = unsafe { - with_instance_state::(ctx, |state| { - let data = if src_len == 0 { - &[][..] - } else { - slice::from_raw_parts(src, src_len as usize) - }; - state.memory_store(mem_ptr as MemPtr, data) - }) - }; - - match stored { - Err(context_err) => context_err.as_status(), - Ok(Err(err)) => write_error(err, err_buf, err_buf_len), - Ok(Ok(())) => vm_exec_status_t::VM_EXEC_OK, - } + unsafe { capi_state_ops::state_mem_store(ctx, mem_ptr, src, src_len, err_buf, err_buf_len) } } /// Returns the size in bytes of the instance memory. @@ -126,24 +79,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_length( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - if length_out.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER; - } - - let length = unsafe { - with_instance_state::(ctx, |state| state.memory_length()) - }; - - match length { - Err(context_err) => context_err.as_status(), - Ok(Err(err)) => write_error(err, err_buf, err_buf_len), - Ok(Ok(length)) => { - unsafe { - *length_out = length; - } - vm_exec_status_t::VM_EXEC_OK - } - } + unsafe { capi_state_ops::state_mem_length(ctx, length_out, err_buf, err_buf_len) } } /// Grows the instance memory by `by_num_pages` pages of 64Kb each. @@ -162,23 +98,8 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_grow( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - if prev_pages_out.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER; - } - - let grown = unsafe { - with_instance_state::(ctx, |state| state.memory_grow(by_num_pages)) - }; - - match grown { - Err(context_err) => context_err.as_status(), - Ok(Err(err)) => write_error(err, err_buf, err_buf_len), - Ok(Ok(prev_pages)) => { - unsafe { - *prev_pages_out = prev_pages; - } - vm_exec_status_t::VM_EXEC_OK - } + unsafe { + capi_state_ops::state_mem_grow(ctx, by_num_pages, prev_pages_out, err_buf, err_buf_len) } } @@ -195,24 +116,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_get_points_used( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - if points_out.is_null() { - return vm_exec_status_t::VM_EXEC_NULL_POINTER; - } - - let points = unsafe { - with_instance_state::(ctx, |state| state.get_points_used()) - }; - - match points { - Err(context_err) => context_err.as_status(), - Ok(Err(err)) => write_error(err, err_buf, err_buf_len), - Ok(Ok(points)) => { - unsafe { - *points_out = points; - } - vm_exec_status_t::VM_EXEC_OK - } - } + unsafe { capi_state_ops::state_get_points_used(ctx, points_out, err_buf, err_buf_len) } } /// Sets the number of points (gas) used by the instance. @@ -228,13 +132,5 @@ pub unsafe extern "C" fn vm_exec_w6_state_set_points_used( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - let set = unsafe { - with_instance_state::(ctx, |state| state.set_points_used(points)) - }; - - match set { - Err(context_err) => context_err.as_status(), - Ok(Err(err)) => write_error(err, err_buf, err_buf_len), - Ok(Ok(())) => vm_exec_status_t::VM_EXEC_OK, - } + unsafe { capi_state_ops::state_set_points_used(ctx, points, err_buf, err_buf_len) } } diff --git a/c-api-wasmer6/src/lib.rs b/c-api-wasmer6/src/lib.rs index 4ac5dd5a..eadf8f12 100644 --- a/c-api-wasmer6/src/lib.rs +++ b/c-api-wasmer6/src/lib.rs @@ -21,6 +21,7 @@ mod capi_logger; +pub mod capi_backend; pub mod capi_executor; pub mod capi_instance; pub mod capi_instance_state; @@ -28,6 +29,7 @@ pub mod capi_instance_state; // The shared C vocabulary, re-exported so that this crate's modules — and the cbindgen run // that produces its header — reach the same definitions the common header publishes. pub use multiversx_chain_vm_executor_c_api_common::{ - capi_call_result, capi_config, capi_opcode_cost, capi_status, capi_strings, - capi_vm_hook_context, capi_vm_hook_pointers, capi_vm_hook_table, capi_vm_hooks, + capi_call_result, capi_config, capi_executor_ops, capi_handles, capi_instance_ops, + capi_opcode_cost, capi_state_ops, capi_status, capi_strings, capi_vm_hook_context, + capi_vm_hook_pointers, capi_vm_hook_table, capi_vm_hooks, }; From 0e8d9c8a54dd94257d652aeecd4d139f96dcd6a0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 9 Oct 2026 12:11:32 +0300 Subject: [PATCH 21/26] wasmer 2 breakpoint reset on call --- .../src/new_traits/wasmer_prod_instance.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs index 0ab4397a..e905423f 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs @@ -35,6 +35,23 @@ impl Instance for WasmerProdInstance { ); } + // Clear the breakpoint global, so the instance is armed for this call rather than + // still halted from the last one. + // + // Under the legacy interface this was the *host's* job — the Go VM did it in + // `runtimeContext.UseWarmInstance`, and the Rust VM never reused an instance at all. + // The new interface does not expose the global, deliberately: it is 2.2's private + // mechanism for stopping a contract, and Wasmer 6 has nothing like it. So the only + // side that can still do this is the executor, and not doing it would make an + // instance single-use after any early exit — a divergence from Wasmer 6, which has no + // such state to leave behind. + if let Err(err) = self + .inner_instance_ref + .set_breakpoint_value(BreakpointValueLegacy::None) + { + return wrap_runtime_error(err); + } + let result = self.inner_instance_ref.call(func_name); match result { From 5cacb0fbf3bd979d2a44c84acff9f815df82c8dc Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 11 Oct 2026 01:43:20 +0300 Subject: [PATCH 22/26] test fix after merge --- vm-executor-experimental/tests/locals_test.rs | 26 +++----- vm-executor-wasmer/tests/locals_test.rs | 64 ++++++++----------- 2 files changed, 34 insertions(+), 56 deletions(-) diff --git a/vm-executor-experimental/tests/locals_test.rs b/vm-executor-experimental/tests/locals_test.rs index 6722abf3..b67e328f 100644 --- a/vm-executor-experimental/tests/locals_test.rs +++ b/vm-executor-experimental/tests/locals_test.rs @@ -1,20 +1,11 @@ use multiversx_chain_vm_executor::{ - CompilationOptions, ExecutorError, Instance, InstanceCallResult, OpcodeConfig, OpcodeCost, - WASM_LOCALS_LIMIT, -}; -use multiversx_chain_vm_executor_wasmer_experimental::{ - ExperimentalInstance, ExperimentalVMHooksBuilderDefault, + CompilationOptions, ExecutorError, ExecutorRuntimeDefault, Instance, InstanceCallResult, + OpcodeConfig, OpcodeCost, WASM_LOCALS_LIMIT, }; +use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalInstance; use std::sync::Arc; use wasmer::wat2wasm; -const COMPILATION_OPTIONS: CompilationOptions = CompilationOptions { - unmetered_locals: 0, - max_memory_grow: 0, - max_memory_grow_delta: 0, - opcode_trace: false, -}; - fn wat_with_locals(num_locals: usize) -> String { format!( r#"(module @@ -29,10 +20,9 @@ fn try_instance(num_locals: usize) -> Result, ExecutorError> { let wat = wat_with_locals(num_locals); let wasm_bytes = wat2wasm(wat.as_bytes()).unwrap(); ExperimentalInstance::try_new_instance( - Box::new(ExperimentalVMHooksBuilderDefault), - Arc::new(OpcodeConfig::default()), + Arc::new(ExecutorRuntimeDefault), &wasm_bytes, - &COMPILATION_OPTIONS, + &CompilationOptions::default(), ) .map(|instance| Box::new(instance) as Box) } @@ -64,12 +54,12 @@ fn points_used_with_locals(num_locals: usize, unmetered_locals: usize) -> u64 { }; let options = CompilationOptions { unmetered_locals, - ..COMPILATION_OPTIONS + opcode_config: Arc::new(opcode_config), + ..Default::default() }; let mut instance = ExperimentalInstance::try_new_instance( - Box::new(ExperimentalVMHooksBuilderDefault), - Arc::new(opcode_config), + Arc::new(ExecutorRuntimeDefault), &wasm_bytes, &options, ) diff --git a/vm-executor-wasmer/tests/locals_test.rs b/vm-executor-wasmer/tests/locals_test.rs index d2b39551..b58c512e 100644 --- a/vm-executor-wasmer/tests/locals_test.rs +++ b/vm-executor-wasmer/tests/locals_test.rs @@ -1,20 +1,11 @@ use multiversx_chain_vm_executor_wasmer::executor_interface::{ - CompilationOptionsLegacy, ExecutorError, ExecutorLegacy, ExecutorService, InstanceLegacy, - OpcodeConfig, OpcodeCost, VMHooksLegacyDefault, WASM_LOCALS_LIMIT, + CompilationOptions, Executor, ExecutorError, ExecutorRuntimeDefault, Instance, + InstanceCallResult, OpcodeConfig, OpcodeCost, WASM_LOCALS_LIMIT, }; -use multiversx_chain_vm_executor_wasmer::{BasicExecutorService, WasmerExecutor}; +use multiversx_chain_vm_executor_wasmer::new_traits::WasmerProdExecutor; +use std::sync::Arc; use wasmer::wat2wasm; -const COMPILATION_OPTIONS: CompilationOptionsLegacy = CompilationOptionsLegacy { - gas_limit: 0, - unmetered_locals: 0, - max_memory_grow: 0, - max_memory_grow_delta: 0, - opcode_trace: false, - metering: false, - runtime_breakpoints: false, -}; - fn wat_with_locals(num_locals: usize) -> String { format!( r#"(module @@ -25,14 +16,14 @@ fn wat_with_locals(num_locals: usize) -> String { ) } -fn try_instance(num_locals: usize) -> Result, ExecutorError> { +fn new_executor() -> WasmerProdExecutor { + WasmerProdExecutor::new(Arc::new(ExecutorRuntimeDefault)) +} + +fn try_instance(num_locals: usize) -> Result, ExecutorError> { let wat = wat_with_locals(num_locals); let wasm_bytes = wat2wasm(wat.as_bytes()).unwrap(); - let service = BasicExecutorService::new(); - let executor = service - .new_executor(Box::new(VMHooksLegacyDefault)) - .unwrap(); - executor.new_instance(&wasm_bytes, &COMPILATION_OPTIONS) + new_executor().new_instance(&wasm_bytes, &CompilationOptions::default()) } #[test] @@ -46,34 +37,31 @@ fn deploy_more_than_maximum_locals_fails() { } const LOCAL_ALLOCATE_COST: u32 = 5; +const POINTS_LIMIT: u64 = 1_000_000; /// All opcodes are free except local allocation, so `points_used` after calling `init` /// is exactly the cost of the metered locals. fn points_used_with_locals(num_locals: usize, unmetered_locals: usize) -> u64 { let wat = wat_with_locals(num_locals); let wasm_bytes = wat2wasm(wat.as_bytes()).unwrap(); - let options = CompilationOptionsLegacy { - gas_limit: 1_000_000, + let opcode_config = OpcodeConfig { + opcode_cost: OpcodeCost { + opcode_localallocate: LOCAL_ALLOCATE_COST, + ..Default::default() + }, + ..Default::default() + }; + let options = CompilationOptions { unmetered_locals, - metering: true, - runtime_breakpoints: true, - ..COMPILATION_OPTIONS + opcode_config: Arc::new(opcode_config), + ..Default::default() }; - let mut executor = WasmerExecutor::new(Box::new(VMHooksLegacyDefault)); - executor - .set_opcode_config(OpcodeConfig { - opcode_cost: OpcodeCost { - opcode_localallocate: LOCAL_ALLOCATE_COST, - ..Default::default() - }, - ..Default::default() - }) - .unwrap(); - let instance = executor.new_instance(&wasm_bytes, &options).unwrap(); - - instance.set_points_used(0).unwrap(); - instance.call("init").unwrap(); + let mut instance = new_executor().new_instance(&wasm_bytes, &options).unwrap(); + assert!(matches!( + instance.call("init", POINTS_LIMIT), + InstanceCallResult::Ok + )); instance.get_points_used().unwrap() } From e6ff497d2a9e8edda1006ab82b0785ccb48180fd Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 12 Oct 2026 13:26:31 +0300 Subject: [PATCH 23/26] C API - wasmer 6 -> wasmer 7 --- Cargo.toml | 2 +- Makefile | 44 ++-- PLAN-go-breakpoint-removal.md | 2 +- PLAN-go-ffi.md | 217 +++++++++++++----- PLAN-rust-vm-caching.md | 60 ++--- c-api-common/libvmexeccapi_common.h | 2 +- c-api-common/src/capi_call_result.rs | 2 +- c-api-common/src/capi_executor_ops.rs | 7 +- c-api-common/src/capi_handles.rs | 42 ++-- c-api-common/src/capi_instance_ops.rs | 13 +- c-api-common/src/capi_vm_hook_context.rs | 4 +- c-api-common/src/lib.rs | 6 +- c-api-wasmer2/src/capi_backend.rs | 15 +- c-api-wasmer2/src/capi_executor.rs | 2 + c-api-wasmer2/src/capi_instance.rs | 3 +- c-api-wasmer2/src/lib.rs | 8 +- c-api-wasmer2/tests/prefix_parity.rs | 14 +- {c-api-wasmer6 => c-api-wasmer7}/Cargo.toml | 2 +- {c-api-wasmer6 => c-api-wasmer7}/build.rs | 2 +- .../cbindgen.toml | 0 .../libvmexeccapi_w7.h | 50 ++-- .../src/capi_backend.rs | 13 +- .../src/capi_executor.rs | 14 +- .../src/capi_instance.rs | 51 ++-- .../src/capi_instance_state.rs | 12 +- .../src/capi_logger.rs | 0 {c-api-wasmer6 => c-api-wasmer7}/src/lib.rs | 2 +- .../tests/ffi_boundary.rs | 110 ++++----- vm-executor-experimental/README.md | 2 +- .../src/new_traits/wasmer_prod_instance.rs | 4 +- .../src/new_traits/executor_runtime.rs | 2 +- vm-executor/src/new_traits/instance_state.rs | 2 +- .../src/new_traits/vm_hooks_early_exit.rs | 2 +- .../src/new_traits/vm_hooks_legacy_adapter.rs | 2 +- 34 files changed, 414 insertions(+), 299 deletions(-) rename {c-api-wasmer6 => c-api-wasmer7}/Cargo.toml (92%) rename {c-api-wasmer6 => c-api-wasmer7}/build.rs (96%) rename {c-api-wasmer6 => c-api-wasmer7}/cbindgen.toml (100%) rename c-api-wasmer6/libvmexeccapi_w6.h => c-api-wasmer7/libvmexeccapi_w7.h (76%) rename {c-api-wasmer6 => c-api-wasmer7}/src/capi_backend.rs (59%) rename {c-api-wasmer6 => c-api-wasmer7}/src/capi_executor.rs (86%) rename {c-api-wasmer6 => c-api-wasmer7}/src/capi_instance.rs (83%) rename {c-api-wasmer6 => c-api-wasmer7}/src/capi_instance_state.rs (93%) rename {c-api-wasmer6 => c-api-wasmer7}/src/capi_logger.rs (100%) rename {c-api-wasmer6 => c-api-wasmer7}/src/lib.rs (96%) rename {c-api-wasmer6 => c-api-wasmer7}/tests/ffi_boundary.rs (88%) diff --git a/Cargo.toml b/Cargo.toml index e29542e5..880c9027 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "c-api-common", "c-api-legacy", "c-api-wasmer2", - "c-api-wasmer6", + "c-api-wasmer7", "vm-executor", "vm-executor-experimental", "vm-executor-wasmer", diff --git a/Makefile b/Makefile index abe7f51a..2856a700 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: capi capi-linux-amd64 capi-linux-arm capi-osx-amd64 capi-osx-arm \ capi-osx-amd64-cross capi-linux-amd64-docker capi-linux-arm64-docker \ - capi-w6 capi-w6-linux-amd64 capi-w6-linux-arm capi-w6-osx-amd64 capi-w6-osx-arm \ + capi-w7 capi-w7-linux-amd64 capi-w7-linux-arm capi-w7-osx-amd64 capi-w7-osx-arm \ capi-w2 capi-w2-linux-amd64 capi-w2-linux-arm capi-w2-osx-amd64 capi-w2-osx-arm \ ci-local clean check-macos @@ -50,33 +50,33 @@ capi-osx-arm: check-macos capi # VM starts consuming it, so the release assets do not grow a library nothing # links against. -capi-w6: - cargo build -p multiversx-chain-vm-executor-c-api-wasmer6 --release +capi-w7: + cargo build -p multiversx-chain-vm-executor-c-api-wasmer7 --release -capi-w6-linux-amd64: capi-w6 - mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer6.so target/release/libvmexeccapi_w6.so - patchelf --set-soname libvmexeccapi_w6.so target/release/libvmexeccapi_w6.so +capi-w7-linux-amd64: capi-w7 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer7.so target/release/libvmexeccapi_w7.so + patchelf --set-soname libvmexeccapi_w7.so target/release/libvmexeccapi_w7.so -capi-w6-linux-arm: capi-w6 - mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer6.so target/release/libvmexeccapi_w6_arm.so - patchelf --set-soname libvmexeccapi_w6_arm.so target/release/libvmexeccapi_w6_arm.so +capi-w7-linux-arm: capi-w7 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer7.so target/release/libvmexeccapi_w7_arm.so + patchelf --set-soname libvmexeccapi_w7_arm.so target/release/libvmexeccapi_w7_arm.so -capi-w6-osx-amd64: check-macos capi-w6 - mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer6.dylib target/release/libvmexeccapi_w6.dylib - install_name_tool -id @rpath/libvmexeccapi_w6.dylib target/release/libvmexeccapi_w6.dylib +capi-w7-osx-amd64: check-macos capi-w7 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer7.dylib target/release/libvmexeccapi_w7.dylib + install_name_tool -id @rpath/libvmexeccapi_w7.dylib target/release/libvmexeccapi_w7.dylib -capi-w6-osx-arm: check-macos capi-w6 - mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer6.dylib target/release/libvmexeccapi_w6_arm.dylib - install_name_tool -id @rpath/libvmexeccapi_w6_arm.dylib target/release/libvmexeccapi_w6_arm.dylib +capi-w7-osx-arm: check-macos capi-w7 + mv target/release/libmultiversx_chain_vm_executor_c_api_wasmer7.dylib target/release/libvmexeccapi_w7_arm.dylib + install_name_tool -id @rpath/libvmexeccapi_w7_arm.dylib target/release/libvmexeccapi_w7_arm.dylib # --- The new C API, Wasmer 2.2 ------------------------------------------------ # The same interface over the executor currently in production. Built alongside the -# Wasmer 6 library rather than instead of it: running the *same* runtime through the +# Wasmer 7 library rather than instead of it: running the *same* runtime through the # *new* boundary is what isolates the boundary as the variable, so a difference against # the legacy `libvmexeccapi` is attributable to the refactor and not to a runtime change. # # Three libraries can therefore be linked at once — `vm_exec_*`, `vm_exec_w2_*` and -# `vm_exec_w6_*` share no exported symbols. On Linux that needs +# `vm_exec_w7_*` share no exported symbols. On Linux that needs # `-Wl,-Bsymbolic-functions`, because the two Wasmer versions do collide on their # internal `wasmer_vm_*` symbols; macOS's two-level namespace handles it without help. @@ -158,11 +158,11 @@ clean: rm -f target/release/libvmexeccapi.dylib rm -f target/release/libvmexeccapi_arm.dylib rm -f c-api/libvmexeccapi.h - rm -f target/release/libvmexeccapi_w6.so - rm -f target/release/libvmexeccapi_w6_arm.so - rm -f target/release/libvmexeccapi_w6.dylib - rm -f target/release/libvmexeccapi_w6_arm.dylib - rm -f c-api-wasmer6/libvmexeccapi_w6.h + rm -f target/release/libvmexeccapi_w7.so + rm -f target/release/libvmexeccapi_w7_arm.so + rm -f target/release/libvmexeccapi_w7.dylib + rm -f target/release/libvmexeccapi_w7_arm.dylib + rm -f c-api-wasmer7/libvmexeccapi_w7.h rm -f target/release/libvmexeccapi_w2.so rm -f target/release/libvmexeccapi_w2_arm.so rm -f target/release/libvmexeccapi_w2.dylib diff --git a/PLAN-go-breakpoint-removal.md b/PLAN-go-breakpoint-removal.md index 18b559c7..fa2ff8f9 100644 --- a/PLAN-go-breakpoint-removal.md +++ b/PLAN-go-breakpoint-removal.md @@ -300,7 +300,7 @@ are the only proof the migration is behaviour-preserving. Split along the semantic/mechanical seam. -### Step A — stop *interpreting* breakpoints (no wasmer 6 dependency) +### Step A — stop *interpreting* breakpoints (no wasmer 7 dependency) 1. Land the Go `InstanceCallResult` and the new instance interface. 2. Convert the three `handleBreakpointIfAny` sites to `resolveCallResult`. diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 08958f76..b8a231fd 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -7,7 +7,7 @@ implemented; Phase 4b is in progress. ## Why The repo was built around Wasmer 2.2, hidden behind the legacy interfaces in -`vm-executor`. Those interfaces cannot express Wasmer 6's ownership model for VM hooks, so a +`vm-executor`. Those interfaces cannot express Wasmer 7's ownership model for VM hooks, so a second set of traits was added in `vm-executor/src/new_traits`, currently driven only by the Rust VM (`mx-sdk-rs/chain/vm`). The Go VM still speaks the legacy C API. This plan covers the new FFI crate, the generator work, and the Go-side changes. @@ -24,16 +24,23 @@ migration is shown to be correct, which is what the hook-sequence differential i | Backend | Interfaces | Crate | Symbols | Library | Go package | | --- | --- | --- | --- | --- | --- | | wasmer 2.2 | legacy | `c-api-legacy` | `vm_exec_*` | `libvmexeccapi` | `wasmer2legacy` | -| wasmer 2.2 | new | `c-api-wasmer2` *(planned)* | `vm_exec_w2_*` | `libvmexeccapi_w2` | `wasmer2` *(planned)* | -| wasmer 6 | new | `c-api-wasmer6` | `vm_exec_w6_*` | `libvmexeccapi_w6` | `wasmer6` | -| — | shared | `c-api-common` *(planned)* | `vm_exec_*` | header only | `wasmercommon` *(planned)* | +| wasmer 2.2 | new | `c-api-wasmer2` | `vm_exec_w2_*` | `libvmexeccapi_w2` | `wasmer2` | +| wasmer 7 | new | `c-api-wasmer7` | `vm_exec_w7_*` | `libvmexeccapi_w7` | `wasmer7` | +| — | shared | `c-api-common` | `vm_exec_*` | header only | `wasmercommon` | The numbers name the **backend**, not the interface generation. The old `vm_exec2_*` prefix meant "second interface set", which stopped working the moment a second backend joined that set; it is retired rather than recycled, so no prefix ever changes meaning. The shared types go into one `libvmexeccapi_common.h` that both backend headers include, which is what lets a -Go package include both and a binary link both without redefinition — and what gives -`wasmercommon` a single set of Go types to wrap. +Go package include both and a binary link both without redefinition. + +`wasmercommon` does *not* wrap those types, as this once assumed it would: cgo types are +per-package, so a shared Go package cannot name them at all. It carries the vocabulary that +has no C in it, and each backend converts at its bridge. The opaque handle types stay +per-backend (`vm_exec_w2_instance_t` against `vm_exec_w7_instance_t`) although nothing forces +it — the two headers never meet in one translation unit — because they are the only thing +stopping a handle from one library reaching a function of the other, and both are deliberately +linked into the same binary. One constraint that falls out: a Go package must not include the legacy header together with a new one. They both declare `vm_exec_compilation_options_t` and `vm_exec_opcode_cost_t`, and the @@ -41,18 +48,18 @@ compilation options differ in layout (seven fields against four). No package doe problem dies with `c-api-legacy`. Cgo `//export` shim names are C symbols too, so they also have to be unique per package: -`w2_*` for the legacy shims and `w6_*` for wasmer 6. The new wasmer 2.2 package will need a +`w2_*` for the legacy shims and `w7_*` for wasmer 7. The new wasmer 2.2 package will need a third prefix rather than reusing `w2_`. | Piece | Path | | --- | --- | | Interfaces, legacy + new | `vm-executor/src/` and `vm-executor/src/new_traits/` | | Wasmer 2.2 backend (legacy native, new traits via a shim) | `vm-executor-wasmer/` | -| Wasmer 6 backend (new traits native) | `vm-executor-experimental/` | +| Wasmer 7 backend (new traits native) | `vm-executor-experimental/` | | C APIs | see the table above | | Code generator (drives **both** repos) | `mx-chain-vm-go/vmhost/vmhooks/generate/` | -| Go VM | `mx-chain-vm-go/`, cgo bridges in `wasmer2legacy/` and `wasmer6/` | -| Rust VM, Wasmer 6 consumer | `mx-sdk-rs/chain/vm/src/executor_impl/we_executor.rs` | +| Go VM | `mx-chain-vm-go/`, cgo bridges in `wasmer2legacy/` and `wasmer7/` | +| Rust VM, Wasmer 7 consumer | `mx-sdk-rs/chain/vm/src/executor_impl/we_executor.rs` | | Rust VM, Wasmer 2.2 consumer | `mx-sdk-rs/chain/wasmer-prod/src/wasmer_prod.rs` | ## How the two interfaces differ @@ -67,10 +74,10 @@ third prefix rather than reusing `w2_`. | VM hooks | `&self`, bare returns, carries a context pointer | `&mut self`, `Result<_, VMHooksEarlyExit>`, no context pointer | The backends disagree in one way that dictates the whole FFI design. Wasmer 2.2 can hand out -an owned, instance-lifetime state (a `Weak`). Wasmer 6 cannot: memory and +an owned, instance-lifetime state (a `Weak`). Wasmer 7 cannot: memory and globals are only reachable through the `StoreMut` inside `FunctionEnvMut`, so the state is a -borrow that lives exactly as long as one host call. **The FFI must target the Wasmer 6 -model** — anything designed against 2.2's model cannot be implemented by Wasmer 6. +borrow that lives exactly as long as one host call. **The FFI must target the Wasmer 7 +model** — anything designed against 2.2's model cannot be implemented by Wasmer 7. ## Settled decisions @@ -79,7 +86,7 @@ model** — anything designed against 2.2's model cannot be implemented by Wasme Copy the Rust VM's shape: `VMHooksDispatcher` → `VMHooksHandler` → `TxVMHooksContext`, which bundles the VM's `TxContextRef` with the executor's `InstanceState`. `S` is generic over ownership, so the same context type serves an owned -state (2.2) and a `&mut` borrow (Wasmer 6). +state (2.2) and a `&mut` borrow (Wasmer 7). Four guarantees the borrow checker gives for free need explicit replacements over C: @@ -89,7 +96,7 @@ Four guarantees the borrow checker gives for free need explicit replacements ove 2. **Exclusivity** — the context must be **stack-allocated per hook invocation**, never a per-executor or thread-local slot. Then nesting (`executeOnDestContext` running a contract from inside a hook) is safe by construction. -3. **Thread affinity** — the Wasmer 6 state borrows the host-call frame. cgo keeps the +3. **Thread affinity** — the Wasmer 7 state borrows the host-call frame. cgo keeps the goroutine on its OS thread for the call, but a goroutine spawned inside a hook must not touch the handle. State it as an invariant in the header. 4. **No panics** — a panic across `extern "C"` aborts the node. Every entry point needs the @@ -135,7 +142,7 @@ and it costs nothing, since the hook returns `Err(VMHooksEarlyExit)` in-process `Cow<'static, str>`. If the text is ever wanted on the Rust side, it does **not** belong on `InstanceState`: that -trait is call-scoped under Wasmer 6, while an early-exit message must outlive the call, and +trait is call-scoped under Wasmer 7, while an early-exit message must outlive the call, and an early exit is a property of the call rather than of the instance. It belongs in the FFI context. Preferred shape is **pull**: one extra hook pointer, `int32_t get_early_exit_message(void* ctx, uint8_t* buf, int32_t buf_len)` returning the @@ -159,7 +166,7 @@ is what flips. | # | Decision | Why it blocks | | --- | --- | --- | -| **G1** | Do the hooks hold the state, or take it as a parameter? | The C ABI passes the context as the first argument of every hook — that *is* the parameter form. If the Rust trait keeps the state bundled, the generator must emit and reconcile two shapes. Deciding after the ABI is frozen turns a refactor into a two-repo migration. The Wasmer 6 builder currently returns `Box`, so a per-hook allocation also rides on this. **Phase 2 found this already answered by the committed Wasmer 6 code**: `ExperimentalVMHooksBuilder::with_vm_hooks` hands the builder the state and lends the hooks back through a closure, so the state is *bundled* in Rust and *passed as a parameter* in C — the two are compatible, and the generator emits one shape, not two. The allocation question is also already resolved: the builder lends `&mut dyn VMHooks` on its own stack frame. **Superseded in scope by G6**, which decides where that builder is attached. | +| **G1** | Do the hooks hold the state, or take it as a parameter? | The C ABI passes the context as the first argument of every hook — that *is* the parameter form. If the Rust trait keeps the state bundled, the generator must emit and reconcile two shapes. Deciding after the ABI is frozen turns a refactor into a two-repo migration. The Wasmer 7 builder currently returns `Box`, so a per-hook allocation also rides on this. **Phase 2 found this already answered by the committed Wasmer 7 code**: `ExperimentalVMHooksBuilder::with_vm_hooks` hands the builder the state and lends the hooks back through a closure, so the state is *bundled* in Rust and *passed as a parameter* in C — the two are compatible, and the generator emits one shape, not two. The allocation question is also already resolved: the builder lends `&mut dyn VMHooks` on its own stack frame. **Superseded in scope by G6**, which decides where that builder is attached. | | **G2** | Does the new executor keep the Go VM's truncating load? | **Settled in Phase 2: yes, it clamps.** `vm_exec2_state_mem_load` writes into the caller's buffer and returns `min(requested, available)`, so the truncation is visible in the return value. Chosen to preserve behaviour contracts have already observed; the strict load stays available on the trait for a caller that wants it. The rule itself moved into `clamped_load_len` in `vm-executor/src/new_traits/memory.rs`, so the FFI and `InstanceState::memory_load_clamped` cannot drift apart. | | **G4** | Which `OpcodeVersion` does each host declare, and does the config stay mutable? | **Resolved in Phase 4b: immutable, with the executor rebuilt on change.** Phase 2 had already taken the immutable branch for the new C API — version and costs arrive in `vm_exec2_executor_config_t`, with no `set_opcode_config`. The obstacle was `GasScheduleChange` (`vmhost/hostCore/host.go:375`), which re-applies both to the *live* executor at an epoch. It is reconcilable because that function already calls `ClearWarmInstanceCache()`, so no instance survives the change: recreating the executor is behaviour-preserving, and is what lets `SetOpcodeConfig` leave the `Executor` interface. Phase 1.6 still has to reconcile the 2.2 backend's `Arc>` if that path is ever built. **Extended by G6's companion decision**: the opcode config is not executor state at all — it folds into `CompilationOptions`, passed per instance, which removes the setter rather than replacing it with a rebuild. | | **G5** | Does one C library carry both backends, selected by a flag? | **Answered: no — one library per backend, link the one you need.** That is already the pattern: a node links four wasmer runtimes today (v1.2/v1.3/v1.4 share one byte-identical `libwasmer`, v1.5 brings `libvmexeccapi`) with zero shared symbols between them. Bundling would make the 32 duplicate `wasmer_vm_*` libcalls a *static* collision the linker silently collapses, breaking one backend's `vmctx` layout; two libraries make it a *dynamic* one that `-Bsymbolic-functions` fixes. Duplication is near zero: one Rust crate with a backend feature, one Go bridge with a build tag. What remains is **verifying `-Bsymbolic-functions` on Linux, before anything links both** — see "Serving both backends" below. | @@ -221,7 +228,7 @@ error buffer itself. coming from the executor's own runtime reference. But this API needs one Go hooks object *per instance* — that is where the Go side keeps the per-instance early-exit slot that Phase 3's shim reads — and there is nowhere in that signature to put it. So `vm_exec2_new_instance` -takes `go_vm_hooks` and builds the instance directly against the Wasmer 6 backend. **Phase +takes `go_vm_hooks` and builds the instance directly against the Wasmer 7 backend. **Phase 1.6's converged hooks-provider trait should take a per-instance host handle**, otherwise the `Executor` trait cannot serve the C API on either backend. @@ -230,7 +237,7 @@ because there is no per-instance host state: Go's provider holds nothing but the per-instance `cgo.Handle` carries only `activeState` (a workaround for the VM still reaching wasm memory through `executor.Instance`) and an error buffer. So the handle becomes **per executor**, passed once to -`vm_exec_w6_new_executor`, and `vm_exec_w6_new_instance` loses its `go_vm_hooks` argument +`vm_exec_w7_new_executor`, and `vm_exec_w7_new_instance` loses its `go_vm_hooks` argument entirely. That is what lets this file go back through the `Executor` trait and serve both backends — once 4b's `MemLoad`/`MemStore` removal has removed `activeState`. @@ -375,14 +382,14 @@ a C struct by value — now across all 277 shims rather than the original three- The two interface sets cannot coexist in one VM: `Instance` lost most of its methods and the inside view moved to `InstanceState`, so a host either speaks the old set or the new one. The -Rust side has already resolved this the right way round — **the new set is not "Wasmer 6's +Rust side has already resolved this the right way round — **the new set is not "Wasmer 7's interface", it is the intersection**. Wasmer 2.2 satisfies it through `vm-executor-wasmer/src/new_traits`, and the Rust VM drives both backends through it; the reverse does not hold, because nothing in the legacy set can express a call-scoped state. That asymmetry is what makes converging safe, and it is worth being explicit about why. The hook context (`capi_vm_hook_context`) enforces the *stricter* of the two lifetimes: a state -handle is valid for exactly one host call. Wasmer 6 requires that. Wasmer 2.2 does not — its +handle is valid for exactly one host call. Wasmer 7 requires that. Wasmer 2.2 does not — its `WasmerProdInstanceState` holds a `Weak` and stays valid for the whole instance — but it satisfies the stricter contract trivially. Writing the Go side against the tighter contract therefore costs nothing on 2.2 and makes it impossible for the VM to @@ -431,7 +438,7 @@ Two libraries does *not* dodge the collision, because the deployment that matter The migration shape here is the v1.2–v1.4 one: ship the new VM module version alongside the old, so a node syncing from genesis can re-execute historical blocks under the backend that produced them. At that point the node links `libvmexeccapi` (wasmer 2.2) **and** -`libvmexeccapi2` (wasmer 6) — and those two shipped libraries have **32 overlapping exported +`libvmexeccapi2` (wasmer 7) — and those two shipped libraries have **32 overlapping exported symbol names, every one of them `wasmer_vm_*`** (measured directly: `llvm-nm --dynamic` on the shipped `.so`, `nm -gU` on the new dylib). Under ELF's flat namespace either library's libcall references can bind to the other's definition. macOS's @@ -455,7 +462,7 @@ item 1 already says. So the differential run is `wasmer2` against `wasmer3`, bot new Go interfaces, with no second Rust artifact. That makes **Phase 1.6 and the `VMHooksLegacyAdapter` activation trick cleanup rather than -migration**: worth doing when the legacy interface set is retired, not before wasmer 6 is live. +migration**: worth doing when the legacy interface set is retired, not before wasmer 7 is live. The `-Bsymbolic-functions` work is what takes its place on the critical path. **Superseded — it was needed, for a reason this section missed.** The argument above is sound @@ -683,7 +690,7 @@ state surface for symmetry would have been surface nothing uses. `executor.VMHooksProvider` has **two** methods, and the asymmetry is deliberate: -- `VMHooksForCall(state)` — Wasmer 6, which hands each host call a context and nothing longer. +- `VMHooksForCall(state)` — Wasmer 7, which hands each host call a context and nothing longer. - `VMHooksForInstanceLifetime()` — Wasmer 2.2, whose legacy C API sets the hooks pointer **per executor, not per instance** (`vm_exec_executor_set_vm_hooks_ptr`; there is no instance-level equivalent, which is also why `SetVMHooksPtr` on the Go interface never had a @@ -789,7 +796,7 @@ instead of a bare error. `verifyMemoryDeclaration` could not fire. It and `ErrMemoryDeclarationMissing` are gone with it; the executors already reject a contract without exactly one memory when they instantiate it, which happens first. -- `MemDump()` — exactly `MemLoad(0, MemLength())`, which is how the wasmer6 adapter already +- `MemDump()` — exactly `MemLoad(0, MemLength())`, which is how the wasmer7 adapter already implemented it. The two tests that used it say so directly now. - `SetVMHooksPtr` / `GetVMHooksPtr` — no VM caller at all, and every implementation of the getter returned zero. That made `TestExecution_MultipleInstances_SameVMHooks` vacuous (it @@ -808,7 +815,7 @@ would otherwise have quietly broken. The translation moved **up**, into `runtimeContext.CallSCFunction`, where it is written once for both backends instead of once per adapter: -- wasmer6 reports what actually happened and no longer stashes the breakpoint itself; +- wasmer7 reports what actually happened and no longer stashes the breakpoint itself; - wasmer2legacy can only say ok-or-failed — its C API cannot tell a trap from a breakpoint, and its middlewares set the global themselves — so it never reports `CallBreakpoint`, and the VM's existing readers are undisturbed; @@ -817,7 +824,7 @@ for both backends instead of once per adapter: This is the hard prerequisite `PLAN-go-breakpoint-removal.md` was waiting on. -**Verified.** The full suite passes on the default backend. Under `$VMEXECUTOR=wasmer6` the +**Verified.** The full suite passes on the default backend. Under `$VMEXECUTOR=wasmer7` the failure set is **byte-identical before and after** (25 failures, compared by stashing the change and re-running), so nothing regressed. The hook-sequence differential on `erc20-c` reproduces its recorded baseline exactly: **9,135 lines, 96 differing, every one of them @@ -898,6 +905,23 @@ Verified by regenerating — which must move nothing — and by running the full `wasmer2legacy` and `wasmer6`, plus the dual-backend trap tests and the hook-sequence differential. +### Done: 6 → 7, when the experimental backend moved to Wasmer 7.5 + +The same rename again, one version later, and for the reason the scheme exists: the number +names the backend, so when the backend changed the name had to. `c-api-wasmer6` → +`c-api-wasmer7`, `vm_exec_w6_*` → `vm_exec_w7_*`, `libvmexeccapi_w6*` → `libvmexeccapi_w7*`, +Makefile targets `capi-w6-*` → `capi-w7-*`; in Go, the `wasmer6` package → `wasmer7` with its +files and `Wasmer6*` types, the `w6_` shim prefix → `w7_`, `WriteWasmer6Cgo` → +`WriteWasmer7Cgo`, and `$VMEXECUTOR=wasmer6` → `wasmer7`. + +`vm-executor-experimental` keeps its name — it is named for its status, not its version, so it +is the one thing a runtime upgrade does not rename. + +Verified the same way: regeneration moves nothing, the header is cbindgen output rather than a +patched copy, `prefix_parity` still holds against `c-api-wasmer2`, both dual-backend trap +orders pass (so Wasmer 7.5 and Wasmer 2.2 still coexist — gate G5 survives the upgrade), and +the three whole-suite failure sets are unchanged apart from the one test the upgrade fixed. + ### Done: the prefix separation, and `c-api-common` **Shim and legacy-type prefixes.** The cgo `//export` names are C symbols, so the legacy @@ -915,7 +939,7 @@ because one output fed both the legacy crate and the new one. | | Lines | Contents | | --- | --- | --- | | `c-api-common` | **2,435** | status, strings, config, opcode cost, panic suppression, call result, and the *whole* hook dispatch path — context ABI, context pool, builder, table, and the generated `VMHooks` implementation | -| `c-api-wasmer6` | **734** | the two opaque handles, the prefixed entry points, and one file naming the backend | +| `c-api-wasmer7` | **734** | the two opaque handles, the prefixed entry points, and one file naming the backend | The hook dispatch moved because the context carries the instance state as an **erased pointer**: nothing in the pool, the table or the 900-odd generated lines ever names a backend type. Making @@ -938,13 +962,13 @@ needs *no* backend file at all, not merely a small one. **The shared header works.** `c-api-common` generates `libvmexeccapi_common.h` with an include guard and an explicit export list (nothing is reachable from an entry point, since the crate -exports no functions); `c-api-wasmer6`'s cbindgen excludes those types and emits +exports no functions); `c-api-wasmer7`'s cbindgen excludes those types and emits `#include "libvmexeccapi_common.h"`. Verified: nine shared types appear once, in the common header, and zero times in the backend header. Shared types are published under the neutral `vm_exec_*` prefix. The rule that fell out is worth stating, because it is what the next crate follows: **types shared → `vm_exec_*`; -opaque handles and functions → `vm_exec_w6_*`.** +opaque handles and functions → `vm_exec_w7_*`.** ### Done: `c-api-wasmer2`, and how the duplication was resolved @@ -955,19 +979,56 @@ header entry at all — the `#[no_mangle]` functions have to be written out lite (`capi_executor_ops`, `capi_instance_ops`, `capi_state_ops`, over the shared `capi_handles`), and each backend crate keeps a file of thin wrappers whose documentation is the header. -The one point of variation is `capi_handles::CapiBackend`, a trait with a single function: -which `Executor` an instance is built from. Because the entry points reach it through a name -that is the same in both crates (`capi_backend::Backend`), **all three entry-point files are -character-identical apart from the prefix** — `c-api-wasmer2/tests/prefix_parity.rs` asserts -exactly that, and the two generated headers are identical under the same substitution. Adding a -backend now costs one `CapiBackend` impl plus three derived files. +The one point of variation is which `Executor` an instance is built from, and it travels as an +ordinary function (`capi_handles::NewBackendExecutor`), handed to `new_executor` and kept on +the `CapiExecutor`. Not a trait: a C API library serves exactly one wasm runtime, fixed when +the crate is compiled, so there is nothing to abstract over and a generic parameter would only +suggest otherwise. And not an argument to `new_instance` either — the executor handle is +already backend-specific (`vm_exec_w2_executor_t` against `vm_exec_w7_executor_t`), so creating +an instance has nothing left to decide and is not asked to. + +Because the entry points reach the backend through a name that is the same in both crates +(`capi_backend::new_backend_executor`), **all three entry-point files are character-identical +apart from the prefix** — `c-api-wasmer2/tests/prefix_parity.rs` asserts exactly that, and the +two generated headers are identical under the same substitution. Adding a backend costs one +function plus three derived files. + +### Why the executor is still rebuilt per instance — and exactly what unblocks it + +`CapiExecutor` holds the configuration, not a `Box`, so `new_instance` builds one +each time. That is wrong in principle — an executor is meant to be created once and reused — +and it becomes wrong in practice at caching Stage 2, when the executor holds the **module +cache**: a per-instance executor means a per-instance cache, which is no cache at all. + +It is not fixable from the Rust side alone. An executor is built around one runtime reference +for its whole life, and this API's runtime handle (`go_vm_hooks`) arrives per instance. The +reason is now pinned precisely, and it is a single field: the Go object that handle identifies, +`instanceHooks`, holds `activeState` — the state of the hook currently running **on that +instance**. Its other two fields are already per-executor or could be: `provider` comes straight +from `wasmerExecutor.provider`, and `stateErrBuf` is scratch. A handle shared by several +instances could not hold `activeState`, because a nested call on a different instance would +clobber the outer one's — which is exactly what `executeOnDestContext` does. + +And `activeState` exists for one reason, already on the Phase 4b list: the VM still reaches +wasm memory through `executor.Instance` (`MemLoad`/`MemStore`) rather than through the state its +hook was handed. So the sequence is fixed and short: + +1. `MemLoad`/`MemStore` leave `executor.Instance` (Phase 4b, outstanding). +2. `instanceHooks.activeState` goes with them, and the handle becomes per-executor. +3. `go_vm_hooks` moves from `vm_exec_w*_new_instance` to `vm_exec_w*_new_executor` — a C ABI + change, coordinated with the Go VM, which is the same step the opcode config is waiting on. +4. `CapiExecutor` holds the `Box`, and `new_instance` becomes a plain call on it. + +Until then the cost is bounded and known: an executor holds only an `Arc` clone of the runtime, +so building one per instance is a box and a refcount bump. It stops being bounded at Stage 2, +which is why step 1 is a prerequisite for the module cache and not merely tidying. Sizes: the three files went from ~700 lines of logic per backend to ~450 lines of wrappers, most of it the C contract, over one shared body. ### Done: the Go `wasmer2` package -A copy of `wasmer6` with the prefix changed — it has to be, because a cgo `//export` name is a +A copy of `wasmer7` with the prefix changed — it has to be, because a cgo `//export` name is a C symbol and two packages cannot export the same one; that is also the entire reason the prefixes exist. The generated shims come from the generator (item 4 below, done: `WriteWasmer2NewCgo`, prefix `w2_`). `testexecutor.ExecWasmer2` is wired up, so @@ -979,7 +1040,7 @@ single-use after any early exit, and the next call trapped immediately on `unrea the legacy interface clearing it was the *host's* job (`runtimeContext.UseWarmInstance` → `SetRuntimeBreakpointValue(BreakpointNone)`), and the new C API deliberately does not expose the global, so the only side that can still do it is the executor. -`WasmerProdInstance::call` now clears it alongside setting the points limit. Wasmer 6 never had +`WasmerProdInstance::call` now clears it alongside setting the points limit. Wasmer 7 never had the bug because it has no such global to leave behind — which is exactly the class of divergence running 2.2 through the new boundary exists to catch. @@ -988,15 +1049,32 @@ divergence running 2.2 through the new boundary exists to catch. 1. ~~**`c-api-common`.**~~ **Done.** ~~**The prefix-parameterised macro question.**~~ **Answered above: shared bodies, literal wrappers, parity enforced by test.** 2. ~~**`c-api-wasmer2`.**~~ **Done** — see above. -3. **`wasmercommon` in Go**, wrapping the common header: the error buffer, the status mapping, - the call result and the hook result, so `wasmer2` and `wasmer6` share them instead of each - carrying a copy. Still outstanding, and deliberately so: it is a refactor of two packages, - and doing it while the second was being brought up would have made any failure ambiguous. - Now that the differential is green except for the known gap, it is safe to take. - Note the constraint it has to work around: cgo types are per-package, so anything whose - signature mentions `C.vm_exec_*` cannot move. What can is the error sentinels, the buffer - and its message decoding (over a plain `int32` status), the call-result enum, and - `readNames`. +3. ~~**`wasmercommon` in Go.**~~ **Done**, after the differential was green rather than before, + so that a failure during the bring-up could never have been the refactor's. + + What moved: the eight error sentinels, `Status` and its mapping to them, `ErrBuf` with its + message decoding, `CallResult`/`CallResultKind`, `ExecutorConfig`/`CompilationOptions` and + the conversion from `executor.CompilationOptions`, and `ReadNames`. + + What could not, and why: **cgo types are per-package**, so `C.vm_exec_status_t` in `wasmer7` + is a different Go type from the identically-named one in `wasmer2`, and nothing whose + signature mentions one can serve both. The bridge file is therefore the conversion point — + every `c*` function now returns `wasmercommon.Status`, so nothing above the bridge deals in + C types and could not accidentally be written against one backend's. + + **`wasmercommon` has no cgo at all**, which was forced rather than chosen: + `VM_EXEC_ERROR_BUF_RECOMMENDED_LEN` is an `extern const int` — a symbol, not a `#define` — + so a cgo package would have to *link a backend library* to read anything from the header, + which is precisely the dependency a backend-neutral package must not have. The buffer length + is passed in by each backend instead, and `Status` is mirrored by hand. The mirror is held + to the header by a compile-time check in each backend's `error*.go`: a mismatch makes one of + a pair of subtractions negative, and a negative constant converted to `uint` does not + compile. Verified by perturbing one constant and watching the build fail. + + Volume is roughly a wash — ~132 duplicated lines left each package, `wasmercommon` is 276 + including its own documentation and the mirror checks. The win is not size: it is that the + vocabulary now has one definition, and one spelling. The names are *not* re-exported from + the backend packages, deliberately, so there is no second way to say `CallOk`. 4. ~~A third cgo shim prefix for the new `wasmer2` package.~~ **Done.** ### What the differential now says @@ -1021,18 +1099,41 @@ holds the wasm runtime constant and varies only the boundary. Every hook call, in order, with every argument and every gas value, is identical. The divergence is the warm-instance path the new C API does not have yet (no `reset`, no module -cache — Phase 6), and it is the same shape already seen against `wasmer6`. +cache — Phase 6), and it is the same shape already seen against `wasmer7`. Two things it has also established: - **The whole-suite failure sets now separate the two variables.** `wasmer2legacy` 0 failures, - `wasmer2` 28, `wasmer6` 33 — and the 28 are a strict *subset* of the 33. So 28 failures + `wasmer2` 28, `wasmer7` 33 — and the 28 are a strict *subset* of the 33. So 28 failures belong to the boundary (all of them the `MemLoad`/`MemStore`-outside-a-hook and `Reset`/`Cache` gaps already on the 4b and Phase 6 lists) and exactly 5 belong to the runtime change: `TestExecution_Call_GasConsumptionOnLocals`, `TestExecution_DeployWASM_MoreThanMaximumLocals`, `TestRustBasicFeaturesBarnard`, `TestRustBasicFeaturesLatest`, `TestRustComposability`. Before this library existed there was no way to make that attribution. + + Re-measured after the Wasmer 7.5 upgrade: `wasmer2legacy` 0, `wasmer2` 28, `wasmer7` 32. The + subset relation still holds, and the count fell by one — but the *membership* changed in both + directions, which the count hides. Two of the five went away: + `TestExecution_Call_GasConsumptionOnLocals` and + `TestExecution_DeployWASM_MoreThanMaximumLocals`, fixed by the locals metering work the + upgrade brought. One is new: **`TestWASMMemories_MultipleMemories`**, which passed on Wasmer + 6.1 and fails on 7.5 — confirmed by running it against the pre-upgrade dylib in a worktree. + Reading 33 → 32 as "one test fixed" would have missed it entirely; this is the case the + per-test attribution exists for. + +- **Open, and the reason the above matters: multi-memory modules are now accepted.** The test + deploys a module declaring five memories and exporting one, and expects `ContractInvalid`; on + 7.5 it returns `Ok`. The cause is that `validate_memories` in + `vm-executor-experimental/src/we_instance.rs` counts **exported** memories — + `instance.exports.iter().memories()` — not declared ones, so one export past five + declarations looks like one memory. That check never had to be right before, because the + engine rejected the module first: `Store::new(Singlepass::default())` sets no explicit + `Features`, and the multi-memory proposal is off by default in 6.1 and on in 7.5. Two + independent fixes, and the second is worth doing regardless of the first: pin `Features` with + multi-memory off rather than inheriting a default that moves under us, and count declared + memories rather than exported ones. The same export-counting appears in + `vm-executor-wasmer/src/wasmer_instance.rs`, where 2.2's defaults still mask it. - **`delegation/v0_5_latest` fails 3 of 27 scenarios with "not enough gas", under *both* new backends.** Previously invisible: `TestDelegation_v0_5_latest` is skipped on arm64, so the differential was the first thing to run it here. Consistent with the cold-instance path, and @@ -1041,8 +1142,8 @@ Two things it has also established: ### Three libraries in one binary, on macOS A `VMEXECUTOR=wasmer2` test binary links all three: `libvmexeccapi` (2.2), `libvmexeccapi_w2` -(2.2 again) and `libvmexeccapi_w6` (6). Two independent copies of Wasmer 2.2 in one process, -and the whole suite runs. `wasmer6/dualbackend` — which had gone stale against the +(2.2 again) and `libvmexeccapi_w7` (6). Two independent copies of Wasmer 2.2 in one process, +and the whole suite runs. `wasmer7/dualbackend` — which had gone stale against the `InstanceCallResult` change and could not compile — is repaired and both trap-handler orderings pass again. @@ -1070,25 +1171,25 @@ unverified there, and `-Wl,-Bsymbolic-functions` remains the open item; see ## Phase 6 — Caching — **re-scoped, see `PLAN-rust-vm-caching.md`** -Was: `reset()` on the Wasmer 6 instance, as a prerequisite for Go's warm-instance tracker, -then `new_instance_from_cache`, then warm instances for wasmer6. +Was: `reset()` on the Wasmer 7 instance, as a prerequisite for Go's warm-instance tracker, +then `new_instance_from_cache`, then warm instances for wasmer7. **`reset()` is off the list.** Measured on a 55 KB contract: compiling costs ~6 ms and instantiating from a module already in memory ~30–50 µs, so reusing a live instance saves -under 1% over instantiating a cached module afresh. And it is not cheap to get — Wasmer 6 +under 1% over instantiating a cached module afresh. And it is not cheap to get — Wasmer 7 *and* 7.4 have no instance reset (`Memory::reset` only zeroes and shrinks memory; no -data-segment or global restore), so it would mean forking Wasmer 6, which is consumed +data-segment or global restore), so it would mean forking Wasmer 7, which is consumed unmodified from crates.io unlike the 2.2 fork. Re-instantiating is behaviourally identical to resetting. So for this backend the warm instance cache **collapses into the module cache**. The cheapest -route for the Go VM is for `Wasmer6Executor` to hold compiled modules in memory and let +route for the Go VM is for `Wasmer7Executor` to hold compiled modules in memory and let `Reset()` keep returning false, which the instance tracker already handles by evicting and recreating. `NewInstanceFromCompiledCodeWithOptions` is already the Wasmer-6-compatible form of reuse. Remaining here: the serialized-artifact path. Note that artifacts carry an ABI version that -`deserialize` rejects on mismatch — it moved from 11 (Wasmer 6.1) to 24 (Wasmer 7.4) — so a +`deserialize` rejects on mismatch — it moved from 11 (Wasmer 7.1) to 24 (Wasmer 7.4) — so a persisted cache needs the Wasmer version in its key or a purge on upgrade. ## Cross-repo note — `mx-sdk-rs` @@ -1120,7 +1221,7 @@ Accumulated breaks, with status: ## Critical path Originally **G1 → Phase 1.6/1.7 → Phase 2 → Phase 3 → Phase 4**. In the event Phase 2 ran -ahead of Phase 1.6/1.7: G1 turned out to be already answered by the committed Wasmer 6 code, +ahead of Phase 1.6/1.7: G1 turned out to be already answered by the committed Wasmer 7 code, and the C ABI is parameter-form regardless, so the trait convergence was not a prerequisite after all — it is now better informed by Phase 2, which found the per-instance hooks argument the converged trait needs. diff --git a/PLAN-rust-vm-caching.md b/PLAN-rust-vm-caching.md index 2ec880b8..4dff3cc5 100644 --- a/PLAN-rust-vm-caching.md +++ b/PLAN-rust-vm-caching.md @@ -2,7 +2,7 @@ Goal as set: bring the Rust VM up to the Go VM's level — contract code compiled once and reused — and implement the executor side that this needs. Two halves: the experimental -(Wasmer 6) executor, and the Rust VM itself. +(Wasmer 7) executor, and the Rust VM itself. The first thing done was to measure what reuse is worth, because that determines the shape of both halves. It turned out to change the plan substantially, so it comes first. @@ -10,7 +10,7 @@ both halves. It turned out to change the plan substantially, so it comes first. ## The measurement A real contract, `test/delegation/v0_4_genesis` (55 KB of wasm), 50 iterations, release build, -Wasmer 6.1 Singlepass on an arm64 Mac. Numbers vary ±20% between runs; the *ratios* do not. +Wasmer 7.1 Singlepass on an arm64 Mac. Numbers vary ±20% between runs; the *ratios* do not. | Operation | Cost | Share of a cold instance | | --- | --- | --- | @@ -29,14 +29,14 @@ follows from that. ### Consequence 1: cache modules, not instances The Go VM keeps a *warm instance* cache and reuses live instances via `Instance.Reset()`. On -Wasmer 6 that model buys ~30–50 µs on a ~6 ms operation — under 1% — over simply instantiating +Wasmer 7 that model buys ~30–50 µs on a ~6 ms operation — under 1% — over simply instantiating a cached module afresh. It is not worth its complexity, and the complexity is considerable: -- **Wasmer 6 has no instance reset, and adding one means forking it.** `Memory::reset` exists +- **Wasmer 7 has no instance reset, and adding one means forking it.** `Memory::reset` exists but, per its own doc, "resets the memory back to zero length" — it does not re-apply data segments and does not touch globals. The Wasmer 2.2 `reset()` the Go VM relies on lives in the **MultiversX fork** (`handle.reset(data_initializers)`, reaching into `wasmer-vm`). - Wasmer 6 is consumed from **crates.io, unmodified** — which is a property worth keeping. + Wasmer 7 is consumed from **crates.io, unmodified** — which is a property worth keeping. - Re-instantiating is **behaviourally identical** to resetting: both yield fresh globals and memory initialised from the module's data segments. Nothing observable to a contract distinguishes them. @@ -88,7 +88,7 @@ pub trait ExecutorRuntime: Send + Sync { /// Builds the VM hooks servicing one host call, around the instance state for that call, /// and lends them to `f` — which is called exactly once. /// - /// Built per call, not per instance: under Wasmer 6 the instance state is only reachable + /// Built per call, not per instance: under Wasmer 7 the instance state is only reachable /// for the duration of a call, so hooks that hold it cannot outlive it. Lending rather /// than returning avoids a `Box` allocation on every hook invocation. /// @@ -111,15 +111,15 @@ compilation options on it — those are arguments to `new_instance`. The runtime reference becomes a per-**executor** handle, where today it is per-instance: ```c -vm_exec_status_t vm_exec_w6_new_executor( - vm_exec_w6_executor_t **executor_out, +vm_exec_status_t vm_exec_w7_new_executor( + vm_exec_w7_executor_t **executor_out, uintptr_t runtime_ref, /* NEW: the host's runtime handle */ const vm_exec_vm_hook_c_func_pointers *hook_table, char *err_buf, int err_buf_len); -vm_exec_status_t vm_exec_w6_new_instance( - vm_exec_w6_executor_t *executor, - vm_exec_w6_instance_t **instance_out, +vm_exec_status_t vm_exec_w7_new_instance( + vm_exec_w7_executor_t *executor, + vm_exec_w7_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const vm_exec_compilation_options_t *options, /* now carries the opcode config */ char *err_buf, int err_buf_len); /* go_vm_hooks is gone */ @@ -173,7 +173,7 @@ The one constraint to respect is the one already stated in 1.1: an `Engine` may metering middleware keeps `global_indexes` populated at compile time. Instantiation does not run middlewares, so recursion is unaffected. -**On the Go side today this is all vacuous anyway**: `Wasmer6Instance.Reset()` returns false, +**On the Go side today this is all vacuous anyway**: `Wasmer7Instance.Reset()` returns false, and both `Cache()` and `NewInstanceFromCompiledCodeWithOptions` return `ErrCacheUnsupported`, so every execution compiles from bytecode and no instance is ever shared. Recursion is safe and slow. That is what the module cache fixes. @@ -183,7 +183,7 @@ the execution resolved at call time (G6), the inner frame's hooks resolve to the context and the outer frame's to the outer, through the host's context stack — which is what that stack is for. On the C side the hook context pool is already a stack: the inner activation is innermost, the outer context is refused while it runs and valid again -afterwards, which `probe_nesting` in `c-api-wasmer6/tests/ffi_boundary.rs` already pins. +afterwards, which `probe_nesting` in `c-api-wasmer7/tests/ffi_boundary.rs` already pins. ## Wasmer 7.4, checked alongside — and one reason to move @@ -202,8 +202,8 @@ serialize → deserialize → instantiate round trip (49 exports intact). **The reason to move is unrelated to caching.** `vm-executor-experimental/src/middlewares/we_metering.rs` has `feed_local_count` commented out under a TODO listing three options ("find alternative / PR to Wasmer / fork Wasmer"), because -Wasmer 6 has no hook for it. The Wasmer 2.2 backend has that function **live**. So the two -backends currently disagree on gas: **Wasmer 6 does not charge the unmetered-locals cost that +Wasmer 7 has no hook for it. The Wasmer 2.2 backend has that function **live**. So the two +backends currently disagree on gas: **Wasmer 7 does not charge the unmetered-locals cost that Wasmer 2.2 charges.** Wasmer 7.4 adds `FunctionMiddleware::locals_info(&mut self, locals: &[ValType])` — option (b) @@ -248,7 +248,7 @@ pub trait Executor { } ``` -The Wasmer 6 implementation holds `{ engine: Engine, module: Module }`. The `Engine` has to +The Wasmer 7 implementation holds `{ engine: Engine, module: Module }`. The `Engine` has to travel with the `Module`, because instantiation needs a `Store` built on the same engine (`Store::new(engine.clone())` — the clone is an `Arc` bump). @@ -282,13 +282,13 @@ If it shows up, add an optional caller-supplied key. Not worth the extra paramet `cache()` already works (`module.serialize()`). Finish the other half: `new_instance_from_cache` should do `Module::deserialize(&engine, bytes)` against an engine built from the same options. -**Security note to carry into the doc comment:** in Wasmer 6.1, `Module::deserialize` delegates +**Security note to carry into the doc comment:** in Wasmer 7.1, `Module::deserialize` delegates to `deserialize_unchecked` — the "checked" variant performs no validation. Artifacts must come from a trusted store. That is true of our own cache, but it must be stated, because the name suggests otherwise. **Artifacts are not portable across Wasmer versions.** The serialized header carries an ABI -version which `deserialize` rejects on mismatch, and it moved from **11** (Wasmer 6.1) to +version which `deserialize` rejects on mismatch, and it moved from **11** (Wasmer 7.1) to **24** (Wasmer 7.4). Any persistent cache therefore needs the Wasmer version in its key, or an explicit purge on upgrade. Irrelevant to the in-memory module cache, which never serializes. @@ -318,7 +318,7 @@ a stack pushed for the whole of `RuntimeRef::execute` and popped at the end: `impl ExecutorRuntime for RuntimeWeakRef` then has one method, which resolves the current context from that stack and builds the dispatcher around the state it was handed. One -implementation serves both backends — the Wasmer 2.2 and Wasmer 6 executors are configured +implementation serves both backends — the Wasmer 2.2 and Wasmer 7 executors are configured identically. ### 2.2 `ContractDebugExecutor` needs nothing special @@ -365,12 +365,12 @@ the sketch: - **The C API stopped naming a backend entirely.** `capi_backend.rs` is deleted and `CapiVMHooksBuilder` implements `ExecutorRuntime` directly; see `PLAN-go-ffi.md` Phase 4c. -- **Both backends converged, not just Wasmer 6.** `WasmerProdRuntimeRef` is deleted; +- **Both backends converged, not just Wasmer 7.** `WasmerProdRuntimeRef` is deleted; `WasmerProdExecutor` takes the same `Arc`. The obstacle was that legacy `VMHooksLegacy` is an *object* built once per instance, with nowhere to report an early exit, while the runtime *lends* hooks per call. Resolved by regenerating `VMHooksLegacyAdapter` over `R: ExecutorRuntime` + `S: InstanceState` — header template only, all 277 bodies - byte-identical — so 2.2 now resolves the execution when the hook runs, exactly as Wasmer 6 + byte-identical — so 2.2 now resolves the execution when the hook runs, exactly as Wasmer 7 does. The stash-and-halt half is a defaulted `InstanceState::handle_early_exit_legacy`, which fails for every backend that does not implement it; a probe removing the 2.2 override fails exactly the OutOfGas and UserError tests and leaves the no-early-exit one passing. @@ -389,7 +389,7 @@ the sketch: after any early exit — which is precisely a reuse bug, found while building `c-api-wasmer2` rather than while building the cache. `WasmerProdInstance::call` now clears it alongside setting the points limit, so an instance is armed for each call rather than still halted - from the last one. Wasmer 6 never had the problem, having no such global; the general point + from the last one. Wasmer 7 never had the problem, having no such global; the general point stands for stages 1–3 either way, that anything a call leaves behind in an instance has to be reset by the executor, because after caching nobody else knows an instance is being reused. @@ -415,6 +415,12 @@ experimental executor. Go VM: the rebuilt dylib drops in with no Go changes and `VMEXECUTOR=wasmer6` failure set is identical before and after — 33 failures, same set, compared by stashing the dylib. +**Since superseded by the move itself.** Wasmer 7.5 is now the experimental backend's runtime, +and everything named for 6 has been renamed to 7 — crate, library, symbols, Go package and +`$VMEXECUTOR` value, so the selector read `wasmer6` above and reads `wasmer7` today. The +failure set is 32: the 33 above less `TestExecution_DeployWASM_MoreThanMaximumLocals`, which +the locals metering work fixed. + One check worth recording, because it nearly produced a false pass: the composability suite (332 tests, contract-to-contract and recursive callers) runs on the **Debugger** by default, not the experimental executor, so it stays green even with the context guard deliberately @@ -435,16 +441,16 @@ item removes. Until then the C API keeps its per-instance handle and the capi ke ## What this means for the Go VM -Not in scope here, but the measurement applies to Go's Wasmer 6 backend too, and it bears on -`PLAN-go-ffi.md` Phase 6 ("`reset()` on the Wasmer 6 instance — prerequisite for Go's +Not in scope here, but the measurement applies to Go's Wasmer 7 backend too, and it bears on +`PLAN-go-ffi.md` Phase 6 ("`reset()` on the Wasmer 7 instance — prerequisite for Go's warm-instance tracker"). It is not a prerequisite, because it is not worth building. The Go VM's existing *compiled-code* path (`NewInstanceFromCompiledCodeWithOptions`) is already the -Wasmer-6-compatible form of reuse: for this backend the warm instance cache collapses into the -module cache. The cheapest route is for `Wasmer6Executor` to keep compiled modules in memory +Wasmer-7-compatible form of reuse: for this backend the warm instance cache collapses into the +module cache. The cheapest route is for `Wasmer7Executor` to keep compiled modules in memory and let `Reset()` keep returning false, which the instance tracker already handles by evicting and recreating. Worth confirming separately, because it is the one place where the Go VM's reuse model and -Wasmer 6's capabilities genuinely disagree. +Wasmer 7's capabilities genuinely disagree. diff --git a/c-api-common/libvmexeccapi_common.h b/c-api-common/libvmexeccapi_common.h index f5ce4971..c72aebb2 100644 --- a/c-api-common/libvmexeccapi_common.h +++ b/c-api-common/libvmexeccapi_common.h @@ -262,7 +262,7 @@ typedef struct vm_exec_executor_config_t { } vm_exec_executor_config_t; /** - * The out-struct of `vm_exec_w6_instance_call`. + * The out-struct of `vm_exec_w7_instance_call`. */ typedef struct vm_exec_call_result_t { enum vm_exec_call_result_kind_t kind; diff --git a/c-api-common/src/capi_call_result.rs b/c-api-common/src/capi_call_result.rs index d4d127b2..aa4dd3e8 100644 --- a/c-api-common/src/capi_call_result.rs +++ b/c-api-common/src/capi_call_result.rs @@ -40,7 +40,7 @@ pub enum vm_exec_call_result_kind_t { VM_EXEC_CALL_BREAKPOINT = 4, } -/// The out-struct of `vm_exec_w6_instance_call`. +/// The out-struct of `vm_exec_w7_instance_call`. #[allow(non_camel_case_types)] #[repr(C)] #[derive(Debug, Clone, Copy)] diff --git a/c-api-common/src/capi_executor_ops.rs b/c-api-common/src/capi_executor_ops.rs index eab982dc..125bd080 100644 --- a/c-api-common/src/capi_executor_ops.rs +++ b/c-api-common/src/capi_executor_ops.rs @@ -9,12 +9,13 @@ use libc::{c_char, c_int}; use std::sync::Arc; use crate::capi_config::vm_exec_executor_config_t; -use crate::capi_handles::CapiExecutor; +use crate::capi_handles::{CapiExecutor, NewBackendExecutor}; use crate::capi_status::{vm_exec_status_t, write_error}; use crate::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; use crate::capi_vm_hook_table::CapiVMHookTable; -/// Creates an executor from one configuration struct and one hook table. +/// Creates an executor from one configuration struct, one hook table and the backend the +/// calling library is for. /// /// # Safety /// @@ -24,6 +25,7 @@ pub unsafe fn new_executor( executor_out: *mut *mut CapiExecutor, config: *const vm_exec_executor_config_t, hook_table: *const vm_exec_vm_hook_c_func_pointers, + new_backend_executor: NewBackendExecutor, err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { @@ -39,6 +41,7 @@ pub unsafe fn new_executor( let capi_executor = CapiExecutor { opcode_config: Arc::new(opcode_config), hook_table: unsafe { CapiVMHookTable::copy_from(hook_table) }, + new_backend_executor, }; unsafe { diff --git a/c-api-common/src/capi_handles.rs b/c-api-common/src/capi_handles.rs index 50f35a5f..95bbde56 100644 --- a/c-api-common/src/capi_handles.rs +++ b/c-api-common/src/capi_handles.rs @@ -1,10 +1,7 @@ -//! What the opaque C handles point to, and the one thing the two C APIs do not share. +//! What the opaque C handles point to. //! //! Both backends' handles hold exactly the same thing, because neither names a backend: an //! executor is a configuration plus a hook table, and an instance is a `Box`. -//! The single point of variation is [`CapiBackend`] — which `Executor` implementation an -//! instance is built from — and it is a trait rather than a parameter so that the shared -//! operations stay plain functions a prefixed entry point can call directly. use std::sync::Arc; @@ -15,13 +12,26 @@ use crate::capi_vm_hook_table::CapiVMHookTable; /// What a `vm_exec__executor_t` points to. /// -/// The shared, immutable configuration: passed once at creation and never mutated. It does -/// not yet hold a `dyn Executor`, because an executor is built around one runtime reference -/// for its whole life and this API's runtime handle still arrives per instance. See -/// `capi_instance_ops::new_instance`. +/// The shared, immutable configuration: passed once at creation and never mutated. The +/// backend is part of that configuration, because the handle is already backend-specific — +/// `vm_exec_w2_executor_t` and `vm_exec_w7_executor_t` are distinct types — so an instance +/// call has nothing left to decide and is not asked to. +/// +/// It does not yet hold a `dyn Executor`, and the reason is on the Go side rather than here. +/// An executor is built around one runtime reference for its whole life, and this API's +/// runtime handle is still supplied per instance — because the Go object it identifies holds +/// `activeState`, the state of the hook currently running *on that instance*, which a handle +/// shared by several instances could not hold. That slot exists only because the VM still +/// reaches wasm memory through `executor.Instance` instead of through the state it was +/// handed. When `MemLoad`/`MemStore` leave that interface (PLAN-go-ffi.md, Phase 4b), the +/// handle becomes per-executor, moves to `new_executor`, and the `Box` moves +/// here with it. pub struct CapiExecutor { pub opcode_config: Arc, pub hook_table: CapiVMHookTable, + + /// Which backend this library is for, supplied by the entry point that created this. + pub new_backend_executor: NewBackendExecutor, } /// What a `vm_exec__instance_t` points to. @@ -29,13 +39,11 @@ pub struct CapiInstance { pub content: Box, } -/// Which wasm runtime a C API is for. +/// Builds the backend's executor around the runtime the C API supplies. /// -/// This is the entire difference between the two libraries. Everything else — the handles, -/// the operations, the hook dispatch, the error convention — is compiled once and shared, -/// which is why adding a backend costs one implementation of this plus a file of prefixed -/// entry points. -pub trait CapiBackend { - /// Builds the backend's executor around the runtime the C API supplies. - fn new_executor(runtime: Arc) -> Box; -} +/// This is the entire difference between the two libraries, and a library has exactly one of +/// them: a C API is built for a single wasm runtime, fixed when the crate is compiled. So it +/// travels as an ordinary function, handed to [`crate::capi_executor_ops::new_executor`] and +/// kept on the [`CapiExecutor`] it builds. Everything else — the handles, the operations, the +/// hook dispatch, the error convention — is compiled once and shared. +pub type NewBackendExecutor = fn(Arc) -> Box; diff --git a/c-api-common/src/capi_instance_ops.rs b/c-api-common/src/capi_instance_ops.rs index 3d2e6c46..f99e3e83 100644 --- a/c-api-common/src/capi_instance_ops.rs +++ b/c-api-common/src/capi_instance_ops.rs @@ -4,8 +4,9 @@ //! calling, gas accounting, and module introspection. Everything a *hook* needs is in //! `capi_state_ops`, reached through a hook context rather than through this handle. //! -//! Only [`new_instance`] is parameterised by backend, and only to pick which `Executor` to -//! build. Everything after that goes through `dyn Instance`. +//! Nothing here takes a backend. The executor handle is already backend-specific, so +//! [`new_instance`] reads the choice off it, and everything after that goes through +//! `dyn Instance`. use libc::{c_char, c_int}; use std::ffi::CStr; @@ -14,19 +15,19 @@ use std::sync::Arc; use crate::capi_call_result::vm_exec_call_result_t; use crate::capi_config::vm_exec_compilation_options_t; -use crate::capi_handles::{CapiBackend, CapiExecutor, CapiInstance}; +use crate::capi_handles::{CapiExecutor, CapiInstance}; use crate::capi_status::{vm_exec_status_t, write_error}; use crate::capi_strings::{copy_pipe_joined, pipe_joined_length}; use crate::capi_vm_hook_context::CapiVMHooksBuilder; -/// Creates an instance of `B`'s backend. +/// Creates an instance, using the backend the executor handle was created for. /// /// # Safety /// /// All pointer arguments must be valid for the duration of the call, and whatever /// `go_vm_hooks` identifies must outlive the instance. #[allow(clippy::too_many_arguments)] -pub unsafe fn new_instance( +pub unsafe fn new_instance( executor: *mut CapiExecutor, instance_out: *mut *mut CapiInstance, wasm_bytes: *const u8, @@ -55,7 +56,7 @@ pub unsafe fn new_instance( go_vm_hooks, capi_executor.hook_table.clone(), )); - let backend_executor = B::new_executor(runtime); + let backend_executor = (capi_executor.new_backend_executor)(runtime); let instance = match backend_executor.new_instance(wasm_bytes, &compilation_options) { Ok(instance) => instance, diff --git a/c-api-common/src/capi_vm_hook_context.rs b/c-api-common/src/capi_vm_hook_context.rs index cb3fac3f..643b8ed8 100644 --- a/c-api-common/src/capi_vm_hook_context.rs +++ b/c-api-common/src/capi_vm_hook_context.rs @@ -1,6 +1,6 @@ //! The hook context: what a VM hook is handed, and what makes it safe to hand over. //! -//! Under Wasmer 6 the instance state is a borrow of the host-call frame — memory and globals +//! Under Wasmer 7 the instance state is a borrow of the host-call frame — memory and globals //! are only reachable through the `StoreMut` inside `FunctionEnvMut`, so the state is valid //! for exactly one hook invocation and not a moment longer. In Rust the borrow checker //! enforces that. Over C it has to be enforced dynamically, which is what this module does. @@ -246,7 +246,7 @@ pub struct CapiVMHooksBuilder { // field is the context pool. The pool is touched exactly where guarantee 3 at the top of this // module already requires single-threaded access — inside a hook invocation, on the thread // cgo has pinned the calling goroutine to — and a goroutine spawned inside a hook is -// forbidden from touching the context at all. This mirrors the same unsafe impl the Wasmer 6 +// forbidden from touching the context at all. This mirrors the same unsafe impl the Wasmer 7 // backend puts on its own hooks wrapper, for the same reason. unsafe impl Send for CapiVMHooksBuilder {} unsafe impl Sync for CapiVMHooksBuilder {} diff --git a/c-api-common/src/lib.rs b/c-api-common/src/lib.rs index a35b5041..3004e66f 100644 --- a/c-api-common/src/lib.rs +++ b/c-api-common/src/lib.rs @@ -22,7 +22,11 @@ //! //! - **The operations themselves**: executor and instance lifecycle, the outer instance //! calls, and the state calls a hook makes. Only one of them is parameterised by backend — -//! which `Executor` an instance is built from, named by `capi_handles::CapiBackend`. +//! which `Executor` an instance is built from, which the entry point hands to +//! `new_executor` as a plain function (`capi_handles::NewBackendExecutor`) and which the +//! `CapiExecutor` then carries. A library serves one wasm runtime, fixed when it is +//! compiled, so there is nothing here to abstract over — and because the executor handle is +//! already backend-specific, creating an instance has nothing left to decide. //! //! The backend crates keep only their opaque handles and their prefixed entry points, which //! are thin wrappers over the operations here. They have to be written out literally rather diff --git a/c-api-wasmer2/src/capi_backend.rs b/c-api-wasmer2/src/capi_backend.rs index 30d84507..ba562fb1 100644 --- a/c-api-wasmer2/src/capi_backend.rs +++ b/c-api-wasmer2/src/capi_backend.rs @@ -1,8 +1,8 @@ //! Which wasm runtime this library is for. //! //! The one file in this crate that names a backend, and the reason the three entry-point -//! files do not: they reach the backend only through this type, under a name that is the -//! same in every backend crate. That is what keeps them identical to the Wasmer 6 copy +//! files do not: they reach the backend only through this function, under a name that is the +//! same in every backend crate. That is what keeps them identical to the Wasmer 7 copy //! apart from the symbol prefix — a property an integration test checks, so the two cannot //! drift. @@ -10,7 +10,6 @@ use multiversx_chain_vm_executor::Executor; use multiversx_chain_vm_executor_wasmer::new_traits::WasmerProdExecutor; use std::sync::Arc; -use crate::capi_handles::CapiBackend; use crate::capi_vm_hook_context::CapiVMHooksBuilder; /// Wasmer 2.2, through the production backend. @@ -18,11 +17,7 @@ use crate::capi_vm_hook_context::CapiVMHooksBuilder; /// Its hooks are the legacy value-returning kind, with no channel for an early exit, so the /// backend wraps the hooks this C API builds in `VMHooksLegacyAdapter`. Nothing about that is /// visible here or to the caller: the adapter asks the runtime for hooks per call, exactly as -/// Wasmer 6 does, so both libraries resolve the current execution when a hook runs. -pub struct Backend; - -impl CapiBackend for Backend { - fn new_executor(runtime: Arc) -> Box { - Box::new(WasmerProdExecutor::new(runtime)) - } +/// Wasmer 7 does, so both libraries resolve the current execution when a hook runs. +pub fn new_backend_executor(runtime: Arc) -> Box { + Box::new(WasmerProdExecutor::new(runtime)) } diff --git a/c-api-wasmer2/src/capi_executor.rs b/c-api-wasmer2/src/capi_executor.rs index 18cb2894..94a8c3b3 100644 --- a/c-api-wasmer2/src/capi_executor.rs +++ b/c-api-wasmer2/src/capi_executor.rs @@ -22,6 +22,7 @@ use libc::{c_char, c_int}; use meta::capi_safe_unwind; +use crate::capi_backend::new_backend_executor; use crate::capi_config::vm_exec_executor_config_t; use crate::capi_executor_ops; use crate::capi_handles::CapiExecutor; @@ -59,6 +60,7 @@ pub unsafe extern "C" fn vm_exec_w2_new_executor( executor_out as *mut *mut CapiExecutor, config, hook_table, + new_backend_executor, err_buf, err_buf_len, ) diff --git a/c-api-wasmer2/src/capi_instance.rs b/c-api-wasmer2/src/capi_instance.rs index 6f730f8b..8af8b027 100644 --- a/c-api-wasmer2/src/capi_instance.rs +++ b/c-api-wasmer2/src/capi_instance.rs @@ -12,7 +12,6 @@ use libc::{c_char, c_int}; use meta::capi_safe_unwind; -use crate::capi_backend::Backend; use crate::capi_call_result::vm_exec_call_result_t; use crate::capi_config::vm_exec_compilation_options_t; use crate::capi_executor::vm_exec_w2_executor_t; @@ -50,7 +49,7 @@ pub unsafe extern "C" fn vm_exec_w2_new_instance( err_buf_len: c_int, ) -> vm_exec_status_t { unsafe { - capi_instance_ops::new_instance::( + capi_instance_ops::new_instance( executor as *mut CapiExecutor, instance_out as *mut *mut CapiInstance, wasm_bytes, diff --git a/c-api-wasmer2/src/lib.rs b/c-api-wasmer2/src/lib.rs index 15079bbc..2ba6f79e 100644 --- a/c-api-wasmer2/src/lib.rs +++ b/c-api-wasmer2/src/lib.rs @@ -1,6 +1,6 @@ //! C API for the new executor interface, over the **Wasmer 2.2** backend. //! -//! The sibling of `c-api-wasmer6`, and deliberately as close to it as two libraries can be: +//! The sibling of `c-api-wasmer7`, and deliberately as close to it as two libraries can be: //! the three entry-point files are character-identical to that crate's apart from the symbol //! prefix, and `tests/prefix_parity.rs` fails if they ever stop being. Everything behind them //! — the operations, the hook dispatch, the error convention — is compiled once in @@ -10,17 +10,17 @@ //! reference the new interfaces have to be measured against. Running the *same* runtime //! through the *new* boundary is what isolates the boundary as the variable: a difference //! between this library and `libvmexeccapi` is a difference the refactor introduced, with no -//! wasm runtime change to confound it. Comparing Wasmer 6 against Wasmer 2.2 cannot say that, +//! wasm runtime change to confound it. Comparing Wasmer 7 against Wasmer 2.2 cannot say that, //! because it changes both at once. //! -//! - **Exported symbols are prefixed `vm_exec_w2_`**, so this library, `libvmexeccapi_w6` and +//! - **Exported symbols are prefixed `vm_exec_w2_`**, so this library, `libvmexeccapi_w7` and //! the legacy `libvmexeccapi` can all be linked into one binary. //! - **Types are shared, not duplicated**: the vocabulary comes from `libvmexeccapi_common.h`, //! which both new headers include, so a caller linking both sees one definition of each. //! //! One thing this library does *not* expose, although the backend supports it: there is no //! instance `reset`, and no `cache`. Those are the warm-instance path the legacy API has and -//! Wasmer 6 does not, and adding them here would make this library's shape depend on which +//! Wasmer 7 does not, and adding them here would make this library's shape depend on which //! backend is underneath — which is the thing it exists to hold constant. A differential //! against the legacy library therefore compares a warm cache against cold instances; the //! hook sequence and the gas accounting, which are what it is measuring, are unaffected. diff --git a/c-api-wasmer2/tests/prefix_parity.rs b/c-api-wasmer2/tests/prefix_parity.rs index b110f5e2..609e32d0 100644 --- a/c-api-wasmer2/tests/prefix_parity.rs +++ b/c-api-wasmer2/tests/prefix_parity.rs @@ -10,17 +10,17 @@ //! A failure here means someone changed one copy and not the other. The fix is to re-derive //! the file rather than to patch it twice. -/// `w6` and `w2` are the same length, so the substitution cannot change alignment or layout — +/// `w7` and `w2` are the same length, so the substitution cannot change alignment or layout — /// the comparison is a plain string equality, not a normalisation. fn to_neutral_prefix(source: &str, prefix: &str) -> String { source.replace(prefix, "wN") } -fn assert_parity(file_name: &str, wasmer2: &str, wasmer6: &str) { +fn assert_parity(file_name: &str, wasmer2: &str, wasmer7: &str) { assert_eq!( to_neutral_prefix(wasmer2, "w2"), - to_neutral_prefix(wasmer6, "w6"), - "{file_name} has diverged between c-api-wasmer2 and c-api-wasmer6; \ + to_neutral_prefix(wasmer7, "w7"), + "{file_name} has diverged between c-api-wasmer2 and c-api-wasmer7; \ re-derive it instead of editing both", ); } @@ -30,7 +30,7 @@ fn executor_entry_points_match() { assert_parity( "capi_executor.rs", include_str!("../src/capi_executor.rs"), - include_str!("../../c-api-wasmer6/src/capi_executor.rs"), + include_str!("../../c-api-wasmer7/src/capi_executor.rs"), ); } @@ -39,7 +39,7 @@ fn instance_entry_points_match() { assert_parity( "capi_instance.rs", include_str!("../src/capi_instance.rs"), - include_str!("../../c-api-wasmer6/src/capi_instance.rs"), + include_str!("../../c-api-wasmer7/src/capi_instance.rs"), ); } @@ -48,6 +48,6 @@ fn instance_state_entry_points_match() { assert_parity( "capi_instance_state.rs", include_str!("../src/capi_instance_state.rs"), - include_str!("../../c-api-wasmer6/src/capi_instance_state.rs"), + include_str!("../../c-api-wasmer7/src/capi_instance_state.rs"), ); } diff --git a/c-api-wasmer6/Cargo.toml b/c-api-wasmer7/Cargo.toml similarity index 92% rename from c-api-wasmer6/Cargo.toml rename to c-api-wasmer7/Cargo.toml index bb915327..fd8f0edd 100644 --- a/c-api-wasmer6/Cargo.toml +++ b/c-api-wasmer7/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "multiversx-chain-vm-executor-c-api-wasmer6" +name = "multiversx-chain-vm-executor-c-api-wasmer7" version = "0.0.0" edition = "2024" publish = false diff --git a/c-api-wasmer6/build.rs b/c-api-wasmer7/build.rs similarity index 96% rename from c-api-wasmer6/build.rs rename to c-api-wasmer7/build.rs index 938962ea..f2719e3c 100644 --- a/c-api-wasmer6/build.rs +++ b/c-api-wasmer7/build.rs @@ -3,7 +3,7 @@ extern crate cbindgen; use cbindgen::{Builder, Config, Language, Style}; use std::{env, fs, path::PathBuf}; -const HEADER_FILE_NAME: &str = "libvmexeccapi_w6"; +const HEADER_FILE_NAME: &str = "libvmexeccapi_w7"; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); diff --git a/c-api-wasmer6/cbindgen.toml b/c-api-wasmer7/cbindgen.toml similarity index 100% rename from c-api-wasmer6/cbindgen.toml rename to c-api-wasmer7/cbindgen.toml diff --git a/c-api-wasmer6/libvmexeccapi_w6.h b/c-api-wasmer7/libvmexeccapi_w7.h similarity index 76% rename from c-api-wasmer6/libvmexeccapi_w6.h rename to c-api-wasmer7/libvmexeccapi_w7.h index 48e4aae5..60dd92ae 100644 --- a/c-api-wasmer6/libvmexeccapi_w6.h +++ b/c-api-wasmer7/libvmexeccapi_w7.h @@ -8,16 +8,16 @@ /** * Opaque handle to an executor. */ -typedef struct vm_exec_w6_executor_t { +typedef struct vm_exec_w7_executor_t { -} vm_exec_w6_executor_t; +} vm_exec_w7_executor_t; /** * Opaque handle to an instance. */ -typedef struct vm_exec_w6_instance_t { +typedef struct vm_exec_w7_instance_t { -} vm_exec_w6_instance_t; +} vm_exec_w7_instance_t; /** * Creates an executor from one configuration struct and one hook table. @@ -26,13 +26,13 @@ typedef struct vm_exec_w6_instance_t { * entry must be populated; see `CapiVMHookTable::copy_from`. * * On success writes the executor to `executor_out` and returns `VM_EXEC_OK`. The caller - * owns the result and must release it with `vm_exec_w6_executor_destroy`. + * owns the result and must release it with `vm_exec_w7_executor_destroy`. * * # Safety * * C API function, works with raw object pointers. */ -vm_exec_status_t vm_exec_w6_new_executor(struct vm_exec_w6_executor_t **executor_out, const vm_exec_executor_config_t *config, const vm_exec_vm_hook_c_func_pointers *hook_table, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w7_new_executor(struct vm_exec_w7_executor_t **executor_out, const vm_exec_executor_config_t *config, const vm_exec_vm_hook_c_func_pointers *hook_table, char *err_buf, int err_buf_len); /** * Destroys an executor. Does nothing if `executor` is null. @@ -44,7 +44,7 @@ vm_exec_status_t vm_exec_w6_new_executor(struct vm_exec_w6_executor_t **executor * * C API function, works with raw object pointers. */ -void vm_exec_w6_executor_destroy(struct vm_exec_w6_executor_t *executor); +void vm_exec_w7_executor_destroy(struct vm_exec_w7_executor_t *executor); /** * Creates an instance. @@ -58,7 +58,7 @@ void vm_exec_w6_executor_destroy(struct vm_exec_w6_executor_t *executor); * * C API function, works with raw object pointers. */ -vm_exec_status_t vm_exec_w6_new_instance(struct vm_exec_w6_executor_t *executor, struct vm_exec_w6_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const vm_exec_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w7_new_instance(struct vm_exec_w7_executor_t *executor, struct vm_exec_w7_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const vm_exec_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); /** * Destroys an instance. Does nothing if `instance` is null. @@ -67,7 +67,7 @@ vm_exec_status_t vm_exec_w6_new_instance(struct vm_exec_w6_executor_t *executor, * * C API function, works with raw object pointers. */ -void vm_exec_w6_instance_destroy(struct vm_exec_w6_instance_t *instance); +void vm_exec_w7_instance_destroy(struct vm_exec_w7_instance_t *instance); /** * Calls an exported function, with a gas limit for this call. @@ -80,7 +80,7 @@ void vm_exec_w6_instance_destroy(struct vm_exec_w6_instance_t *instance); * * C API function, works with raw object pointers. */ -vm_exec_status_t vm_exec_w6_instance_call(struct vm_exec_w6_instance_t *instance, const char *func_name, uint64_t points_limit, vm_exec_call_result_t *result_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w7_instance_call(struct vm_exec_w7_instance_t *instance, const char *func_name, uint64_t points_limit, vm_exec_call_result_t *result_out, char *err_buf, int err_buf_len); /** * Returns the number of points (gas) used so far. @@ -89,7 +89,7 @@ vm_exec_status_t vm_exec_w6_instance_call(struct vm_exec_w6_instance_t *instance * * C API function, works with raw object pointers. */ -vm_exec_status_t vm_exec_w6_instance_get_points_used(struct vm_exec_w6_instance_t *instance, uint64_t *points_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w7_instance_get_points_used(struct vm_exec_w7_instance_t *instance, uint64_t *points_out, char *err_buf, int err_buf_len); /** * Checks that every exported function takes no arguments and returns nothing. @@ -100,7 +100,7 @@ vm_exec_status_t vm_exec_w6_instance_get_points_used(struct vm_exec_w6_instance_ * * C API function, works with raw object pointers. */ -int vm_exec_w6_instance_check_signatures(const struct vm_exec_w6_instance_t *instance); +int vm_exec_w7_instance_check_signatures(const struct vm_exec_w7_instance_t *instance); /** * Checks whether the contract exports a function with the given name. @@ -111,16 +111,16 @@ int vm_exec_w6_instance_check_signatures(const struct vm_exec_w6_instance_t *ins * * C API function, works with raw object pointers. */ -int vm_exec_w6_instance_has_function(const struct vm_exec_w6_instance_t *instance, const char *func_name); +int vm_exec_w7_instance_has_function(const struct vm_exec_w7_instance_t *instance, const char *func_name); /** - * Buffer length needed by `vm_exec_w6_instance_exported_function_names`. + * Buffer length needed by `vm_exec_w7_instance_exported_function_names`. * * # Safety * * C API function, works with raw object pointers. */ -int vm_exec_w6_instance_exported_function_names_length(const struct vm_exec_w6_instance_t *instance); +int vm_exec_w7_instance_exported_function_names_length(const struct vm_exec_w7_instance_t *instance); /** * Writes the exported function names, pipe-separated, e.g. `"init|endpoint1|endpoint2"`. @@ -132,16 +132,16 @@ int vm_exec_w6_instance_exported_function_names_length(const struct vm_exec_w6_i * * C API function, works with raw object pointers. */ -int vm_exec_w6_instance_exported_function_names(const struct vm_exec_w6_instance_t *instance, char *dest, int dest_len); +int vm_exec_w7_instance_exported_function_names(const struct vm_exec_w7_instance_t *instance, char *dest, int dest_len); /** - * Buffer length needed by `vm_exec_w6_instance_imported_function_names`. + * Buffer length needed by `vm_exec_w7_instance_imported_function_names`. * * # Safety * * C API function, works with raw object pointers. */ -int vm_exec_w6_instance_imported_function_names_length(const struct vm_exec_w6_instance_t *instance); +int vm_exec_w7_instance_imported_function_names_length(const struct vm_exec_w7_instance_t *instance); /** * Writes the names of the VM hooks the contract imports, pipe-separated. @@ -153,7 +153,7 @@ int vm_exec_w6_instance_imported_function_names_length(const struct vm_exec_w6_i * * C API function, works with raw object pointers. */ -int vm_exec_w6_instance_imported_function_names(const struct vm_exec_w6_instance_t *instance, char *dest, int dest_len); +int vm_exec_w7_instance_imported_function_names(const struct vm_exec_w7_instance_t *instance, char *dest, int dest_len); /** * Loads from instance memory into a caller-provided buffer, **truncating** at the end of @@ -172,7 +172,7 @@ int vm_exec_w6_instance_imported_function_names(const struct vm_exec_w6_instance * * C API function, works with raw object pointers. */ -int64_t vm_exec_w6_state_mem_load(vm_exec_hook_context_t *ctx, int32_t mem_ptr, uint8_t *dest, int32_t dest_len, char *err_buf, int err_buf_len); +int64_t vm_exec_w7_state_mem_load(vm_exec_hook_context_t *ctx, int32_t mem_ptr, uint8_t *dest, int32_t dest_len, char *err_buf, int err_buf_len); /** * Writes to instance memory. The range must fit entirely inside the memory. @@ -181,7 +181,7 @@ int64_t vm_exec_w6_state_mem_load(vm_exec_hook_context_t *ctx, int32_t mem_ptr, * * C API function, works with raw object pointers. */ -vm_exec_status_t vm_exec_w6_state_mem_store(vm_exec_hook_context_t *ctx, int32_t mem_ptr, const uint8_t *src, int32_t src_len, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w7_state_mem_store(vm_exec_hook_context_t *ctx, int32_t mem_ptr, const uint8_t *src, int32_t src_len, char *err_buf, int err_buf_len); /** * Returns the size in bytes of the instance memory. @@ -190,7 +190,7 @@ vm_exec_status_t vm_exec_w6_state_mem_store(vm_exec_hook_context_t *ctx, int32_t * * C API function, works with raw object pointers. */ -vm_exec_status_t vm_exec_w6_state_mem_length(vm_exec_hook_context_t *ctx, uint64_t *length_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w7_state_mem_length(vm_exec_hook_context_t *ctx, uint64_t *length_out, char *err_buf, int err_buf_len); /** * Grows the instance memory by `by_num_pages` pages of 64Kb each. @@ -201,7 +201,7 @@ vm_exec_status_t vm_exec_w6_state_mem_length(vm_exec_hook_context_t *ctx, uint64 * * C API function, works with raw object pointers. */ -vm_exec_status_t vm_exec_w6_state_mem_grow(vm_exec_hook_context_t *ctx, uint32_t by_num_pages, uint32_t *prev_pages_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w7_state_mem_grow(vm_exec_hook_context_t *ctx, uint32_t by_num_pages, uint32_t *prev_pages_out, char *err_buf, int err_buf_len); /** * Returns the number of points (gas) used by the instance so far. @@ -210,7 +210,7 @@ vm_exec_status_t vm_exec_w6_state_mem_grow(vm_exec_hook_context_t *ctx, uint32_t * * C API function, works with raw object pointers. */ -vm_exec_status_t vm_exec_w6_state_get_points_used(vm_exec_hook_context_t *ctx, uint64_t *points_out, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w7_state_get_points_used(vm_exec_hook_context_t *ctx, uint64_t *points_out, char *err_buf, int err_buf_len); /** * Sets the number of points (gas) used by the instance. @@ -219,4 +219,4 @@ vm_exec_status_t vm_exec_w6_state_get_points_used(vm_exec_hook_context_t *ctx, u * * C API function, works with raw object pointers. */ -vm_exec_status_t vm_exec_w6_state_set_points_used(vm_exec_hook_context_t *ctx, uint64_t points, char *err_buf, int err_buf_len); +vm_exec_status_t vm_exec_w7_state_set_points_used(vm_exec_hook_context_t *ctx, uint64_t points, char *err_buf, int err_buf_len); diff --git a/c-api-wasmer6/src/capi_backend.rs b/c-api-wasmer7/src/capi_backend.rs similarity index 59% rename from c-api-wasmer6/src/capi_backend.rs rename to c-api-wasmer7/src/capi_backend.rs index 02a137ab..45c81b32 100644 --- a/c-api-wasmer6/src/capi_backend.rs +++ b/c-api-wasmer7/src/capi_backend.rs @@ -1,7 +1,7 @@ //! Which wasm runtime this library is for. //! //! The one file in this crate that names a backend, and the reason the three entry-point -//! files do not: they reach the backend only through this type, under a name that is the +//! files do not: they reach the backend only through this function, under a name that is the //! same in every backend crate. That is what keeps them identical to the Wasmer 2.2 copy //! apart from the symbol prefix — a property an integration test checks, so the two cannot //! drift. @@ -10,14 +10,9 @@ use multiversx_chain_vm_executor::Executor; use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; use std::sync::Arc; -use crate::capi_handles::CapiBackend; use crate::capi_vm_hook_context::CapiVMHooksBuilder; -/// Wasmer 6, through the experimental backend. -pub struct Backend; - -impl CapiBackend for Backend { - fn new_executor(runtime: Arc) -> Box { - Box::new(ExperimentalExecutor::new(runtime)) - } +/// Wasmer 7, through the experimental backend. +pub fn new_backend_executor(runtime: Arc) -> Box { + Box::new(ExperimentalExecutor::new(runtime)) } diff --git a/c-api-wasmer6/src/capi_executor.rs b/c-api-wasmer7/src/capi_executor.rs similarity index 86% rename from c-api-wasmer6/src/capi_executor.rs rename to c-api-wasmer7/src/capi_executor.rs index a0877b85..f8ffe867 100644 --- a/c-api-wasmer6/src/capi_executor.rs +++ b/c-api-wasmer7/src/capi_executor.rs @@ -7,7 +7,7 @@ //! //! Note that it does not yet wrap a `dyn Executor`. An executor is built around one runtime //! reference, for its whole life, and this API's runtime handle is still supplied per -//! instance at `vm_exec_w6_new_instance` — so the executor is built there, per instance, and +//! instance at `vm_exec_w7_new_instance` — so the executor is built there, per instance, and //! this struct holds the configuration it will be built from. The handle becomes //! per-executor, and the `dyn Executor` moves here, once the Go VM stops reaching wasm memory //! through `executor.Instance`; see `PLAN-go-ffi.md`. @@ -22,6 +22,7 @@ use libc::{c_char, c_int}; use meta::capi_safe_unwind; +use crate::capi_backend::new_backend_executor; use crate::capi_config::vm_exec_executor_config_t; use crate::capi_executor_ops; use crate::capi_handles::CapiExecutor; @@ -31,7 +32,7 @@ use crate::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; /// Opaque handle to an executor. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec_w6_executor_t; +pub struct vm_exec_w7_executor_t; /// Creates an executor from one configuration struct and one hook table. /// @@ -39,7 +40,7 @@ pub struct vm_exec_w6_executor_t; /// entry must be populated; see `CapiVMHookTable::copy_from`. /// /// On success writes the executor to `executor_out` and returns `VM_EXEC_OK`. The caller -/// owns the result and must release it with `vm_exec_w6_executor_destroy`. +/// owns the result and must release it with `vm_exec_w7_executor_destroy`. /// /// # Safety /// @@ -47,8 +48,8 @@ pub struct vm_exec_w6_executor_t; #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] -pub unsafe extern "C" fn vm_exec_w6_new_executor( - executor_out: *mut *mut vm_exec_w6_executor_t, +pub unsafe extern "C" fn vm_exec_w7_new_executor( + executor_out: *mut *mut vm_exec_w7_executor_t, config: *const vm_exec_executor_config_t, hook_table: *const vm_exec_vm_hook_c_func_pointers, err_buf: *mut c_char, @@ -59,6 +60,7 @@ pub unsafe extern "C" fn vm_exec_w6_new_executor( executor_out as *mut *mut CapiExecutor, config, hook_table, + new_backend_executor, err_buf, err_buf_len, ) @@ -75,6 +77,6 @@ pub unsafe extern "C" fn vm_exec_w6_new_executor( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -pub unsafe extern "C" fn vm_exec_w6_executor_destroy(executor: *mut vm_exec_w6_executor_t) { +pub unsafe extern "C" fn vm_exec_w7_executor_destroy(executor: *mut vm_exec_w7_executor_t) { unsafe { capi_executor_ops::executor_destroy(executor as *mut CapiExecutor) } } diff --git a/c-api-wasmer6/src/capi_instance.rs b/c-api-wasmer7/src/capi_instance.rs similarity index 83% rename from c-api-wasmer6/src/capi_instance.rs rename to c-api-wasmer7/src/capi_instance.rs index 951ae9da..747041db 100644 --- a/c-api-wasmer6/src/capi_instance.rs +++ b/c-api-wasmer7/src/capi_instance.rs @@ -12,10 +12,9 @@ use libc::{c_char, c_int}; use meta::capi_safe_unwind; -use crate::capi_backend::Backend; use crate::capi_call_result::vm_exec_call_result_t; use crate::capi_config::vm_exec_compilation_options_t; -use crate::capi_executor::vm_exec_w6_executor_t; +use crate::capi_executor::vm_exec_w7_executor_t; use crate::capi_handles::{CapiExecutor, CapiInstance}; use crate::capi_instance_ops; use crate::capi_status::vm_exec_status_t; @@ -23,7 +22,7 @@ use crate::capi_status::vm_exec_status_t; /// Opaque handle to an instance. #[allow(non_camel_case_types)] #[repr(C)] -pub struct vm_exec_w6_instance_t; +pub struct vm_exec_w7_instance_t; /// Creates an instance. /// @@ -39,9 +38,9 @@ pub struct vm_exec_w6_instance_t; #[allow(clippy::too_many_arguments)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] -pub unsafe extern "C" fn vm_exec_w6_new_instance( - executor: *mut vm_exec_w6_executor_t, - instance_out: *mut *mut vm_exec_w6_instance_t, +pub unsafe extern "C" fn vm_exec_w7_new_instance( + executor: *mut vm_exec_w7_executor_t, + instance_out: *mut *mut vm_exec_w7_instance_t, wasm_bytes: *const u8, wasm_bytes_len: u32, options: *const vm_exec_compilation_options_t, @@ -50,7 +49,7 @@ pub unsafe extern "C" fn vm_exec_w6_new_instance( err_buf_len: c_int, ) -> vm_exec_status_t { unsafe { - capi_instance_ops::new_instance::( + capi_instance_ops::new_instance( executor as *mut CapiExecutor, instance_out as *mut *mut CapiInstance, wasm_bytes, @@ -70,7 +69,7 @@ pub unsafe extern "C" fn vm_exec_w6_new_instance( /// C API function, works with raw object pointers. #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] -pub unsafe extern "C" fn vm_exec_w6_instance_destroy(instance: *mut vm_exec_w6_instance_t) { +pub unsafe extern "C" fn vm_exec_w7_instance_destroy(instance: *mut vm_exec_w7_instance_t) { unsafe { capi_instance_ops::instance_destroy(instance as *mut CapiInstance) } } @@ -86,8 +85,8 @@ pub unsafe extern "C" fn vm_exec_w6_instance_destroy(instance: *mut vm_exec_w6_i #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] -pub unsafe extern "C" fn vm_exec_w6_instance_call( - instance: *mut vm_exec_w6_instance_t, +pub unsafe extern "C" fn vm_exec_w7_instance_call( + instance: *mut vm_exec_w7_instance_t, func_name: *const c_char, points_limit: u64, result_out: *mut vm_exec_call_result_t, @@ -114,8 +113,8 @@ pub unsafe extern "C" fn vm_exec_w6_instance_call( #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] -pub unsafe extern "C" fn vm_exec_w6_instance_get_points_used( - instance: *mut vm_exec_w6_instance_t, +pub unsafe extern "C" fn vm_exec_w7_instance_get_points_used( + instance: *mut vm_exec_w7_instance_t, points_out: *mut u64, err_buf: *mut c_char, err_buf_len: c_int, @@ -140,8 +139,8 @@ pub unsafe extern "C" fn vm_exec_w6_instance_get_points_used( #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec_w6_instance_check_signatures( - instance: *const vm_exec_w6_instance_t, +pub unsafe extern "C" fn vm_exec_w7_instance_check_signatures( + instance: *const vm_exec_w7_instance_t, ) -> c_int { unsafe { capi_instance_ops::instance_check_signatures(instance as *const CapiInstance) } } @@ -156,14 +155,14 @@ pub unsafe extern "C" fn vm_exec_w6_instance_check_signatures( #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec_w6_instance_has_function( - instance: *const vm_exec_w6_instance_t, +pub unsafe extern "C" fn vm_exec_w7_instance_has_function( + instance: *const vm_exec_w7_instance_t, func_name: *const c_char, ) -> c_int { unsafe { capi_instance_ops::instance_has_function(instance as *const CapiInstance, func_name) } } -/// Buffer length needed by `vm_exec_w6_instance_exported_function_names`. +/// Buffer length needed by `vm_exec_w7_instance_exported_function_names`. /// /// # Safety /// @@ -171,8 +170,8 @@ pub unsafe extern "C" fn vm_exec_w6_instance_has_function( #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names_length( - instance: *const vm_exec_w6_instance_t, +pub unsafe extern "C" fn vm_exec_w7_instance_exported_function_names_length( + instance: *const vm_exec_w7_instance_t, ) -> c_int { unsafe { capi_instance_ops::instance_exported_function_names_length(instance as *const CapiInstance) @@ -190,8 +189,8 @@ pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names_length( #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names( - instance: *const vm_exec_w6_instance_t, +pub unsafe extern "C" fn vm_exec_w7_instance_exported_function_names( + instance: *const vm_exec_w7_instance_t, dest: *mut c_char, dest_len: c_int, ) -> c_int { @@ -204,7 +203,7 @@ pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names( } } -/// Buffer length needed by `vm_exec_w6_instance_imported_function_names`. +/// Buffer length needed by `vm_exec_w7_instance_imported_function_names`. /// /// # Safety /// @@ -212,8 +211,8 @@ pub unsafe extern "C" fn vm_exec_w6_instance_exported_function_names( #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names_length( - instance: *const vm_exec_w6_instance_t, +pub unsafe extern "C" fn vm_exec_w7_instance_imported_function_names_length( + instance: *const vm_exec_w7_instance_t, ) -> c_int { unsafe { capi_instance_ops::instance_imported_function_names_length(instance as *const CapiInstance) @@ -231,8 +230,8 @@ pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names_length( #[allow(clippy::cast_ptr_alignment)] #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_c_int())] -pub unsafe extern "C" fn vm_exec_w6_instance_imported_function_names( - instance: *const vm_exec_w6_instance_t, +pub unsafe extern "C" fn vm_exec_w7_instance_imported_function_names( + instance: *const vm_exec_w7_instance_t, dest: *mut c_char, dest_len: c_int, ) -> c_int { diff --git a/c-api-wasmer6/src/capi_instance_state.rs b/c-api-wasmer7/src/capi_instance_state.rs similarity index 93% rename from c-api-wasmer6/src/capi_instance_state.rs rename to c-api-wasmer7/src/capi_instance_state.rs index cc1309fd..5c0f98ee 100644 --- a/c-api-wasmer6/src/capi_instance_state.rs +++ b/c-api-wasmer7/src/capi_instance_state.rs @@ -37,7 +37,7 @@ use crate::capi_vm_hook_context::vm_exec_hook_context_t; /// C API function, works with raw object pointers. #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC.as_i64())] -pub unsafe extern "C" fn vm_exec_w6_state_mem_load( +pub unsafe extern "C" fn vm_exec_w7_state_mem_load( ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, dest: *mut u8, @@ -55,7 +55,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_load( /// C API function, works with raw object pointers. #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] -pub unsafe extern "C" fn vm_exec_w6_state_mem_store( +pub unsafe extern "C" fn vm_exec_w7_state_mem_store( ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, src: *const u8, @@ -73,7 +73,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_store( /// C API function, works with raw object pointers. #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] -pub unsafe extern "C" fn vm_exec_w6_state_mem_length( +pub unsafe extern "C" fn vm_exec_w7_state_mem_length( ctx: *mut vm_exec_hook_context_t, length_out: *mut u64, err_buf: *mut c_char, @@ -91,7 +91,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_length( /// C API function, works with raw object pointers. #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] -pub unsafe extern "C" fn vm_exec_w6_state_mem_grow( +pub unsafe extern "C" fn vm_exec_w7_state_mem_grow( ctx: *mut vm_exec_hook_context_t, by_num_pages: u32, prev_pages_out: *mut u32, @@ -110,7 +110,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_mem_grow( /// C API function, works with raw object pointers. #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] -pub unsafe extern "C" fn vm_exec_w6_state_get_points_used( +pub unsafe extern "C" fn vm_exec_w7_state_get_points_used( ctx: *mut vm_exec_hook_context_t, points_out: *mut u64, err_buf: *mut c_char, @@ -126,7 +126,7 @@ pub unsafe extern "C" fn vm_exec_w6_state_get_points_used( /// C API function, works with raw object pointers. #[unsafe(no_mangle)] #[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] -pub unsafe extern "C" fn vm_exec_w6_state_set_points_used( +pub unsafe extern "C" fn vm_exec_w7_state_set_points_used( ctx: *mut vm_exec_hook_context_t, points: u64, err_buf: *mut c_char, diff --git a/c-api-wasmer6/src/capi_logger.rs b/c-api-wasmer7/src/capi_logger.rs similarity index 100% rename from c-api-wasmer6/src/capi_logger.rs rename to c-api-wasmer7/src/capi_logger.rs diff --git a/c-api-wasmer6/src/lib.rs b/c-api-wasmer7/src/lib.rs similarity index 96% rename from c-api-wasmer6/src/lib.rs rename to c-api-wasmer7/src/lib.rs index eadf8f12..bffd0478 100644 --- a/c-api-wasmer6/src/lib.rs +++ b/c-api-wasmer7/src/lib.rs @@ -4,7 +4,7 @@ //! shipping while this one is brought up. The two differ in four ways that matter to a //! caller: //! -//! - **Exported symbols are prefixed `vm_exec_w6_`**, so both libraries can be linked into one +//! - **Exported symbols are prefixed `vm_exec_w7_`**, so both libraries can be linked into one //! binary. The legacy API already owns `vm_exec_new_executor` and friends. //! - **Errors travel through a caller-provided buffer**, not a thread-local last-error //! singleton. See `capi_status`. diff --git a/c-api-wasmer6/tests/ffi_boundary.rs b/c-api-wasmer7/tests/ffi_boundary.rs similarity index 88% rename from c-api-wasmer6/tests/ffi_boundary.rs rename to c-api-wasmer7/tests/ffi_boundary.rs index 4637ae21..17239f92 100644 --- a/c-api-wasmer6/tests/ffi_boundary.rs +++ b/c-api-wasmer7/tests/ffi_boundary.rs @@ -17,24 +17,24 @@ use std::sync::{Arc, Mutex, OnceLock}; use multiversx_chain_vm_executor::{ Executor, ExecutorRuntime, InstanceState, VMHooks, VMHooksDefault, }; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_call_result::{ +use multiversx_chain_vm_executor_c_api_wasmer7::capi_call_result::{ vm_exec_call_result_kind_t, vm_exec_call_result_t, }; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_config::{ +use multiversx_chain_vm_executor_c_api_wasmer7::capi_config::{ vm_exec_compilation_options_t, vm_exec_executor_config_t, }; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_executor::{ - vm_exec_w6_executor_destroy, vm_exec_w6_executor_t, vm_exec_w6_new_executor, +use multiversx_chain_vm_executor_c_api_wasmer7::capi_executor::{ + vm_exec_w7_executor_destroy, vm_exec_w7_executor_t, vm_exec_w7_new_executor, }; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_instance::*; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_instance_state::*; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_opcode_cost::vm_exec_opcode_cost_t; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_status::vm_exec_status_t; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_context::{ +use multiversx_chain_vm_executor_c_api_wasmer7::capi_instance::*; +use multiversx_chain_vm_executor_c_api_wasmer7::capi_instance_state::*; +use multiversx_chain_vm_executor_c_api_wasmer7::capi_opcode_cost::vm_exec_opcode_cost_t; +use multiversx_chain_vm_executor_c_api_wasmer7::capi_status::vm_exec_status_t; +use multiversx_chain_vm_executor_c_api_wasmer7::capi_vm_hook_context::{ CapiVMHooksBuilder, vm_exec_hook_context_t, vm_exec_hook_result_t, }; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; -use multiversx_chain_vm_executor_c_api_wasmer6::capi_vm_hook_table::CapiVMHookTable; +use multiversx_chain_vm_executor_c_api_wasmer7::capi_vm_hook_pointers::vm_exec_vm_hook_c_func_pointers; +use multiversx_chain_vm_executor_c_api_wasmer7::capi_vm_hook_table::CapiVMHookTable; use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; /// One memory page, five known bytes at offset 0, and endpoints that call VM hooks. @@ -135,7 +135,7 @@ fn load_through_context( ) -> Result, i64> { let mut buf = vec![0u8; len.max(0) as usize]; let loaded = unsafe { - vm_exec_w6_state_mem_load(ctx, mem_ptr, buf.as_mut_ptr(), len, std::ptr::null_mut(), 0) + vm_exec_w7_state_mem_load(ctx, mem_ptr, buf.as_mut_ptr(), len, std::ptr::null_mut(), 0) }; if loaded < 0 { return Err(loaded); @@ -158,7 +158,7 @@ extern "C" fn mock_get_gas_left(ctx: *mut vm_exec_hook_context_t) -> vm_exec_hoo } let mut length = 0u64; - let status = unsafe { vm_exec_w6_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; + let status = unsafe { vm_exec_w7_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; if status != vm_exec_status_t::VM_EXEC_OK || length != 65536 { log.failures .push(format!("mem_length in getGasLeft: {status:?}, {length}")); @@ -208,16 +208,16 @@ fn mock_hook_table() -> &'static vm_exec_vm_hook_c_func_pointers { // --- Driving the exported functions ------------------------------------------------------- -fn new_executor(err: &mut ErrBuf) -> *mut vm_exec_w6_executor_t { +fn new_executor(err: &mut ErrBuf) -> *mut vm_exec_w7_executor_t { let cost = opcode_cost(); let config = vm_exec_executor_config_t { opcode_version: 1, opcode_cost: &cost, }; - let mut executor: *mut vm_exec_w6_executor_t = std::ptr::null_mut(); + let mut executor: *mut vm_exec_w7_executor_t = std::ptr::null_mut(); let status = unsafe { - vm_exec_w6_new_executor( + vm_exec_w7_new_executor( &mut executor, &config, mock_hook_table(), @@ -237,16 +237,16 @@ fn new_executor(err: &mut ErrBuf) -> *mut vm_exec_w6_executor_t { } fn new_instance( - executor: *mut vm_exec_w6_executor_t, + executor: *mut vm_exec_w7_executor_t, err: &mut ErrBuf, -) -> *mut vm_exec_w6_instance_t { +) -> *mut vm_exec_w7_instance_t { let options = compilation_options(); let wasm = CONTRACT_WAT.as_bytes(); let go_vm_hooks = 0xBEEFusize; - let mut instance: *mut vm_exec_w6_instance_t = std::ptr::null_mut(); + let mut instance: *mut vm_exec_w7_instance_t = std::ptr::null_mut(); let status = unsafe { - vm_exec_w6_new_instance( + vm_exec_w7_new_instance( executor, &mut instance, wasm.as_ptr(), @@ -269,7 +269,7 @@ fn new_instance( } fn call( - instance: *mut vm_exec_w6_instance_t, + instance: *mut vm_exec_w7_instance_t, func_name: &str, err: &mut ErrBuf, ) -> vm_exec_call_result_t { @@ -281,7 +281,7 @@ fn call( }; let status = unsafe { - vm_exec_w6_instance_call( + vm_exec_w7_instance_call( instance, name.as_ptr(), 1_000_000, @@ -295,9 +295,9 @@ fn call( } fn names( - instance: *mut vm_exec_w6_instance_t, - length: unsafe extern "C" fn(*const vm_exec_w6_instance_t) -> c_int, - write: unsafe extern "C" fn(*const vm_exec_w6_instance_t, *mut c_char, c_int) -> c_int, + instance: *mut vm_exec_w7_instance_t, + length: unsafe extern "C" fn(*const vm_exec_w7_instance_t) -> c_int, + write: unsafe extern "C" fn(*const vm_exec_w7_instance_t, *mut c_char, c_int) -> c_int, ) -> Vec { let needed = unsafe { length(instance) }; assert!(needed > 0, "expected a non-empty name list"); @@ -338,29 +338,29 @@ fn lifecycle() { let mut points = u64::MAX; let status = unsafe { - vm_exec_w6_instance_get_points_used(instance, &mut points, err.ptr(), ERR_BUF_LEN) + vm_exec_w7_instance_get_points_used(instance, &mut points, err.ptr(), ERR_BUF_LEN) }; assert_eq!(status, vm_exec_status_t::VM_EXEC_OK); assert_ne!(points, u64::MAX, "points_used was not written"); - assert_eq!(unsafe { vm_exec_w6_instance_check_signatures(instance) }, 1); + assert_eq!(unsafe { vm_exec_w7_instance_check_signatures(instance) }, 1); let present = CString::new("doNothing").unwrap(); let absent = CString::new("noSuchEndpoint").unwrap(); assert_eq!( - unsafe { vm_exec_w6_instance_has_function(instance, present.as_ptr()) }, + unsafe { vm_exec_w7_instance_has_function(instance, present.as_ptr()) }, 1 ); assert_eq!( - unsafe { vm_exec_w6_instance_has_function(instance, absent.as_ptr()) }, + unsafe { vm_exec_w7_instance_has_function(instance, absent.as_ptr()) }, 0 ); assert_eq!( names( instance, - vm_exec_w6_instance_exported_function_names_length, - vm_exec_w6_instance_exported_function_names, + vm_exec_w7_instance_exported_function_names_length, + vm_exec_w7_instance_exported_function_names, ), vec![ "callGetGasLeft".to_string(), @@ -373,15 +373,15 @@ fn lifecycle() { assert_eq!( names( instance, - vm_exec_w6_instance_imported_function_names_length, - vm_exec_w6_instance_imported_function_names, + vm_exec_w7_instance_imported_function_names_length, + vm_exec_w7_instance_imported_function_names, ), vec!["getGasLeft".to_string(), "signalError".to_string()] ); unsafe { - vm_exec_w6_instance_destroy(instance); - vm_exec_w6_executor_destroy(executor); + vm_exec_w7_instance_destroy(instance); + vm_exec_w7_executor_destroy(executor); } } @@ -423,20 +423,20 @@ fn hook_dispatch() { drop(log); unsafe { - vm_exec_w6_instance_destroy(instance); - vm_exec_w6_executor_destroy(executor); + vm_exec_w7_instance_destroy(instance); + vm_exec_w7_executor_destroy(executor); } } #[test] fn null_arguments_are_refused_without_a_message() { let mut err = ErrBuf::new(); - let mut executor: *mut vm_exec_w6_executor_t = std::ptr::null_mut(); + let mut executor: *mut vm_exec_w7_executor_t = std::ptr::null_mut(); // A null config cannot be reported through the error buffer, because the buffer itself may // be the thing that is null, so these return a dedicated status instead. let status = unsafe { - vm_exec_w6_new_executor( + vm_exec_w7_new_executor( &mut executor, std::ptr::null(), std::ptr::null(), @@ -448,7 +448,7 @@ fn null_arguments_are_refused_without_a_message() { assert!(executor.is_null()); assert_eq!( - unsafe { vm_exec_w6_instance_check_signatures(std::ptr::null()) }, + unsafe { vm_exec_w7_instance_check_signatures(std::ptr::null()) }, vm_exec_status_t::VM_EXEC_NULL_POINTER.as_c_int() ); } @@ -462,9 +462,9 @@ fn an_invalid_opcode_version_is_reported_through_the_buffer() { opcode_cost: &cost, }; - let mut executor: *mut vm_exec_w6_executor_t = std::ptr::null_mut(); + let mut executor: *mut vm_exec_w7_executor_t = std::ptr::null_mut(); let status = unsafe { - vm_exec_w6_new_executor( + vm_exec_w7_new_executor( &mut executor, &config, mock_hook_table(), @@ -484,7 +484,7 @@ fn a_context_is_rejected_when_no_call_is_active() { assert_eq!( unsafe { - vm_exec_w6_state_mem_length(std::ptr::null_mut(), &mut 0u64, err.ptr(), ERR_BUF_LEN) + vm_exec_w7_state_mem_length(std::ptr::null_mut(), &mut 0u64, err.ptr(), ERR_BUF_LEN) }, vm_exec_status_t::VM_EXEC_NULL_POINTER ); @@ -492,7 +492,7 @@ fn a_context_is_rejected_when_no_call_is_active() { let mut points = 0u64; assert_eq!( unsafe { - vm_exec_w6_state_get_points_used( + vm_exec_w7_state_get_points_used( std::ptr::null_mut(), &mut points, err.ptr(), @@ -557,7 +557,7 @@ impl ExecutorRuntime for ProbingHooksBuilder { fn mem_load(ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, dest: &mut [u8], len: i32) -> i64 { unsafe { - vm_exec_w6_state_mem_load( + vm_exec_w7_state_mem_load( ctx, mem_ptr, dest.as_mut_ptr(), @@ -570,7 +570,7 @@ fn mem_load(ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, dest: &mut [u8], len fn mem_store(ctx: *mut vm_exec_hook_context_t, mem_ptr: i32, src: &[u8]) -> vm_exec_status_t { unsafe { - vm_exec_w6_state_mem_store( + vm_exec_w7_state_mem_store( ctx, mem_ptr, src.as_ptr(), @@ -585,7 +585,7 @@ fn probe_state_ops(ctx: *mut vm_exec_hook_context_t, outcome: &mut Outcome) { const PAGE: u64 = 65536; let mut length = 0u64; - let status = unsafe { vm_exec_w6_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; + let status = unsafe { vm_exec_w7_state_mem_length(ctx, &mut length, std::ptr::null_mut(), 0) }; outcome.check( "mem_length", status == vm_exec_status_t::VM_EXEC_OK && length == PAGE, @@ -662,10 +662,10 @@ fn probe_state_ops(ctx: *mut vm_exec_hook_context_t, outcome: &mut Outcome) { ); // Gas, through the state handle rather than the instance handle. - let status = unsafe { vm_exec_w6_state_set_points_used(ctx, 42, std::ptr::null_mut(), 0) }; + let status = unsafe { vm_exec_w7_state_set_points_used(ctx, 42, std::ptr::null_mut(), 0) }; let mut points = 0u64; let get_status = - unsafe { vm_exec_w6_state_get_points_used(ctx, &mut points, std::ptr::null_mut(), 0) }; + unsafe { vm_exec_w7_state_get_points_used(ctx, &mut points, std::ptr::null_mut(), 0) }; outcome.check( "points used round-trip", status == vm_exec_status_t::VM_EXEC_OK @@ -677,9 +677,9 @@ fn probe_state_ops(ctx: *mut vm_exec_hook_context_t, outcome: &mut Outcome) { // Growing reports the page count from before the growth. let mut prev_pages = 0u32; let status = - unsafe { vm_exec_w6_state_mem_grow(ctx, 1, &mut prev_pages, std::ptr::null_mut(), 0) }; + unsafe { vm_exec_w7_state_mem_grow(ctx, 1, &mut prev_pages, std::ptr::null_mut(), 0) }; let mut length_after = 0u64; - unsafe { vm_exec_w6_state_mem_length(ctx, &mut length_after, std::ptr::null_mut(), 0) }; + unsafe { vm_exec_w7_state_mem_length(ctx, &mut length_after, std::ptr::null_mut(), 0) }; outcome.check( "mem_grow", status == vm_exec_status_t::VM_EXEC_OK && prev_pages == 1 && length_after == 2 * PAGE, @@ -704,7 +704,7 @@ fn probe_nesting( let mut length = 0u64; let inner_status = - vm_exec_w6_state_mem_length(inner_ctx, &mut length, std::ptr::null_mut(), 0); + vm_exec_w7_state_mem_length(inner_ctx, &mut length, std::ptr::null_mut(), 0); outcome.check( "the innermost context works", inner_status == vm_exec_status_t::VM_EXEC_OK, @@ -712,7 +712,7 @@ fn probe_nesting( ); let outer_status = - vm_exec_w6_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); + vm_exec_w7_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); outcome.check( "an outer context is rejected while a nested call runs", outer_status == vm_exec_status_t::VM_EXEC_STALE_CONTEXT, @@ -722,7 +722,7 @@ fn probe_nesting( // ... and is usable again once the nested call has returned. let mut length = 0u64; - let status = vm_exec_w6_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); + let status = vm_exec_w7_state_mem_length(outer_ctx, &mut length, std::ptr::null_mut(), 0); outcome.check( "the outer context works again after the nested call", status == vm_exec_status_t::VM_EXEC_OK, @@ -776,7 +776,7 @@ fn state_ops() { let escaped = outcome.escaped_context as *mut vm_exec_hook_context_t; let mut length = 0u64; let status = - unsafe { vm_exec_w6_state_mem_length(escaped, &mut length, std::ptr::null_mut(), 0) }; + unsafe { vm_exec_w7_state_mem_length(escaped, &mut length, std::ptr::null_mut(), 0) }; assert_eq!( status, vm_exec_status_t::VM_EXEC_STALE_CONTEXT, diff --git a/vm-executor-experimental/README.md b/vm-executor-experimental/README.md index 6ee91582..f6650a5b 100644 --- a/vm-executor-experimental/README.md +++ b/vm-executor-experimental/README.md @@ -6,4 +6,4 @@ This adapter is currently only usable from the Rust VM, via a new set of traits. The plan is to stabilize this implementation enough, so that it can be used in production. This implementation is intended to eventually replace the one in `multiversx-chain-vm-executor-wasmer`. -Wasmer 6.0 is used in this implementation, and is not forked. However, several of the fixes used in production were not yet applied to this version. It remains to be seen if the official Wasmer implementation will remain in use, or if another fork will be necessary. +Wasmer 7.0 is used in this implementation, and is not forked. However, several of the fixes used in production were not yet applied to this version. It remains to be seen if the official Wasmer implementation will remain in use, or if another fork will be necessary. diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs index e905423f..5d4bcd0a 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs @@ -41,9 +41,9 @@ impl Instance for WasmerProdInstance { // Under the legacy interface this was the *host's* job — the Go VM did it in // `runtimeContext.UseWarmInstance`, and the Rust VM never reused an instance at all. // The new interface does not expose the global, deliberately: it is 2.2's private - // mechanism for stopping a contract, and Wasmer 6 has nothing like it. So the only + // mechanism for stopping a contract, and Wasmer 7 has nothing like it. So the only // side that can still do this is the executor, and not doing it would make an - // instance single-use after any early exit — a divergence from Wasmer 6, which has no + // instance single-use after any early exit — a divergence from Wasmer 7, which has no // such state to leave behind. if let Err(err) = self .inner_instance_ref diff --git a/vm-executor/src/new_traits/executor_runtime.rs b/vm-executor/src/new_traits/executor_runtime.rs index 2fea5327..ea7f7e28 100644 --- a/vm-executor/src/new_traits/executor_runtime.rs +++ b/vm-executor/src/new_traits/executor_runtime.rs @@ -12,7 +12,7 @@ pub trait ExecutorRuntime: Send + Sync { /// Builds the VM hooks servicing one host call, around the instance state for that call, /// and lends them to `f` — which implementations must call exactly once. /// - /// **Built per call, not per instance.** Under Wasmer 6 the instance state is only + /// **Built per call, not per instance.** Under Wasmer 7 the instance state is only /// reachable for the duration of a call — memory and globals live behind the `StoreMut` /// the host function is handed — so hooks that hold it cannot outlive the call and /// cannot be returned to the caller. Lending rather than returning also avoids a diff --git a/vm-executor/src/new_traits/instance_state.rs b/vm-executor/src/new_traits/instance_state.rs index 2e3762dd..ea4d8c20 100644 --- a/vm-executor/src/new_traits/instance_state.rs +++ b/vm-executor/src/new_traits/instance_state.rs @@ -70,7 +70,7 @@ pub trait InstanceState { /// **Only the Wasmer 2.2 backend implements this**, and only because its hooks return /// plain values: a [`crate::VMHooksLegacy`] method has no channel for an early exit, so /// the exit has to be put somewhere and the instance stopped by tripping its breakpoint - /// global. Wasmer 6 hooks return the early exit directly and never come through here. + /// global. Wasmer 7 hooks return the early exit directly and never come through here. /// /// Both halves belong to the instance — it owns the stash and it owns the global — which /// is why they are one method on the state rather than two capabilities a backend has to diff --git a/vm-executor/src/new_traits/vm_hooks_early_exit.rs b/vm-executor/src/new_traits/vm_hooks_early_exit.rs index e527a008..f51e9ad4 100644 --- a/vm-executor/src/new_traits/vm_hooks_early_exit.rs +++ b/vm-executor/src/new_traits/vm_hooks_early_exit.rs @@ -44,7 +44,7 @@ impl VMHooksEarlyExit { /// /// **Only the legacy backend asks.** Its hooks return plain values, with no channel for /// an early exit, so the instance has to be stopped by tripping its breakpoint global. - /// Wasmer 6 hooks return the early exit directly and never come here. + /// Wasmer 7 hooks return the early exit directly and never come here. /// /// The codes are the high-level VM's own return codes, and for a while this looked like /// something only a host could decide. It is not: the Go VM and the Rust VM use the same diff --git a/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs b/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs index 319ff7fb..bf27b874 100644 --- a/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs +++ b/vm-executor/src/new_traits/vm_hooks_legacy_adapter.rs @@ -19,7 +19,7 @@ use crate::{ /// life, while a runtime lends hooks per call. This is the join: it holds the runtime and the /// state of one instance, and on every hook call asks the runtime for hooks around that /// state. So the execution a hook serves is resolved when the hook runs, exactly as on -/// Wasmer 6, and not captured when the instance was created. +/// Wasmer 7, and not captured when the instance was created. /// /// The state is cloned per call because the legacy hooks are reached through a shared /// reference. Backends make it a handle to the instance, so the clone is cheap. From d399e20efa8b59f306f93158a35b72886fe5b2e4 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 12 Oct 2026 23:38:22 +0300 Subject: [PATCH 24/26] new instance from cache: FFI, wasmer 2 adapter, wasmer 7 --- PLAN-go-ffi.md | 65 +++++- PLAN-rust-vm-caching.md | 35 ++- c-api-common/src/capi_handles.rs | 21 ++ c-api-common/src/capi_instance_ops.rs | 178 ++++++++++++++- c-api-wasmer2/libvmexeccapi_w2.h | 42 ++++ c-api-wasmer2/src/capi_instance.rs | 98 +++++++++ c-api-wasmer2/tests/ffi_boundary.rs | 187 ++++++++++++++++ c-api-wasmer7/libvmexeccapi_w7.h | 42 ++++ c-api-wasmer7/src/capi_instance.rs | 98 +++++++++ c-api-wasmer7/tests/ffi_boundary.rs | 187 ++++++++++++++++ vm-executor-experimental/src/we_error.rs | 4 +- vm-executor-experimental/src/we_executor.rs | 16 +- vm-executor-experimental/src/we_instance.rs | 75 ++++++- vm-executor-experimental/tests/cache_test.rs | 208 ++++++++++++++++++ .../src/new_traits/wasmer_error.rs | 4 +- .../src/new_traits/wasmer_prod_executor.rs | 69 ++++-- vm-executor-wasmer/tests/cache_test.rs | 121 ++++++++++ 17 files changed, 1395 insertions(+), 55 deletions(-) create mode 100644 vm-executor-experimental/tests/cache_test.rs create mode 100644 vm-executor-wasmer/tests/cache_test.rs diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index b8a231fd..9ff76898 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -1188,9 +1188,68 @@ route for the Go VM is for `Wasmer7Executor` to hold compiled modules in memory recreating. `NewInstanceFromCompiledCodeWithOptions` is already the Wasmer-6-compatible form of reuse. -Remaining here: the serialized-artifact path. Note that artifacts carry an ABI version that -`deserialize` rejects on mismatch — it moved from 11 (Wasmer 7.1) to 24 (Wasmer 7.4) — so a -persisted cache needs the Wasmer version in its key or a purge on upgrade. +### Done: the serialized-artifact path, on both new backends + +`Cache()` and `NewInstanceFromCompiledCodeWithOptions` work, so the VM's compiled-code cache +is live for `wasmer7` and `wasmer2` as well as `wasmer2legacy`, and +`instance caching is not supported by this executor` is gone from the logs. + +Three new C entry points per library, bodies shared in `c-api-common` as usual: +`new_instance_from_cache`, and `instance_cache_length` / `instance_cache`. Reading the +artifact out is the same two-call shape as the function-name readers, with one difference +worth knowing: the **length call does the work**. An artifact is a few hundred KB, so +serializing it twice to answer the same question is not worth the symmetry; the length call +produces it and leaves it on the `CapiInstance` for the copy call. Calling the copy without +the length is an error rather than an empty result, and a short buffer is refused rather than +part-filled. + +On the Rust side this needed only a split of compile-from-instantiate inside +`ExperimentalInstance` (stage 1 of `PLAN-rust-vm-caching.md`, in the small): `new_store` is +shared, and the two entry points differ only in `Module::new` against `Module::deserialize`. +The 2.2 backend already had `try_new_instance_from_cache` for the legacy executor, so +`WasmerProdExecutor` gained the same shape and the two are now at parity. + +**`Module::deserialize` does not validate.** In Wasmer 7.5 it still delegates to +`deserialize_unchecked`, so it maps whatever it is handed as executable code. That is fine for +the VM, whose artifacts come from its own store keyed by code hash, but it is said in the doc +comment on every layer — Rust, C and Go — because the name suggests otherwise. Artifacts also +carry an ABI version that `deserialize` rejects on mismatch (11 in Wasmer 7.1, 24 in 7.4), so +an upgrade invalidates a persisted cache loudly rather than silently; a persistent cache still +needs the Wasmer version in its key, or a purge on upgrade, or every entry will fail this way. +A caller that gets an error here falls back to compiling, so a stale or corrupt entry is a +recoverable miss. + +**Still not here:** the in-executor module cache (stage 2), which is a separate optimization — +pure memoization on `(hash(wasm), hash(options))`, with no host semantics — and `reset()`, +for the reasons above. + +### What the tests said, and a gap they did not cover + +Adding caching changed no failure counts at all: `wasmer2legacy` 0, `wasmer2` 28, `wasmer7` 32, +before and after. That is not reassuring, it is the finding — **nothing in the suite was +testing caching**. `TestExecution_CachingCompiledCode` calls a contract four times and checks +four successes, which is exactly what happens when caching does not work, and it passed +throughout the period when `Cache()` returned an error. + +So the coverage was added with the feature, at each layer that can fail on its own: + +- Rust, per backend (`cache_test.rs` in both backend crates): the artifact round-trips, and + **metering is identical across it**. That is the one that matters — the middlewares are + compile-time and nothing re-runs them on the way back, so a deserialization that lost the + metering globals would make contracts run free rather than fail, and every other assertion + would still pass. +- C API, per library (`ffi_boundary.rs`): the round trip through the real entry points, plus + the two-call protocol's failure modes — copy without length, short buffer, non-artifact + bytes. +- Go, per package (`cache7_test.go`, `cache2_test.go`): the cgo marshalling. This is the only + place a variable-length *binary* result crosses the boundary, so an off-by-one in the + length/copy pair has nothing else to catch it. +- Go, host level: `TestExecution_CachingCompiledCode_SavesAnArtifact` asserts the store is + populated with something that is not the bytecode, and + `TestExecution_CachingCompiledCode_UsesTheArtifact` proves the artifact is *read* by + swapping in a valid artifact for a different contract and requiring the call to come back + `FunctionNotFound`. Verified to bite: disabling the cached path fails it with that message + while the original test still passes. ## Cross-repo note — `mx-sdk-rs` diff --git a/PLAN-rust-vm-caching.md b/PLAN-rust-vm-caching.md index 4dff3cc5..5c8f961e 100644 --- a/PLAN-rust-vm-caching.md +++ b/PLAN-rust-vm-caching.md @@ -277,10 +277,33 @@ gets it without writing any cache code. *Decision deferred:* the Go VM already has a code hash and would be paying that 14 µs twice. If it shows up, add an optional caller-supplied key. Not worth the extra parameter up front. -### 1.3 The serialized-artifact path - -`cache()` already works (`module.serialize()`). Finish the other half: `new_instance_from_cache` -should do `Module::deserialize(&engine, bytes)` against an engine built from the same options. +### 1.3 The serialized-artifact path — **done** + +`cache()` already worked (`module.serialize()`); `new_instance_from_cache` now does +`Module::deserialize(&store, bytes)` against a store built from the same options, on both +backends. Shipped ahead of 1.2 rather than after it, because the Go VM's compiled-code cache +was blocked on exactly this and the module cache is an independent optimization — see +`PLAN-go-ffi.md`, Phase 6, for the C and Go halves and the tests. + +What the implementation added to the sketch below: + +- **1.1 arrived with it, in the small.** The split had to happen to have somewhere to + deserialize *into*: `new_store` builds the store, and the two entry points differ only in + `Module::new` against `Module::deserialize`. What did not arrive is the public + `CompiledModule` trait and `Executor::compile`/`instantiate` — nothing needs them until the + module cache does, and inventing the trait first would have fixed its shape before the cache + could argue about it. +- **The middlewares are still pushed on the deserialize path**, which does nothing to the + module — it already carries its instrumentation — but keeps the two paths building the store + identically. Worth knowing when 1.2 lands: on that path the compiler config is dead weight, + and a module cache that keeps its engine will want to skip it. +- **The metering globals survive because they are reached by name**, not by the middleware's + stored `GlobalIndex`. `get_global_value_u64(instance, store, METERING_POINTS_USED)` reads + the instance's exports, and the globals are created *and exported* during compilation, so + they are in the artifact. Had the runtime path used the stored indexes instead, deserialized + modules would have run unmetered — which is why the round-trip metering test exists. +- Confirmed against Wasmer **7.5**: `deserialize` still delegates to `deserialize_unchecked`, + so the note below stands as written. **Security note to carry into the doc comment:** in Wasmer 7.1, `Module::deserialize` delegates to `deserialize_unchecked` — the "checked" variant performs no validation. Artifacts must come @@ -341,11 +364,11 @@ supertrait, no escape hatch — which was the weak point of the rejected per-ins | Stage | Work | Depends on | | --- | --- | --- | | 0 | ~~`ExecutorRuntime` on the executor; `OpcodeConfig` folded into `CompilationOptions`; `new_instance(wasm, options)`~~ — **done**, see below | — | -| 1 | Executor: split compile from instantiate; `CompiledModule` | 0 | +| 1 | Executor: split compile from instantiate; `CompiledModule` — the split is done, the public trait is not (see 1.3) | 0 | | 2 | Executor: module cache, keyed on wasm + options | 1 | | 3 | Rust VM: context stack, `impl ExecutorRuntime for RuntimeWeakRef`, delete the slot | 0 | | 4 | Measure on a scenario suite | 2, 3 | -| 5 | Serialized-artifact path (`new_instance_from_cache`), keyed including the Wasmer version | 1 | +| 5 | ~~Serialized-artifact path (`new_instance_from_cache`)~~ — **done**, ahead of 2–4, because the Go VM's compiled-code cache was blocked on it | 1 | Stages 1–2 and 3 are independent once 0 lands. diff --git a/c-api-common/src/capi_handles.rs b/c-api-common/src/capi_handles.rs index 95bbde56..0487359d 100644 --- a/c-api-common/src/capi_handles.rs +++ b/c-api-common/src/capi_handles.rs @@ -37,6 +37,27 @@ pub struct CapiExecutor { /// What a `vm_exec__instance_t` points to. pub struct CapiInstance { pub content: Box, + + /// The serialized module, held between the two halves of the cache call. + /// + /// Reading a variable-length result out of this API is a length call followed by a copy + /// call, the same shape the function-name readers use. For names that costs a cheap + /// recomputation; for an artifact it would mean serializing a few hundred KB twice, so + /// the length call produces it and leaves it here for the copy call to read. + /// + /// It lives until the instance is destroyed, or until a later length call replaces it. + /// Nothing invalidates it in between, because the module an instance was built from does + /// not change — only the instance's own memory and globals do, and those are not in it. + pub cached_artifact: Option>, +} + +impl CapiInstance { + pub fn new(content: Box) -> Self { + CapiInstance { + content, + cached_artifact: None, + } + } } /// Builds the backend's executor around the runtime the C API supplies. diff --git a/c-api-common/src/capi_instance_ops.rs b/c-api-common/src/capi_instance_ops.rs index f99e3e83..c7ee3366 100644 --- a/c-api-common/src/capi_instance_ops.rs +++ b/c-api-common/src/capi_instance_ops.rs @@ -9,9 +9,10 @@ //! `dyn Instance`. use libc::{c_char, c_int}; +use multiversx_chain_vm_executor::{CompilationOptions, Executor, ExecutorError, Instance}; use std::ffi::CStr; -use std::slice; use std::sync::Arc; +use std::{ptr, slice}; use crate::capi_call_result::vm_exec_call_result_t; use crate::capi_config::vm_exec_compilation_options_t; @@ -37,12 +38,90 @@ pub unsafe fn new_instance( err_buf: *mut c_char, err_buf_len: c_int, ) -> vm_exec_status_t { - if executor.is_null() || instance_out.is_null() || wasm_bytes.is_null() || options.is_null() { + unsafe { + build_instance( + executor, + instance_out, + wasm_bytes, + wasm_bytes_len, + options, + go_vm_hooks, + err_buf, + err_buf_len, + |backend, bytes, options| backend.new_instance(bytes, options), + ) + } +} + +/// Creates an instance from a previously cached artifact, skipping compilation. +/// +/// `cache_bytes` is what [`instance_cache`] produced, for the same contract and the same +/// backend. It is **executable code, not a module to validate**: see +/// `ExperimentalInstance::try_new_instance_from_cache` for what the backend does and does not +/// check. A caller that cannot vouch for the artifact's origin must use [`new_instance`] on +/// the wasm instead. +/// +/// An artifact from a different Wasmer version is rejected rather than misread — its header +/// carries an ABI version — so an upgrade shows up as an error here, not as a wrong answer. +/// +/// # Safety +/// +/// All pointer arguments must be valid for the duration of the call, and whatever +/// `go_vm_hooks` identifies must outlive the instance. +#[allow(clippy::too_many_arguments)] +pub unsafe fn new_instance_from_cache( + executor: *mut CapiExecutor, + instance_out: *mut *mut CapiInstance, + cache_bytes: *const u8, + cache_bytes_len: u32, + options: *const vm_exec_compilation_options_t, + go_vm_hooks: usize, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + build_instance( + executor, + instance_out, + cache_bytes, + cache_bytes_len, + options, + go_vm_hooks, + err_buf, + err_buf_len, + |backend, bytes, options| backend.new_instance_from_cache(bytes, options), + ) + } +} + +/// The half the two creation entry points share: everything except which `Executor` method +/// turns the input bytes into an instance. +/// +/// # Safety +/// +/// As [`new_instance`]. +#[allow(clippy::too_many_arguments)] +unsafe fn build_instance( + executor: *mut CapiExecutor, + instance_out: *mut *mut CapiInstance, + bytes: *const u8, + bytes_len: u32, + options: *const vm_exec_compilation_options_t, + go_vm_hooks: usize, + err_buf: *mut c_char, + err_buf_len: c_int, + build: impl FnOnce( + &dyn Executor, + &[u8], + &CompilationOptions, + ) -> Result, ExecutorError>, +) -> vm_exec_status_t { + if executor.is_null() || instance_out.is_null() || bytes.is_null() || options.is_null() { return vm_exec_status_t::VM_EXEC_NULL_POINTER; } let capi_executor = unsafe { &*executor }; - let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_len as usize) }; + let bytes = unsafe { slice::from_raw_parts(bytes, bytes_len as usize) }; let compilation_options = unsafe { (*options).to_compilation_options(capi_executor.opcode_config.clone()) }; @@ -58,13 +137,102 @@ pub unsafe fn new_instance( )); let backend_executor = (capi_executor.new_backend_executor)(runtime); - let instance = match backend_executor.new_instance(wasm_bytes, &compilation_options) { + let instance = match build(backend_executor.as_ref(), bytes, &compilation_options) { Ok(instance) => instance, Err(err) => return write_error(err, err_buf, err_buf_len), }; unsafe { - *instance_out = Box::into_raw(Box::new(CapiInstance { content: instance })); + *instance_out = Box::into_raw(Box::new(CapiInstance::new(instance))); + } + + vm_exec_status_t::VM_EXEC_OK +} + +/// Serializes the instance's module and reports the buffer size [`instance_cache`] needs. +/// +/// This is the first half of the usual two-call read, but it does the work rather than just +/// measuring it: the artifact is produced here and held on the instance for the copy call, +/// because serializing a few hundred KB twice to answer the same question is not worth the +/// symmetry with the function-name readers. The artifact stays valid until the instance is +/// destroyed or another length call replaces it. +/// +/// # Safety +/// +/// All pointer arguments must be valid for the duration of the call. +pub unsafe fn instance_cache_length( + instance: *mut CapiInstance, + length_out: *mut u32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if instance.is_null() || length_out.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let capi_instance = unsafe { &mut *instance }; + + let artifact = match capi_instance.content.cache() { + Ok(artifact) => artifact, + Err(err) => return write_error(err, err_buf, err_buf_len), + }; + + unsafe { + *length_out = artifact.len() as u32; + } + capi_instance.cached_artifact = Some(artifact); + + vm_exec_status_t::VM_EXEC_OK +} + +/// Copies the artifact produced by the preceding [`instance_cache_length`] call. +/// +/// Calling this without a preceding length call is an error rather than a silent empty +/// result, because the alternative — serializing here too — would make the cost of the pair +/// depend on the order the caller happened to use. +/// +/// `dest_len` must be at least the length that call reported; a short buffer is refused, not +/// truncated, since half an artifact would deserialize to nothing useful and the caller has +/// no way to notice. +/// +/// # Safety +/// +/// `instance` must be valid, and `dest` must be writable for `dest_len` bytes. +pub unsafe fn instance_cache( + instance: *mut CapiInstance, + dest: *mut u8, + dest_len: u32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if instance.is_null() || dest.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let capi_instance = unsafe { &mut *instance }; + + let Some(artifact) = capi_instance.cached_artifact.as_ref() else { + return write_error( + "no cached artifact; call the cache length function first", + err_buf, + err_buf_len, + ); + }; + + if (dest_len as usize) < artifact.len() { + return write_error( + format!( + "destination buffer of {} bytes is too small for a {}-byte artifact", + dest_len, + artifact.len() + ), + err_buf, + err_buf_len, + ); + } + + unsafe { + ptr::copy_nonoverlapping(artifact.as_ptr(), dest, artifact.len()); } vm_exec_status_t::VM_EXEC_OK diff --git a/c-api-wasmer2/libvmexeccapi_w2.h b/c-api-wasmer2/libvmexeccapi_w2.h index d83e71b8..5ecc0ed7 100644 --- a/c-api-wasmer2/libvmexeccapi_w2.h +++ b/c-api-wasmer2/libvmexeccapi_w2.h @@ -60,6 +60,22 @@ void vm_exec_w2_executor_destroy(struct vm_exec_w2_executor_t *executor); */ vm_exec_status_t vm_exec_w2_new_instance(struct vm_exec_w2_executor_t *executor, struct vm_exec_w2_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const vm_exec_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); +/** + * Creates an instance from a cached artifact, skipping compilation. + * + * `cache_bytes` is what `vm_exec_w2_instance_cache` produced, from this same library. It is + * **executable code that is mapped, not validated** — an artifact from an untrusted source + * must not be passed here; compile the wasm instead. An artifact from a different Wasmer + * version is refused, because its header carries an ABI version. + * + * `go_vm_hooks` is as in `vm_exec_w2_new_instance`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_new_instance_from_cache(struct vm_exec_w2_executor_t *executor, struct vm_exec_w2_instance_t **instance_out, const uint8_t *cache_bytes, uint32_t cache_bytes_len, const vm_exec_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); + /** * Destroys an instance. Does nothing if `instance` is null. * @@ -155,6 +171,32 @@ int vm_exec_w2_instance_imported_function_names_length(const struct vm_exec_w2_i */ int vm_exec_w2_instance_imported_function_names(const struct vm_exec_w2_instance_t *instance, char *dest, int dest_len); +/** + * Serializes the instance's module and reports how many bytes `vm_exec_w2_instance_cache` + * will write. + * + * The artifact is produced by this call and held until the copy call reads it, so the pair + * serializes once rather than twice. Call this first; see the copy call. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_instance_cache_length(struct vm_exec_w2_instance_t *instance, uint32_t *length_out, char *err_buf, int err_buf_len); + +/** + * Copies the artifact produced by the preceding `vm_exec_w2_instance_cache_length` call. + * + * `dest_len` must be at least the length that call reported. A short buffer is refused + * rather than filled partially, because a truncated artifact is indistinguishable from a + * whole one until it fails to deserialize. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_instance_cache(struct vm_exec_w2_instance_t *instance, uint8_t *dest, uint32_t dest_len, char *err_buf, int err_buf_len); + /** * Loads from instance memory into a caller-provided buffer, **truncating** at the end of * the memory. diff --git a/c-api-wasmer2/src/capi_instance.rs b/c-api-wasmer2/src/capi_instance.rs index 8af8b027..fe13192f 100644 --- a/c-api-wasmer2/src/capi_instance.rs +++ b/c-api-wasmer2/src/capi_instance.rs @@ -62,6 +62,46 @@ pub unsafe extern "C" fn vm_exec_w2_new_instance( } } +/// Creates an instance from a cached artifact, skipping compilation. +/// +/// `cache_bytes` is what `vm_exec_w2_instance_cache` produced, from this same library. It is +/// **executable code that is mapped, not validated** — an artifact from an untrusted source +/// must not be passed here; compile the wasm instead. An artifact from a different Wasmer +/// version is refused, because its header carries an ABI version. +/// +/// `go_vm_hooks` is as in `vm_exec_w2_new_instance`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[allow(clippy::too_many_arguments)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_new_instance_from_cache( + executor: *mut vm_exec_w2_executor_t, + instance_out: *mut *mut vm_exec_w2_instance_t, + cache_bytes: *const u8, + cache_bytes_len: u32, + options: *const vm_exec_compilation_options_t, + go_vm_hooks: usize, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::new_instance_from_cache( + executor as *mut CapiExecutor, + instance_out as *mut *mut CapiInstance, + cache_bytes, + cache_bytes_len, + options, + go_vm_hooks, + err_buf, + err_buf_len, + ) + } +} + /// Destroys an instance. Does nothing if `instance` is null. /// /// # Safety @@ -243,3 +283,61 @@ pub unsafe extern "C" fn vm_exec_w2_instance_imported_function_names( ) } } + +/// Serializes the instance's module and reports how many bytes `vm_exec_w2_instance_cache` +/// will write. +/// +/// The artifact is produced by this call and held until the copy call reads it, so the pair +/// serializes once rather than twice. Call this first; see the copy call. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_instance_cache_length( + instance: *mut vm_exec_w2_instance_t, + length_out: *mut u32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::instance_cache_length( + instance as *mut CapiInstance, + length_out, + err_buf, + err_buf_len, + ) + } +} + +/// Copies the artifact produced by the preceding `vm_exec_w2_instance_cache_length` call. +/// +/// `dest_len` must be at least the length that call reported. A short buffer is refused +/// rather than filled partially, because a truncated artifact is indistinguishable from a +/// whole one until it fails to deserialize. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_instance_cache( + instance: *mut vm_exec_w2_instance_t, + dest: *mut u8, + dest_len: u32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::instance_cache( + instance as *mut CapiInstance, + dest, + dest_len, + err_buf, + err_buf_len, + ) + } +} diff --git a/c-api-wasmer2/tests/ffi_boundary.rs b/c-api-wasmer2/tests/ffi_boundary.rs index 0c4f0ecf..c046e2ae 100644 --- a/c-api-wasmer2/tests/ffi_boundary.rs +++ b/c-api-wasmer2/tests/ffi_boundary.rs @@ -783,3 +783,190 @@ fn state_ops() { "a context used after its call must be rejected" ); } + +/// Reads an artifact out through the two-call cache convention. +fn read_artifact(instance: *mut vm_exec_w2_instance_t, err: &mut ErrBuf) -> Vec { + let mut needed: u32 = 0; + let status = + unsafe { vm_exec_w2_instance_cache_length(instance, &mut needed, err.ptr(), ERR_BUF_LEN) }; + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "cache_length failed: {}", + err.message() + ); + assert!(needed > 0, "an artifact cannot be empty"); + + let mut buf = vec![0u8; needed as usize]; + let status = unsafe { + vm_exec_w2_instance_cache(instance, buf.as_mut_ptr(), needed, err.ptr(), ERR_BUF_LEN) + }; + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "cache failed: {}", + err.message() + ); + + buf +} + +/// The serialized-artifact path across the boundary: out through the two-call read, back in +/// through `new_instance_from_cache`, and the restored instance is a working instance. +#[test] +fn cache_round_trip() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + let artifact = read_artifact(instance, &mut err); + assert_ne!( + artifact.as_slice(), + CONTRACT_WAT.as_bytes(), + "the artifact must be the compiled module, not the source" + ); + + let options = compilation_options(); + let mut restored: *mut vm_exec_w2_instance_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec_w2_new_instance_from_cache( + executor, + &mut restored, + artifact.as_ptr(), + artifact.len() as u32, + &options, + 0xBEEFusize, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "new_instance_from_cache failed: {}", + err.message() + ); + assert!(!restored.is_null()); + + // The restored instance runs, and its hooks still reach the host: `callGetGasLeft` goes + // out through the mock table, which a module that lost its imports could not do. + let result = call(restored, "doNothing", &mut err); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); + let result = call(restored, "callGetGasLeft", &mut err); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); + + assert_eq!( + names( + restored, + vm_exec_w2_instance_exported_function_names_length, + vm_exec_w2_instance_exported_function_names, + ), + names( + instance, + vm_exec_w2_instance_exported_function_names_length, + vm_exec_w2_instance_exported_function_names, + ), + ); + + unsafe { + vm_exec_w2_instance_destroy(restored); + vm_exec_w2_instance_destroy(instance); + vm_exec_w2_executor_destroy(executor); + } +} + +/// The copy call depends on the length call having run, and says so rather than handing back +/// an empty buffer that would deserialize into a confusing error later. +#[test] +fn cache_copy_without_length_is_refused() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + let mut buf = [0u8; 64]; + let status = unsafe { + vm_exec_w2_instance_cache( + instance, + buf.as_mut_ptr(), + buf.len() as u32, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_ERROR); + assert!( + err.message().contains("length"), + "unexpected message: {}", + err.message() + ); + + unsafe { + vm_exec_w2_instance_destroy(instance); + vm_exec_w2_executor_destroy(executor); + } +} + +/// A buffer shorter than the reported length is refused rather than part-filled: half an +/// artifact is indistinguishable from a whole one until it fails to load. +#[test] +fn cache_copy_into_a_short_buffer_is_refused() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + let mut needed: u32 = 0; + let status = + unsafe { vm_exec_w2_instance_cache_length(instance, &mut needed, err.ptr(), ERR_BUF_LEN) }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_OK); + + let mut buf = vec![0u8; needed as usize - 1]; + let status = unsafe { + vm_exec_w2_instance_cache( + instance, + buf.as_mut_ptr(), + needed - 1, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_ERROR); + assert!( + err.message().contains("too small"), + "unexpected message: {}", + err.message() + ); + + unsafe { + vm_exec_w2_instance_destroy(instance); + vm_exec_w2_executor_destroy(executor); + } +} + +/// Bytes that are not an artifact are rejected at the boundary with a message, not a panic +/// and not a silent success. +#[test] +fn instance_from_non_artifact_bytes_is_refused() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + + let junk = b"not a wasmer artifact"; + let options = compilation_options(); + let mut restored: *mut vm_exec_w2_instance_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec_w2_new_instance_from_cache( + executor, + &mut restored, + junk.as_ptr(), + junk.len() as u32, + &options, + 0xBEEFusize, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_ERROR); + assert!(restored.is_null()); + assert!(!err.message().is_empty()); + + unsafe { vm_exec_w2_executor_destroy(executor) } +} diff --git a/c-api-wasmer7/libvmexeccapi_w7.h b/c-api-wasmer7/libvmexeccapi_w7.h index 60dd92ae..df9f6c0a 100644 --- a/c-api-wasmer7/libvmexeccapi_w7.h +++ b/c-api-wasmer7/libvmexeccapi_w7.h @@ -60,6 +60,22 @@ void vm_exec_w7_executor_destroy(struct vm_exec_w7_executor_t *executor); */ vm_exec_status_t vm_exec_w7_new_instance(struct vm_exec_w7_executor_t *executor, struct vm_exec_w7_instance_t **instance_out, const uint8_t *wasm_bytes, uint32_t wasm_bytes_len, const vm_exec_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); +/** + * Creates an instance from a cached artifact, skipping compilation. + * + * `cache_bytes` is what `vm_exec_w7_instance_cache` produced, from this same library. It is + * **executable code that is mapped, not validated** — an artifact from an untrusted source + * must not be passed here; compile the wasm instead. An artifact from a different Wasmer + * version is refused, because its header carries an ABI version. + * + * `go_vm_hooks` is as in `vm_exec_w7_new_instance`. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w7_new_instance_from_cache(struct vm_exec_w7_executor_t *executor, struct vm_exec_w7_instance_t **instance_out, const uint8_t *cache_bytes, uint32_t cache_bytes_len, const vm_exec_compilation_options_t *options, uintptr_t go_vm_hooks, char *err_buf, int err_buf_len); + /** * Destroys an instance. Does nothing if `instance` is null. * @@ -155,6 +171,32 @@ int vm_exec_w7_instance_imported_function_names_length(const struct vm_exec_w7_i */ int vm_exec_w7_instance_imported_function_names(const struct vm_exec_w7_instance_t *instance, char *dest, int dest_len); +/** + * Serializes the instance's module and reports how many bytes `vm_exec_w7_instance_cache` + * will write. + * + * The artifact is produced by this call and held until the copy call reads it, so the pair + * serializes once rather than twice. Call this first; see the copy call. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w7_instance_cache_length(struct vm_exec_w7_instance_t *instance, uint32_t *length_out, char *err_buf, int err_buf_len); + +/** + * Copies the artifact produced by the preceding `vm_exec_w7_instance_cache_length` call. + * + * `dest_len` must be at least the length that call reported. A short buffer is refused + * rather than filled partially, because a truncated artifact is indistinguishable from a + * whole one until it fails to deserialize. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w7_instance_cache(struct vm_exec_w7_instance_t *instance, uint8_t *dest, uint32_t dest_len, char *err_buf, int err_buf_len); + /** * Loads from instance memory into a caller-provided buffer, **truncating** at the end of * the memory. diff --git a/c-api-wasmer7/src/capi_instance.rs b/c-api-wasmer7/src/capi_instance.rs index 747041db..e15ae183 100644 --- a/c-api-wasmer7/src/capi_instance.rs +++ b/c-api-wasmer7/src/capi_instance.rs @@ -62,6 +62,46 @@ pub unsafe extern "C" fn vm_exec_w7_new_instance( } } +/// Creates an instance from a cached artifact, skipping compilation. +/// +/// `cache_bytes` is what `vm_exec_w7_instance_cache` produced, from this same library. It is +/// **executable code that is mapped, not validated** — an artifact from an untrusted source +/// must not be passed here; compile the wasm instead. An artifact from a different Wasmer +/// version is refused, because its header carries an ABI version. +/// +/// `go_vm_hooks` is as in `vm_exec_w7_new_instance`. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[allow(clippy::too_many_arguments)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w7_new_instance_from_cache( + executor: *mut vm_exec_w7_executor_t, + instance_out: *mut *mut vm_exec_w7_instance_t, + cache_bytes: *const u8, + cache_bytes_len: u32, + options: *const vm_exec_compilation_options_t, + go_vm_hooks: usize, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::new_instance_from_cache( + executor as *mut CapiExecutor, + instance_out as *mut *mut CapiInstance, + cache_bytes, + cache_bytes_len, + options, + go_vm_hooks, + err_buf, + err_buf_len, + ) + } +} + /// Destroys an instance. Does nothing if `instance` is null. /// /// # Safety @@ -243,3 +283,61 @@ pub unsafe extern "C" fn vm_exec_w7_instance_imported_function_names( ) } } + +/// Serializes the instance's module and reports how many bytes `vm_exec_w7_instance_cache` +/// will write. +/// +/// The artifact is produced by this call and held until the copy call reads it, so the pair +/// serializes once rather than twice. Call this first; see the copy call. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w7_instance_cache_length( + instance: *mut vm_exec_w7_instance_t, + length_out: *mut u32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::instance_cache_length( + instance as *mut CapiInstance, + length_out, + err_buf, + err_buf_len, + ) + } +} + +/// Copies the artifact produced by the preceding `vm_exec_w7_instance_cache_length` call. +/// +/// `dest_len` must be at least the length that call reported. A short buffer is refused +/// rather than filled partially, because a truncated artifact is indistinguishable from a +/// whole one until it fails to deserialize. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w7_instance_cache( + instance: *mut vm_exec_w7_instance_t, + dest: *mut u8, + dest_len: u32, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::instance_cache( + instance as *mut CapiInstance, + dest, + dest_len, + err_buf, + err_buf_len, + ) + } +} diff --git a/c-api-wasmer7/tests/ffi_boundary.rs b/c-api-wasmer7/tests/ffi_boundary.rs index 17239f92..1e839566 100644 --- a/c-api-wasmer7/tests/ffi_boundary.rs +++ b/c-api-wasmer7/tests/ffi_boundary.rs @@ -783,3 +783,190 @@ fn state_ops() { "a context used after its call must be rejected" ); } + +/// Reads an artifact out through the two-call cache convention. +fn read_artifact(instance: *mut vm_exec_w7_instance_t, err: &mut ErrBuf) -> Vec { + let mut needed: u32 = 0; + let status = + unsafe { vm_exec_w7_instance_cache_length(instance, &mut needed, err.ptr(), ERR_BUF_LEN) }; + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "cache_length failed: {}", + err.message() + ); + assert!(needed > 0, "an artifact cannot be empty"); + + let mut buf = vec![0u8; needed as usize]; + let status = unsafe { + vm_exec_w7_instance_cache(instance, buf.as_mut_ptr(), needed, err.ptr(), ERR_BUF_LEN) + }; + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "cache failed: {}", + err.message() + ); + + buf +} + +/// The serialized-artifact path across the boundary: out through the two-call read, back in +/// through `new_instance_from_cache`, and the restored instance is a working instance. +#[test] +fn cache_round_trip() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + let artifact = read_artifact(instance, &mut err); + assert_ne!( + artifact.as_slice(), + CONTRACT_WAT.as_bytes(), + "the artifact must be the compiled module, not the source" + ); + + let options = compilation_options(); + let mut restored: *mut vm_exec_w7_instance_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec_w7_new_instance_from_cache( + executor, + &mut restored, + artifact.as_ptr(), + artifact.len() as u32, + &options, + 0xBEEFusize, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "new_instance_from_cache failed: {}", + err.message() + ); + assert!(!restored.is_null()); + + // The restored instance runs, and its hooks still reach the host: `callGetGasLeft` goes + // out through the mock table, which a module that lost its imports could not do. + let result = call(restored, "doNothing", &mut err); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); + let result = call(restored, "callGetGasLeft", &mut err); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); + + assert_eq!( + names( + restored, + vm_exec_w7_instance_exported_function_names_length, + vm_exec_w7_instance_exported_function_names, + ), + names( + instance, + vm_exec_w7_instance_exported_function_names_length, + vm_exec_w7_instance_exported_function_names, + ), + ); + + unsafe { + vm_exec_w7_instance_destroy(restored); + vm_exec_w7_instance_destroy(instance); + vm_exec_w7_executor_destroy(executor); + } +} + +/// The copy call depends on the length call having run, and says so rather than handing back +/// an empty buffer that would deserialize into a confusing error later. +#[test] +fn cache_copy_without_length_is_refused() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + let mut buf = [0u8; 64]; + let status = unsafe { + vm_exec_w7_instance_cache( + instance, + buf.as_mut_ptr(), + buf.len() as u32, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_ERROR); + assert!( + err.message().contains("length"), + "unexpected message: {}", + err.message() + ); + + unsafe { + vm_exec_w7_instance_destroy(instance); + vm_exec_w7_executor_destroy(executor); + } +} + +/// A buffer shorter than the reported length is refused rather than part-filled: half an +/// artifact is indistinguishable from a whole one until it fails to load. +#[test] +fn cache_copy_into_a_short_buffer_is_refused() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + let mut needed: u32 = 0; + let status = + unsafe { vm_exec_w7_instance_cache_length(instance, &mut needed, err.ptr(), ERR_BUF_LEN) }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_OK); + + let mut buf = vec![0u8; needed as usize - 1]; + let status = unsafe { + vm_exec_w7_instance_cache( + instance, + buf.as_mut_ptr(), + needed - 1, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_ERROR); + assert!( + err.message().contains("too small"), + "unexpected message: {}", + err.message() + ); + + unsafe { + vm_exec_w7_instance_destroy(instance); + vm_exec_w7_executor_destroy(executor); + } +} + +/// Bytes that are not an artifact are rejected at the boundary with a message, not a panic +/// and not a silent success. +#[test] +fn instance_from_non_artifact_bytes_is_refused() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + + let junk = b"not a wasmer artifact"; + let options = compilation_options(); + let mut restored: *mut vm_exec_w7_instance_t = std::ptr::null_mut(); + let status = unsafe { + vm_exec_w7_new_instance_from_cache( + executor, + &mut restored, + junk.as_ptr(), + junk.len() as u32, + &options, + 0xBEEFusize, + err.ptr(), + ERR_BUF_LEN, + ) + }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_ERROR); + assert!(restored.is_null()); + assert!(!err.message().is_empty()); + + unsafe { vm_exec_w7_executor_destroy(executor) } +} diff --git a/vm-executor-experimental/src/we_error.rs b/vm-executor-experimental/src/we_error.rs index 35cc33a5..b06d3eb5 100644 --- a/vm-executor-experimental/src/we_error.rs +++ b/vm-executor-experimental/src/we_error.rs @@ -23,8 +23,8 @@ pub enum ExperimentalError { #[error("instance memory access error: {0}")] MemoryAccess(String), - #[error("creating an instance from cache is not supported")] - InstanceFromCacheUnsupported, + #[error("cache artifact is empty")] + EmptyCacheArtifact, #[error("resetting an instance is not supported")] ResetUnsupported, diff --git a/vm-executor-experimental/src/we_executor.rs b/vm-executor-experimental/src/we_executor.rs index e37354dc..55c0ca4b 100644 --- a/vm-executor-experimental/src/we_executor.rs +++ b/vm-executor-experimental/src/we_executor.rs @@ -40,9 +40,19 @@ impl Executor for ExperimentalExecutor { fn new_instance_from_cache( &self, - _cache_bytes: &[u8], - _compilation_options: &CompilationOptions, + cache_bytes: &[u8], + compilation_options: &CompilationOptions, ) -> Result, ExecutorError> { - Err(ExperimentalError::InstanceFromCacheUnsupported.into()) + if cache_bytes.is_empty() { + return Err(ExperimentalError::EmptyCacheArtifact.into()); + } + + let instance = ExperimentalInstance::try_new_instance_from_cache( + self.runtime.clone(), + cache_bytes, + compilation_options, + )?; + + Ok(Box::new(instance)) } } diff --git a/vm-executor-experimental/src/we_instance.rs b/vm-executor-experimental/src/we_instance.rs index a61ae100..20bfc972 100644 --- a/vm-executor-experimental/src/we_instance.rs +++ b/vm-executor-experimental/src/we_instance.rs @@ -1,8 +1,8 @@ +use crate::ExperimentalError; use crate::middlewares::{ Breakpoints, Metering, OpcodeControl, OpcodeTracer, ProtectedGlobals, get_breakpoint_value, get_points_used, set_points_limit, }; -use crate::ExperimentalError; use crate::{we_imports::generate_import_object, we_vm_hooks::VMHooksWrapper}; use log::trace; use multiversx_chain_vm_executor::{ @@ -71,26 +71,81 @@ fn prepare_wasmer_instance_inner( }) } +/// Builds the store the module will be compiled into, or deserialized against. +/// +/// The middlewares are pushed here rather than at instantiation because they are +/// *compile-time* transforms: they rewrite the module and add the metering and breakpoint +/// globals. A deserialized module already carries that instrumentation, so pushing them again +/// changes nothing about it — what matters on that path is only that the engine agrees with +/// the artifact, which is why both paths build the store the same way. +/// +/// One store per instance, and so one engine per compiled module. The metering middleware +/// keeps its global indexes in a `Mutex>` filled in during compilation, so +/// compiling a second contract through the same engine would overwrite the first one's. See +/// `PLAN-rust-vm-caching.md`, 1.1. +fn new_store(compilation_options: &CompilationOptions) -> Store { + // Use Singlepass compiler with the default settings + let mut compiler = Singlepass::default(); + + // Push middlewares + push_middlewares(&mut compiler, compilation_options); + + Store::new(compiler) +} + impl ExperimentalInstance { pub fn try_new_instance( runtime: Arc, wasm_bytes: &[u8], compilation_options: &CompilationOptions, ) -> Result { - // Use Singlepass compiler with the default settings - let mut compiler = Singlepass::default(); - - // Push middlewares - push_middlewares(&mut compiler, compilation_options); - - // Create the store - let mut store: Store = Store::new(compiler); + let store = new_store(compilation_options); trace!("Compiling module ..."); let module = Module::new(&store, wasm_bytes)?; + Self::try_from_module(runtime, store, &module) + } + + /// Instantiates a module from a previously serialized artifact, skipping compilation. + /// + /// The artifact comes from [`Instance::cache`], which is `Module::serialize`. + /// + /// # Trust + /// + /// `Module::deserialize` is **not** a validating load: in Wasmer 7.5 it delegates to + /// `deserialize_unchecked`, so it maps whatever it is given as executable code. The name + /// suggests otherwise, which is why it is said here. Artifacts must come from a store the + /// host trusts as much as it trusts itself — for the Go VM that is its own compiled-code + /// cache, written by `cache()` on this same machine. + /// + /// A mismatched artifact is rejected rather than misread, but only as far as the header + /// goes: it carries a Wasmer ABI version (11 in 7.1, 24 in 7.4) that `deserialize` + /// refuses on mismatch. So an upgrade invalidates a persisted cache loudly, as an error + /// here, rather than silently — but a cache that outlives an upgrade still has to be + /// keyed by Wasmer version or purged, because every entry in it will fail this way. + pub fn try_new_instance_from_cache( + runtime: Arc, + cache_bytes: &[u8], + compilation_options: &CompilationOptions, + ) -> Result { + let store = new_store(compilation_options); + + trace!("Deserializing module ..."); + // SAFETY: see the trust note above — the caller vouches for the artifact's origin. + let module = unsafe { Module::deserialize(&store, cache_bytes)? }; + + Self::try_from_module(runtime, store, &module) + } + + /// The half the two entry points share: everything after there is a `Module`. + fn try_from_module( + runtime: Arc, + mut store: Store, + module: &Module, + ) -> Result { let inner = rc_new_cyclic_fallible(|weak| { - prepare_wasmer_instance_inner(runtime, &module, &mut store, weak) + prepare_wasmer_instance_inner(runtime, module, &mut store, weak) })?; Ok(ExperimentalInstance { diff --git a/vm-executor-experimental/tests/cache_test.rs b/vm-executor-experimental/tests/cache_test.rs new file mode 100644 index 00000000..0c15ba10 --- /dev/null +++ b/vm-executor-experimental/tests/cache_test.rs @@ -0,0 +1,208 @@ +//! The serialized-artifact path: `Instance::cache` out, `Executor::new_instance_from_cache` in. +//! +//! The point of these tests is not that instantiation succeeds — that is the easy half — but +//! that the instance which comes back is *the same instance*. A serialized artifact carries +//! the middleware instrumentation baked in at compile time, and nothing re-runs the +//! middlewares on the way back. So the thing to pin is metering: if deserialization ever +//! stopped carrying the metering globals, contracts would run unmetered rather than fail, and +//! every other test here would still pass. + +use multiversx_chain_vm_executor::{ + CompilationOptions, Executor, ExecutorError, ExecutorRuntimeDefault, Instance, + InstanceCallResult, OpcodeConfig, OpcodeCost, +}; +use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; +use std::sync::Arc; +use wasmer::wat2wasm; + +const LOCAL_ALLOCATE_COST: u32 = 5; +const POINTS_LIMIT: u64 = 1_000_000; +const NUM_LOCALS: usize = 20; + +/// Locals are the only priced opcode, so `points_used` after `init` is exactly +/// `NUM_LOCALS * LOCAL_ALLOCATE_COST` — a number that is wrong in an obvious way if the +/// metering middleware is missing (it would be 0) rather than merely different. +fn metered_options() -> CompilationOptions { + CompilationOptions { + unmetered_locals: 0, + opcode_config: Arc::new(OpcodeConfig { + opcode_cost: OpcodeCost { + opcode_localallocate: LOCAL_ALLOCATE_COST, + ..Default::default() + }, + ..Default::default() + }), + ..Default::default() + } +} + +fn metered_wasm() -> Vec { + let wat = format!( + r#"(module + (func (export "init") + (local {})) + (memory (export "memory") 17))"#, + "i32 ".repeat(NUM_LOCALS) + ); + wat2wasm(wat.as_bytes()).unwrap().into_owned() +} + +/// A contract with host imports and several exports, so that the introspection the Go VM runs +/// against every new instance — endpoint names, imported hook names, signature check — is +/// exercised on the restored side too. +fn imports_wasm() -> Vec { + let wat = r#"(module + (type $void (func)) + (type $i32_i32 (func (param i32 i32))) + (import "env" "signalError" (func $signalError (type $i32_i32))) + (import "env" "getGasLeft" (func $getGasLeft (result i64))) + (func (export "init") (type $void)) + (func (export "add") (type $void)) + (func (export "getSum") (type $void)) + (func (export "callBack") (type $void)) + (memory (export "memory") 2))"#; + wat2wasm(wat.as_bytes()).unwrap().into_owned() +} + +fn executor() -> ExperimentalExecutor { + ExperimentalExecutor::new(Arc::new(ExecutorRuntimeDefault)) +} + +fn call_init(instance: &mut Box) -> u64 { + assert!(matches!( + instance.call("init", POINTS_LIMIT), + InstanceCallResult::Ok + )); + instance.get_points_used().unwrap() +} + +fn from_cache(cache_bytes: &[u8]) -> Result, ExecutorError> { + executor().new_instance_from_cache(cache_bytes, &metered_options()) +} + +/// The load-bearing one. Everything else here is a guard around it. +#[test] +fn cached_module_meters_identically() { + let wasm = metered_wasm(); + let options = metered_options(); + + let mut compiled = executor().new_instance(&wasm, &options).unwrap(); + let compiled_points = call_init(&mut compiled); + + // Not merely "equal to itself": the expected value is derived from the gas schedule, so a + // round trip that silently dropped metering could not pass by also dropping it here. + assert_eq!( + compiled_points, + NUM_LOCALS as u64 * LOCAL_ALLOCATE_COST as u64 + ); + + let artifact = compiled.cache().unwrap(); + let mut restored = from_cache(&artifact).unwrap(); + + assert_eq!(call_init(&mut restored), compiled_points); +} + +#[test] +fn cached_module_keeps_its_endpoints_and_imports() { + let wasm = imports_wasm(); + let options = CompilationOptions::default(); + + let compiled = executor().new_instance(&wasm, &options).unwrap(); + let artifact = compiled.cache().unwrap(); + + let restored = executor() + .new_instance_from_cache(&artifact, &options) + .unwrap(); + + assert_eq!( + restored.get_exported_function_names(), + compiled.get_exported_function_names() + ); + assert_eq!( + restored.get_imported_function_names(), + compiled.get_imported_function_names() + ); + assert!(restored.has_function("add")); + assert!(!restored.has_function("missingEndpoint")); + assert!(restored.check_signatures()); +} + +/// A cached instance is a fresh instance, not a resumed one: its globals start where a newly +/// compiled one's would. This is what lets the Go VM treat the artifact cache as a substitute +/// for compiling, and it is the reason `reset()` is not needed — see +/// `PLAN-rust-vm-caching.md`, 1.4. +#[test] +fn cached_module_starts_fresh_each_time() { + let artifact = executor() + .new_instance(&metered_wasm(), &metered_options()) + .unwrap() + .cache() + .unwrap(); + + let mut first = from_cache(&artifact).unwrap(); + let mut second = from_cache(&artifact).unwrap(); + + // Run one of them twice over, so "fresh" is a claim about the second instance rather than + // about an artifact that has simply not been used yet. + let first_points = call_init(&mut first); + let _ = first.call("init", POINTS_LIMIT); + + assert_eq!(call_init(&mut second), first_points); +} + +/// An artifact can be re-serialized from an instance that was itself restored from one, so a +/// host is free to re-cache without going back to the wasm. +#[test] +fn cached_module_can_be_cached_again() { + let artifact = executor() + .new_instance(&metered_wasm(), &metered_options()) + .unwrap() + .cache() + .unwrap(); + + let artifact_again = from_cache(&artifact).unwrap().cache().unwrap(); + + let mut twice_restored = from_cache(&artifact_again).unwrap(); + assert_eq!( + call_init(&mut twice_restored), + NUM_LOCALS as u64 * LOCAL_ALLOCATE_COST as u64 + ); +} + +/// The artifact is Wasmer's own format, not a copy of the input. Worth stating because the Go +/// VM stores it under the *code hash*, so the two are easy to confuse at the call site. +#[test] +fn artifact_is_not_the_wasm() { + let wasm = metered_wasm(); + let artifact = executor() + .new_instance(&wasm, &metered_options()) + .unwrap() + .cache() + .unwrap(); + + assert_ne!(artifact, wasm); + assert!(!artifact.is_empty()); +} + +#[test] +fn empty_artifact_is_rejected() { + let err = from_cache(&[]).err().unwrap(); + assert!(err.to_string().contains("empty"), "unexpected error: {err}"); +} + +/// Deserialization is unchecked in the sense that it does not *validate* the code, but it does +/// check the header, so a buffer that is not an artifact is an error rather than a jump into +/// arbitrary bytes. That is the only guarantee being claimed here — it is not a licence to +/// deserialize untrusted input. +#[test] +fn non_artifact_bytes_are_rejected() { + let err = from_cache(b"not a wasmer artifact").err().unwrap(); + assert!(!err.to_string().is_empty()); +} + +/// The wasm itself is not an artifact. The two reach the executor through different entry +/// points, so feeding one to the other has to fail rather than quietly work. +#[test] +fn wasm_bytes_are_not_a_valid_artifact() { + assert!(from_cache(&metered_wasm()).is_err()); +} diff --git a/vm-executor-wasmer/src/new_traits/wasmer_error.rs b/vm-executor-wasmer/src/new_traits/wasmer_error.rs index 2da3143c..ab6d6f8e 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_error.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_error.rs @@ -20,6 +20,6 @@ pub enum WasmerExecutorError { #[error("instance call error: {0}")] WrappedInstance(String), - #[error("creating an instance from cache is not supported")] - InstanceFromCacheUnsupported, + #[error("cache artifact is empty")] + EmptyCacheArtifact, } diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs index f61ef0dc..9ac4c47f 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_executor.rs @@ -1,7 +1,7 @@ use crate::WasmerInstance; use crate::executor_interface::{ - CompilationOptions, Executor, ExecutorError, ExecutorRuntime, Instance, VMHooksLegacyAdapter, - check_missing_wasm, + CompilationOptions, Executor, ExecutorError, ExecutorRuntime, Instance, OpcodeConfig, + VMHooksLegacy, VMHooksLegacyAdapter, check_missing_wasm, }; use rc_new_cyclic_fallible::rc_new_cyclic_fallible; use std::{ @@ -28,17 +28,15 @@ impl WasmerProdExecutor { WasmerProdExecutor { runtime } } - fn new_instance_from_bytes( + /// Builds an instance from either source. + /// + /// The two paths differ only in how the `WasmerInstance` is constructed — compile, or + /// deserialize — and everything around that is identical: the cyclic hook wiring and the + /// wrapper. So `build` is the one thing passed in. + fn new_instance_with( &self, - wasm_bytes: &[u8], - compilation_options: &CompilationOptions, + build: impl FnOnce(Rc) -> Result, ) -> Result, ExecutorError> { - // The 2.2 middleware wants the config behind a `Mutex`, which was how the gas - // schedule used to be swapped under a live executor. Nothing mutates it any more: - // the config now arrives per instance in the compilation options, so this is a - // private copy that no one else holds a handle to. - let opcode_config = Arc::new(Mutex::new((*compilation_options.opcode_config).clone())); - let inner_instance_ref = rc_new_cyclic_fallible(|weak| { // The legacy import object wants a hooks object for the instance's life, while // the runtime lends hooks per call; the adapter is the join. The hooks an @@ -50,20 +48,21 @@ impl WasmerProdExecutor { WasmerProdInstanceState::new(weak.clone()), ); - WasmerInstance::try_new_instance( - Rc::new(vm_hooks), - opcode_config.clone(), - wasm_bytes, - &compilation_options.to_legacy(), - ) + build(Rc::new(vm_hooks)) })?; - let wasmer_instance_ref = WasmerProdInstance::new(inner_instance_ref); - - Ok(Box::new(wasmer_instance_ref)) + Ok(Box::new(WasmerProdInstance::new(inner_instance_ref))) } } +/// The 2.2 middleware wants the config behind a `Mutex`, which was how the gas schedule used +/// to be swapped under a live executor. Nothing mutates it any more: the config now arrives +/// per instance in the compilation options, so this is a private copy that no one else holds +/// a handle to. +fn private_opcode_config(compilation_options: &CompilationOptions) -> Arc> { + Arc::new(Mutex::new((*compilation_options.opcode_config).clone())) +} + impl Executor for WasmerProdExecutor { fn new_instance( &self, @@ -72,14 +71,36 @@ impl Executor for WasmerProdExecutor { ) -> Result, ExecutorError> { check_missing_wasm(wasm_bytes)?; - self.new_instance_from_bytes(wasm_bytes, compilation_options) + let opcode_config = private_opcode_config(compilation_options); + let legacy = compilation_options.to_legacy(); + self.new_instance_with(|vm_hooks| { + WasmerInstance::try_new_instance(vm_hooks, opcode_config, wasm_bytes, &legacy) + }) } + /// Instantiates a previously serialized module. + /// + /// Same trust note as the Wasmer 7 backend: `Module::deserialize` maps executable code + /// without validating it, so artifacts must come from a store the host trusts. Here they + /// come from [`Instance::cache`] on this same library. fn new_instance_from_cache( &self, - _cache_bytes: &[u8], - _compilation_options: &CompilationOptions, + cache_bytes: &[u8], + compilation_options: &CompilationOptions, ) -> Result, ExecutorError> { - Err(WasmerExecutorError::InstanceFromCacheUnsupported.into()) + if cache_bytes.is_empty() { + return Err(WasmerExecutorError::EmptyCacheArtifact.into()); + } + + let opcode_config = private_opcode_config(compilation_options); + let legacy = compilation_options.to_legacy(); + self.new_instance_with(|vm_hooks| { + WasmerInstance::try_new_instance_from_cache( + vm_hooks, + opcode_config, + cache_bytes, + &legacy, + ) + }) } } diff --git a/vm-executor-wasmer/tests/cache_test.rs b/vm-executor-wasmer/tests/cache_test.rs new file mode 100644 index 00000000..e3423d3e --- /dev/null +++ b/vm-executor-wasmer/tests/cache_test.rs @@ -0,0 +1,121 @@ +//! The serialized-artifact path on the Wasmer 2.2 backend, through the *new* traits. +//! +//! The same properties as the Wasmer 7 copy in `vm-executor-experimental/tests/cache_test.rs`, +//! asserted against the runtime that is in production. Both are worth having: the two +//! backends reach `new_instance_from_cache` through one trait but deserialize with two very +//! different Wasmer versions, and this is the one that has to keep matching what +//! `wasmer2legacy` has always done. + +use multiversx_chain_vm_executor::{ + CompilationOptions, Executor, ExecutorError, ExecutorRuntimeDefault, Instance, + InstanceCallResult, OpcodeConfig, OpcodeCost, +}; +use multiversx_chain_vm_executor_wasmer::new_traits::WasmerProdExecutor; +use std::sync::Arc; +use wasmer::wat2wasm; + +const LOCAL_ALLOCATE_COST: u32 = 5; +const POINTS_LIMIT: u64 = 1_000_000; +const NUM_LOCALS: usize = 20; + +fn metered_options() -> CompilationOptions { + CompilationOptions { + unmetered_locals: 0, + opcode_config: Arc::new(OpcodeConfig { + opcode_cost: OpcodeCost { + opcode_localallocate: LOCAL_ALLOCATE_COST, + ..Default::default() + }, + ..Default::default() + }), + ..Default::default() + } +} + +fn metered_wasm() -> Vec { + let wat = format!( + r#"(module + (func (export "init") + (local {})) + (memory (export "memory") 17))"#, + "i32 ".repeat(NUM_LOCALS) + ); + wat2wasm(wat.as_bytes()).unwrap().into_owned() +} + +fn executor() -> WasmerProdExecutor { + WasmerProdExecutor::new(Arc::new(ExecutorRuntimeDefault)) +} + +fn call_init(instance: &mut Box) -> u64 { + assert!(matches!( + instance.call("init", POINTS_LIMIT), + InstanceCallResult::Ok + )); + instance.get_points_used().unwrap() +} + +fn from_cache(cache_bytes: &[u8]) -> Result, ExecutorError> { + executor().new_instance_from_cache(cache_bytes, &metered_options()) +} + +/// Metering has to survive the round trip, or contracts restored from cache run free. +#[test] +fn cached_module_meters_identically() { + let wasm = metered_wasm(); + let options = metered_options(); + + let mut compiled = executor().new_instance(&wasm, &options).unwrap(); + let compiled_points = call_init(&mut compiled); + + assert_eq!( + compiled_points, + NUM_LOCALS as u64 * LOCAL_ALLOCATE_COST as u64 + ); + + let artifact = compiled.cache().unwrap(); + let mut restored = from_cache(&artifact).unwrap(); + + assert_eq!(call_init(&mut restored), compiled_points); +} + +#[test] +fn cached_module_keeps_its_endpoints() { + let wasm = metered_wasm(); + let options = metered_options(); + + let compiled = executor().new_instance(&wasm, &options).unwrap(); + let artifact = compiled.cache().unwrap(); + let restored = from_cache(&artifact).unwrap(); + + assert_eq!( + restored.get_exported_function_names(), + compiled.get_exported_function_names() + ); + assert!(restored.has_function("init")); + assert!(restored.check_signatures()); +} + +#[test] +fn artifact_is_not_the_wasm() { + let wasm = metered_wasm(); + let artifact = executor() + .new_instance(&wasm, &metered_options()) + .unwrap() + .cache() + .unwrap(); + + assert_ne!(artifact, wasm); + assert!(!artifact.is_empty()); +} + +#[test] +fn empty_artifact_is_rejected() { + let err = from_cache(&[]).err().unwrap(); + assert!(err.to_string().contains("empty"), "unexpected error: {err}"); +} + +#[test] +fn wasm_bytes_are_not_a_valid_artifact() { + assert!(from_cache(&metered_wasm()).is_err()); +} From 1ada8bf7d1a90ae2ee3e84c2c2f388f68a3adcb9 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 13 Oct 2026 09:55:52 +0300 Subject: [PATCH 25/26] wasmer 7 reset - recreates the instance, reuses module --- PLAN-go-ffi.md | 37 ++- PLAN-reset-via-middlewares.md | 229 +++++++++++++++++ PLAN-rust-vm-caching.md | 37 ++- c-api-common/src/capi_instance_ops.rs | 30 +++ c-api-wasmer2/libvmexeccapi_w2.h | 16 ++ c-api-wasmer2/src/capi_instance.rs | 25 ++ c-api-wasmer2/tests/ffi_boundary.rs | 55 ++++ c-api-wasmer7/libvmexeccapi_w7.h | 16 ++ c-api-wasmer7/src/capi_instance.rs | 25 ++ c-api-wasmer7/tests/ffi_boundary.rs | 55 ++++ vm-executor-experimental/src/we_error.rs | 3 - vm-executor-experimental/src/we_instance.rs | 61 ++++- vm-executor-experimental/tests/reset_test.rs | 241 ++++++++++++++++++ .../src/new_traits/wasmer_prod_instance.rs | 2 +- vm-executor/src/new_traits/instance_new.rs | 18 +- 15 files changed, 821 insertions(+), 29 deletions(-) create mode 100644 PLAN-reset-via-middlewares.md create mode 100644 vm-executor-experimental/tests/reset_test.rs diff --git a/PLAN-go-ffi.md b/PLAN-go-ffi.md index 9ff76898..2f948052 100644 --- a/PLAN-go-ffi.md +++ b/PLAN-go-ffi.md @@ -1122,6 +1122,12 @@ Two things it has also established: Reading 33 → 32 as "one test fixed" would have missed it entirely; this is the case the per-test attribution exists for. + After `reset()` landed: `wasmer2legacy` 0, `wasmer2` **27**, `wasmer7` **31**. One test on + each — `TestRustAdderLog` — and nothing newly broken. The subset relation still holds. A + single test is a smaller return than the warm tier sounds like it should give, and it is the + same point the measurement makes from the other direction: almost everything the warm cache + would have bought was already bought by the compiled-code cache. + - **Open, and the reason the above matters: multi-memory modules are now accepted.** The test deploys a module declaring five memories and exporting one, and expects `ContractInvalid`; on 7.5 it returns `Ok`. The cause is that `validate_memories` in @@ -1174,19 +1180,24 @@ unverified there, and `-Wl,-Bsymbolic-functions` remains the open item; see Was: `reset()` on the Wasmer 7 instance, as a prerequisite for Go's warm-instance tracker, then `new_instance_from_cache`, then warm instances for wasmer7. -**`reset()` is off the list.** Measured on a 55 KB contract: compiling costs ~6 ms and -instantiating from a module already in memory ~30–50 µs, so reusing a live instance saves -under 1% over instantiating a cached module afresh. And it is not cheap to get — Wasmer 7 -*and* 7.4 have no instance reset (`Memory::reset` only zeroes and shrinks memory; no -data-segment or global restore), so it would mean forking Wasmer 7, which is consumed -unmodified from crates.io unlike the 2.2 fork. Re-instantiating is behaviourally identical to -resetting. - -So for this backend the warm instance cache **collapses into the module cache**. The cheapest -route for the Go VM is for `Wasmer7Executor` to hold compiled modules in memory and let -`Reset()` keep returning false, which the instance tracker already handles by evicting and -recreating. `NewInstanceFromCompiledCodeWithOptions` is already the Wasmer-6-compatible form -of reuse. +**The economics**, measured on a 55 KB contract: compiling costs ~6 ms, instantiating from a +module already in memory ~30–50 µs. So the warm tier is worth under 1% over a compiled-code +cache hit, and the compiled-code cache is where the real win is. That ordering is why the +serialized-artifact path shipped first. + +**`reset()` was off the list, and is now done.** The original argument was that Wasmer 7 has +no instance reset — true, `Memory::reset` empties the memory and restores neither contents nor +globals — and therefore that getting one meant forking Wasmer 7. That conclusion was wrong. +Instantiating a module *is* the operation that produces a fresh instance, so +`ExperimentalInstance::reset` rebuilds the store from the existing engine and re-instantiates +in place, behind the same handle. Slower than the 2.2 fork's in-place rewind, and stricter: +tables and passive-element state come back too. + +So `Reset()` now returns true on `wasmer7` and `wasmer2`, and the warm instance tier is live +on all three backends rather than collapsing into the module cache. `Instance::reset` takes +`&mut self` to say what it does. The faster in-place design — exporting the contract's globals +from a middleware, restoring memory from a per-module snapshot — is written up in +`PLAN-reset-via-middlewares.md` and deliberately not built, on the 1% above. ### Done: the serialized-artifact path, on both new backends diff --git a/PLAN-reset-via-middlewares.md b/PLAN-reset-via-middlewares.md new file mode 100644 index 00000000..da82d182 --- /dev/null +++ b/PLAN-reset-via-middlewares.md @@ -0,0 +1,229 @@ +# Resetting a Wasmer 7 instance in place, using the middlewares + +**Status:** design only, not built. The shipped implementation re-instantiates the module +instead — see `ExperimentalInstance::reset`. This document exists so that the faster version +can be picked up without re-deriving it, and so the decision not to build it yet is recorded +with its reasons rather than as a shrug. + +**Repo:** `mx-vm-executor-rs`, crate `vm-executor-experimental`. + +## Why this document exists + +Wasmer 7 has no `Instance::reset`. The Wasmer 2.2 fork does, because MultiversX added it +(`lib/vm/src/instance/reset.rs` is not upstream), and that is what makes warm instances work +on the 2.2 path today. + +The obvious conclusion — "so Wasmer 7 cannot reset, and getting it means forking Wasmer 7" — +is wrong, and it is worth being precise about why, because it is the kind of conclusion that +hardens into received wisdom. Wasmer 7 *can* reset. Everything needed is reachable from +inside our own middlewares, which already do the one thing that looks impossible from the +outside: create globals and export them so the host can reach them at runtime. + +What follows is how. The shipped implementation does something simpler and slower, and the +last section says why that was the right call for now. + +## What a reset has to restore + +A reset has to leave the instance in the state it had immediately after instantiation. +Enumerated against what a contract can actually touch under this VM's rules: + +| State | Must restore? | Why | +| --- | --- | --- | +| Linear memory contents | **yes** | any store writes it | +| Linear memory length | **yes** | `memory.grow` is allowed and metered | +| Mutable globals (contract's own) | **yes** | `global.set` is allowed | +| Immutable globals | no | nothing can write them | +| Metering / breakpoint globals | **yes** | written on every call | +| Tables | no, under OpcodeVersion **V2** | `TableSet`/`TableGrow`/`TableInit` are all `Cost::Illegal` in V2 | +| Passive data segments | no | `memory.init` and `data.drop` are `Cost::Illegal` in **both** V1 and V2 | +| Passive element segments | no | `elem.drop` is `Cost::Illegal` in both; nothing can retire a segment | + +Two of these deserve their own note. + +**Passive segments are inert under this VM.** Since neither `data.drop` nor `elem.drop` is +ever legal, the per-instance "which segments are still live" bookkeeping cannot change, so +there is nothing to restore. The 2.2 fork's `reset_passive_elements` is, in this VM, a no-op +that re-runs an initialization-only function — it even carries +`debug_assert!(passive_elements.is_empty(), "should only be called once, at initialization +time")`, inert in release builds but a clear signal of intent. + +**Tables are a live gap on the 2.2 path, under V1.** V1 whitelists `TableSet`, `TableGrow` +and `TableInit`, all of which mutate a table, and the fork's `InstanceHandle::reset` restores +passive elements, globals and memories — *not* tables. So under V1 a contract that mutates its +own table and is then served a warm instance observes its own previous mutation. V2 drops all +five table opcodes, which closes it. The practical exposure is small, because the stated +rationale for dropping them in V2 is that no contract built with current tooling can emit +them, so this is hand-written wasm in pre-V2 epochs. It is recorded here because a design for +"reset in place" has to decide what it covers, and because it is an existing asymmetry between +what the whitelist permits and what reset restores. + +## The part that looks impossible, and is not + +A host holding a `wasmer::Instance` can reach globals only through the module's **exports**: +`instance.exports.get_global(name)`. A contract's own globals are typically not exported — +`test/contracts/wasmbacking/multiple-mutable` declares four mutable globals and exports none +of them — so from outside, they are unreachable and unresettable. + +But we are not outside. `ModuleMiddleware::transform_module_info(&self, &mut ModuleInfo)` runs +during compilation with full mutable access to the module's metadata, and it is already used +for exactly this: `we_helpers::create_global_index` creates a global, pushes its initializer, +and inserts an entry into `module_info.exports`. That is the only reason +`set_points_used(instance, store, value)` can work at all. + +So the capability already exists in production. It simply has not been pointed at the +contract's own globals. + +`ModuleInfo` carries everything needed: + +| Field | Use | +| --- | --- | +| `globals: PrimaryMap` | which globals exist, and whether each is mutable | +| `global_initializers: PrimaryMap` | **the values to restore to** | +| `exports: IndexMap` | where to add the handles the host will use | +| `num_imported_globals: usize` | to tell local globals from imported ones | +| `custom_sections` / `custom_sections_data` | an alternative place to stash data, see below | + +## The design + +### 1. A `ResettableGlobals` middleware + +In `transform_module_info`, walk the local globals and export every **mutable** one under a +reserved name: + +```rust +const RESET_GLOBAL_PREFIX: &str = "mx_resettable_global_"; + +fn transform_module_info(&self, module_info: &mut ModuleInfo) -> Result<(), MiddlewareError> { + let first_local = module_info.num_imported_globals; + let mut exported = Vec::new(); + + for (index, global_type) in module_info.globals.iter().skip(first_local) { + if global_type.mutability.is_const() { + continue; // nothing can write it, so nothing to restore + } + let local = LocalGlobalIndex::new(index.as_u32() as usize - first_local); + let name = format!("{RESET_GLOBAL_PREFIX}{}", local.as_u32()); + module_info.exports.insert(name.clone(), ExportIndex::Global(index)); + exported.push((name, module_info.global_initializers[local])); + } + + *self.resettable.lock().unwrap() = Some(exported); + Ok(()) +} +``` + +Skipping immutable globals is not an optimization detail — it is what keeps the export list +small for typical contracts, which have one mutable stack pointer and a handful of constants. + +Imported globals are skipped too: they are not local, have no initializer here, and this VM +provides none. + +### 2. Carry the initializers to where reset runs + +The middleware learns the initializers at compile time; reset needs them at run time, and in +between the module may have been serialized into the compiled-code cache and reloaded in +another process. Two options: + +- **Recover them at instantiation.** `Module::info()` is public and returns the same + `ModuleInfo`, including `global_initializers` and `exports`. So nothing has to be carried: + the instance can rebuild the (name, initial value) list from the module it was instantiated + from, by scanning exports whose name starts with the reserved prefix. **This is the simpler + option and the recommended one** — it works identically on the fresh-compile and + from-cache paths, with no extra state and no serialization concerns. +- **Stash them in a custom section.** `custom_sections` and `custom_sections_data` are both + `pub` on `ModuleInfo`, and `Module::custom_sections(name)` is public for reading, so a + middleware could write an encoded table that survives serialization. Only worth it if the + recovery scan ever proves too slow, which at a handful of globals it will not. + +### 3. Restore memory from a snapshot + +The memory half has no equivalent trick, because active data initializers are **not** in +`ModuleInfo` — only `passive_data` is — and `Module` in Wasmer 7.5 exposes `imports()`, +`exports()`, `custom_sections()` and `info()`, with no `artifact()`. The fork reaches them via +`self.module.artifact().data_initializers()`; that door is closed here. + +It does not need to be open. The initial memory image is simply *what the memory contains +immediately after instantiation, before any call runs*, so capture it there: + +```rust +// at instantiation, before the first call +let view = memory.view(&store); +let initial_pages = view.size(); +let mut image = vec![0u8; view.data_size() as usize]; +view.read(0, &mut image)?; +``` + +and restore it with `Memory::reset` → `grow(initial_pages)` → `MemoryView::write(0, &image)`. +`MemoryView::read`/`write`, `Memory::grow` and `Memory::reset` are all public in 7.5. + +**The image belongs to the module, not the instance.** Every instance of a module starts +identical, so it should hang off the compiled module behind an `Arc` — one copy per module, +shared by every instance and every reset. For a two-page contract that is 128 KB once, rather +than 128 KB per cached instance. + +A denser variant stores only the non-zero extents (the data segments' ranges), at the cost of +a sparse write loop instead of one `write`. Not worth it until a profile says so. + +### 4. What reset then does + +``` +for (name, initial) in resettable_globals: + instance.exports.get_global(name).set(store, initial) +set metering + breakpoint globals (already possible today) +memory.reset(store) (to zero length) +memory.grow(store, initial_pages) +memory.view(store).write(0, &module.initial_image) +``` + +No table work and no passive-segment work, per the table above. + +## One step that is not yet verified + +`Memory::reset` is documented as *"Resets the memory back to zero length"*. Whether a +subsequent `grow` back to the declared minimum is well-behaved — and whether a memory sitting +at zero length while its `MemoryType` declares a non-zero minimum is a state Wasmer is happy +with — has not been tested. **Probe this first.** If it misbehaves, the fallback is to skip +the shrink and only rewrite the first `initial_pages` worth of memory, accepting that a +contract which grew its memory keeps the larger allocation across a reset. That is a +divergence from the 2.2 fork's behaviour, which does shrink, and would need its own +justification. + +## Risks to handle + +**Reserved-name collisions are already a latent bug.** `create_global_index` inserts into +`module_info.exports` with no collision check, so a contract that exports a global named +`metering_points_limit` silently displaces the middleware's own export today. Adding a family +of `mx_resettable_global_*` names widens that surface. The fix is a compile-time guard that +rejects a module already exporting any reserved name, and it is worth doing **independently of +this design**, since the existing exposure does not depend on it. + +**Extra exports change the artifact.** More entries in `ModuleInfo::exports` means different +serialized bytes, so every entry in the compiled-code cache is invalidated and must be purged +on rollout. Execution semantics and gas are unaffected — exports cost nothing and are not +reachable by the contract — so this is a cache-migration concern rather than a consensus one, +but it has to be sequenced deliberately. + +**Nothing new becomes visible to the contract.** A contract addresses globals by index, not by +export name, so exporting a global grants the contract no access it did not already have. The +`ProtectedGlobals` middleware is unaffected: it blocks `global.set` on the middleware's own +indices, which these are not. + +## Why this was not built + +The shipped `reset` re-instantiates the module into a fresh store, reusing the engine. It is +slower — a full instantiation, including rebuilding the import object, instead of a few global +writes and one memory copy — and it is correct for exactly the same reason: instantiating a +module *is* the operation that produces a fresh instance. It is also strictly more thorough +than the design above and than the 2.2 fork, since tables and passive-element state come back +too, without anyone having to reason about whether they could have changed. + +The measured case for going faster is weak. Instantiating an already-compiled module costs +~30–50 µs against ~6 ms to compile it, so the whole warm-instance tier is worth under 1% over +a compiled-code cache hit — and the in-place design optimizes a fraction of that 1%. Against +that sits a new middleware, a reserved-name guard, a per-module memory image, an artifact +migration, and an unverified assumption about `Memory::reset`. + +So: build this when there is a reason beyond speed — per-instance host state that must survive +reuse, or a profile that shows instantiation actually mattering — and not before. The point of +writing it down is that the question should be reopened on evidence, not re-derived from +scratch, and certainly not dismissed with "Wasmer 7 can't reset", which is false. diff --git a/PLAN-rust-vm-caching.md b/PLAN-rust-vm-caching.md index 5c8f961e..e71372e5 100644 --- a/PLAN-rust-vm-caching.md +++ b/PLAN-rust-vm-caching.md @@ -318,10 +318,39 @@ explicit purge on upgrade. Irrelevant to the in-memory module cache, which never This tier matters less for the Rust VM (in-process, so the module cache covers it) than for persistence across process restarts. Lower priority than 1.1 and 1.2. -### 1.4 What is explicitly not built - -`reset()` stays `Err(ResetUnsupported)`, with the measurement and the fork argument recorded in -the doc comment so the question is not reopened without new evidence. +### 1.4 `reset()` — **built, by re-instantiating** + +This section used to say `reset()` stays unsupported because Wasmer 7 has no instance reset +and getting one would mean forking. The first half is true; the conclusion was wrong, and it +is worth correcting rather than quietly dropping, because it was wrong in a way that would +have kept a door shut. + +Wasmer 7 does not need to offer a reset. **Instantiating a module is the operation that +produces a fresh instance**, so `ExperimentalInstance::reset` builds a new `Store` from the +existing engine — a refcount clone, so the module stays compiled — re-instantiates, and swaps +both into `self` behind the same handle. `Instance::reset` therefore takes `&mut self` now; +`&self` would have forced interior mutability to express something that was never shared. + +What it costs: a full instantiation (~30–50 µs) rather than the handful of writes the 2.2 +fork's in-place reset does. What it buys beyond that fork: tables and passive-element +bookkeeping come back too, because nothing is being selectively rewound — so the correctness +question "could this have changed?" does not arise per field. + +It is exposed through the C API as `vm_exec__instance_reset`, for both backends, and +the Go VM's `Reset()` now returns true, so the warm instance tier is live on `wasmer7` and +`wasmer2` as well as `wasmer2legacy`. + +**The in-place version remains unbuilt, and now has a design.** See +`PLAN-reset-via-middlewares.md`: the contract's own globals *are* reachable, by having a +middleware export them at compile time — the same mechanism the metering globals already use +— and memory can be restored from a per-module snapshot taken at instantiation. That document +also records the two things found while establishing it: passive segments are inert under this +VM's opcode whitelist in both versions, and the 2.2 fork's reset does not restore **tables**, +which V1 allows contracts to mutate. + +The measurement still says the in-place version is not worth building: the whole warm tier is +under 1% over a compiled-code cache hit, and this optimizes a fraction of that. Reopen it on +evidence, not on principle. ## Side 2 — the Rust VM diff --git a/c-api-common/src/capi_instance_ops.rs b/c-api-common/src/capi_instance_ops.rs index c7ee3366..fe13c908 100644 --- a/c-api-common/src/capi_instance_ops.rs +++ b/c-api-common/src/capi_instance_ops.rs @@ -149,6 +149,36 @@ unsafe fn build_instance( vm_exec_status_t::VM_EXEC_OK } +/// Returns the instance to the state it was in immediately after creation. +/// +/// This is what makes an instance reusable for a second execution: memory contents and +/// length, every mutable global, and the metering and breakpoint globals with them. The +/// instance keeps its identity, so the caller's handle stays valid. +/// +/// A backend that cannot do it reports an error, and the caller is expected to destroy the +/// instance and build another rather than reuse it. Reusing an instance whose reset failed +/// would carry the previous execution's state into the next one. +/// +/// # Safety +/// +/// All pointer arguments must be valid for the duration of the call. +pub unsafe fn instance_reset( + instance: *mut CapiInstance, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + if instance.is_null() { + return vm_exec_status_t::VM_EXEC_NULL_POINTER; + } + + let capi_instance = unsafe { &mut *instance }; + + match capi_instance.content.reset() { + Ok(()) => vm_exec_status_t::VM_EXEC_OK, + Err(err) => write_error(err, err_buf, err_buf_len), + } +} + /// Serializes the instance's module and reports the buffer size [`instance_cache`] needs. /// /// This is the first half of the usual two-call read, but it does the work rather than just diff --git a/c-api-wasmer2/libvmexeccapi_w2.h b/c-api-wasmer2/libvmexeccapi_w2.h index 5ecc0ed7..c987f175 100644 --- a/c-api-wasmer2/libvmexeccapi_w2.h +++ b/c-api-wasmer2/libvmexeccapi_w2.h @@ -197,6 +197,22 @@ vm_exec_status_t vm_exec_w2_instance_cache_length(struct vm_exec_w2_instance_t * */ vm_exec_status_t vm_exec_w2_instance_cache(struct vm_exec_w2_instance_t *instance, uint8_t *dest, uint32_t dest_len, char *err_buf, int err_buf_len); +/** + * Returns the instance to the state it was in immediately after creation, so it can serve + * another execution. + * + * Memory contents and length, every mutable global, and the metering and breakpoint globals + * are all restored. The handle stays valid — the instance is rebuilt behind it, not replaced. + * + * On failure the caller must destroy the instance rather than reuse it: an instance whose + * reset failed still carries the previous execution's state. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w2_instance_reset(struct vm_exec_w2_instance_t *instance, char *err_buf, int err_buf_len); + /** * Loads from instance memory into a caller-provided buffer, **truncating** at the end of * the memory. diff --git a/c-api-wasmer2/src/capi_instance.rs b/c-api-wasmer2/src/capi_instance.rs index fe13192f..26c90f2e 100644 --- a/c-api-wasmer2/src/capi_instance.rs +++ b/c-api-wasmer2/src/capi_instance.rs @@ -341,3 +341,28 @@ pub unsafe extern "C" fn vm_exec_w2_instance_cache( ) } } + +/// Returns the instance to the state it was in immediately after creation, so it can serve +/// another execution. +/// +/// Memory contents and length, every mutable global, and the metering and breakpoint globals +/// are all restored. The handle stays valid — the instance is rebuilt behind it, not replaced. +/// +/// On failure the caller must destroy the instance rather than reuse it: an instance whose +/// reset failed still carries the previous execution's state. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w2_instance_reset( + instance: *mut vm_exec_w2_instance_t, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::instance_reset(instance as *mut CapiInstance, err_buf, err_buf_len) + } +} diff --git a/c-api-wasmer2/tests/ffi_boundary.rs b/c-api-wasmer2/tests/ffi_boundary.rs index c046e2ae..35b2af1d 100644 --- a/c-api-wasmer2/tests/ffi_boundary.rs +++ b/c-api-wasmer2/tests/ffi_boundary.rs @@ -970,3 +970,58 @@ fn instance_from_non_artifact_bytes_is_refused() { unsafe { vm_exec_w2_executor_destroy(executor) } } + +/// Reset across the boundary: the entry point is wired, and the handle survives it. +/// +/// Only the wiring is asserted here. What reset actually *clears* is checked in the backend's +/// own `reset_test.rs`, against globals and memory this layer cannot see — and the hook log +/// this file uses to observe memory is process-global, so a test that mutated it would race +/// the hook-dispatch tests rather than add coverage. +#[test] +fn reset_keeps_the_handle_usable() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + let before = names( + instance, + vm_exec_w2_instance_exported_function_names_length, + vm_exec_w2_instance_exported_function_names, + ); + let result = call(instance, "doNothing", &mut err); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); + + let status = unsafe { vm_exec_w2_instance_reset(instance, err.ptr(), ERR_BUF_LEN) }; + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "reset failed: {}", + err.message() + ); + + // The same pointer still calls, and still describes the same module: reset rebuilds the + // instance behind the handle rather than invalidating it. + let result = call(instance, "doNothing", &mut err); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); + assert_eq!( + names( + instance, + vm_exec_w2_instance_exported_function_names_length, + vm_exec_w2_instance_exported_function_names, + ), + before + ); + + unsafe { + vm_exec_w2_instance_destroy(instance); + vm_exec_w2_executor_destroy(executor); + } +} + +/// A null instance is refused without a message, like every other entry point. +#[test] +fn reset_refuses_a_null_instance() { + let mut err = ErrBuf::new(); + let status = unsafe { vm_exec_w2_instance_reset(std::ptr::null_mut(), err.ptr(), ERR_BUF_LEN) }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_NULL_POINTER); +} diff --git a/c-api-wasmer7/libvmexeccapi_w7.h b/c-api-wasmer7/libvmexeccapi_w7.h index df9f6c0a..b2ff0c90 100644 --- a/c-api-wasmer7/libvmexeccapi_w7.h +++ b/c-api-wasmer7/libvmexeccapi_w7.h @@ -197,6 +197,22 @@ vm_exec_status_t vm_exec_w7_instance_cache_length(struct vm_exec_w7_instance_t * */ vm_exec_status_t vm_exec_w7_instance_cache(struct vm_exec_w7_instance_t *instance, uint8_t *dest, uint32_t dest_len, char *err_buf, int err_buf_len); +/** + * Returns the instance to the state it was in immediately after creation, so it can serve + * another execution. + * + * Memory contents and length, every mutable global, and the metering and breakpoint globals + * are all restored. The handle stays valid — the instance is rebuilt behind it, not replaced. + * + * On failure the caller must destroy the instance rather than reuse it: an instance whose + * reset failed still carries the previous execution's state. + * + * # Safety + * + * C API function, works with raw object pointers. + */ +vm_exec_status_t vm_exec_w7_instance_reset(struct vm_exec_w7_instance_t *instance, char *err_buf, int err_buf_len); + /** * Loads from instance memory into a caller-provided buffer, **truncating** at the end of * the memory. diff --git a/c-api-wasmer7/src/capi_instance.rs b/c-api-wasmer7/src/capi_instance.rs index e15ae183..9a733a72 100644 --- a/c-api-wasmer7/src/capi_instance.rs +++ b/c-api-wasmer7/src/capi_instance.rs @@ -341,3 +341,28 @@ pub unsafe extern "C" fn vm_exec_w7_instance_cache( ) } } + +/// Returns the instance to the state it was in immediately after creation, so it can serve +/// another execution. +/// +/// Memory contents and length, every mutable global, and the metering and breakpoint globals +/// are all restored. The handle stays valid — the instance is rebuilt behind it, not replaced. +/// +/// On failure the caller must destroy the instance rather than reuse it: an instance whose +/// reset failed still carries the previous execution's state. +/// +/// # Safety +/// +/// C API function, works with raw object pointers. +#[allow(clippy::cast_ptr_alignment)] +#[unsafe(no_mangle)] +#[capi_safe_unwind(vm_exec_status_t::VM_EXEC_PANIC)] +pub unsafe extern "C" fn vm_exec_w7_instance_reset( + instance: *mut vm_exec_w7_instance_t, + err_buf: *mut c_char, + err_buf_len: c_int, +) -> vm_exec_status_t { + unsafe { + capi_instance_ops::instance_reset(instance as *mut CapiInstance, err_buf, err_buf_len) + } +} diff --git a/c-api-wasmer7/tests/ffi_boundary.rs b/c-api-wasmer7/tests/ffi_boundary.rs index 1e839566..d8b59891 100644 --- a/c-api-wasmer7/tests/ffi_boundary.rs +++ b/c-api-wasmer7/tests/ffi_boundary.rs @@ -970,3 +970,58 @@ fn instance_from_non_artifact_bytes_is_refused() { unsafe { vm_exec_w7_executor_destroy(executor) } } + +/// Reset across the boundary: the entry point is wired, and the handle survives it. +/// +/// Only the wiring is asserted here. What reset actually *clears* is checked in the backend's +/// own `reset_test.rs`, against globals and memory this layer cannot see — and the hook log +/// this file uses to observe memory is process-global, so a test that mutated it would race +/// the hook-dispatch tests rather than add coverage. +#[test] +fn reset_keeps_the_handle_usable() { + let mut err = ErrBuf::new(); + let executor = new_executor(&mut err); + let instance = new_instance(executor, &mut err); + + let before = names( + instance, + vm_exec_w7_instance_exported_function_names_length, + vm_exec_w7_instance_exported_function_names, + ); + let result = call(instance, "doNothing", &mut err); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); + + let status = unsafe { vm_exec_w7_instance_reset(instance, err.ptr(), ERR_BUF_LEN) }; + assert_eq!( + status, + vm_exec_status_t::VM_EXEC_OK, + "reset failed: {}", + err.message() + ); + + // The same pointer still calls, and still describes the same module: reset rebuilds the + // instance behind the handle rather than invalidating it. + let result = call(instance, "doNothing", &mut err); + assert_eq!(result.kind, vm_exec_call_result_kind_t::VM_EXEC_CALL_OK); + assert_eq!( + names( + instance, + vm_exec_w7_instance_exported_function_names_length, + vm_exec_w7_instance_exported_function_names, + ), + before + ); + + unsafe { + vm_exec_w7_instance_destroy(instance); + vm_exec_w7_executor_destroy(executor); + } +} + +/// A null instance is refused without a message, like every other entry point. +#[test] +fn reset_refuses_a_null_instance() { + let mut err = ErrBuf::new(); + let status = unsafe { vm_exec_w7_instance_reset(std::ptr::null_mut(), err.ptr(), ERR_BUF_LEN) }; + assert_eq!(status, vm_exec_status_t::VM_EXEC_NULL_POINTER); +} diff --git a/vm-executor-experimental/src/we_error.rs b/vm-executor-experimental/src/we_error.rs index b06d3eb5..5d70dcd5 100644 --- a/vm-executor-experimental/src/we_error.rs +++ b/vm-executor-experimental/src/we_error.rs @@ -25,7 +25,4 @@ pub enum ExperimentalError { #[error("cache artifact is empty")] EmptyCacheArtifact, - - #[error("resetting an instance is not supported")] - ResetUnsupported, } diff --git a/vm-executor-experimental/src/we_instance.rs b/vm-executor-experimental/src/we_instance.rs index 20bfc972..2a7743fa 100644 --- a/vm-executor-experimental/src/we_instance.rs +++ b/vm-executor-experimental/src/we_instance.rs @@ -19,6 +19,16 @@ const MAX_MEMORY_PAGES_ALLOWED: Pages = Pages(20); pub struct ExperimentalInstance { wasmer_store: wasmer::Store, inner: Rc, + + /// Kept so the instance can be rebuilt in place by [`Instance::reset`]. + /// + /// Cheap to hold — `Module` is a handle over refcounted compiled code, not a copy of it — + /// and it is the only way to re-instantiate without going back to the wasm bytes. + module: Module, + + /// Kept for the same reason: re-instantiating means building a fresh import object, and + /// that needs the runtime the hooks are reached through. + runtime: Arc, } pub struct ExperimentalInstanceInner { @@ -104,7 +114,7 @@ impl ExperimentalInstance { trace!("Compiling module ..."); let module = Module::new(&store, wasm_bytes)?; - Self::try_from_module(runtime, store, &module) + Self::try_from_module(runtime, store, module) } /// Instantiates a module from a previously serialized artifact, skipping compilation. @@ -135,22 +145,24 @@ impl ExperimentalInstance { // SAFETY: see the trust note above — the caller vouches for the artifact's origin. let module = unsafe { Module::deserialize(&store, cache_bytes)? }; - Self::try_from_module(runtime, store, &module) + Self::try_from_module(runtime, store, module) } /// The half the two entry points share: everything after there is a `Module`. fn try_from_module( runtime: Arc, mut store: Store, - module: &Module, + module: Module, ) -> Result { let inner = rc_new_cyclic_fallible(|weak| { - prepare_wasmer_instance_inner(runtime, module, &mut store, weak) + prepare_wasmer_instance_inner(runtime.clone(), &module, &mut store, weak) })?; Ok(ExperimentalInstance { wasmer_store: store, inner, + module, + runtime, }) } @@ -338,8 +350,45 @@ impl Instance for ExperimentalInstance { get_points_used(&self.inner.wasmer_instance, &mut self.wasmer_store) } - fn reset(&self) -> Result<(), ExecutorError> { - Err(ExperimentalError::ResetUnsupported.into()) + /// Re-instantiates the module into a fresh store, in place. + /// + /// Wasmer 7 has no `Instance::reset` — the only `reset` it offers is `Memory::reset`, + /// which empties the memory and restores neither its contents nor the globals — so there + /// is nothing to rewind an instance with. What there is, is the module: instantiating it + /// again produces exactly the state a fresh instance would have, because that is the same + /// operation. So this throws the instance away and builds another one behind the same + /// handle. + /// + /// **The store is rebuilt, not reused.** It owns the instance's memory and globals, so + /// keeping it would keep the very state being discarded. The *engine* is reused — cloned + /// out of the old store, which is a refcount bump — and that is what makes this cheap: + /// the module stays compiled, and only instantiation is repeated. + /// + /// Compared with the Wasmer 2.2 fork's `Instance::reset`, which rewinds globals and + /// memories in place, this is slower — it re-runs the whole instantiation, including + /// rebuilding the import object. It is also stricter: tables and passive-element + /// bookkeeping come back too, which the fork's version does not restore. See + /// `PLAN-reset-via-middlewares.md` for the faster in-place design and why it was not + /// built yet. + /// + /// On failure the instance is left untouched rather than half-rebuilt, since the new + /// store and inner are only swapped in once both exist. + fn reset(&mut self) -> Result<(), ExecutorError> { + // Same engine, so the already-compiled module is still valid in the new store. + let mut store = Store::new(self.wasmer_store.engine().clone()); + + let runtime = self.runtime.clone(); + let module = self.module.clone(); + let inner = rc_new_cyclic_fallible(|weak| { + prepare_wasmer_instance_inner(runtime, &module, &mut store, weak) + })?; + + // Dropping the old store releases the old instance's memory and globals, and with + // them the import object holding the previous `Weak` back-reference. + self.wasmer_store = store; + self.inner = inner; + + Ok(()) } fn cache(&self) -> Result, ExecutorError> { diff --git a/vm-executor-experimental/tests/reset_test.rs b/vm-executor-experimental/tests/reset_test.rs new file mode 100644 index 00000000..584337c6 --- /dev/null +++ b/vm-executor-experimental/tests/reset_test.rs @@ -0,0 +1,241 @@ +//! `Instance::reset` on the Wasmer 7 backend. +//! +//! The implementation re-instantiates the module into a fresh store, so the thing to pin is +//! not that it runs but that it actually *clears* — every piece of per-execution state a +//! contract can leave behind has to be gone afterwards. A reset that silently kept state +//! would be worse than no reset at all: the host would reuse the instance believing it was +//! clean, and one execution's leftovers would be visible to the next. +//! +//! The checks are made **by the contract, not by the host**. This backend exposes neither +//! memory nor globals from outside a host call, and the globals here are deliberately not +//! exported — so nothing outside the module could observe them, and equally nothing outside +//! could have restored them by hand. That is precisely the state a host-side reset cannot +//! reach, and the reason this backend rebuilds the instance instead. See +//! `PLAN-reset-via-middlewares.md` for what it would take to do it in place. +//! +//! So each fixture exports an `expect_*` function that traps unless the state is what it +//! should be, and the assertion is on the call's outcome. + +use multiversx_chain_vm_executor::{ + CompilationOptions, Executor, ExecutorRuntimeDefault, Instance, InstanceCallResult, + OpcodeConfig, OpcodeCost, +}; +use multiversx_chain_vm_executor_wasmer_experimental::ExperimentalExecutor; +use std::sync::Arc; +use wasmer::wat2wasm; + +const POINTS_LIMIT: u64 = 1_000_000; + +fn executor() -> ExperimentalExecutor { + ExperimentalExecutor::new(Arc::new(ExecutorRuntimeDefault)) +} + +fn options() -> CompilationOptions { + CompilationOptions { + max_memory_grow: 10, + max_memory_grow_delta: 10, + ..Default::default() + } +} + +fn instance(wat: &str) -> Box { + let wasm = wat2wasm(wat.as_bytes()).unwrap().into_owned(); + executor().new_instance(&wasm, &options()).unwrap() +} + +/// The contract's own assertion passed. +fn call_ok(instance: &mut Box, func: &str) { + match instance.call(func, POINTS_LIMIT) { + InstanceCallResult::Ok => {} + other => panic!("expected {func} to succeed, got {}", describe(&other)), + } +} + +/// The contract's own assertion failed, by trapping. +fn call_traps(instance: &mut Box, func: &str) { + match instance.call(func, POINTS_LIMIT) { + InstanceCallResult::RuntimeError(_) => {} + other => panic!("expected {func} to trap, got {}", describe(&other)), + } +} + +fn describe(result: &InstanceCallResult) -> &'static str { + match result { + InstanceCallResult::Ok => "Ok", + InstanceCallResult::FunctionNotFound => "FunctionNotFound", + InstanceCallResult::RuntimeError(_) => "RuntimeError", + InstanceCallResult::VMHooksEarlyExit(_) => "VMHooksEarlyExit", + InstanceCallResult::Breakpoint(_) => "Breakpoint", + } +} + +/// A mutable global that is **not exported**, bumped on every call. +const COUNTER_WAT: &str = r#" +(module + (memory (export "memory") 1) + (global $counter (mut i32) (i32.const 0)) + (func (export "bump") + (global.set $counter (i32.add (global.get $counter) (i32.const 1)))) + (func (export "expect_one") + (if (i32.ne (global.get $counter) (i32.const 1)) (then unreachable))) +) +"#; + +/// Memory with an active data segment, so a reset has to re-apply it rather than merely zero +/// the page. Offset 0 is covered by the segment; offset 64 is not. +const MEMORY_WAT: &str = r#" +(module + (memory (export "memory") 1) + (data (i32.const 0) "init") + (func (export "scribble") + (i32.store8 (i32.const 0) (i32.const 88)) + (i32.store8 (i32.const 64) (i32.const 99))) + (func (export "expect_pristine") + ;; 105 is 'i', the first byte of the data segment + (if (i32.ne (i32.load8_u (i32.const 0)) (i32.const 105)) (then unreachable)) + (if (i32.ne (i32.load8_u (i32.const 64)) (i32.const 0)) (then unreachable))) +) +"#; + +/// Grows its own memory, so a reset has to restore the *length*, not just the contents. +const GROW_WAT: &str = r#" +(module + (memory (export "memory") 1) + (func (export "grow") (drop (memory.grow (i32.const 1)))) + (func (export "expect_one_page") + (if (i32.ne (memory.size) (i32.const 1)) (then unreachable))) +) +"#; + +#[test] +fn reset_clears_an_unexported_mutable_global() { + let mut instance = instance(COUNTER_WAT); + + call_ok(&mut instance, "bump"); + call_ok(&mut instance, "expect_one"); + + call_ok(&mut instance, "bump"); + call_traps(&mut instance, "expect_one"); // now 2 — the fixture really does accumulate + + instance.reset().unwrap(); + + call_ok(&mut instance, "bump"); + call_ok(&mut instance, "expect_one"); // back to 1, so the global was cleared +} + +#[test] +fn reset_restores_the_memory_image() { + let mut instance = instance(MEMORY_WAT); + + call_ok(&mut instance, "expect_pristine"); + call_ok(&mut instance, "scribble"); + call_traps(&mut instance, "expect_pristine"); + + instance.reset().unwrap(); + + // Both halves matter: the data segment is re-applied, and bytes it does not cover are + // zero again. Zeroing alone would fail the first check; re-applying alone, the second. + call_ok(&mut instance, "expect_pristine"); +} + +#[test] +fn reset_restores_the_memory_length() { + let mut instance = instance(GROW_WAT); + + call_ok(&mut instance, "expect_one_page"); + call_ok(&mut instance, "grow"); + call_traps(&mut instance, "expect_one_page"); + + instance.reset().unwrap(); + + call_ok(&mut instance, "expect_one_page"); +} + +/// Priced options, since the default schedule is free and would make any gas assertion +/// vacuous. `global.set` is what `bump` does, so that is the one opcode that needs a cost. +fn metered_options() -> CompilationOptions { + CompilationOptions { + opcode_config: Arc::new(OpcodeConfig { + opcode_cost: OpcodeCost { + opcode_globalset: 10, + ..Default::default() + }, + ..Default::default() + }), + ..options() + } +} + +#[test] +fn reset_clears_the_gas_counter() { + let wasm = wat2wasm(COUNTER_WAT.as_bytes()).unwrap().into_owned(); + let mut instance = executor().new_instance(&wasm, &metered_options()).unwrap(); + + call_ok(&mut instance, "bump"); + assert!( + instance.get_points_used().unwrap() > 0, + "the call should have cost something" + ); + + instance.reset().unwrap(); + + assert_eq!( + instance.get_points_used().unwrap(), + 0, + "the metering global survived the reset" + ); +} + +/// A reset instance is the same instance: same module, same endpoints, still callable. The +/// host holds it across the reset, so its identity has to survive even though its state does +/// not. +#[test] +fn reset_keeps_the_instance_usable_and_identical() { + let mut instance = instance(COUNTER_WAT); + let exported = instance.get_exported_function_names(); + let imported = instance.get_imported_function_names(); + + instance.reset().unwrap(); + + assert_eq!(instance.get_exported_function_names(), exported); + assert_eq!(instance.get_imported_function_names(), imported); + assert!(instance.has_function("bump")); + assert!(instance.check_signatures()); + + call_ok(&mut instance, "bump"); +} + +#[test] +fn reset_is_repeatable() { + let mut instance = instance(COUNTER_WAT); + + for _ in 0..3 { + call_ok(&mut instance, "bump"); + call_ok(&mut instance, "expect_one"); + instance.reset().unwrap(); + } +} + +/// An instance restored from a cached artifact resets like any other: the artifact carries +/// the module, and reset re-instantiates the module. +#[test] +fn reset_works_on_an_instance_from_cache() { + let wasm = wat2wasm(COUNTER_WAT.as_bytes()).unwrap().into_owned(); + let artifact = executor() + .new_instance(&wasm, &options()) + .unwrap() + .cache() + .unwrap(); + + let mut instance = executor() + .new_instance_from_cache(&artifact, &options()) + .unwrap(); + + call_ok(&mut instance, "bump"); + call_ok(&mut instance, "bump"); + + instance.reset().unwrap(); + + call_ok(&mut instance, "bump"); + call_ok(&mut instance, "expect_one"); +} diff --git a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs index 5d4bcd0a..68ad7802 100644 --- a/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs +++ b/vm-executor-wasmer/src/new_traits/wasmer_prod_instance.rs @@ -103,7 +103,7 @@ impl Instance for WasmerProdInstance { .map_err(|err| WasmerExecutorError::WrappedInstance(err).into()) } - fn reset(&self) -> Result<(), ExecutorError> { + fn reset(&mut self) -> Result<(), ExecutorError> { self.inner_instance_ref .reset() .map_err(|err| WasmerExecutorError::WrappedInstance(err).into()) diff --git a/vm-executor/src/new_traits/instance_new.rs b/vm-executor/src/new_traits/instance_new.rs index 00f2193e..f7fa9ab8 100644 --- a/vm-executor/src/new_traits/instance_new.rs +++ b/vm-executor/src/new_traits/instance_new.rs @@ -32,8 +32,22 @@ pub trait Instance { /// Returns the number of points(gas) used by the given instance. fn get_points_used(&mut self) -> Result; - /// Resets an instance, cleaning memories and globals. - fn reset(&self) -> Result<(), ExecutorError>; + /// Returns the instance to the state it was in immediately after creation, so that it can + /// serve another execution. + /// + /// Memory contents and length, every mutable global, and anything else a call can leave + /// behind must be restored. What must *not* change is the instance's identity: the host + /// holds this instance across the reset, so a backend has to rebuild in place rather than + /// hand back a new object. + /// + /// Takes `&mut self` because that is what it is: the whole point is to mutate the + /// instance. A backend may need to replace its internals wholesale — see + /// `ExperimentalInstance::reset`, which rebuilds the store — and `&self` would force it to + /// reach for interior mutability to express something that is not shared at all. + /// + /// A backend that cannot offer this returns an error, and the host is expected to discard + /// the instance and build a fresh one. + fn reset(&mut self) -> Result<(), ExecutorError>; /// Caches an instance. fn cache(&self) -> Result, ExecutorError>; From 0b5485db09caf11298d2b2de50e36de619faf084 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 13 Oct 2026 17:28:10 +0300 Subject: [PATCH 26/26] ExperimentalExecutor docs --- vm-executor-experimental/src/we_instance.rs | 51 ++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/vm-executor-experimental/src/we_instance.rs b/vm-executor-experimental/src/we_instance.rs index 2a7743fa..671cbc3e 100644 --- a/vm-executor-experimental/src/we_instance.rs +++ b/vm-executor-experimental/src/we_instance.rs @@ -16,23 +16,72 @@ use wasmer::{Extern, ExternType, Module, Pages, Store}; const MAX_MEMORY_PAGES_ALLOWED: Pages = Pages(20); +/// One instantiated contract, and everything needed to put it back the way it started. +/// +/// # What it owns +/// +/// A store, an instance inside that store, and the two things a rebuild needs: the compiled +/// module and the runtime the imports are built from. The store is *per instance*, not per +/// executor — see [`new_store`] for why the metering middleware forces that — so this struct +/// is the whole of one contract's world: its memory, its globals, its tables, and the engine +/// its code was compiled by. +/// +/// # Not `Sync`, not `Send`, and not reentrant +/// +/// `inner` is an `Rc`, so an instance belongs to one thread; the Go VM honours that because +/// cgo pins a goroutine to its OS thread for the duration of a call. +/// +/// Reentrancy is the caller's obligation, not something checked here: every entry point that +/// can touch the store takes `&mut self`, so calling one while a call is in flight would +/// alias. Nesting into a *different* instance is fine and is what the Go VM does for +/// `executeOnDestContext`; re-entering the same one is refused by the host, in +/// `Wasmer7Instance.inCall`. +/// +/// # The back-reference, and why `inner` is separate +/// +/// The import object a hook is dispatched through has to be able to find the instance's +/// memory, but it is built *before* the instance exists. [`ExperimentalInstanceInner`] is +/// the knot that ties: it is created through `rc_new_cyclic_fallible`, so the imports can +/// capture a `Weak` to it that becomes valid the moment instantiation succeeds. That is the +/// only reason the wasmer instance is not simply a field here. pub struct ExperimentalInstance { + /// Owns the instance's memory, globals and tables, and the engine its module was + /// compiled by. Replaced wholesale by [`Instance::reset`]. wasmer_store: wasmer::Store, + + /// The wasmer instance, behind the `Rc` the import object holds a `Weak` to. inner: Rc, /// Kept so the instance can be rebuilt in place by [`Instance::reset`]. /// /// Cheap to hold — `Module` is a handle over refcounted compiled code, not a copy of it — - /// and it is the only way to re-instantiate without going back to the wasm bytes. + /// and it is the only way to re-instantiate without going back to the wasm bytes. It is + /// also what [`Instance::cache`] serializes. module: Module, /// Kept for the same reason: re-instantiating means building a fresh import object, and /// that needs the runtime the hooks are reached through. + /// + /// An `Arc` rather than a concrete type, because the runtime is the + /// host's — the Go VM's hook bridge, or the Rust VM's — and the backend must not know + /// which. It is cloned into each import object, so the imports and this struct keep the + /// runtime alive between them for as long as either exists. runtime: Arc, } +/// The part of an instance that a hook's import object can hold a `Weak` to. +/// +/// Split out of [`ExperimentalInstance`] for one reason, given in that type's documentation: +/// the import object has to be built before the instance it belongs to exists, so the back +/// reference has to go through something tied by `rc_new_cyclic_fallible`. pub struct ExperimentalInstanceInner { pub wasmer_instance: wasmer::Instance, + + /// The export name of the contract's single memory, resolved once at instantiation. + /// + /// Held rather than looked up per access because every hook that touches memory would + /// otherwise re-scan the export list, and because the "exactly one memory" rule is + /// checked at instantiation — recording the name is what carries that check forward. pub memory_name: String, }