作者:theanarkh来源:编程杂技
前言:最近在写Node.jsAddon的过程中,遇到了一个问题,然后发现是ObjectWrap弱引用导致的,本文介绍一下具体的问题和排查过程,以及ObjectWrap的使用问题。
ObjectWrap用于写Addon的时候导出C++对象给JS层使用,大致用法如下。首先定义一个C++类。
classDemo:publicnode::ObjectWrap{public:staticvoidcreate(constFunctionCallbackInfoValueargs){newDemo(args.This());}Demo(LocalObjectobject):node::ObjectWrap(){}private:uv_timer_ttimer;};
然后导出这个类到JS。
voidInitialize(LocalObjectexports,LocalValuemodule,LocalContextcontext){Isolate*isolate=context-GetIsolate();LocalFunctionTemplatedemo=FunctionTemplate::New(isolate,Demo::create);char*str="Demo";LocalStringname=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,,);
然后使用的过程中我们发现,定时器随机触发了几次后,就不触发了。经过多种测试无果后,我不得不编译一个debug版本的Node.js进行单步调试,然后就发现了有意思的事情。第一次进入pollio阶段时,一切正常,1秒后超时。
但是后面再次进入pollio阶段时,诡异的事情发生了。
超时时间变成了一个很大的数字,正常来说,我设置的每隔一秒超时一次,这里应该是1才对,为什么会出现一个诡异的数字呢。思考了一下,猜想是这块内存被释放了,然后里面保存了一些脏数据,接着我给Demo类加了个析构函数。
~Demo(){LOG("dead");}
然后发现,这个类对象居然被析构了。通过栈追踪发现逻辑来自于ObjectWrap的WeakCallback。
WeakCallback的代码如下。
staticvoidWeakCallback(constv8::WeakCallbackInfoObjectWrapdata){ObjectWrap*wrap=data.GetParameter();wrap-handle_.Reset();deletewrap;}
deletewrap就是delete了Demo对象。而这个WeakCallback的源头来自ObjectWrap的MakeWeak。
inlinevoidMakeWeak(){persistent().SetWeak(this,WeakCallback,v8::WeakCallbackType::kParameter);}
这个MakeWeak又来源于Wrap。
inlinevoidWrap(v8::Localv8::Objecthandle){//关联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。而gc的时候就把C++对象回收了。
setInterval(()={Buffer.from(x.repeat(10))},)
这个问题的解决方式就是调用ObjectWrap的Ref函数消除弱引用(或者在JS层保持对这个对象的引用)。
virtualvoidRef(){persistent().ClearWeak();refs_++;}
回过头来看看Node.js中另一个类似功能的类BaseObject。
BaseObject::BaseObject(Environment*env,v8::Localv8::Objectobject):persistent_handle_(env-isolate(),object),env_(env){object-SetAlignedPointerInInternalField(BaseObject::kSlot,static_castvoid*(this));}
它并没有设置弱引用的逻辑。所以在Node.js的C++模块里,我们也看不到主动调用Ref的代码。这或许是使用ObjectWrap时需要注意的问题。
总结:大致分析了ObjectWrap相关的这个问题,但是其实排查过程比描述的繁琐和困难,主要是一开始没有用debug版本的Node.js进行调试,把排查聚焦在打快照的地方了,因为那里涉及了多线程操作同一个isolate,所以以为是V8API使用方式的问题。总的来说,如果碰到Node.js诡异的一些问题,不妨打个debug版本的Node.js进行调试,可能会更快地找到问题,从中也能学到很多东西。