c++v8embedded-v8

Access JavaScript array in v8 C++ function handler


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

Solution

  • 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 Locals. 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);