Node.js ObjectWrap 的弱引用问题

开发 前端
最近在写 Node.js Addon 的过程中,遇到了一个问题,然后发现是 ObjectWrap 弱引用导致的,本文介绍一下具体的问题和排查过程,以及 ObjectWrap 的使用问题。

前言:最近在写 Node.js Addon 的过程中,遇到了一个问题,然后发现是 ObjectWrap 弱引用导致的,本文介绍一下具体的问题和排查过程,以及 ObjectWrap 的使用问题。

ObjectWrap 用于写 Addon 的时候导出 C++ 对象给 JS 层使用,大致用法如下。首先定义一个 C++ 类。

  1. class Demo: public node::ObjectWrap { 
  2.      public
  3.          static void create(const FunctionCallbackInfo<Value>& args) { 
  4.                     new Demo(args.This()); 
  5.          } 
  6.          Demo(Local<Object> object): node::ObjectWrap() {} 
  7.      private: 
  8.         uv_timer_t timer; 
  9.  
  10. }; 

然后导出这个类到 JS。

  1. void Initialize( 
  2.   Local<Object> exports, 
  3.   Local<Value> module, 
  4.   Local<Context> context 
  5. ) { 
  6.   Isolate *isolate = context->GetIsolate(); 
  7.   Local<FunctionTemplate> demo = FunctionTemplate::New(isolate, Demo::create); 
  8.   char * str = "Demo"
  9.   Local<String> name = String::NewFromUtf8(isolate, str, NewStringType::kNormal, strlen(str)).ToLocalChecked(); 
  10.   demo->InstanceTemplate()->SetInternalFieldCount(1); 
  11.   exports->Set(context, name, demo->GetFunction(context).ToLocalChecked()).Check(); 
  12.  
  13.  
  14. NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize) 

然后在 JS 通过以下方式调用。

 

  1. const { Demo } = require('demo.node'); 
  2.  
  3. const demo = new Demo(); 

可以看到 C++ Demo 类中有一个 uv_timer_t 成员。主要用来定时去抓取 V8 堆快照,所以把它注册到 Libuv 中。

 

  1. uv_timer_init(loop, &timer); 
  2.  
  3. uv_timer_start(&timer, timer_cb, 1000, 1000); 

然后使用的过程中我们发现,定时器随机触发了几次后,就不触发了。经过多种测试无果后,我不得不编译一个 debug 版本的 Node.js 进行单步调试,然后就发现了有意思的事情。第一次进入 poll io 阶段时,一切正常,1 秒后超时。

 

 

 

 

但是后面再次进入 poll io 阶段时,诡异的事情发生了。

 

 

 

 

超时时间变成了一个很大的数字,正常来说,我设置的每隔一秒超时一次,这里应该是 1才对,为什么会出现一个诡异的数字呢。思考了一下,猜想是这块内存被释放了,然后里面保存了一些脏数据,接着我给 Demo 类加了个析构函数。

 

  1. ~Demo() { 
  2.   LOG("dead"); 
  3.  

然后发现,这个类对象居然被析构了。通过栈追踪发现逻辑来自于 ObjectWrap 的 WeakCallback。

 

 

 

 

WeakCallback 的代码如下。

  1. static void WeakCallback(const v8::WeakCallbackInfo<ObjectWrap>& data) { 
  2.    ObjectWrap* wrap = data.GetParameter(); 
  3.    wrap->handle_.Reset(); 
  4.    delete wrap; 
  5.  

delete wrap 就是 delete 了 Demo 对象。而这个 WeakCallback 的源头来自 ObjectWrap 的 MakeWeak。

  1. inline void MakeWeak() { 
  2.     persistent().SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter); 

这个 MakeWeak 又来源于 Wrap。

  1. inline void Wrap(v8::Local<v8::Object> handle) { 
  2.     // 关联 C++ 对象和 Demo 对象 
  3.     handle->SetAlignedPointerInInternalField(0, this); 
  4.     persistent().Reset(v8::Isolate::GetCurrent(), handle); 
  5.     MakeWeak(); 
  6.  

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++ 对象回收了。

 

  1. setInterval(() => { 
  2.     Buffer.from('x'.repeat('10')) 
  3.  
  4. },3000) 

这个问题的解决方式就是调用 ObjectWrap 的 Ref 函数消除弱引用(或者在 JS 层保持对这个对象的引用)。

 

  1. virtual void Ref() { 
  2.     persistent().ClearWeak(); 
  3.     refs_++; 

回过头来看看 Node.js 中另一个类似功能的类 BaseObject。

  1. BaseObject::BaseObject(Environment* env, v8::Local<v8::Object> object) 
  2.     : persistent_handle_(env->isolate(), object), env_(env) { 
  3.   object->SetAlignedPointerInInternalField(BaseObject::kSlot, static_cast<void*>(this)); 
  4.  

它并没有设置弱引用的逻辑。所以在 Node.js 的 C++ 模块里,我们也看不到主动调用 Ref 的代码。这或许是使用 ObjectWrap 时需要注意的问题。

 

 

 

总结:大致分析了 ObjectWrap 相关的这个问题,但是其实排查过程比描述的繁琐和困难,主要是一开始没有用 debug 版本的 Node.js 进行调试,把排查聚焦在打快照的地方了,因为那里涉及了多线程操作同一个 isolate,所以以为是 V8 API 使用方式的问题。总的来说,如果碰到 Node.js 诡异的一些问题,不妨打个 debug 版本的 Node.js 进行调试,可能会更快地找到问题,从中也能学到很多东西。

 

责任编辑:姜华 来源: 编程杂技
相关推荐

2017-04-10 13:28:32

Node.jsJavaScript

2022-06-23 06:34:56

Node.js子线程

2023-06-30 23:25:46

HTTP模块内存

2013-11-01 09:34:56

Node.js技术

2015-03-10 10:59:18

Node.js开发指南基础介绍

2020-05-29 15:33:28

Node.js框架JavaScript

2012-02-03 09:25:39

Node.js

2021-12-25 22:29:57

Node.js 微任务处理事件循环

2011-09-08 13:46:14

node.js

2011-09-02 14:47:48

Node

2011-09-09 14:23:13

Node.js

2011-11-01 10:30:36

Node.js

2012-10-24 14:56:30

IBMdw

2011-11-10 08:55:00

Node.js

2011-11-02 09:04:15

Node.js

2021-11-06 18:40:27

js底层模块

2021-09-26 05:06:04

Node.js模块机制

2019-07-09 14:50:15

Node.js前端工具

2015-06-23 15:27:53

HproseNode.js

2021-02-10 07:38:43

Node.js后端框架
点赞
收藏

51CTO技术栈公众号