JavaScript functions can normally only be called from a native addon's main
thread. If an addon creates additional threads, then node-addon-api functions
that require a Napi::Env
, Napi::Value
, or Napi::Reference
must not be
called from those threads.
When an addon has additional threads and JavaScript functions need to be invoked
based on the processing completed by those threads, those threads must
communicate with the addon's main thread so that the main thread can invoke the
JavaScript function on their behalf. The thread-safe function APIs provide an
easy way to do this. These APIs provide two types --
Napi::ThreadSafeFunction
and
Napi::TypedThreadSafeFunction
-- as well as
APIs to create, destroy, and call objects of this type. The differences between
the two are subtle and are highlighted below.
Regardless of which type you choose, the APIs between the two are similar.
Napi::[Typed]ThreadSafeFunction::New()
creates a persistent reference that
holds a JavaScript function which can be called from multiple threads. The calls
happen asynchronously. This means that values with which the JavaScript callback
is to be called will be placed in a queue, and, for each value in the queue, a
call will eventually be made to the JavaScript function.
Napi::[Typed]ThreadSafeFunction
objects are destroyed when every thread which
uses the object has called Release()
or has received a return status of
napi_closing
in response to a call to BlockingCall()
or NonBlockingCall()
.
The queue is emptied before the Napi::[Typed]ThreadSafeFunction
is destroyed.
It is important that Release()
be the last API call made in conjunction with a
given Napi::[Typed]ThreadSafeFunction
, because after the call completes, there
is no guarantee that the Napi::[Typed]ThreadSafeFunction
is still allocated.
For the same reason it is also important that no more use be made of a
thread-safe function after receiving a return value of napi_closing
in
response to a call to BlockingCall()
or NonBlockingCall()
. Data associated
with the Napi::[Typed]ThreadSafeFunction
can be freed in its Finalizer
callback which was passed to [Typed]ThreadSafeFunction::New()
.
Once the number of threads making use of a Napi::[Typed]ThreadSafeFunction
reaches zero, no further threads can start making use of it by calling
Acquire()
. In fact, all subsequent API calls associated with it, except
Release()
, will return an error value of napi_closing
.
The choice between Napi::ThreadSafeFunction
and
Napi::TypedThreadSafeFunction
depends largely on how you plan to execute your
native C++ code (the "callback") on the Node.js thread.
This API is designed without Node-API 5 native support for the optional JavaScript function callback feature.
This API has some dynamic functionality, in that:
- The
[Non]BlockingCall()
methods provide aNapi::Function
parameter as the callback to run when processing the data item on the main thread -- theCallJs
callback. Since the callback is a parameter, it can be changed for every call. - Different C++ data types may be passed with each call of
[Non]BlockingCall()
to match the specific data type as specified in theCallJs
callback.
Note that this functionality comes with some additional overhead and situational memory leaks:
- The API acts as a "broker" between the underlying
napi_threadsafe_function
, and dynamically constructs a wrapper for your callback on the heap for every call to[Non]BlockingCall()
. - In acting in this "broker" fashion, the API will call the underlying "make
call" Node-API method on this packaged item. If the API has determined the
thread-safe function is no longer accessible (eg. all threads have released
yet there are still items on the queue), the callback passed to
[Non]BlockingCall will not execute. This means it is impossible to perform
clean-up for calls that never execute their
CallJs
callback. This may lead to memory leaks if you are dynamically allocating memory. - The
CallJs
does not receive the thread-safe function's context as a parameter. In order for the callback to access the context, it must have a reference to either (1) the context directly, or (2) the thread-safe function to callGetContext()
. Furthermore, theGetContext()
method is not type-safe, as the method returns an object that can be "any-casted", instead of having a static type.
The TypedThreadSafeFunction
class is a new implementation to address the
drawbacks listed above. The API is designed with Node-API 5's support of an
optional function callback. The API will correctly allow developers to pass
std::nullptr
instead of a const Function&
for the callback function
specified in ::New
. It also provides helper APIs to target Node-API 4 and
construct a no-op Function
or to target Node-API 5 and "construct" a
std::nullptr
callback. This allows a single codebase to use the same APIs,
with just a switch of the NAPI_VERSION
compile-time constant.
The removal of the dynamic call functionality has the following implications:
- The API does not act as a "broker" compared to the
Napi::ThreadSafeFunction
. Once Node.js finalizes the thread-safe function, theCallJs
callback will execute with an emptyNapi::Env
for any remaining items on the queue. This provides the ability to handle any necessary cleanup of the item's data. - The callback does receive the context as a parameter, so a call to
GetContext()
is not necessary. This context type is specified as the first template argument specified to::New
, ensuring type safety. - The
New()
constructor accepts theCallJs
callback as the second type argument. The callback must be statically defined for the API to access it. This affords the ability to statically pass the context as the correct type across all methods. - Only one C++ data type may be specified to every call to
[Non]BlockingCall()
-- the third template argument specified to::New
. Any "dynamic call data" must be implemented by the user.
In summary, it may be best to use Napi::TypedThreadSafeFunction
if:
- static, compile-time support for targeting Node-API 4 or 5+ with an optional JavaScript callback feature is desired;
- the callback can have
static
storage class and will not change across calls to[Non]BlockingCall()
; - cleanup of items' data is required (eg. deleting dynamically-allocated data that is created at the caller level).
Otherwise, Napi::ThreadSafeFunction
may be a better choice.