nodejs / node-addon-api

Module for using Node-API from C++

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 of Function when other finalisers are actually called?
  • Or, would node keep a reference to the Function for other reasons?

This is the call stack when the Function's destructor is called

image

FreeEnvironment worries me. Am I reading this correctly?

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.

And btw, I don't think this is a node-addon-api matter.

For reference, this is where the destructor is correctly registered

status = napi_add_finalizer(env, obj, data, finalizer, hint, nullptr);

which will in turn delete the lambda and all its captures by value.