Skip to content

Commit 48e6b58

Browse files
Merge pull request #742 from KevinEady/contexted-tsfn-api-gcc-4
tsfn: implement TypedThreadSafeFunction
2 parents d5e3721 + 5e5b9ce commit 48e6b58

21 files changed

+2252
-46
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ The following is the documentation for node-addon-api.
121121
- [AsyncWorker](doc/async_worker.md)
122122
- [AsyncContext](doc/async_context.md)
123123
- [AsyncWorker Variants](doc/async_worker_variants.md)
124-
- [Thread-safe Functions](doc/threadsafe_function.md)
124+
- [Thread-safe Functions](doc/threadsafe.md)
125+
- [ThreadSafeFunction](doc/threadsafe_function.md)
126+
- [TypedThreadSafeFunction](doc/typed_threadsafe_function.md)
125127
- [Promises](doc/promises.md)
126128
- [Version management](doc/version_management.md)
127129

doc/threadsafe.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Thread-safe Functions
2+
3+
JavaScript functions can normally only be called from a native addon's main
4+
thread. If an addon creates additional threads, then node-addon-api functions
5+
that require a `Napi::Env`, `Napi::Value`, or `Napi::Reference` must not be
6+
called from those threads.
7+
8+
When an addon has additional threads and JavaScript functions need to be invoked
9+
based on the processing completed by those threads, those threads must
10+
communicate with the addon's main thread so that the main thread can invoke the
11+
JavaScript function on their behalf. The thread-safe function APIs provide an
12+
easy way to do this. These APIs provide two types --
13+
[`Napi::ThreadSafeFunction`](threadsafe_function.md) and
14+
[`Napi::TypedThreadSafeFunction`](typed_threadsafe_function.md) -- as well as
15+
APIs to create, destroy, and call objects of this type. The differences between
16+
the two are subtle and are [highlighted below](#implementation-differences).
17+
Regardless of which type you choose, the APIs between the two are similar.
18+
19+
`Napi::[Typed]ThreadSafeFunction::New()` creates a persistent reference that
20+
holds a JavaScript function which can be called from multiple threads. The calls
21+
happen asynchronously. This means that values with which the JavaScript callback
22+
is to be called will be placed in a queue, and, for each value in the queue, a
23+
call will eventually be made to the JavaScript function.
24+
25+
`Napi::[Typed]ThreadSafeFunction` objects are destroyed when every thread which
26+
uses the object has called `Release()` or has received a return status of
27+
`napi_closing` in response to a call to `BlockingCall()` or `NonBlockingCall()`.
28+
The queue is emptied before the `Napi::[Typed]ThreadSafeFunction` is destroyed.
29+
It is important that `Release()` be the last API call made in conjunction with a
30+
given `Napi::[Typed]ThreadSafeFunction`, because after the call completes, there
31+
is no guarantee that the `Napi::[Typed]ThreadSafeFunction` is still allocated.
32+
For the same reason it is also important that no more use be made of a
33+
thread-safe function after receiving a return value of `napi_closing` in
34+
response to a call to `BlockingCall()` or `NonBlockingCall()`. Data associated
35+
with the `Napi::[Typed]ThreadSafeFunction` can be freed in its `Finalizer`
36+
callback which was passed to `[Typed]ThreadSafeFunction::New()`.
37+
38+
Once the number of threads making use of a `Napi::[Typed]ThreadSafeFunction`
39+
reaches zero, no further threads can start making use of it by calling
40+
`Acquire()`. In fact, all subsequent API calls associated with it, except
41+
`Release()`, will return an error value of `napi_closing`.
42+
43+
## Implementation Differences
44+
45+
The choice between `Napi::ThreadSafeFunction` and
46+
`Napi::TypedThreadSafeFunction` depends largely on how you plan to execute your
47+
native C++ code (the "callback") on the Node.js thread.
48+
49+
### [`Napi::ThreadSafeFunction`](threadsafe_function.md)
50+
51+
This API is designed without N-API 5 native support for [the optional JavaScript
52+
function callback feature](https://github.com/nodejs/node/commit/53297e66cb).
53+
`::New` methods that do not have a `Function` parameter will construct a
54+
_new_, no-op `Function` on the environment to pass to the underlying N-API
55+
call.
56+
57+
This API has some dynamic functionality, in that:
58+
- The `[Non]BlockingCall()` methods provide a `Napi::Function` parameter as the
59+
callback to run when processing the data item on the main thread -- the
60+
`CallJs` callback. Since the callback is a parameter, it can be changed for
61+
every call.
62+
- Different C++ data types may be passed with each call of `[Non]BlockingCall()`
63+
to match the specific data type as specified in the `CallJs` callback.
64+
65+
Note that this functionality comes with some **additional overhead** and
66+
situational **memory leaks**:
67+
- The API acts as a "broker" between the underlying `napi_threadsafe_function`,
68+
and dynamically constructs a wrapper for your callback on the heap for every
69+
call to `[Non]BlockingCall()`.
70+
- In acting in this "broker" fashion, the API will call the underlying "make
71+
call" N-API method on this packaged item. If the API has determined the
72+
thread-safe function is no longer accessible (eg. all threads have released
73+
yet there are still items on the queue), **the callback passed to
74+
[Non]BlockingCall will not execute**. This means it is impossible to perform
75+
clean-up for calls that never execute their `CallJs` callback. **This may lead
76+
to memory leaks** if you are dynamically allocating memory.
77+
- The `CallJs` does not receive the thread-safe function's context as a
78+
parameter. In order for the callback to access the context, it must have a
79+
reference to either (1) the context directly, or (2) the thread-safe function
80+
to call `GetContext()`. Furthermore, the `GetContext()` method is not
81+
_type-safe_, as the method returns an object that can be "any-casted", instead
82+
of having a static type.
83+
84+
### [`Napi::TypedThreadSafeFunction`](typed_threadsafe_function.md)
85+
86+
The `TypedThreadSafeFunction` class is a new implementation to address the
87+
drawbacks listed above. The API is designed with N-API 5's support of an
88+
optional function callback. The API will correctly allow developers to pass
89+
`std::nullptr` instead of a `const Function&` for the callback function
90+
specified in `::New`. It also provides helper APIs to _target_ N-API 4 and
91+
construct a no-op `Function` **or** to target N-API 5 and "construct" a
92+
`std::nullptr` callback. This allows a single codebase to use the same APIs,
93+
with just a switch of the `NAPI_VERSION` compile-time constant.
94+
95+
The removal of the dynamic call functionality has the following implications:
96+
- The API does _not_ act as a "broker" compared to the
97+
`Napi::ThreadSafeFunction`. Once Node.js finalizes the thread-safe function,
98+
the `CallJs` callback will execute with an empty `Napi::Env` for any remaining
99+
items on the queue. This provides the ability to handle any necessary cleanup
100+
of the item's data.
101+
- The callback _does_ receive the context as a parameter, so a call to
102+
`GetContext()` is _not_ necessary. This context type is specified as the
103+
**first template argument** specified to `::New`, ensuring type safety.
104+
- The `New()` constructor accepts the `CallJs` callback as the **second type
105+
argument**. The callback must be statically defined for the API to access it.
106+
This affords the ability to statically pass the context as the correct type
107+
across all methods.
108+
- Only one C++ data type may be specified to every call to `[Non]BlockingCall()`
109+
-- the **third template argument** specified to `::New`. Any "dynamic call
110+
data" must be implemented by the user.
111+
112+
113+
### Usage Suggestions
114+
115+
In summary, it may be best to use `Napi::TypedThreadSafeFunction` if:
116+
117+
- static, compile-time support for targeting N-API 4 or 5+ with an optional
118+
JavaScript callback feature is desired;
119+
- the callback can have `static` storage class and will not change across calls
120+
to `[Non]BlockingCall()`;
121+
- cleanup of items' data is required (eg. deleting dynamically-allocated data
122+
that is created at the caller level).
123+
124+
Otherwise, `Napi::ThreadSafeFunction` may be a better choice.

doc/threadsafe_function.md

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,10 @@
11
# ThreadSafeFunction
22

3-
JavaScript functions can normally only be called from a native addon's main
4-
thread. If an addon creates additional threads, then node-addon-api functions
5-
that require a `Napi::Env`, `Napi::Value`, or `Napi::Reference` must not be
6-
called from those threads.
7-
8-
When an addon has additional threads and JavaScript functions need to be invoked
9-
based on the processing completed by those threads, those threads must
10-
communicate with the addon's main thread so that the main thread can invoke the
11-
JavaScript function on their behalf. The thread-safe function APIs provide an
12-
easy way to do this.
13-
14-
These APIs provide the type `Napi::ThreadSafeFunction` as well as APIs to
15-
create, destroy, and call objects of this type.
16-
`Napi::ThreadSafeFunction::New()` creates a persistent reference that holds a
17-
JavaScript function which can be called from multiple threads. The calls happen
18-
asynchronously. This means that values with which the JavaScript callback is to
19-
be called will be placed in a queue, and, for each value in the queue, a call
20-
will eventually be made to the JavaScript function.
21-
22-
`Napi::ThreadSafeFunction` objects are destroyed when every thread which uses
23-
the object has called `Release()` or has received a return status of
24-
`napi_closing` in response to a call to `BlockingCall()` or `NonBlockingCall()`.
25-
The queue is emptied before the `Napi::ThreadSafeFunction` is destroyed. It is
26-
important that `Release()` be the last API call made in conjunction with a given
27-
`Napi::ThreadSafeFunction`, because after the call completes, there is no
28-
guarantee that the `Napi::ThreadSafeFunction` is still allocated. For the same
29-
reason it is also important that no more use be made of a thread-safe function
30-
after receiving a return value of `napi_closing` in response to a call to
31-
`BlockingCall()` or `NonBlockingCall()`. Data associated with the
32-
`Napi::ThreadSafeFunction` can be freed in its `Finalizer` callback which was
33-
passed to `ThreadSafeFunction::New()`.
34-
35-
Once the number of threads making use of a `Napi::ThreadSafeFunction` reaches
36-
zero, no further threads can start making use of it by calling `Acquire()`. In
37-
fact, all subsequent API calls associated with it, except `Release()`, will
38-
return an error value of `napi_closing`.
3+
The `Napi::ThreadSafeFunction` type provides APIs for threads to communicate
4+
with the addon's main thread to invoke JavaScript functions on their behalf.
5+
Documentation can be found for an [overview of the API](threadsafe.md), as well
6+
as [differences between the two thread-safe function
7+
APIs](threadsafe.md#implementation-differences).
398

409
## Methods
4110

@@ -92,7 +61,8 @@ New(napi_env env,
9261
- `maxQueueSize`: Maximum size of the queue. `0` for no limit.
9362
- `initialThreadCount`: The initial number of threads, including the main
9463
thread, which will be making use of this function.
95-
- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`.
64+
- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. It
65+
can be retreived by calling `GetContext()`.
9666
- `[optional] finalizeCallback`: Function to call when the `ThreadSafeFunction`
9767
is being destroyed. This callback will be invoked on the main thread when the
9868
thread-safe function is about to be destroyed. It receives the context and the
@@ -101,24 +71,24 @@ New(napi_env env,
10171
`uv_thread_join()`. It is important that, aside from the main loop thread,
10272
there be no threads left using the thread-safe function after the finalize
10373
callback completes. Must implement `void operator()(Env env, DataType* data,
104-
Context* hint)`, skipping `data` or `hint` if they are not provided.
105-
Can be retrieved via `GetContext()`.
74+
ContextType* hint)`, skipping `data` or `hint` if they are not provided. Can
75+
be retrieved via `GetContext()`.
10676
- `[optional] data`: Data to be passed to `finalizeCallback`.
10777

10878
Returns a non-empty `Napi::ThreadSafeFunction` instance.
10979

11080
### Acquire
11181

11282
Add a thread to this thread-safe function object, indicating that a new thread
113-
will start making use of the thread-safe function.
83+
will start making use of the thread-safe function.
11484

11585
```cpp
11686
napi_status Napi::ThreadSafeFunction::Acquire()
11787
```
11888

11989
Returns one of:
12090
- `napi_ok`: The thread has successfully acquired the thread-safe function
121-
for its use.
91+
for its use.
12292
- `napi_closing`: The thread-safe function has been marked as closing via a
12393
previous call to `Abort()`.
12494

@@ -136,7 +106,7 @@ napi_status Napi::ThreadSafeFunction::Release()
136106
Returns one of:
137107
- `napi_ok`: The thread-safe function has been successfully released.
138108
- `napi_invalid_arg`: The thread-safe function's thread-count is zero.
139-
- `napi_generic_failure`: A generic error occurred when attemping to release
109+
- `napi_generic_failure`: A generic error occurred when attempting to release
140110
the thread-safe function.
141111

142112
### Abort
@@ -258,10 +228,10 @@ Value Start( const CallbackInfo& info )
258228
// Create a native thread
259229
nativeThread = std::thread( [count] {
260230
auto callback = []( Napi::Env env, Function jsCallback, int* value ) {
261-
// Transform native data into JS data, passing it to the provided
231+
// Transform native data into JS data, passing it to the provided
262232
// `jsCallback` -- the TSFN's JavaScript function.
263233
jsCallback.Call( {Number::New( env, *value )} );
264-
234+
265235
// We're finished with the data.
266236
delete value;
267237
};

0 commit comments

Comments
 (0)