Node-addon-api: How do I save a callback for later with node-addon-api?

Created on 23 Jan 2019  ·  10Comments  ·  Source: nodejs/node-addon-api

I want my C library to be able to call a JS function multiple times. I got it to work using Nan but am having trouble converting it to N-API/node-addon-api.

How do I save a JS callback function and call it later from C?

Here's what I have using Nan:

Persistent<Function> r_log;
void sendLogMessageToJS(char* msg) {
    if (!r_log.IsEmpty()) {
        Isolate* isolate = Isolate::GetCurrent();
        Local<Function> func = Local<Function>::New(isolate, r_log);
        if (!func.IsEmpty()) {
        const unsigned argc = 1;
        Local<Value> argv[argc] = {
            String::NewFromUtf8(isolate, msg)
        };
        func->Call(Null(isolate), argc, argv);
        }
    }
}
NAN_METHOD(register_logger) {
    Isolate* isolate = info.GetIsolate();
    if (info[0]->IsFunction()) {
        Local<Function> func = Local<Function>::Cast(info[0]);
        Function * ptr = *func;
        r_log.Reset(isolate, func);
        myclibrary_register_logger(sendLogMessageToJS);
    } else {
        r_log.Reset();
    }
}

How do I do the equivalent with node-addon-api? All the examples I've seen immediately call the callback or use AsyncWorker to somehow save the callback. I can't figure out how AsyncWorker is doing it.

It's also important to note that I make a new string inside the sendLogMessageToJS function (which requires an Env afaik).

Most helpful comment

@mhdawson Thank you for the pointers. Here's my complete working code (for anyone else who wonders this):

FunctionReference r_log;
void emitLogInJS(char* msg) {
  if (r_log != nullptr) {
    Env env = r_log.Env();
    String message = String::New(env, msg);
    std::vector<napi_value> args = {message}; 
    r_log.Call(args);
  } 
}
void register_logger(const CallbackInfo& info) {
  r_log = Persistent(info[0].As<Function>());
  myclibrary_register_logger(emitLogInJS);
}

All 10 comments

You should just be able to use

Napi::FunctionReference r_log = Napi::FunctionReference::Persistent(const Napi::Function& value);

in place of the Persistent you are using.

See the docs here: https://github.com/nodejs/node-addon-api/blob/master/doc/function_reference.md

As a subclass of Napi::Reference Napi:FunctionReference was the Ref, Reset etc. calls.

Note that you may need to call void Napi::Reference::SuppressDestruct(); as described in the doc as well.

In terms of needing an ENV in sendLogMessageToJS you should pass it an Env or one of the Napi:: objects from which you can get an env.

@mhdawson Thank you for the pointers. Here's my complete working code (for anyone else who wonders this):

FunctionReference r_log;
void emitLogInJS(char* msg) {
  if (r_log != nullptr) {
    Env env = r_log.Env();
    String message = String::New(env, msg);
    std::vector<napi_value> args = {message}; 
    r_log.Call(args);
  } 
}
void register_logger(const CallbackInfo& info) {
  r_log = Persistent(info[0].As<Function>());
  myclibrary_register_logger(emitLogInJS);
}

@iffy @mhdawson
I wonder how this is working asynchronously - I'm getting the following error before my program crashes, when calling back from another thread the way you do. I also tried to add r_log.SuppressDestruct() in register_logger(), to no avail.

#
# Fatal error in HandleScope::HandleScope
# Entering the V8 API without proper locking in place
#

ハンドルされていない例外: System.AccessViolationException: 保護されているメモリに読み取りまたは書き込み操作を行おうとしました。他のメモリが壊れていることが考えられます。
   場所 napi_open_escapable_handle_scope(napi_env__* , napi_escapable_handle_scope__** )
   場所 Napi.EscapableHandleScope.{ctor}(EscapableHandleScope* , Env env)
   場所 Napi.FunctionReference.Call(FunctionReference* , Value* , vector<napi_value__ \*\,std::allocator<napi_value__ \*> >* args)

