I'm creating pure C++ objects (from C++ only) and then attaching them to JS-exposed objects returned as API wrapper instances. The means of attachment is using External::New
stored via SetInternalField
- pretty much following exactly the pattern outlined in the V8 Embedder's Guide.
These objects are intended to be memory-managed by JS, but even after dropping the references in JS and triggering garbage collection, the C++ objects remain. The destructors are not called, and if I save an independent reference elsewhere, it still works.
I think it makes sense that V8's cleanup of local values wouldn't know how (or whether) to destruct the contents of a void*
, but isn't this a prominent use-case? There's no way I can find to hook in any way to the de-scoping/cleanup of externals or locals in general. Further, anything I do with persistents (like MakeWeak
) aren't going to affect the locals that become unreachable on the JS side (right?).
How can I ensure that these C++ objects get eventually (preferably immediately) destroyed when the JS wrapper objects containing them fall out of scope?
Example instantiation that gets passed to JS:
Local<Value> createWindow(HWND handle, Isolate* isolate) {
// Build an instance from a premade FunctionTemplate with object
// and method prototypes, and SetInternalFieldCount(1)
Local<Function> fn = Local<Function>::New(isolate, window);
constructingInternally = true;
Local<Object> obj = fn->NewInstance(Context::New(isolate)).ToLocalChecked();
constructingInternally = false;
CppWindow* win = CppWindow(handle);
lastWin = win; // added for debugging
obj->SetInternalField(0, External::New(isolate, win));
return obj;
}
Update: after many hours of blind, trial-and-error guesswork, I finally have code that compiles and a callback that triggers when the object is garbage collected. Much time was lost attempting to work with the Local<External>;
or the containing Local<Object>;
in the callback, where for no reason I comprehend, these attempts would refuse to compile with confusing errors (such as "'Local<External>' has no member 'Value'", or "'Local<Object>' has no member 'GetInternalField'"). When they finally compiled, the process would silently die in the callback, during the process of trying to reach my object. Eventually I wised up and passed in a direct pointer to the object, which I am now able to delete directly.
Local<Value> createWindow(HWND handle, Isolate* isolate) {
Local<Function> fn = Local<Function>::New(isolate, window);
constructingInternally = true;
Local<Object> obj = fn->NewInstance(Context::New(isolate)).ToLocalChecked();
constructingInternally = false;
CppWindow* win = CppWindow(handle);
obj->SetInternalField(0, External::New(isolate, win));
Persistent<Object>(isolate, obj).SetWeak(
win,
[](const WeakCallbackInfo<CppWindow>& data) {
delete data.GetParameter();
},
WeakCallbackType::kParameter
);
return obj;
}
That there isn't a single code sample anywhere demonstrating this plain, straightforward use-case is utterly criminal. The second link jmrk offered at least allowed me to piece together some of the vague clues I needed to eventually work it out, but those sources were almost equal parts misleading to a C++ novice.
Correct, V8 has no way to magically guess that your C++ objects exist, or how to free them.
The usual way to solve this is by using Persistents. For a weak persistent handle, you can set a callback, which will be called when V8 frees the last reference to the object. In that callback, you can then call any destructors needed to free up any dependent C++ objects.
A potential alternative is to manage the objects yourself, if you can predict their lifetime (e.g., depending on what your application is doing, "while a request is being handled" or somesuch). That option could be faster, could be simpler to implement, -- or could be impossible if you have no way to predict object lifetime.