竹笋

首页 » 问答 » 问答 » NodejsObjectWrap的弱引
TUhjnbcbe - 2023/6/25 20:49:00

作者: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进行调试,可能会更快地找到问题,从中也能学到很多东西。

1
查看完整版本: NodejsObjectWrap的弱引