Expose a proxy object to evaluateOnCallFrame for WebAssembly

When debugging WebAssembly, calls to evaluateOnCallFrame always return
undefined. This CL enables evaluateOnCallFrame for WebAssembly and
creates a proxy object that is injected into the evaluation context.

Bug: chromium:1127914
Change-Id: I3f5cff3be2c9de45c7b1f3f7ed4fc2e1cc545ac6
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2429265
Commit-Queue: Philip Pfaffe <pfaffe@chromium.org>
Reviewed-by: Clemens Backes <clemensb@chromium.org>
Reviewed-by: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Cr-Commit-Position: refs/heads/master@{#70315}
This commit is contained in:
Philip Pfaffe 2020-10-05 15:41:41 +02:00 committed by Commit Bot
parent a5024f9b4f
commit ae3f94bd2a
6 changed files with 449 additions and 13 deletions

View File

@ -18,6 +18,7 @@
#include "src/objects/contexts.h"
#include "src/snapshot/snapshot.h"
#include "src/wasm/wasm-debug.h"
#include "src/wasm/wasm-js.h"
namespace v8 {
namespace internal {
@ -106,6 +107,9 @@ V8_EXPORT MaybeHandle<Object> DebugEvaluate::WebAssembly(
StackTraceFrameIterator it(isolate, frame_id);
if (!it.is_wasm()) return isolate->factory()->undefined_value();
WasmFrame* frame = WasmFrame::cast(it.frame());
Handle<JSProxy> context_extension = WasmJs::GetJSDebugProxy(frame);
DisableBreak disable_break_scope(isolate->debug(), /*disable=*/true);
@ -114,12 +118,14 @@ V8_EXPORT MaybeHandle<Object> DebugEvaluate::WebAssembly(
return {};
}
Handle<Context> context = isolate->native_context();
Handle<JSObject> receiver(context->global_proxy(), isolate);
Handle<ScopeInfo> scope_info =
ScopeInfo::CreateForWithScope(isolate, Handle<ScopeInfo>::null());
Handle<Context> context = isolate->factory()->NewWithContext(
isolate->native_context(), scope_info, context_extension);
Handle<Object> result;
if (!DebugEvaluate::Evaluate(isolate, shared_info, context, receiver, source,
throw_on_side_effect)
if (!DebugEvaluate::Evaluate(isolate, shared_info, context, context_extension,
source, throw_on_side_effect)
.ToHandle(&result)) {
return {};
}

View File

@ -340,6 +340,12 @@ class DebugInfoImpl {
debug_break_fp);
}
const WasmFunction& GetFunctionAtAddress(Address pc) {
FrameInspectionScope scope(this, pc);
auto* module = native_module_->module();
return module->functions[scope.code->index()];
}
Handle<JSObject> GetLocalScopeObject(Isolate* isolate, Address pc, Address fp,
Address debug_break_fp) {
FrameInspectionScope scope(this, pc);
@ -909,6 +915,10 @@ WasmValue DebugInfo::GetStackValue(int index, Address pc, Address fp,
return impl_->GetStackValue(index, pc, fp, debug_break_fp);
}
const wasm::WasmFunction& DebugInfo::GetFunctionAtAddress(Address pc) {
return impl_->GetFunctionAtAddress(pc);
}
Handle<JSObject> DebugInfo::GetLocalScopeObject(Isolate* isolate, Address pc,
Address fp,
Address debug_break_fp) {

View File

@ -34,6 +34,7 @@ class NativeModule;
class WasmCode;
class WireBytesRef;
class WasmValue;
struct WasmFunction;
// Side table storing information used to inspect Liftoff frames at runtime.
// This table is only created on demand for debugging, so it is not optimized
@ -153,6 +154,9 @@ class V8_EXPORT_PRIVATE DebugInfo {
WasmValue GetLocalValue(int local, Address pc, Address fp,
Address debug_break_fp);
int GetStackDepth(Address pc);
const wasm::WasmFunction& GetFunctionAtAddress(Address pc);
WasmValue GetStackValue(int index, Address pc, Address fp,
Address debug_break_fp);

View File

@ -10,13 +10,16 @@
#include "src/api/api-inl.h"
#include "src/api/api-natives.h"
#include "src/ast/ast.h"
#include "src/base/logging.h"
#include "src/base/overflowing-math.h"
#include "src/common/assert-scope.h"
#include "src/execution/execution.h"
#include "src/execution/frames-inl.h"
#include "src/execution/isolate.h"
#include "src/handles/handles.h"
#include "src/heap/factory.h"
#include "src/init/v8.h"
#include "src/objects/js-collection-inl.h"
#include "src/objects/js-promise-inl.h"
#include "src/objects/objects-inl.h"
#include "src/objects/templates.h"
@ -25,10 +28,12 @@
#include "src/trap-handler/trap-handler.h"
#include "src/wasm/streaming-decoder.h"
#include "src/wasm/value-type.h"
#include "src/wasm/wasm-debug.h"
#include "src/wasm/wasm-engine.h"
#include "src/wasm/wasm-limits.h"
#include "src/wasm/wasm-objects-inl.h"
#include "src/wasm/wasm-serialization.h"
#include "src/wasm/wasm-value.h"
using v8::internal::wasm::ErrorThrower;
using v8::internal::wasm::ScheduledErrorThrower;
@ -1581,7 +1586,7 @@ constexpr const char* kName_WasmTableObject = "WebAssembly.Table";
}
void WebAssemblyInstanceGetExports(
const v8::FunctionCallbackInfo<v8::Value>& args) {
const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
HandleScope scope(isolate);
@ -2020,9 +2025,9 @@ Handle<JSFunction> InstallFunc(Isolate* isolate, Handle<JSObject> object,
}
Handle<JSFunction> InstallConstructorFunc(Isolate* isolate,
Handle<JSObject> object,
const char* str,
FunctionCallback func) {
Handle<JSObject> object,
const char* str,
FunctionCallback func) {
return InstallFunc(isolate, object, str, func, 1, true, DONT_ENUM);
}
@ -2281,6 +2286,388 @@ void WasmJs::Install(Isolate* isolate, bool exposed_on_global_object) {
runtime_error, DONT_ENUM);
}
namespace {
void SetMapValue(Isolate* isolate, Handle<JSMap> map, Handle<Object> key,
Handle<Object> value) {
DCHECK(!map.is_null() && !key.is_null() && !value.is_null());
Handle<Object> argv[] = {key, value};
Execution::CallBuiltin(isolate, isolate->map_set(), map, arraysize(argv),
argv)
.Check();
}
Handle<Object> GetMapValue(Isolate* isolate, Handle<JSMap> map,
Handle<Object> key) {
DCHECK(!map.is_null() && !key.is_null());
Handle<Object> argv[] = {key};
return Execution::CallBuiltin(isolate, isolate->map_get(), map,
arraysize(argv), argv)
.ToHandleChecked();
}
// Look up a name in a name table. Name tables are stored under the "names"
// property of the handler and map names to index.
base::Optional<int> ResolveValueSelector(Isolate* isolate,
Handle<Name> property,
Handle<JSObject> handler,
bool enable_index_lookup) {
size_t index = 0;
if (enable_index_lookup && property->AsIntegerIndex(&index) &&
index < kMaxInt) {
return static_cast<int>(index);
}
Handle<Object> name_table =
JSObject::GetProperty(isolate, handler, "names").ToHandleChecked();
DCHECK(name_table->IsJSMap());
Handle<Object> object =
GetMapValue(isolate, Handle<JSMap>::cast(name_table), property);
if (object->IsUndefined()) return {};
DCHECK(object->IsNumeric());
return NumberToInt32(*object);
}
// Helper for unpacking a maybe name that makes a default with an index if
// the name is empty. If the name is not empty, it's prefixed with a $.
Handle<String> GetNameOrDefault(Isolate* isolate,
MaybeHandle<String> maybe_name,
const char* default_name_prefix, int index) {
Handle<String> name;
if (maybe_name.ToHandle(&name)) {
return isolate->factory()
->NewConsString(isolate->factory()->NewStringFromAsciiChecked("$"),
name)
.ToHandleChecked();
}
// Maximum length of the default names: $memory-2147483648\0
static constexpr int kMaxStrLen = 19;
EmbeddedVector<char, kMaxStrLen> value;
DCHECK_LT(strlen(default_name_prefix) + /*strlen(kMinInt)*/ 11, kMaxStrLen);
int len = SNPrintF(value, "%s%d", default_name_prefix, index);
return isolate->factory()->InternalizeString(value.SubVector(0, len));
}
// Generate names for the locals. Names either come from the name table,
// otherwise the default $varX is used.
std::vector<Handle<String>> GetLocalNames(Handle<WasmInstanceObject> instance,
Address pc) {
wasm::NativeModule* native_module = instance->module_object().native_module();
wasm::DebugInfo* debug_info = native_module->GetDebugInfo();
int num_locals = debug_info->GetNumLocals(pc);
auto* isolate = instance->GetIsolate();
wasm::ModuleWireBytes module_wire_bytes(
instance->module_object().native_module()->wire_bytes());
const wasm::WasmFunction& function = debug_info->GetFunctionAtAddress(pc);
std::vector<Handle<String>> names;
for (int i = 0; i < num_locals; ++i) {
wasm::WireBytesRef local_name_ref =
debug_info->GetLocalName(function.func_index, i);
DCHECK(module_wire_bytes.BoundsCheck(local_name_ref));
Vector<const char> name_vec =
module_wire_bytes.GetNameOrNull(local_name_ref);
names.emplace_back(GetNameOrDefault(
isolate,
name_vec.empty() ? MaybeHandle<String>()
: isolate->factory()->NewStringFromUtf8(name_vec),
"$var", i));
}
return names;
}
Handle<WasmInstanceObject> GetInstance(Isolate* isolate,
Handle<JSObject> handler) {
Handle<Object> instance =
JSObject::GetProperty(isolate, handler, "instance").ToHandleChecked();
DCHECK(instance->IsWasmInstanceObject());
return Handle<WasmInstanceObject>::cast(instance);
}
Address GetPC(Isolate* isolate, Handle<JSObject> handler) {
Handle<Object> pc =
JSObject::GetProperty(isolate, handler, "pc").ToHandleChecked();
DCHECK(pc->IsBigInt());
return Handle<BigInt>::cast(pc)->AsUint64();
}
Address GetFP(Isolate* isolate, Handle<JSObject> handler) {
Handle<Object> fp =
JSObject::GetProperty(isolate, handler, "fp").ToHandleChecked();
DCHECK(fp->IsBigInt());
return Handle<BigInt>::cast(fp)->AsUint64();
}
Address GetCalleeFP(Isolate* isolate, Handle<JSObject> handler) {
Handle<Object> callee_fp =
JSObject::GetProperty(isolate, handler, "callee_fp").ToHandleChecked();
DCHECK(callee_fp->IsBigInt());
return Handle<BigInt>::cast(callee_fp)->AsUint64();
}
// Convert a WasmValue to an appropriate JS representation.
static Handle<Object> WasmValueToObject(Isolate* isolate,
wasm::WasmValue value) {
auto* factory = isolate->factory();
switch (value.type().kind()) {
case wasm::ValueType::kI32:
return factory->NewNumberFromInt(value.to_i32());
case wasm::ValueType::kI64:
return BigInt::FromInt64(isolate, value.to_i64());
case wasm::ValueType::kF32:
return factory->NewNumber(value.to_f32());
case wasm::ValueType::kF64:
return factory->NewNumber(value.to_f64());
case wasm::ValueType::kS128: {
wasm::Simd128 s128 = value.to_s128();
Handle<JSArrayBuffer> buffer;
if (!isolate->factory()
->NewJSArrayBufferAndBackingStore(
kSimd128Size, InitializedFlag::kUninitialized)
.ToHandle(&buffer)) {
isolate->FatalProcessOutOfHeapMemory(
"failed to allocate backing store");
}
memcpy(buffer->allocation_base(), s128.bytes(), buffer->byte_length());
return isolate->factory()->NewJSTypedArray(kExternalUint8Array, buffer, 0,
buffer->byte_length());
}
case wasm::ValueType::kRef:
return value.to_externref();
default:
break;
}
return factory->undefined_value();
}
bool HasLocalImpl(Isolate* isolate, Handle<Name> property,
Handle<JSObject> handler, bool enable_index_lookup) {
Handle<WasmInstanceObject> instance = GetInstance(isolate, handler);
base::Optional<int> index =
ResolveValueSelector(isolate, property, handler, enable_index_lookup);
if (!index) return false;
Address pc = GetPC(isolate, handler);
wasm::DebugInfo* debug_info =
instance->module_object().native_module()->GetDebugInfo();
int num_locals = debug_info->GetNumLocals(pc);
return 0 <= index && index < num_locals;
}
// Has trap callback for the locals index space proxy.
void HasLocalCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
if (args.Length() < 2) return;
Isolate* isolate = reinterpret_cast<Isolate*>(args.GetIsolate());
DCHECK(args.This()->IsObject());
Handle<JSObject> handler =
Handle<JSObject>::cast(Utils::OpenHandle(*args.This()));
DCHECK(args[1]->IsName());
Handle<Name> property = Handle<Name>::cast(Utils::OpenHandle(*args[1]));
args.GetReturnValue().Set(HasLocalImpl(isolate, property, handler, true));
}
Handle<Object> GetLocalImpl(Isolate* isolate, Handle<Name> property,
Handle<JSObject> handler,
bool enable_index_lookup) {
Factory* factory = isolate->factory();
Handle<WasmInstanceObject> instance = GetInstance(isolate, handler);
base::Optional<int> index =
ResolveValueSelector(isolate, property, handler, enable_index_lookup);
if (!index) return factory->undefined_value();
Address pc = GetPC(isolate, handler);
Address fp = GetFP(isolate, handler);
Address callee_fp = GetCalleeFP(isolate, handler);
wasm::DebugInfo* debug_info =
instance->module_object().native_module()->GetDebugInfo();
int num_locals = debug_info->GetNumLocals(pc);
if (0 > index || index >= num_locals) return factory->undefined_value();
wasm::WasmValue value = debug_info->GetLocalValue(*index, pc, fp, callee_fp);
return WasmValueToObject(isolate, value);
}
// Get trap callback for the locals index space proxy.
void GetLocalCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
DCHECK_GE(args.Length(), 2);
Isolate* isolate = reinterpret_cast<Isolate*>(args.GetIsolate());
DCHECK(args.This()->IsObject());
Handle<JSObject> handler =
Handle<JSObject>::cast(Utils::OpenHandle(*args.This()));
DCHECK(args[1]->IsName());
Handle<Name> property = Handle<Name>::cast(Utils::OpenHandle(*args[1]));
args.GetReturnValue().Set(
Utils::ToLocal(GetLocalImpl(isolate, property, handler, true)));
}
// Has trap callback for the top-level proxy.
void HasToplevelCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
DCHECK_GE(args.Length(), 2);
Isolate* isolate = reinterpret_cast<Isolate*>(args.GetIsolate());
DCHECK(args[0]->IsObject());
Handle<JSObject> target = Handle<JSObject>::cast(Utils::OpenHandle(*args[0]));
DCHECK(args[1]->IsName());
Handle<Name> property = Handle<Name>::cast(Utils::OpenHandle(*args[1]));
// First check if the property exists on the target.
if (JSObject::HasProperty(target, property).FromMaybe(false)) {
args.GetReturnValue().Set(true);
return;
}
// Now check the index space proxies in order if they know the property.
Handle<Object> namespace_proxy =
JSObject::GetProperty(isolate, target, "locals").ToHandleChecked();
DCHECK(namespace_proxy->IsJSProxy());
Handle<JSObject> namespace_handler(
JSObject::cast(Handle<JSProxy>::cast(namespace_proxy)->handler()),
isolate);
if (HasLocalImpl(isolate, property, namespace_handler, false)) {
args.GetReturnValue().Set(true);
return;
}
args.GetReturnValue().Set(false);
}
// Get trap callback for the top-level proxy.
void GetToplevelCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
if (args.Length() < 2) return;
Isolate* isolate = reinterpret_cast<Isolate*>(args.GetIsolate());
DCHECK(args[0]->IsObject());
Handle<JSObject> target = Handle<JSObject>::cast(Utils::OpenHandle(*args[0]));
DCHECK(args[1]->IsName());
Handle<Name> property = Handle<Name>::cast(Utils::OpenHandle(*args[1]));
// First, check if the property is a proper property on the target. If so,
// return its value.
Handle<Object> value =
JSObject::GetProperty(isolate, target, property).ToHandleChecked();
if (!value->IsUndefined()) {
args.GetReturnValue().Set(Utils::ToLocal(value));
return;
}
// Try the index space proxies in the correct disambiguation order.
Handle<Object> namespace_proxy =
JSObject::GetProperty(isolate, target, "locals").ToHandleChecked();
DCHECK(!namespace_proxy->IsUndefined() && namespace_proxy->IsJSProxy());
Handle<JSObject> namespace_handler(
JSObject::cast(Handle<JSProxy>::cast(namespace_proxy)->handler()),
isolate);
value = GetLocalImpl(isolate, property, namespace_handler, false);
if (!value->IsUndefined()) args.GetReturnValue().Set(Utils::ToLocal(value));
}
// Populate a JSMap with name->index mappings from an ordered list of names.
Handle<JSMap> GetNameTable(Isolate* isolate,
const std::vector<Handle<String>>& names) {
Factory* factory = isolate->factory();
Handle<JSMap> name_table = factory->NewJSMap();
for (size_t i = 0; i < names.size(); ++i) {
SetMapValue(isolate, name_table, names[i], factory->NewNumberFromInt64(i));
}
return name_table;
}
// Produce a JSProxy with a given name table and get and has trap handlers.
Handle<JSProxy> GetJSProxy(
WasmFrame* frame, Handle<JSMap> name_table,
void (*get_callback)(const v8::FunctionCallbackInfo<v8::Value>&),
void (*has_callback)(const v8::FunctionCallbackInfo<v8::Value>&)) {
Isolate* isolate = frame->isolate();
Factory* factory = isolate->factory();
Handle<JSObject> target = factory->NewJSObjectWithNullProto();
Handle<JSObject> handler = factory->NewJSObjectWithNullProto();
// Besides the name table, the get and has traps need access to the instance
// and frame information.
JSObject::AddProperty(isolate, handler, "names", name_table, DONT_ENUM);
Handle<WasmInstanceObject> instance(frame->wasm_instance(), isolate);
JSObject::AddProperty(isolate, handler, "instance", instance, DONT_ENUM);
Handle<BigInt> pc = BigInt::FromInt64(isolate, frame->pc());
JSObject::AddProperty(isolate, handler, "pc", pc, DONT_ENUM);
Handle<BigInt> fp = BigInt::FromInt64(isolate, frame->fp());
JSObject::AddProperty(isolate, handler, "fp", fp, DONT_ENUM);
Handle<BigInt> callee_fp = BigInt::FromInt64(isolate, frame->callee_fp());
JSObject::AddProperty(isolate, handler, "callee_fp", callee_fp, DONT_ENUM);
InstallFunc(isolate, handler, "get", get_callback, 3, false, READ_ONLY);
InstallFunc(isolate, handler, "has", has_callback, 2, false, READ_ONLY);
return factory->NewJSProxy(target, handler);
}
} // namespace
// This function generates the JS debug proxy for a given Wasm frame. The debug
// proxy is used when evaluating debug JS expressions on a wasm frame and let's
// the developer inspect the engine state from JS. The proxy provides the
// following interface:
//
// type WasmSimdValue = Uint8Array;
// type WasmValue = number | bigint | object | WasmSimdValue;
// type WasmFunction = (... args : WasmValue[]) = > WasmValue;
// type WasmExport = {name : string} & ({func : number} | {table : number} |
// {mem : number} | {global : number});
// type WasmImport = {name : string, module : string} &
// ({func : number} | {table : number} | {mem : number} |
// {global : number});
// interface WasmInterface {
// $globalX: WasmValue;
// $varX: WasmValue;
// $funcX(a : WasmValue /*, ...*/) : WasmValue;
// readonly $memoryX : WebAssembly.Memory;
// readonly $tableX : WebAssembly.Table;
// readonly memories : {[nameOrIndex:string | number] : WebAssembly.Memory};
// readonly tables : {[nameOrIndex:string | number] : WebAssembly.Table};
// readonly stack : WasmValue[];
// readonly imports : {[nameOrIndex:string | number] : WasmImport};
// readonly exports : {[nameOrIndex:string | number] : WasmExport};
// readonly globals : {[nameOrIndex:string | number] : WasmValue};
// readonly locals : {[nameOrIndex:string | number] : WasmValue};
// readonly functions : {[nameOrIndex:string | number] : WasmFunction};
// }
//
// The wasm index spaces memories, tables, imports, exports, globals, locals
// functions are JSProxies that lazily produce values either by index or by
// name. A top level JSProxy is wrapped around those for top-level lookup of
// names in the disambiguation order  memory, local, table, function, global.
// Import and export names are not globally resolved.
Handle<JSProxy> WasmJs::GetJSDebugProxy(WasmFrame* frame) {
Isolate* isolate = frame->isolate();
Factory* factory = isolate->factory();
Handle<WasmInstanceObject> instance(frame->wasm_instance(), isolate);
// The top level proxy delegates lookups to the index space proxies.
Handle<JSObject> handler = factory->NewJSObjectWithNullProto();
InstallFunc(isolate, handler, "get", GetToplevelCallback, 3, false,
READ_ONLY);
InstallFunc(isolate, handler, "has", HasToplevelCallback, 2, false,
READ_ONLY);
Handle<JSObject> target = factory->NewJSObjectWithNullProto();
// Generate JSMaps per index space for name->index lookup. Every index space
// proxy is associated with its table for local name lookup.
auto local_name_table =
GetNameTable(isolate, GetLocalNames(instance, frame->pc()));
auto locals =
GetJSProxy(frame, local_name_table, GetLocalCallback, HasLocalCallback);
JSObject::AddProperty(isolate, target, "locals", locals, READ_ONLY);
return factory->NewJSProxy(target, handler);
}
#undef ASSIGN
#undef EXTRACT_THIS

View File

@ -9,6 +9,8 @@
namespace v8 {
namespace internal {
class JSProxy;
class WasmFrame;
namespace wasm {
class StreamingDecoder;
@ -19,6 +21,8 @@ class WasmJs {
public:
V8_EXPORT_PRIVATE static void Install(Isolate* isolate,
bool exposed_on_global_object);
V8_EXPORT_PRIVATE static Handle<JSProxy> GetJSDebugProxy(WasmFrame* frame);
};
} // namespace internal

View File

@ -525,15 +525,37 @@ WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_JavaScript) {
uint16_t index = 0;
runner.builder().AddIndirectFunctionTable(&index, 1);
TestCode<int> code(
TestCode<int64_t> code(
&runner,
{WASM_SET_GLOBAL(0, WASM_I32V_2('B')),
WASM_SET_LOCAL(0, WASM_I32V_2('A')), WASM_RETURN1(WASM_GET_LOCAL(0))},
{ValueType::kI32});
WASM_SET_LOCAL(0, WASM_I64V_2('A')), WASM_RETURN1(WASM_GET_LOCAL(0))},
{ValueType::kI64});
code.BreakOnReturn(&runner);
Isolate* isolate = runner.main_isolate();
Handle<String> snippet = V8String(isolate, "213");
Handle<String> snippet =
V8String(isolate,
"JSON.stringify(["
//"$global0, "
//"$table0, "
"$var0, "
//"$main, "
//"$memory0, "
//"globals[0], "
//"tables[0], "
"locals[0], "
//"functions[0], "
//"memories[0], "
//"memories, "
//"tables, "
//"stack, "
//"imports, "
//"exports, "
//"globals, "
"locals, "
//"functions, "
"], (k, v) => k === 'at' || typeof v === 'undefined' || typeof "
"v === 'object' ? v : v.toString())");
WasmJSBreakHandler break_handler(isolate, snippet);
CHECK(!code.Run(&runner).is_null());
@ -541,7 +563,10 @@ WASM_COMPILED_EXEC_TEST(WasmDebugEvaluate_JavaScript) {
WasmJSBreakHandler::EvaluationResult result =
break_handler.result().ToChecked();
CHECK_WITH_MSG(result.error.IsNothing(), result.error.ToChecked().c_str());
CHECK_EQ(result.result.ToChecked(), "213");
CHECK_EQ(result.result.ToChecked(), "[\"65\",\"65\",{}]");
//"[\"66\",{},\"65\",\"function 0() { [native code] }\",{},"
//"\"66\",{},\"65\",\"function 0() { [native code] }\",{},"
//"{},{},{\"0\":\"65\"},{},{},{},{},{}]");
}
} // namespace