There are times we want to expose native APIs to JavaScript world to enrich its capabilities, which can usually be done by calling JS VM’s API, like V8 embedder API (Node-API) or JavaScriptCore API. It’s usually an easy task when the exposed API is clean and simple, but things can get complicated when you have complex references between JavaScript and C++. In this article, I will share some techniques to help you manage JavaScript Native Object’s lifetime.
Example case
Imagine you need to implement an EventEmitter
in DOM Node API, it’s usage is like the following:
const node = new Node()
node.addEventListener('touchstart', () => {
console.log('touchstart event fired in node:', node)
})
The addEventListener
native method can be roughly implemented in C++ like the following code snippet, it’ll receive two parameters and the second one is the callback function.
void AddEventListener(
const v8::FunctionCallbackInfo<v8::Value>& args) {
auto* isolate = args.GetIsolate();
auto* self = Unwrap(args.This());
// we don't care about the event name for now.
v8::Local<v8::Object> name = args[0];
v8::Local<v8::Object> callback =
args[1]->ToObject(isolate->GetCurrentContext()).ToLocalChecked();
}
We know that the callback function is asynchronous, which means it will be invoked at any time when the node is touched by user. So here comes the question: where should we store the callback
function? As you may probably know, the v8::Local<T>
is a temporary stack-allocated handle, it can not be accessed after the AddEventListener
native function finishes its execution.
Be careful of Persistent Handle
Actually, v8::Persistent<T>
is one of the solutions (not really). To put it simplely, one of the usage of v8::Persistent<T>
is to make the T
value outlive the V8 GC (somewhat like a global variable). So that in this case, we can obtain callback
function via v8::Persistent<T>
at any time. Yes, it satisfies our needs. We can assign callback
to callback_
(which is a v8::Persistent handle), and then access the callback_
later in the FireCallback
method. callback_
will be Reset (release) when Node is released.
class Node {
public:
~Node() {
callback_.Reset();
}
void AddEventListener(
const v8::FunctionCallbackInfo<v8::Value>& args) {
// ...
callback_.Reset(isolate, callback);
callback_.ClearWeak();
}
void FireCallback() {
auto callback = g_callback.Get(g_isolate).As<v8::Function>();
// ThisObject() is fake, it returns the JS this object.
callback->Call(g_context, ThisObject(), 0, nullptr)
.ToLocalChecked();
}
private:
v8::Persistent<v8::Object> callback_;
};
So currently the EventEmitter method in the DOM Node API is correctly implemented, right? Actually, not really! There is a fatal issue: Memory Leak in the callback. But how does that happen? Let’s recall the JS example above, we printed out node itself in the event callback (which is commonly seen in JS), which forms a closure that contains a reference to the node
instance. Since the callback is a persistent handle
, which is free from being GCed, the callback is retained by the node
, node
is referenced in the closure. Cyclic Reference! no one is going to die. Although in pure JS, circular reference is fine, because they will be GC sweeped out all together. Whereas in this case, the callback is made free from GC, and it also makes the node reachable, in which case it is unable to be GCed.
It’s a fairly typical misuse of the V8 API, which will make you frustrated to pinpoint the memory leak. How can be solve this problem then?
Let GC help you
We all know that GC is absolutely good at circular reference
, why don’t we make GC to tackle this problem? What we need is to make the callback
function outlive node
instance so that it can be safely whenever you want. One simple way is to make node
instance reference to callback
function, so that when node
is alive, the callback
function is also alive (GC reachable).
void AddEventListener(
const v8::FunctionCallbackInfo<v8::Value>& args) {
// ...
auto private_key = v8::Private::New(isolate);
ThisObject().SetPrive(context, private_key, callback);
}
We make the callback
function a private member of node
instance, so that they’ll co-alive. Problem solved!
To be continued…