从std::shared_ptr取裸指针适配第三方库的内存安全问题咨询
Hey there! Let's break down your problem step by step, fix your incorrect approaches, and show you the safe way to handle std::shared_ptr with Napi::External for your Node-API addon.
First, the problems with your two approaches
Initial approach: Hidden dangling pointer risk
When you pass pSomeObject.get() to Napi::External::New, you're giving it a raw pointer to your SomeObject instance. The issue here is that once the enclosing scope ends, the stack-allocated std::shared_ptr<SomeObject> pSomeObject gets destroyed. If this was the last reference to SomeObject, its reference count drops to 0 and the object is freed—but the Napi::External still holds that raw pointer. This creates a dangling pointer, and any future use of that pointer (like in your MyNapiObjectWrapper) is undefined behavior. Tests might pass now, but this will eventually cause crashes or memory corruption in production.
Modified approach: Severe stack memory misuse
Your second approach passes &pSomeObject (the address of the stack-allocated shared_ptr) to Napi::External. This is even more dangerous: when the scope ends, the stack frame for this block is unwound, and the memory where pSomeObject lived is reclaimed. The Napi::External now holds a pointer to invalid memory, and when your MyNapiObjectWrapper constructor dereferences it, you're accessing freed stack memory. The reference count spike you saw during debugging was just a temporary artifact—this code will fail unpredictably.
The safe, compliant way to handle this
The core rule here is: bind the lifetime of your std::shared_ptr to the lifetime of your Napi object. Node.js manages Napi objects via garbage collection, so we need to ensure SomeObject isn't freed until the Napi wrapper is destroyed. Here are two reliable solutions:
Solution 1: Use Napi::External's Finalizer to capture the shared_ptr
The Napi::External::New method accepts a third parameter: a finalizer callback that runs when the External object is garbage-collected. We can capture a copy of our shared_ptr in this callback, which keeps the SomeObject instance alive until the External is destroyed:
{ std::shared_ptr<SomeObject> pSomeObject = something.CreateSomeObject(); // Create External with raw pointer, and use a finalizer to hold the shared_ptr copy auto ext = Napi::External<SomeObject>::New( info.Env(), pSomeObject.get(), [pSomeObject](Napi::Env env, SomeObject* ptr) { // This lambda runs when the External is GC'd; the shared_ptr copy is destroyed here, dropping the reference count } ); auto instance = MyNapiObjectWrapper::Create({ ext }); return instance; }
If your MyNapiObjectWrapper needs to hold a reference to SomeObject, you can either store the raw pointer (relying on the finalizer to keep it alive) or, better yet, modify the setup to pass a shared_ptr copy directly to the wrapper.
Solution 2: Heap-allocate the shared_ptr, manage it with a Finalizer
If your MyNapiObjectWrapper already has a std::shared_ptr<SomeObject> member, this is the most robust approach. We allocate the shared_ptr on the heap (so it outlives the stack scope), pass its address to Napi::External, and use a finalizer to clean up the heap allocation:
{ std::shared_ptr<SomeObject> pSomeObject = something.CreateSomeObject(); // Move the shared_ptr to heap memory to avoid stack destruction auto heapSharedPtr = new std::shared_ptr<SomeObject>(std::move(pSomeObject)); auto ext = Napi::External<std::shared_ptr<SomeObject>>::New( info.Env(), heapSharedPtr, [](Napi::Env env, std::shared_ptr<SomeObject>* ptr) { delete ptr; // Destroy the heap-allocated shared_ptr, dropping the reference count } ); auto instance = MyNapiObjectWrapper::Create({ ext }); return instance; }
Then update your MyNapiObjectWrapper constructor to take ownership of the shared_ptr:
MyNapiObjectWrapper(const CallbackInfo& info) { Napi::Env env = info.Env(); Napi::HandleScope scope(env); auto external = info[0].As<const Napi::External<std::shared_ptr<SomeObject>>>(); // Move the heap-allocated shared_ptr into our member (avoids extra reference count increment) this->someObject = std::move(*external.Data()); }
This guarantees:
- The heap-allocated
shared_ptrsurvives until theNapi::Externalis GC'd - Your
MyNapiObjectWrapperholds a valid reference toSomeObjectfor its entire lifetime - No dangling pointers or invalid memory accesses
Correcting your memory management misconceptions
- Stack variable lifetimes matter: Stack-allocated variables (like your original
pSomeObject) are destroyed immediately when their scope ends. Never pass pointers to stack variables to objects with longer lifetimes (like Napi objects). - shared_ptr addresses are not magic: Passing the address of a
shared_ptr(stack-allocated) doesn't extend its lifetime. When the stackshared_ptris destroyed, that address becomes invalid—dereferencing it is illegal, regardless of the reference count of the object it pointed to. - Napi objects need explicit lifetime bindings: Node.js's GC decides when to destroy Napi objects, so you can't rely on C++ scope rules to manage their associated memory. Use finalizers to tie C++ resource lifetimes to Napi object lifetimes.
内容的提问来源于stack exchange,提问作者Nick Hyland

