Skip to content

Commit d092a32

Browse files
committed
doc: wip with tsfn documentation
1 parent a7a5352 commit d092a32

File tree

4 files changed

+456
-44
lines changed

4 files changed

+456
-44
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ to ideas specified in the **ECMA262 Language Specification**.
5555

5656
<a name="setup"></a>
5757

58-
node-addon-api is based on [N-API](https://nodejs.org/api/n-api.html) and supports using different N-API versions.
59-
This allows addons built with it to run with Node.js versions which support the targeted N-API version.
58+
node-addon-api is based on [N-API](https://nodejs.org/api/n-api.html) and supports using different N-API versions.
59+
This allows addons built with it to run with Node.js versions which support the targeted N-API version.
6060
**However** the node-addon-api support model is to support only the active LTS Node.js versions. This means that
6161
every year there will be a new major which drops support for the Node.js LTS version which has gone out of service.
6262

@@ -116,7 +116,9 @@ The following is the documentation for node-addon-api.
116116
- [AsyncWorker](doc/async_worker.md)
117117
- [AsyncContext](doc/async_context.md)
118118
- [AsyncWorker Variants](doc/async_worker_variants.md)
119-
- [Thread-safe Functions](doc/threadsafe_function.md)
119+
- [Thread-safe Functions](doc/threadsafe.md)
120+
- [ThreadSafeFunction](doc/threadsafe_function.md)
121+
- [ThreadSafeFunctionEx](doc/threadsafe_function_ex.md)
120122
- [Promises](doc/promises.md)
121123
- [Version management](doc/version_management.md)
122124

doc/threadsafe.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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::ThreadSafeFunctionEx`](threadsafe_function_ex.md) -- as well as APIs to
15+
create, destroy, and call objects of this type. The differences between the two
16+
are subtle and are [highlighted below](#implementation-differences). Regardless
17+
of which type you choose, the API between the two are similar.
18+
19+
`Napi::ThreadSafeFunction[Ex]::New()` creates a persistent reference that holds
20+
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::ThreadSafeFunction[Ex]` 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::ThreadSafeFunction[Ex]` is destroyed. It
29+
is important that `Release()` be the last API call made in conjunction with a
30+
given `Napi::ThreadSafeFunction[Ex]`, because after the call completes, there is
31+
no guarantee that the `Napi::ThreadSafeFunction[Ex]` is still allocated. For the
32+
same reason it is also important that no more use be made of a thread-safe
33+
function after receiving a return value of `napi_closing` in response to a call
34+
to `BlockingCall()` or `NonBlockingCall()`. Data associated with the
35+
`Napi::ThreadSafeFunction[Ex]` can be freed in its `Finalizer` callback which
36+
was passed to `ThreadSafeFunction[Ex]::New()`.
37+
38+
Once the number of threads making use of a `Napi::ThreadSafeFunction[Ex]`
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 `Napi::ThreadSafeFunctionEx`
46+
depends largely on how you plan to execute your native C++ code (the "callback")
47+
on the Node thread.
48+
49+
### [`Napi::ThreadSafeFunction`](threadsafe_function.md)
50+
51+
This API is designed without N-API 5 native support for [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+
However, this functionality comes with some **additional overhead** and
66+
situational **memory leaks**:
67+
- The API acts as a "middle-man" between the underlying
68+
`napi_threadsafe_function`, and dynamically constructs a wrapper for your
69+
callback on the heap for every call to `[Non]BlockingCall()`.
70+
- In acting in this "middle-man" fashion, the API will call the underlying "make
71+
call" N-API method on this packaged item. If the API has determined the
72+
threadsafe function is no longer accessible (eg. all threads have Released yet
73+
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 threadsafe 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 threadsafe 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::ThreadSafeFunctionEx`](threadsafe_function_ex.md)
85+
86+
The `ThreadSafeFunctionEx` 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" an
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 additional side effects:
96+
- The API does _not_ act as a "middle-man" compared to the non-`Ex`. Once Node
97+
finalizes the threadsafe function, the `CallJs` callback will execute with an
98+
empty `Napi::Env` for any remaining items on the queue. This provides the the
99+
ability to handle any necessary clean up of the item's data.
100+
- The callback _does_ receive the context as a parameter, so a call to
101+
`GetContext()` is _not_ necessary. This context type is specified as the
102+
**first type argument** specified to `::New`, ensuring type safety.
103+
- The `New()` constructor accepts the `CallJs` callback as the **second type
104+
argument**. The callback must be statically defined for the API to access it.
105+
This affords the ability to statically pass the context as the correct type
106+
across all methods.
107+
- Only one C++ data type may be specified to every call to `[Non]BlockingCall()`
108+
-- the **third type argument** specified to `::New`. Any "dynamic call data"
109+
must be implemented by the user.
110+
111+
112+
### Usage Suggestions
113+
114+
In summary, it may be best to use `Napi::ThreadSafeFunctionEx` if:
115+
116+
- static, compile-time support for targeting N-API 4 or 5+ with an optional
117+
JavaScript callback feature is desired;
118+
- the callback can have `static` storage class and will not change across calls
119+
to `[Non]BlockingCall()`;
120+
- cleanup of items' data is required (eg. deleting dynamically-allocated data
121+
that is created at the caller level).
122+
123+
Otherwise, `Napi::ThreadSafeFunction` may be a better choice.

doc/threadsafe_function.md

Lines changed: 10 additions & 41 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

@@ -93,6 +62,7 @@ New(napi_env env,
9362
- `initialThreadCount`: The initial number of threads, including the main
9463
thread, which will be making use of this function.
9564
- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`.
65+
Can be retreived via `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
@@ -102,23 +72,22 @@ New(napi_env env,
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,
10474
Context* hint)`, skipping `data` or `hint` if they are not provided.
105-
Can be retreived via `GetContext()`.
10675
- `[optional] data`: Data to be passed to `finalizeCallback`.
10776

10877
Returns a non-empty `Napi::ThreadSafeFunction` instance.
10978

11079
### Acquire
11180

11281
Add a thread to this thread-safe function object, indicating that a new thread
113-
will start making use of the thread-safe function.
82+
will start making use of the thread-safe function.
11483

11584
```cpp
11685
napi_status Napi::ThreadSafeFunction::Acquire()
11786
```
11887

11988
Returns one of:
12089
- `napi_ok`: The thread has successfully acquired the thread-safe function
121-
for its use.
90+
for its use.
12291
- `napi_closing`: The thread-safe function has been marked as closing via a
12392
previous call to `Abort()`.
12493

@@ -258,10 +227,10 @@ Value Start( const CallbackInfo& info )
258227
// Create a native thread
259228
nativeThread = std::thread( [count] {
260229
auto callback = []( Napi::Env env, Function jsCallback, int* value ) {
261-
// Transform native data into JS data, passing it to the provided
230+
// Transform native data into JS data, passing it to the provided
262231
// `jsCallback` -- the TSFN's JavaScript function.
263232
jsCallback.Call( {Number::New( env, *value )} );
264-
233+
265234
// We're finished with the data.
266235
delete value;
267236
};

0 commit comments

Comments
 (0)