Node.js ObjectWrap 的弱引用問題
前言:最近在寫 Node.js Addon 的過程中,遇到了一個問題,然后發(fā)現(xiàn)是 ObjectWrap 弱引用導(dǎo)致的,本文介紹一下具體的問題和排查過程,以及 ObjectWrap 的使用問題。
ObjectWrap 用于寫 Addon 的時候?qū)С?C++ 對象給 JS 層使用,大致用法如下。首先定義一個 C++ 類。
- 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;
- };
然后導(dǎo)出這個類到 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)
然后在 JS 通過以下方式調(diào)用。
- const { Demo } = require('demo.node');
- const demo = new Demo();
可以看到 C++ Demo 類中有一個 uv_timer_t 成員。主要用來定時去抓取 V8 堆快照,所以把它注冊到 Libuv 中。
- uv_timer_init(loop, &timer);
- uv_timer_start(&timer, timer_cb, 1000, 1000);
然后使用的過程中我們發(fā)現(xiàn),定時器隨機觸發(fā)了幾次后,就不觸發(fā)了。經(jīng)過多種測試無果后,我不得不編譯一個 debug 版本的 Node.js 進行單步調(diào)試,然后就發(fā)現(xiàn)了有意思的事情。第一次進入 poll io 階段時,一切正常,1 秒后超時。
但是后面再次進入 poll io 階段時,詭異的事情發(fā)生了。
超時時間變成了一個很大的數(shù)字,正常來說,我設(shè)置的每隔一秒超時一次,這里應(yīng)該是 1才對,為什么會出現(xiàn)一個詭異的數(shù)字呢。思考了一下,猜想是這塊內(nèi)存被釋放了,然后里面保存了一些臟數(shù)據(jù),接著我給 Demo 類加了個析構(gòu)函數(shù)。
- ~Demo() {
- LOG("dead");
- }
然后發(fā)現(xiàn),這個類對象居然被析構(gòu)了。通過棧追蹤發(fā)現(xiàn)邏輯來自于 ObjectWrap 的 WeakCallback。
WeakCallback 的代碼如下。
- static void WeakCallback(const v8::WeakCallbackInfo<ObjectWrap>& data) {
- ObjectWrap* wrap = data.GetParameter();
- wrap->handle_.Reset();
- delete wrap;
- }
delete wrap 就是 delete 了 Demo 對象。而這個 WeakCallback 的源頭來自 ObjectWrap 的 MakeWeak。
- inline void MakeWeak() {
- persistent().SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);
- }
這個 MakeWeak 又來源于 Wrap。
- inline void Wrap(v8::Local<v8::Object> handle) {
- // 關(guān)聯(lián) C++ 對象和 Demo 對象
- handle->SetAlignedPointerInInternalField(0, this);
- persistent().Reset(v8::Isolate::GetCurrent(), handle);
- MakeWeak();
- }
Wrap 是創(chuàng)建 Demo 對象時調(diào)用的函數(shù)。用于關(guān)聯(lián) JS 層對象和 C++ 對象,關(guān)系如下。
所以 JS 創(chuàng)建一個 Demo 對象的時候,就會指向一個 C++ 對象,然后 Demo 對象也有個持久句柄指向這個 C++ 對象。但是它默認情況下調(diào)用了 MakeWeak,也就是弱引用。而 JS 層在創(chuàng)建完 Demo 對象后就離開了作用域,因為 JS 模塊是被函數(shù)包裹起來的,執(zhí)行完變量就被 gc了,除非通過 module.exports 或全局變量保持對 C++ 對象的引用。所以就導(dǎo)致了 C++ 對象最終被 Demo 對象以弱引用的方式引用著,等待 gc 的時候被回收。這里又引出了另一個問題,當我把抓取快照的代碼改成一些簡單的代碼時,并不容易觸發(fā)這個問題,原因在于它沒有觸發(fā) gc。后來我嘗試在 JS 層分配一些內(nèi)存,最終也成功觸發(fā)了這個問題,因為下面的代碼會導(dǎo)致 gc。而 gc 的時候就把 C++ 對象回收了。
- setInterval(() => {
- Buffer.from('x'.repeat('10'))
- },3000)
這個問題的解決方式就是調(diào)用 ObjectWrap 的 Ref 函數(shù)消除弱引用(或者在 JS 層保持對這個對象的引用)。
- virtual void Ref() {
- persistent().ClearWeak();
- refs_++;
- }
回過頭來看看 Node.js 中另一個類似功能的類 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));
- }
它并沒有設(shè)置弱引用的邏輯。所以在 Node.js 的 C++ 模塊里,我們也看不到主動調(diào)用 Ref 的代碼。這或許是使用 ObjectWrap 時需要注意的問題。
總結(jié):大致分析了 ObjectWrap 相關(guān)的這個問題,但是其實排查過程比描述的繁瑣和困難,主要是一開始沒有用 debug 版本的 Node.js 進行調(diào)試,把排查聚焦在打快照的地方了,因為那里涉及了多線程操作同一個 isolate,所以以為是 V8 API 使用方式的問題??偟膩碚f,如果碰到 Node.js 詭異的一些問題,不妨打個 debug 版本的 Node.js 進行調(diào)試,可能會更快地找到問題,從中也能學(xué)到很多東西。