Napi::Function, C++ lambdas & finalisers
audetto opened this issue · comments
I have a question about finalisers and c++ closures.
The code looks like this
Napi::Value getContext(env)
{
Napi::Object obj = Napi::Object::New(env);
std::shared_ptr<Context> context = .....; // create a new C++ context
const auto invoke = [context](const Napi::CallbackInfo &info) -> Napi::Value
{
return Invoke(context, info);
};
obj.Set(Napi::String::New(env, "invoke"), Napi::Function::New(env, invoke));
return obj;
}
so I can write
context1 = module.getContext();
context2 = module.getContext();
context1.invoke(.....);
context2.invoke(.....);
context1 = null;
context2 = null;
To keep the 2 separate.
Everything works, but I only see the destructor of Context
being called at the end of the script.
I know that without some yielding
it won't work, but I think this is a different issue (if an issue it is).
I already see finalisers being called for other objects created by my addon, so the yielding
(not shown above) seems to work, but this is not a finaliser I control directly.
The lambda invoke
captures by value the shared_ptr
context, so when the lambda's destructor is called, I expect Context
's destructor to be called to.
- Is there any reason why
node
would delay the call to the finaliser ofFunction
when other finalisers are actually called? - Or, would
node
keep a reference to theFunction
for other reasons?
The V8 garbage collector runs whenever the engine determines it needs to be ran. Setting a variable to null
will not immediately trigger the garbage collector.
If you launch the node process with the --expose-gc
flag, you can then call global.gc()
to force node to run garbage collection.
I know that without some yielding it won't work, but I think this is a different issue (if an issue it is).
Currently, finalizers are scheduled to run using SetImmediate
, so the current execution/tick executing JavaScript of the event loop needs to end somehow before the finalizers execute. This limitation is remedied by nodejs/node#42651
I know. I inserted a lot of SetImmediate. This is why i say that other finalisers from the same addon are run. They are attached to a Napi::External.
This means that I am sure I give node the chance to run them.
But the one implicitly associated to a Napi::Function seem to behave differently.
I make sure as well the script runs long enough to see 1000s of my other objects being collected. But still cannot see these ones.
I've made some progress.
The finaliser is indeed called, but a lot less frequently than the ones associated to externals.
I was running too few iterations of my code to see it.
I see external's finalisers being called after about 100 iterations, but it takes 1000 for Napi::Function
's.
I've moved the function generation to javascript
function createInvoker() {
const context = createNewContext(); // a Napi::External from my addon module
function invoker(...args) {
return invoke(context, ...args); // from my addon module
}
return invoker;
}
And now, I see both externals being finalised at the same pace.
What I think it happens is that some parameter in the gc configuration slows down the finalisers associated to functions, as opposed to the one associated to externals, because maybe normally they are less urgent.
So I will stick to this setup which seems to work well.