当前位置: 首页 > 科技观察

Node.jsObjectWrap的弱引用问题

时间:2023-03-16 01:55:51 科技观察

前言:最近在写Node.jsAddon的过程中遇到了一个问题,后来发现是ObjectWrap的弱引用引起的。本文介绍具体问题及排查过程,以及ObjectWrap的使用问题。ObjectWrap用于在编写Addon时将C++对象导出到JS层。一般用法如下。首先定义一个C++类。classDemo:publicnode::ObjectWrap{public:staticvoidcreate(constFunctionCallbackInfo&args){newDemo(args.This());}Demo(Localobject):node::ObjectWrap(){}private:uv_timer_ttimer;};然后将这个类导出到JS。voidInitialize(Localexports,Localmodule,Localcontext){Isolate*isolate=context->GetIsolate();Localdemo=FunctionTemplate::New(isolate,Demo::create)复制代码;char*str="Demo";Localname=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)然后在JS中按如下方式调用。const{Demo}=require('demo.node');constdemo=newDemo();可以看到C++Demo类中有一个uv_timer_t成员。主要用于定时抓取V8堆快照,所以在Libuv中注册。uv_timer_init(loop,&timer);uv_timer_start(&timer,timer_cb,1000,1000);然后在使用的过程中,我们发现定时器随机触发几次后,就触发不了了。经过几次无果而终的测试后,我不得不编译一个调试版本的Node.js来单步执行它,并发现了一些有趣的东西。第一次进入pollio阶段,一切正常,1秒后超时。但是当它后面再次进入pollio阶段的时候,奇怪的事情发生了。超时时间变成了一个很大的数字。通常,我每秒设置一次超时。这里应该是1。为什么会有一个奇怪的数字?想了想,我猜想这块内存被释放了,然后里面存放了一些脏数据,然后我给Demo类加了一个析构函数。~Demo(){LOG("dead");}然后发现这个类的对象居然被析构了。通过stacktrace,发现逻辑来自于ObjectWrap的WeakCallback。WeakCallback的代码如下。staticvoidWeakCallback(constv8::WeakCallbackInfo&data){ObjectWrap*wrap=data.GetParameter();wrap->handle_.Reset();deletwrap;}deletewrap是删除Demo对象。而这个WeakCallback的来源来自于ObjectWrap的MakeWeak。inlinevoidMakeWeak(){persistent().SetWeak(this,WeakCallback,v8::WeakCallbackType::kParameter);}这个MakeWeak又来自Wrap。inlinevoidWrap(v8::Localhandle){//关联C++对象和Demo对象handle->SetAlignedPointerInInternalField(0,this);persistent().Reset(v8::Isolate::GetCurrent(),handle);MakeWeak();}Wrap是创建Demo对象时调用的函数。用于关联JS层对象和C++对象,关系如下。所以当JS创建一个Demo对象时,它指向一个C++对象,然后这个Demo对象也有一个指向这个C++对象的持久化句柄。但是它默认调用了MakeWeak,也就是弱引用。JS层在创建Demo对象后就离开了作用域,因为JS模块被一个函数包裹,变量执行后就是gc了,除非通过module.exports或者全局变量保持对C++对象的引用。因此,C++对象最终被Demo对象作为弱引用引用,在等待gc的过程中被回收。这就引出了另外一个问题,当我将抓取快照的代码改成一些简单的代码时,由于不触发gc,所以不容易触发这个问题。后来尝试在JS层分配一些内存,终于触发了这个问题,因为下面的代码会导致gc。C++对象在gc期间被回收。setInterval(()=>{Buffer.from('x'.repeat('10'))},3000)这个问题的解决方法是调用ObjectWrap的Ref函数去除弱引用(或者保留对this的引用对象在JS层引用)。virtualvoidRef(){persistent().ClearWeak();refs_++;}让我们回顾一下BaseObject,Node.js中另一个具有类似功能的类。BaseObject::BaseObject(环境*env,v8::Localobject):persistent_handle_(env->isolate(),object),env_(env){object->SetAlignedPointerInInternalField(BaseObject::kSlot,static_cast(this));}没有设置弱引用逻辑。所以在Node.js的C++模块中,我们看不到主动调用Ref的代码。这可能是使用ObjectWrap时需要注意的问题。总结:大致分析了ObjectWrap相关的问题,但是排查过程比描述的更繁琐和困难,主要是因为我没有使用调试版的Node。同一个isolate被多个线程操作过,所以我觉得是V8API的使用方式有问题。一般来说,如果你在使用Node.js的时候遇到一些奇怪的问题,不妨打开一个调试版的Node.js进行调试。您可能会更快地找到问题并从中学到很多东西。