ObjectWrap weak reference is the cause of node.js Addon. This article introduces the specific problem and troubleshooting process, as well as the use of ObjectWrap.

ObjectWrap is used to export C++ objects to the JS layer when writing Addon. Start by defining a C++ class.

class Demo: public node::ObjectWrap {
 public:
 static void create(const FunctionCallbackInfo<Value>& args) {
                new Demo(args.This());
     }
 Demo(Local<Object> object): node::ObjectWrap() {}
 private:
 uv_timer_t timer;
};
Copy the code

Then export the class to JS.

void Initialize(
  Local<Object> exports,
  Local<Value> module,
  Local<Context> context
) {
  Isolate *isolate = context->GetIsolate();
  Local<FunctionTemplate> demo = FunctionTemplate::New(isolate, Demo::create);
  char * str = "Demo";
  Local<String> name = String::NewFromUtf8(isolate, str, NewStringType::kNormal, strlen(str)).ToLocalChecked();
  demo->InstanceTemplate()->SetInternalFieldCount(1);
  exports->Set(context, name, demo->GetFunction(context).ToLocalChecked()).Check();
}

NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
Copy the code

It is then called in JS in the following way.

const { Demo } = require('demo.node');
const demo = new Demo();
Copy the code

You can see that the C++ Demo class has a uv_timer_t member. Mainly used to take V8 heap snapshots on a regular basis, so register it with Libuv.

uv_timer_init(loop, &timer);
uv_timer_start(&timer, timer_cb, 1000, 1000);
Copy the code

Then in the process of using it, we found that the timer randomly triggered a few times, then it did not trigger. After multiple tests with no results, I had to compile a debug version of Node.js to step through, and found something interesting. The first time you enter the Poll IO phase, everything is fine, and the poll IO phase times out after 1 second.But when it came to the poll IO stage again, something strange happened.The timeout has become a very large number. Normally, I set it to timeout every second. This should be 1. After thinking about it, I assumed that the memory block was freed and some dirty data was stored in it. Then I added a destructor to the Demo class.

~Demo() {
  LOG("dead");
}
Copy the code

Then you discover that the class object is destructed. Through stack tracing, it is found that the logic comes from ObjectWrap’s WeakCallback.WeakCallback code is as follows.

static void WeakCallback(
     const v8::WeakCallbackInfo<ObjectWrap>& data) {
   ObjectWrap* wrap = data.GetParameter();
   wrap->handle_.Reset();
   delete wrap;
}
Copy the code

Delete wrap deletes the Demo object. The source of WeakCallback comes from ObjectWrap MakeWeak.

 inline void MakeWeak() {
    persistent().SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);
 }
Copy the code

This MakeWeak comes from Wrap again.

The inline void Wrap (v8: : Local < v8: : Object > handle) {/ / associated c + + Object and the Demo Object handle - > SetAlignedPointerInInternalField (0, this); persistent().Reset(v8::Isolate::GetCurrent(), handle); MakeWeak(); }Copy the code

Wrap is the function that is called when the Demo object is created. Used to associate JS layer objects with C++ objects as follows.So when JS creates a Demo object, it points to a C++ object, and the Demo object also has a persistent handle to the C++ object. But by default, it demotes MakeWeak, or weak references. The JS layer goes out of scope after the Demo object is created, because JS modules are wrapped in functions and the variables are gc after execution, unless reference to C++ objects is kept through module.exports or global variables. As a result, C++ objects end up being referred to as weak references by Demo objects, waiting to be collected by gc. This leads to another problem, when I change the code that captures the snapshot to something simpler, it is not easy to trigger this problem because it does not trigger gc. I later tried to allocate some memory in the JS layer, which eventually triggered the problem as well, because the following code causes GC. The C++ object is reclaimed by gc.

setInterval(() => {
    Buffer.from('x'.repeat('10'))
},3000)
Copy the code

The solution to this problem is to call ObjectWrap’s Ref function to eliminate weak references (or keep references to the object in the JS layer).

 virtual void Ref() {
    persistent().ClearWeak();
    refs_++;
 }
Copy the code

Take a look back at another Node.js class with similar functionality, BaseObject.

BaseObject::BaseObject(Environment* env, v8::Local<v8::Object> object)
    : persistent_handle_(env->isolate(), object), env_(env) {
  object->SetAlignedPointerInInternalField(BaseObject::kSlot, static_cast<void*>(this));
}
Copy the code

It does not set the logic for weak references. So we don’t see active Ref calls in the C++ module of node.js. This is probably something to be aware of when using ObjectWrap.

Conclusion: The problem related to ObjectWrap is generally analyzed, but in fact the troubleshooting process is more complicated and difficult than described. The main reason is that the debugging of Node.js with the debug version is not used at the beginning, and the troubleshooting is focused on the snapshot, because it involves multi-thread operation of the same ISOLATE. So I thought it was the way the V8 API was being used. In general, if you are experiencing some weird problems with Node.js, you might want to use the debug version of Node.js. You will find the problem faster and learn a lot from it.