I am trying to figure out how to access a JavaScript array in embedded v8 through a C++ function
Example JavaScript invoking my custom function:
my_func([1, 2, 3]);
If I explicitly craft a typed array such as Uint8Array
(eg, my_func(new Uint8Array([1,2,3])
), I can access it from the v8::FunctionCallbackInfo
object like this:
void my_func(const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::Local<v8::Uint8Array> arr = info[0].As<v8::Uint8Array>();
// can access the raw buffer or specific elements via `arr` now
}
However, I can't figure out a similar approach for a simple v8::Array
object, and the documentation/sample code is lacking on this topic. Can anyone point me in the right direction on how to accomplish this?
I've tried a few variations on this, but it causes a fault:
v8::Local<v8::Array> arr = info[0].As<v8::Array>();
v8::MaybeLocal<v8::Number> n;
arr->Get(ctx, 0).Cast<v8::Number>(n); // fault is triggered here
auto x = n.ToLocalChecked();
Fault:
#
# Fatal error in ../../src/api/api-inl.h, line 171
# Debug check failed: v8::internal::ValueHelper::IsEmpty(that) || IsJSReceiver(v8::internal::Tagged<v8::internal::Object>( v8::internal::ValueHelper::ValueAsAddress(that))).
#
I also tried the top two answers on the linked question, which successfully executed but gave the wrong data. I suspect this is due to a v8 internal distinction between the values coming from JavaScript vs data instantiated within C++ but am not familiar enough with v8 to say for sure.
v8::Handle<v8::Array> array = v8::Handle<v8::Array>::Cast(info[0]);
auto i = array->Get(ctx, 0).ToLocalChecked();
// `i` looks like a truncated pointer from a quick glance, definitely not the correct value
v8::Handle<v8::Object> obj = info[0]->ToObject(ctx).ToLocalChecked();
v8::Local<v8::Value> element = obj->Get(ctx, 0).ToLocalChecked();
auto i = element->ToUint32(ctx).ToLocalChecked();
// this behaves the same as the previous snippet
This may sound obvious, but it can be helpful to try to understand what you need, rather than just blindly trying snippets you've seen elsewhere :-)
In this case, the relevant concepts are:
(1) Checking for empty Local
s. Most operations on JavaScript objects can throw, in which case they produce no return value (because they threw an exception instead of returning a value); the V8 API makes this obvious by using the MaybeLocal
type: whenever you have one of those, you should check for an exception, or conversely: check if you actually have a value. In a few special cases where you can guarantee this (usually that means: because you already checked before, by some other way), you can use the ToLocalChecked
helper. The "Checked" part of the name means: if the MaybeLocal
was indeed empty (due to an exception), this will crash. So usually, to avoid crashing, you'd use the pattern:
v8::MaybeLocal<ResultType> maybe = SomeOperationThatCanThrow();
v8::Local<ResultType> result;
if (!maybe.ToLocal(&result)) {
// Handle error...
return;
}
// If `ToLocal` returned `true`, `result` is now populated.
Use(result);
(2) Type checking and casting. Most operations on JavaScript objects can return a value of arbitrary type, because JavaScript is an untyped language. So the V8 API provides generic types, specific types, and facilities to check a value's type and convert it to a more specific type: v8::Value
is just about anything. Various IsFoo()
methods can check its type; unless you can guarantee that a value has a particular type, you'll want to check its type before casting it. For casting, v8::Local
has the static function Cast<TargetType>
, and the equivalent non-static helper As<TargetType>
, which can be more convenient. What they have in common is that they're only valid if the value does have the right type; with V8_ENABLE_CHECKS
they'll crash otherwise, without they'll just give you an invalid handle that will most likely crash on one of the next few things you'll do with it. So, a typical pattern is:
v8::Local<v8::Value> value = SomethingThatReturnsAValue();
if (value->IsObject()) {
v8::Local<v8::Object> obj1 = v8::Local<v8::Object>::Cast(value);
v8::Local<v8::Object> obj2 = value.As<v8::Object>(); // Equivalent.
} else {
// Handle the case where `value` isn't an Object; perhaps by
// checking for some other type, or by performing a to-object
// conversion.
...
}
Now, putting both of those together, you'll likely want something like:
if (!info[0]->IsArray()) {
// TODO: Handle this somehow.
return;
}
v8::Local<v8::Array> arr = info[0].As<v8::Array>();
v8::MaybeLocal<v8::Value> maybe_element = arr->Get(ctx, 0);
v8::Local<v8::Value> element;
if (!maybe_element.ToLocal(&element)) {
// TODO: Handle this somehow.
return;
}
if (!element->IsNumber()) {
// TODO: Handle this somehow.
return;
}
v8::Local<v8::Number> number = element.As<v8::Number>();
std::cout << "The number is " << number->Value() << std::endl;
And if you think "nah, all those checks look complicated, surely I don't need all of them?", try calling your function as follows:
my_func("");
my_func([]);
my_func(["what now?"]);
let nasty = [];
Object.defineProperty(nasty, 0, {get: () => { throw "not today;" }});
my_func(nasty);