Skip to content

Ability to get reference counter value from Napi::Reference object  #702

Closed

Description

@ikokostya

There is no way to get reference count value from Napi::Reference object without side effects. I can use only Ref() or Unref() method, which returns the resulting reference count value.

What is the reason to not export this functionality as public API?

Activity

  1. mhdawson commented on Apr 13, 2020

    @mhdawson
    Member

    There was not specific reason, other than we did not think of it as requirement. What is your use case for it?

  2. ikokostya commented on Apr 13, 2020

    @ikokostya
    ContributorAuthor

    What is your use case for it?

    I want to use ref counter for detecting if there are no active workers for some object.

    Consider some class with asynchronous methods:

    class Foo {
      foo(): Promise<void>;
      bar(): Promise<void>;
    
      dispose(): Promise<void>;
    }

    Each async method increases ref counter for Foo instance to prevent GC. In this case native object can be disposed only if ref count is equal to 0:

    class Foo : public Napi::ObjectWrap<Foo> { 
      // ...
    };
    
    class DisposeWorker : public Napi::AsyncWorker {
      // ...
    };
    
    Napi::Value Foo::Dispose(const Napi::CallbackInfo& info) {
      if (RefCount() > 0) {
        // Execute worker in future when ref count will be equal to 0.
        pending_dispose_worker_ = new DisposeWorker();
        return;
      }
      // Call worker immediately.
      auto worker = new DisposeWorker();
      worker->Queue();
    }
  3. gabrielschulhof commented on Apr 20, 2020

    @gabrielschulhof
    Contributor

    @ikokostya if you want to execute some code when a certain Napi::Object goes out of scope you can use Napi::Object::AddFinalizer to attach a callback that will be called when the Napi::Object is about to be garbage-collected.

  4. gabrielschulhof commented on Apr 20, 2020

    @gabrielschulhof
    Contributor

    @ikokostya something like this might work:

    class Foo: public Napi::ObjectWrap<Foo> {
     public:
      Foo(const Napi::FunctionCallbackInfo& info) {
        auto worker = new DisposeWorker();
        info
          .This()
          .As<Napi::Object>()
          .AddFinalizer([](Napi::Env env, DisposeWorker* worker) {
            worker->Queue();
          }, worker);
      }
    };
  5. ikokostya commented on Apr 20, 2020

    @ikokostya
    ContributorAuthor

    @gabrielschulhof I have another use case. The dispose worker calls non-trivial destructor on the corresponding native object:

    class DisposeWorker : public AsyncWorker {
     public:
      DisposeWorker(Napi::Env env, Foo* foo) : AsyncWorker(env), foo_(foo) {
        foo_->Ref();
      }
    
      ~DisposeWorker() {
        foo_->Unref();
      }
    
     protected:
      void Execute() override {
        foo_->native_handle.release(); // where native_handle is std::unique_ptr<NativeFoo>
      }
    
     private:
      Foo* foo_;
    }; 

    When dispose will be called from JavaScript

    await foo.dispose();

    I cannot just queue new worker

    auto worker = new DisposeWorker(info.Env(), this);
    worker->Queue();

    because there may exist another worker threads on the native object. Instead, I need to wait while other worker threads will be executed before start the dispose worker.

    Napi::Object::AddFinalizer doesn't help me here, because JavaScript code keeps reference to the foo object and GC will not be called.

  6. davedoesdev commented on May 8, 2020

    @davedoesdev
    Contributor

    Should finalizer always be called before napi_get_reference_value starts returning null for result?

    I'm seeing occasions where my finalizer gets called later, meaning Reference#Value() is throwing an Invalid Argument exception.

  7. mhdawson commented on May 8, 2020

    @mhdawson
    Member

    I don't believe there is any guarantee as to when the finalizer will be called except that it will be sometime after the object is collected. This means it is reasonable for napi_get_reference_value to return NULL before the finalizer is run.

    Do you have a small recreate? The code in Value checks if the
    _ref is nullptr so I'm wondering how the call to napi_get_reference_value is being passed an invalid value.

  8. davedoesdev commented on May 8, 2020

    @davedoesdev
    Contributor

    I can try to get a small recreate. But in the meantime I believe the problem to be that napi_get_reference_value in Value() is succeeding but returning NULL in value.
    The invalid argument exception then comes from the call to T(_env, value):
    In my case T is Buffer<uint8_t> so it ends up calling TypedArrayOf<T>::TypedArrayOf(napi_env env, napi_value value). This then calls napi_get_typedarray_info which fails and results in an error being called.

  9. davedoesdev commented on May 12, 2020

    @davedoesdev
    Contributor

    @mhdawson I got a small repro.

    stalereference.cc

    #include "napi.h"
    
    using namespace Napi;
    
    static Reference<Buffer<uint8_t>> weak;
    
    void CreateWeak(const CallbackInfo& info) {
        weak = Weak(Buffer<uint8_t>::New(info.Env(), 1));
        weak.SuppressDestruct();
    }
    
    void CallValue(const CallbackInfo&) {
        if (!weak.IsEmpty()) {
            weak.Value();
        }
    }
    
    Object InitStaleReference(Env env) {
        Object exports = Object::New(env);
    
        exports["createWeak"] = Function::New(env, CreateWeak);
        exports["callValue"] = Function::New(env, CallValue);
    
        return exports;
    }

    stalereference.js

    'use strict';
    const buildType = process.config.target_defaults.default_configuration;
    
    const test = binding => {
        binding.stalereference.createWeak();
        global.gc();
        binding.stalereference.callValue();
    };
    
    test(require(`./build/${buildType}/binding.node`));
    test(require(`./build/${buildType}/binding_noexcept.node`));
  10. mhdawson commented on May 12, 2020

    @mhdawson
    Member

    Thanks, I've created a test we can add to our test suite and am looking at how we should fix.

  11. mhdawson commented on May 13, 2020

    @mhdawson
    Member

    @davedoesdev can you check out the fix in #726.

    Note that you will need to call IsEmpty() on the value after getting it back from weak.Value() to check if the value was returned or not. The weak.IsEmpty() does not check if the underlying object is present or not just if the weak itself wraps a N-API reference.

  12. davedoesdev commented on May 13, 2020

    @davedoesdev
    Contributor

    Yep, works for me - thanks!

  13. ikokostya commented on May 14, 2020

    @ikokostya
    ContributorAuthor

    I solved my original problem without Napi::Reference. Just to be clear, current value of Napi::Reference instance can be got using napi_get_reference_value function. Closing issue.

  14. ikokostya commented on May 14, 2020

    @ikokostya
    ContributorAuthor

    Oops, missed that there is open associated PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions