Search code examples
c++node.jsgarbage-collectionv8

How can I clean up C++ objects tracked in Local<External> references?


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.


Solution

  • 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.