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.
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 MaybeLocal
s to Local
s 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 throwsA
's constructor returns null
A
's constructor installs a getter for update
that throwsA
'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.)