Search code examples
v8embedded-v8

V8 embedding: null `this` from method inside class inheriting from one created using Function::New


I am trying to embed V8 with a custom class. It is made to be extended and used in JS.

When accessing this in a method other than the constructor, it is null. It should be able to persist data for that instance of the class.

Am I missing something?

JavaScript

import { Behavior } from '@giz/ecs';

class A extends Behavior {
  constructor(a) {
    super(a);

    this.a = 10;

    print(this); // this works
  }

  update() {
    print(this); // null
  }
}

A;

How I am creating Behavior

    Local<Module> ecsModule = Module::CreateSyntheticModule(
        isolate,
        String::NewFromUtf8(isolate, "@giz/ecs").ToLocalChecked(),
        {String::NewFromUtf8(isolate, "Behavior").ToLocalChecked()},
        [](Local<Context> context, Local<Module> module) -> MaybeLocal<Value>
        {
            auto isolate = context->GetIsolate();
            // behavior constructor right now is an empty function
            auto behavior = Function::New(context, behaviorConstructor).ToLocalChecked();

            module->SetSyntheticModuleExport(
                String::NewFromUtf8(isolate, "Behavior").ToLocalChecked(),
                behavior);

            return MaybeLocal<Value>(True(isolate));
        });
    ecsModule->InstantiateModule(context, ScriptingSystem::moduleResolutionCallback);

    // store the module for later use
    Global<Module> *globalModule = new Global<Module>();
    globalModule->Reset(isolate, ecsModule);
    modules["@giz/ecs"] = globalModule;

Instantiating the class and calling the update function

    v8::HandleScope handle_scope(isolate);
    // globalContext is a v8::Global<v8::Context> with the print function created
    v8::Local<Context> context = Local<Context>::New(isolate, globalContext);
    Context::Scope contextScope(context);

    // ...
    // create script origin, script source, instantiate module, ..
    // ...

    Local<Value> returnValue = module->Evaluate(context).ToLocalChecked();
    Local<Object> returnedBehavior = returnValue->ToObject(context).ToLocalChecked();

    Local<Value> arguments[1];
    arguments[0] = wrapEntity(*behavior->entity);

    // creates an instance of the behavior
    auto instance = returnedBehavior->CallAsConstructor(context, 1, arguments)
                        .ToLocalChecked()
                        ->ToObject(context)
                        .ToLocalChecked();

    // calls the update function
    instance->Get(context, String::NewFromUtf8(isolate, "update").ToLocalChecked())
        .ToLocalChecked()
        ->ToObject(context)
        .ToLocalChecked()
        ->CallAsFunction(context, Null(isolate), 0, nullptr);

Thanks.


Solution

  • The problem is in the last line:

    ->CallAsFunction(context, Null(isolate), 0, nullptr);

    The second argument to CallAsFunction is the "receiver", which is the thing that will be accessible as this inside the called function: since you pass Null(isolate) there, this will be null. Try passing instance instead.
    (Essentially, your current code is doing the equivalent of let instance = new A(); instance.update.call(null);.)


    Side note: please be aware that ToLocalChecked will crash your process if the previous operation threw an exception. The requirement to convert MaybeLocals to Locals explicitly has been introduced for the purpose of pointing out all the places where C++ code should check for exceptions, and handle them somehow. Only in the rare cases where you can guarantee that an operation won't throw (such as allocating a short string), using ToLocalChecked is safe. Examples that will currently crash your process:

    • A's constructor throws
    • A's constructor returns null
    • A's constructor installs a getter for update that throws
    • A's constructor installs a getter for update that returns null

    A common pattern is:

    Local<...> value;
    if (!FunctionThatReturnsMaybeLocal(...).ToLocal(&value)) {
      /* Handle error: show message, skip silently, ... */
      return;
    }
    // Now work with {value}.
    

    (Yes, working with JavaScript from C++ is difficult.)