| Index: src/wasm/wasm-debug.cc
|
| diff --git a/src/wasm/wasm-debug.cc b/src/wasm/wasm-debug.cc
|
| index f1b01593cf437415f2d4143af5e03734149a2851..1ee45d5ccfa29de433ddedef17ec3b39a0e889a5 100644
|
| --- a/src/wasm/wasm-debug.cc
|
| +++ b/src/wasm/wasm-debug.cc
|
| @@ -5,10 +5,15 @@
|
| #include "src/wasm/wasm-debug.h"
|
|
|
| #include "src/assert-scope.h"
|
| +#include "src/compiler/wasm-compiler.h"
|
| #include "src/debug/debug.h"
|
| #include "src/factory.h"
|
| +#include "src/frames-inl.h"
|
| #include "src/isolate.h"
|
| +// We need src/assembler-$arch-inl.h, and macro-assembler.h includes this.
|
| +#include "src/macro-assembler.h"
|
| #include "src/wasm/module-decoder.h"
|
| +#include "src/wasm/wasm-interpreter.h"
|
| #include "src/wasm/wasm-module.h"
|
|
|
| using namespace v8::internal;
|
| @@ -21,6 +26,9 @@ enum {
|
| kWasmDebugInfoWasmBytesHash,
|
| kWasmDebugInfoFunctionByteOffsets,
|
| kWasmDebugInfoFunctionScripts,
|
| + kWasmDebugInfoInterpreterAddr,
|
| + kWasmDebugInfoInterpretedFunctions,
|
| + kWasmDebugInfoDebugInfos,
|
| kWasmDebugInfoNumEntries
|
| };
|
|
|
| @@ -68,17 +76,312 @@ std::pair<int, int> GetFunctionOffsetAndLength(Handle<WasmDebugInfo> debug_info,
|
|
|
| Vector<const uint8_t> GetFunctionBytes(Handle<WasmDebugInfo> debug_info,
|
| int func_index) {
|
| - SeqOneByteString *module_bytes =
|
| - wasm::GetWasmBytes(debug_info->wasm_object());
|
| std::pair<int, int> offset_and_length =
|
| GetFunctionOffsetAndLength(debug_info, func_index);
|
| + SeqOneByteString *module_bytes =
|
| + wasm::GetWasmBytes(debug_info->wasm_object());
|
| return Vector<const uint8_t>(
|
| module_bytes->GetChars() + offset_and_length.first,
|
| offset_and_length.second);
|
| }
|
|
|
| +FixedArray *GetOrCreateInterpretedFunctions(Handle<WasmDebugInfo> debug_info) {
|
| + Object *maybe_arr = debug_info->get(kWasmDebugInfoInterpretedFunctions);
|
| + Isolate *isolate = debug_info->GetIsolate();
|
| + if (!maybe_arr->IsUndefined(isolate)) return FixedArray::cast(maybe_arr);
|
| +
|
| + FixedArray *new_arr = *isolate->factory()->NewFixedArray(
|
| + wasm::GetNumberOfFunctions(debug_info->wasm_object()));
|
| + debug_info->set(kWasmDebugInfoInterpretedFunctions, new_arr);
|
| + return new_arr;
|
| +}
|
| +
|
| +void RedirectFunction(Code *code, Code *old_target, Code *new_target) {
|
| + for (RelocIterator it(code, RelocInfo::kCodeTargetMask); !it.done();
|
| + it.next()) {
|
| + DCHECK(RelocInfo::IsCodeTarget(it.rinfo()->rmode()));
|
| + Code *target = Code::GetCodeFromTargetAddress(it.rinfo()->target_address());
|
| + if (target != old_target) continue;
|
| + it.rinfo()->set_target_address(new_target->instruction_start(),
|
| + SKIP_WRITE_BARRIER, SKIP_ICACHE_FLUSH);
|
| + }
|
| +}
|
| +
|
| +void RedirectFunctionEverywhere(Handle<JSObject> wasm, Handle<Code> old_target,
|
| + Handle<Code> new_target) {
|
| + // Redirect in all wasm functions.
|
| + for (int i = 0, e = wasm::GetNumberOfFunctions(*wasm); i < e; ++i)
|
| + RedirectFunction(wasm::GetWasmFunctionCode(*wasm, i), *old_target,
|
| + *new_target);
|
| +
|
| + // Redirect in the code of all exported functions.
|
| + Isolate *isolate = wasm->GetIsolate();
|
| + // For AsmJs modules, the exports are stored on the wasm object itself.
|
| + MaybeHandle<Object> maybe_exports =
|
| + JSReceiver::GetProperty(isolate, wasm, "exports");
|
| + Handle<JSObject> exports_object =
|
| + maybe_exports.is_null()
|
| + ? wasm
|
| + : Handle<JSObject>::cast(maybe_exports.ToHandleChecked());
|
| +
|
| + Handle<FixedArray> values;
|
| + if (JSReceiver::GetOwnValues(Handle<JSReceiver>::cast(exports_object),
|
| + ALL_PROPERTIES)
|
| + .ToHandle(&values)) {
|
| + for (int i = 0, e = values->length(); i != e; ++i) {
|
| + if (!values->get(i)->IsJSFunction()) continue;
|
| + Code *code = JSFunction::cast(values->get(i))->code();
|
| + RedirectFunction(code, *old_target, *new_target);
|
| + }
|
| + }
|
| +}
|
| +
|
| +class InterpreterHandle {
|
| + v8::base::AccountingAllocator allocator;
|
| + Zone zone;
|
| + Handle<Object> global_ref;
|
| + std::unique_ptr<const WasmModule> module;
|
| + std::unique_ptr<WasmModuleInstance> module_instance;
|
| + Isolate *isolate;
|
| +
|
| + uint8_t *parameter_buffer;
|
| + uint32_t parameter_buffer_size = 0;
|
| +#ifdef DEBUG
|
| + // Track the function index for which the last buffer was allocated.
|
| + // This is updated on AllocateParameterBuffer, and checked at Execute.
|
| + uint32_t parameter_buffer_taken = static_cast<uint32_t>(-1);
|
| +#endif
|
| +
|
| + public:
|
| + WasmInterpreter interpreter;
|
| +
|
| + // Initialize in the right order. WasmInterpreter has to be allocate in place,
|
| + // since it's not std::move'able.
|
| + explicit InterpreterHandle(WasmDebugInfo *debug_info)
|
| + : allocator(),
|
| + zone(&allocator),
|
| + module(CreateModule(&zone, debug_info)),
|
| + isolate(debug_info->GetIsolate()),
|
| + interpreter(CreateModuleInstance(module.get(), debug_info), &zone) {
|
| + global_ref = debug_info->GetIsolate()->global_handles()->Create(debug_info);
|
| + GlobalHandles::MakeWeak(global_ref.location(), this,
|
| + InterpreterHandle::Finalize,
|
| + v8::WeakCallbackType::kParameter);
|
| + }
|
| +
|
| + static void Finalize(const v8::WeakCallbackInfo<void> &data) {
|
| + void *param = data.GetParameter();
|
| + InterpreterHandle *handle = reinterpret_cast<InterpreterHandle *>(param);
|
| + GlobalHandles::ClearWeakness(handle->global_ref.location());
|
| + GlobalHandles::Destroy(handle->global_ref.location());
|
| + delete handle;
|
| + }
|
| +
|
| + static const WasmModule *CreateModule(Zone *zone, WasmDebugInfo *debug_info) {
|
| + SeqOneByteString *wasm_bytes =
|
| + wasm::GetWasmBytes(debug_info->wasm_object());
|
| + const byte *bytes_start = wasm_bytes->GetChars();
|
| + const byte *bytes_end = bytes_start + wasm_bytes->length();
|
| + ModuleResult module_result =
|
| + DecodeWasmModule(debug_info->GetIsolate(), zone, bytes_start, bytes_end,
|
| + false, ModuleOrigin::kWasmOrigin);
|
| + DCHECK(module_result.ok());
|
| + return module_result.val;
|
| + }
|
| +
|
| + static WasmModuleInstance *CreateModuleInstance(const WasmModule *module,
|
| + WasmDebugInfo *debug_info) {
|
| + JSArrayBuffer *memory =
|
| + wasm::GetWasmMemoryBuffer(debug_info->wasm_object());
|
| +
|
| + WasmModuleInstance *instance = new WasmModuleInstance(module);
|
| + instance->mem_start = reinterpret_cast<byte *>(memory->backing_store());
|
| + instance->mem_size = memory->byte_length()->Number();
|
| + instance->mem_buffer = handle(memory, debug_info->GetIsolate());
|
| +
|
| + return instance;
|
| + }
|
| +
|
| + uint8_t *GetParameterBuffer(uint32_t function_index) {
|
| + DCHECK_LE(function_index, module->functions.size());
|
| + DCHECK_EQ(static_cast<uint32_t>(-1), parameter_buffer_taken);
|
| +#ifdef DEBUG
|
| + parameter_buffer_taken = function_index;
|
| +#endif
|
| + uint32_t size_needed = 0;
|
| + FunctionSig *sig = module->functions[function_index].sig;
|
| + for (size_t i = 0, e = sig->parameter_count(); i != e; ++i) {
|
| + size_needed += 1 << ElementSizeLog2Of(sig->GetParam(i));
|
| + }
|
| + if (size_needed > parameter_buffer_size) {
|
| + uint32_t alloc_size = 2 * parameter_buffer_size;
|
| + if (size_needed > alloc_size) alloc_size = size_needed;
|
| + parameter_buffer = zone.NewArray<uint8_t>(alloc_size);
|
| + parameter_buffer_size = alloc_size;
|
| + }
|
| + return parameter_buffer;
|
| + }
|
| +
|
| + void Execute(uint32_t func_index) {
|
| + DCHECK_EQ(parameter_buffer_taken, func_index);
|
| +#ifdef DEBUG
|
| + parameter_buffer_taken = static_cast<uint32_t>(-1);
|
| +#endif
|
| + FunctionSig *sig = module->functions[func_index].sig;
|
| + DCHECK_LE(sig->parameter_count(), static_cast<size_t>(kMaxInt));
|
| + int num_params = static_cast<int>(sig->parameter_count());
|
| + ScopedVector<WasmVal> wasm_args(num_params);
|
| + uint8_t *buf_ptr = parameter_buffer;
|
| + for (int i = 0; i < num_params; ++i) {
|
| + uint32_t param_size = 1 << ElementSizeLog2Of(sig->GetParam(i));
|
| + switch (sig->GetParam(i)) {
|
| + case kAstI32:
|
| + DCHECK_EQ(param_size, sizeof(uint32_t));
|
| + wasm_args[i] = WasmVal(*reinterpret_cast<uint32_t *>(buf_ptr));
|
| + break;
|
| + case kAstI64:
|
| + DCHECK_EQ(param_size, sizeof(uint64_t));
|
| + wasm_args[i] = WasmVal(*reinterpret_cast<uint64_t *>(buf_ptr));
|
| + break;
|
| + case kAstF32:
|
| + DCHECK_EQ(param_size, sizeof(float));
|
| + wasm_args[i] = WasmVal(*reinterpret_cast<float *>(buf_ptr));
|
| + break;
|
| + case kAstF64:
|
| + DCHECK_EQ(param_size, sizeof(double));
|
| + wasm_args[i] = WasmVal(*reinterpret_cast<double *>(buf_ptr));
|
| + break;
|
| + default:
|
| + UNREACHABLE();
|
| + }
|
| + buf_ptr += param_size;
|
| + }
|
| +
|
| + WasmInterpreter::Thread *thread = interpreter.GetThread(0);
|
| + // We do not support reentering an already running interpreter at the moment
|
| + // (like INTERPRETER -> JS -> WASM -> INTERPRETER).
|
| + DCHECK(thread->state() == WasmInterpreter::STOPPED ||
|
| + thread->state() == WasmInterpreter::FINISHED);
|
| + thread->Reset();
|
| + thread->PushFrame(&module->functions[func_index], wasm_args.start());
|
| + WasmInterpreter::State state;
|
| + do {
|
| + state = thread->Run();
|
| + switch (state) {
|
| + case WasmInterpreter::State::PAUSED: {
|
| + // We hit a breakpoint.
|
| + StackTraceFrameIterator frame_it(isolate);
|
| + WasmInterpretedFrame *top_frame =
|
| + WasmInterpretedFrame::cast(frame_it.frame());
|
| + isolate->debug()->Break(top_frame);
|
| + } break;
|
| + case WasmInterpreter::State::FINISHED:
|
| + // Perfect, just break the switch and exit the loop.
|
| + break;
|
| + case WasmInterpreter::State::TRAPPED:
|
| + // TODO(clemensh): Generate appropriate JS exception.
|
| + UNIMPLEMENTED();
|
| + break;
|
| + // STOPPED and RUNNING should never occur here.
|
| + case WasmInterpreter::State::STOPPED:
|
| + case WasmInterpreter::State::RUNNING:
|
| + default:
|
| + UNREACHABLE();
|
| + }
|
| + } while (state != WasmInterpreter::State::FINISHED);
|
| + }
|
| +
|
| + void EnsureInterpreterRedirect(Handle<WasmDebugInfo> debug_info,
|
| + int func_index) {
|
| + Isolate *isolate = debug_info->GetIsolate();
|
| + Handle<FixedArray> interpreted_functions(
|
| + GetOrCreateInterpretedFunctions(debug_info), isolate);
|
| + DCHECK_LT(func_index, interpreted_functions->length());
|
| + if (!interpreted_functions->get(func_index)->IsUndefined(isolate)) return;
|
| +
|
| + Handle<JSObject> wasm(debug_info->wasm_object(), isolate);
|
| + Handle<Code> new_code = compiler::CompileWasmToInterpreter(
|
| + isolate, func_index, module->functions[func_index].sig, wasm);
|
| + Handle<Code> old_code(wasm::GetWasmFunctionCode(*wasm, func_index),
|
| + isolate);
|
| + interpreted_functions->set(func_index, *new_code);
|
| +
|
| + RedirectFunctionEverywhere(wasm, old_code, new_code);
|
| + }
|
| +};
|
| +
|
| +InterpreterHandle *GetOrCreateInterpreterHandle(WasmDebugInfo *debug_info) {
|
| + DisallowHeapAllocation no_gc;
|
| + // We store the raw pointer to the InterpreterHandle in a slot in the debug
|
| + // info.
|
| + // Since the pointer is aligned, it will look like a Smi, and the gc will
|
| + // ignore it.
|
| + Object *handle_obj = debug_info->get(kWasmDebugInfoInterpreterAddr);
|
| + if (!handle_obj->IsUndefined(debug_info->GetIsolate())) {
|
| + DCHECK(!handle_obj->IsHeapObject());
|
| + return reinterpret_cast<InterpreterHandle *>(handle_obj);
|
| + }
|
| +
|
| + InterpreterHandle *handle = new InterpreterHandle(debug_info);
|
| + handle_obj = reinterpret_cast<Object *>(handle);
|
| + DCHECK(!handle_obj->IsHeapObject());
|
| + debug_info->set(kWasmDebugInfoInterpreterAddr, handle_obj);
|
| + return handle;
|
| +}
|
| +
|
| +InterpreterHandle *GetInterpreterHandleIfExists(WasmDebugInfo *debug_info) {
|
| + DisallowHeapAllocation no_gc;
|
| + Object *handle_obj = debug_info->get(kWasmDebugInfoInterpreterAddr);
|
| + return handle_obj->IsUndefined(debug_info->GetIsolate())
|
| + ? nullptr
|
| + : reinterpret_cast<InterpreterHandle *>(handle_obj);
|
| +}
|
| +
|
| } // namespace
|
|
|
| +WasmInstructionIterator::WasmInstructionIterator(const byte *start,
|
| + const byte *end)
|
| + : start_(start), end_(end) {
|
| + base::AccountingAllocator allocator;
|
| + Zone tmp_zone(&allocator);
|
| + AstLocalDecls locals(&tmp_zone);
|
| + bool valid = DecodeLocalDecls(locals, start, end);
|
| + DCHECK(valid);
|
| + USE(valid);
|
| + pc_ = start + locals.decls_encoded_size;
|
| +}
|
| +
|
| +void WasmInstructionIterator::Next() {
|
| + DCHECK(!Done());
|
| + int len = OpcodeLength(pc_, end_);
|
| + DCHECK_LE(0, len); // 0 means validation error / read past the end.
|
| + pc_ += len;
|
| +}
|
| +
|
| +Script *InterpreterFrameInfo::GetScript() const {
|
| + return WasmDebugInfo::GetFunctionScript(debug_info_, func_index_);
|
| +}
|
| +
|
| +InterpreterFrameIterator::InterpreterFrameIterator(
|
| + Handle<WasmDebugInfo> debug_info)
|
| + : frame_nr_(0),
|
| + frame_count_(GetOrCreateInterpreterHandle(*debug_info)
|
| + ->interpreter.GetThread(0)
|
| + ->GetFrameCount()),
|
| + debug_info_(debug_info) {}
|
| +
|
| +InterpreterFrameInfo InterpreterFrameIterator::GetFrameInfo() const {
|
| + DCHECK(!Done());
|
| + WasmInterpreter::Thread *thread =
|
| + GetOrCreateInterpreterHandle(*debug_info_)->interpreter.GetThread(0);
|
| + DCHECK_EQ(frame_count_, thread->GetFrameCount());
|
| + DCHECK_LE(0, frame_nr_);
|
| + std::unique_ptr<const WasmInterpreterFrame> frame(
|
| + thread->GetFrame(frame_nr_));
|
| + return InterpreterFrameInfo(debug_info_, frame->function()->func_index,
|
| + frame->pc());
|
| +}
|
| +
|
| Handle<WasmDebugInfo> WasmDebugInfo::New(Handle<JSObject> wasm) {
|
| Isolate *isolate = wasm->GetIsolate();
|
| Factory *factory = isolate->factory();
|
| @@ -98,7 +401,7 @@ Handle<WasmDebugInfo> WasmDebugInfo::New(Handle<JSObject> wasm) {
|
| return Handle<WasmDebugInfo>::cast(arr);
|
| }
|
|
|
| -bool WasmDebugInfo::IsDebugInfo(Object *object) {
|
| +bool WasmDebugInfo::IsWasmDebugInfo(Object *object) {
|
| if (!object->IsFixedArray()) return false;
|
| FixedArray *arr = FixedArray::cast(object);
|
| Isolate *isolate = arr->GetIsolate();
|
| @@ -108,11 +411,15 @@ bool WasmDebugInfo::IsDebugInfo(Object *object) {
|
| (arr->get(kWasmDebugInfoFunctionByteOffsets)->IsUndefined(isolate) ||
|
| arr->get(kWasmDebugInfoFunctionByteOffsets)->IsByteArray()) &&
|
| (arr->get(kWasmDebugInfoFunctionScripts)->IsUndefined(isolate) ||
|
| - arr->get(kWasmDebugInfoFunctionScripts)->IsFixedArray());
|
| + arr->get(kWasmDebugInfoFunctionScripts)->IsFixedArray()) &&
|
| + (arr->get(kWasmDebugInfoInterpretedFunctions)->IsUndefined(isolate) ||
|
| + arr->get(kWasmDebugInfoInterpretedFunctions)->IsFixedArray()) &&
|
| + (arr->get(kWasmDebugInfoDebugInfos)->IsUndefined(isolate) ||
|
| + arr->get(kWasmDebugInfoDebugInfos)->IsFixedArray());
|
| }
|
|
|
| WasmDebugInfo *WasmDebugInfo::cast(Object *object) {
|
| - DCHECK(IsDebugInfo(object));
|
| + DCHECK(IsWasmDebugInfo(object));
|
| return reinterpret_cast<WasmDebugInfo *>(object);
|
| }
|
|
|
| @@ -120,6 +427,25 @@ JSObject *WasmDebugInfo::wasm_object() {
|
| return JSObject::cast(get(kWasmDebugInfoWasmObj));
|
| }
|
|
|
| +void WasmDebugInfo::SetBreakpoint(Handle<WasmDebugInfo> debug_info,
|
| + int func_index, int byte_offset) {
|
| + DCHECK_LE(0, func_index);
|
| + DCHECK_LE(0, byte_offset);
|
| + InterpreterHandle *handle = GetOrCreateInterpreterHandle(*debug_info);
|
| + handle->EnsureInterpreterRedirect(debug_info, func_index);
|
| + handle->interpreter.SetBreakpoint(static_cast<uint32_t>(func_index),
|
| + byte_offset, true);
|
| +}
|
| +
|
| +bool WasmDebugInfo::HasBreakpoint(int func_index, int byte_offset) {
|
| + DCHECK_LE(0, func_index);
|
| + DCHECK_LE(0, byte_offset);
|
| + InterpreterHandle *handle = GetInterpreterHandleIfExists(this);
|
| + return handle &&
|
| + handle->interpreter.GetBreakpoint(static_cast<uint32_t>(func_index),
|
| + byte_offset);
|
| +}
|
| +
|
| Script *WasmDebugInfo::GetFunctionScript(Handle<WasmDebugInfo> debug_info,
|
| int func_index) {
|
| Isolate *isolate = debug_info->GetIsolate();
|
| @@ -228,3 +554,73 @@ Handle<FixedArray> WasmDebugInfo::GetFunctionOffsetTable(
|
|
|
| return offset_table;
|
| }
|
| +
|
| +DebugInfo *WasmDebugInfo::GetDebugInfo(Handle<WasmDebugInfo> debug_info,
|
| + int func_index) {
|
| + Object *maybe_arr = debug_info->get(kWasmDebugInfoDebugInfos);
|
| + Isolate *isolate = debug_info->GetIsolate();
|
| + Handle<FixedArray> arr;
|
| + if (maybe_arr->IsUndefined(isolate)) {
|
| + arr = isolate->factory()->NewFixedArray(
|
| + wasm::GetNumberOfFunctions(debug_info->wasm_object()));
|
| + debug_info->set(kWasmDebugInfoDebugInfos, *arr);
|
| + } else {
|
| + arr = handle(FixedArray::cast(maybe_arr));
|
| + }
|
| +
|
| + DCHECK_LE(func_index, static_cast<uint32_t>(arr->length()));
|
| + Object *maybe_info = arr->get(static_cast<int>(func_index));
|
| + if (!maybe_info->IsUndefined(isolate)) return DebugInfo::cast(maybe_info);
|
| +
|
| + Handle<AbstractCode> code(AbstractCode::cast(
|
| + wasm::GetWasmFunctionCode(debug_info->wasm_object(), func_index)));
|
| + Handle<DebugInfo> new_debug_info = isolate->factory()->NewDebugInfo(code);
|
| + arr->set(static_cast<int>(func_index), *new_debug_info);
|
| + return *new_debug_info;
|
| +}
|
| +
|
| +WasmDebugInfo::InstructionType WasmDebugInfo::GetInstructionType(
|
| + int func_index, int byte_offset) {
|
| + SeqOneByteString *bytes = wasm::GetWasmBytes(wasm_object());
|
| + DCHECK(byte_offset >= 0 && byte_offset < bytes->length());
|
| + switch (static_cast<WasmOpcode>(bytes->Get(byte_offset))) {
|
| + case WasmOpcode::kExprReturn:
|
| + return RETURN;
|
| + case WasmOpcode::kExprCallImport:
|
| + case WasmOpcode::kExprCallFunction:
|
| + case WasmOpcode::kExprCallIndirect:
|
| + return CALL;
|
| + default:
|
| + return OTHER;
|
| + }
|
| +}
|
| +
|
| +WasmInstructionIterator WasmDebugInfo::GetInstructionIterator(
|
| + Handle<WasmDebugInfo> debug_info, int func_index) {
|
| + Vector<const uint8_t> function_bytes =
|
| + GetFunctionBytes(debug_info, func_index);
|
| +
|
| + return WasmInstructionIterator(function_bytes.start(), function_bytes.end());
|
| +}
|
| +
|
| +InterpreterFrameIterator WasmDebugInfo::GetInterpreterFrameIterator() {
|
| + return InterpreterFrameIterator(handle(this));
|
| +}
|
| +
|
| +Object *WasmDebugInfo::GetInterpreterArgBuffer(int func_index) {
|
| + printf("GetInterpreterArgBuffer %d\n", func_index);
|
| + DCHECK_LE(0, func_index);
|
| + InterpreterHandle *interp_handle = GetOrCreateInterpreterHandle(this);
|
| + uint8_t *buf =
|
| + interp_handle->GetParameterBuffer(static_cast<uint32_t>(func_index));
|
| + Object *obj = reinterpret_cast<Object *>(buf);
|
| + DCHECK(!obj->IsHeapObject());
|
| + return obj;
|
| +}
|
| +
|
| +void WasmDebugInfo::RunInterpreter(int func_index) {
|
| + printf("RunInterpreter %d\n", func_index);
|
| + DCHECK_LE(0, func_index);
|
| + InterpreterHandle *interp_handle = GetOrCreateInterpreterHandle(this);
|
| + interp_handle->Execute(static_cast<uint32_t>(func_index));
|
| +}
|
|
|