(I'm using C++/CLI, hence the C++ and .Net mix)

I guess I should mention that I'm not doing anything asynchronous. Calls to emitLogInJS are always results of synchronous calls initiated by JS.

@iffy That would explain it... So they are always on the JS thread, but still made after the call to register_logger, that registered the callback, finished.

Is there a way to call the saved callback from another thread? I always get the same error as @twoi

Interested in how t do this async as well

@Mattyzero @brentthierens N-API has napi_threadsafe_function, which allows calls into JS from different threads. We're still working on the C++ wrapper for it. You can use the thread-safe function without the wrapper, though, because N-API code and the node-addon-api C++ wrapper code can be mixed freely:

using namespace Napi;

static Value doSomethingUsefulWithData(Env env, void* data) {
  // Convert `data` into a JavaScript value and return it.
}

// Runs on the JS thread.
static void
FinalizeTSFN(napi_env env, void* data, void* context) {
  // This is where you would wait for the threads to quit. This
  // function will only be called when all the threads are done using
  // the tsfn so, presumably, they can be joined here.
}

// Runs on the JS thread.
static void
CallIntoJS(napi_env env, napi_value js_cb, void* context, void* data) {
  if (env != nullptr && js_cb != nullptr) {
    // `data` was passed to `napi_call_threadsafe_function()` by one
    // of the threads. The order in which the threads add data as
    // they call `napi_call_threadsafe_function()` is the order in
    // which they will be given to this callback.
    //
    // `context` was passed to `napi_create_threadsafe_function()` and
    // is being provided here.
    //
    Function::New(env, js_cb).Call({
      DoSomethingUsefulWithData(env, data)
    });
  } else {
    // The tsfn is in the process of getting cleaned up and there are
    // still items left on the queue. This function gets called with
    // each `void*` item, but with `env` and `js_cb` set to `NULL`,
    // because calls can no longer be made into JS, but the `void*`s
    // may still need to be freed.
  }
}

// Runs on the JS thread.
static void
CreateThreadsafeCallback(const CallbackInfo& info) {
  if (!info[0].IsFunction()) {
    Error::New(info.Env(), "First argument must be a function")
      .ThrowAsJavaScriptException();
    return;
  }

  napi_threadsafe_function tsfn;

  NAPI_THROW_IF_FAILED_VOID(info.Env(),
    napi_create_threadsafe_function(info.Env(),
      info[0],
      nullptr,
      String::New(info.Env(), "My thread-safe function"),
      0, // for an unlimited queue size
      1, // initially only used from the main thread
      nullptr, // data to make use of during finalization
      FinalizeTSFN, // gets called when the tsfn goes out of use
      nullptr, // data that can be set here and retrieved on any thread
      CallIntoJS, // function to call into JS
      &tsfn));

  // Now you can pass `tsfn` to any number of threads. Each one must
  // first call `napi_threadsafe_function_acquire()`. Then it may call
  // `napi_call_threadsafe_function()` any number of times. If on one
  // of those occasions the return value from
  // `napi_call_threadsafe_function()` is `napi_closing`, the thread
  // must make no more calls to any of the thread-safe function APIs.
  // If it never receives `napi_closing` from
  // `napi_call_threadsafe_function()` then, before exiting, the
  // thread must call `napi_release_threadsafe_function()`.
}

@gabrielschulhof Regarding your comment at the end about either using napi_closing or napi_release_threadsafe_function: I'm in a situation where my Node script has completed running (top to bottom), but is hanging because I never released the main thread's reference to the threadsafe function. How can I queue up a call to napi_release_threadsafe_function to be called when the Node.js main thread is ready to be done?

@iffy ideally you would call napi_release_threadsafe_function() from the main thread immediately after having made sure that the other threads each have a reference to the thread-safe function. So, for example, the main thread could release the thread-safe function when the first call comes in from the secondary thread. You could for example do it in the napi_threadsafe_function_call_js. You can govern this we a single boolean, which you can safely check without having to protect it with a mutex, because you would be setting it from the main thread after napi_create_threadsafe_function and you would be checking it from napi_threadsafe_function_call_js, both of which run on the main thread. The other threads would have no business with it.

Was this page helpful?
0 / 5 - 0 ratings