Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2575,6 +2575,43 @@ object just created has been garbage collected.
JavaScript `ArrayBuffer`s are described in
[Section ArrayBuffer objects][] of the ECMAScript Language Specification.

#### `napi_create_external_sharedarraybuffer`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the node_api_ prefix for all node Node-API functions instead of napi_.


<!-- YAML
added: REPLACEME
napiVersion: 1
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be an experimental API first.

Suggested change
napiVersion: 1

-->

```c
napi_status
napi_create_external_sharedarraybuffer(napi_env env,
void* external_data,
size_t byte_length,
void (*finalize_cb)(
void* external_data,
void* finalize_hint),
void* finalize_hint,
napi_value* result)
```

* `[in] env`: The environment that the API is invoked under.
* `[in] external_data`: Pointer to the underlying byte buffer of the
`SharedArrayBuffer`.
* `[in] byte_length`: The length in bytes of the underlying buffer.
* `[in] finalize_cb`: Optional callback to call when the `SharedArrayBuffer` is
being collected. Because a `SharedArrayBuffer` can outlive the environment
it was created in, the callback does not get receive a reference to `env`.
* `[in] finalize_hint`: Optional hint to pass to the finalize callback during
collection.
* `[out] result`: A `napi_value` representing a JavaScript `SharedArrayBuffer`.

Returns `napi_ok` if the API succeeded.

Create a `SharedArrayBuffer` with externally managed memory.

See the entry on [`napi_create_external_arraybuffer`][] for runtime
compatibility.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a difference worth pointing out here is that the finalizer in napi_create_external_arraybuffer will be invoked on the JS thread. But this finalize_cb could be invoked on an arbitrary thread.


#### `napi_create_external_buffer`

<!-- YAML
Expand Down
7 changes: 7 additions & 0 deletions src/js_native_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,13 @@ napi_create_external_arraybuffer(napi_env env,
node_api_basic_finalize finalize_cb,
void* finalize_hint,
napi_value* result);
NAPI_EXTERN napi_status NAPI_CDECL napi_create_external_sharedarraybuffer(
Copy link
Copy Markdown
Member

@legendecas legendecas Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an experimental API first.

Suggested change
NAPI_EXTERN napi_status NAPI_CDECL napi_create_external_sharedarraybuffer(
#ifdef NAPI_EXPERIMENTAL
#define NODE_API_EXPERIMENTAL_HAS_CREATE_EXTERNAL_SHAREDARRAYBUFFER
NAPI_EXTERN napi_status NAPI_CDECL napi_create_external_sharedarraybuffer(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the macro be NODE_API_EXPERIMENTAL_HAS_CREATE_EXTERNAL_SHAREDARRAYBUFFER?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New Node-API functions should be prefixed with node_api_ and not napi_.

napi_env env,
void* external_data,
size_t byte_length,
void (*finalize_cb)(void* external_data, void* finalize_hint),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically do not use the inline function pointer types.
Can we use the node_api_basic_finalize here?
I guess it should OK to document there that env is null there.

void* finalize_hint,
napi_value* result);
#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED
NAPI_EXTERN napi_status NAPI_CDECL napi_get_arraybuffer_info(
napi_env env, napi_value arraybuffer, void** data, size_t* byte_length);
Expand Down
47 changes: 47 additions & 0 deletions src/js_native_api_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,42 @@ napi_status NewExternalString(napi_env env,
return status;
}

napi_status NewExternalSharedArrayBuffer(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this implementation directly into the node_api_create_external_sharedarraybuffer. This is the pattern we use for all other Node-API functions. In practice it avoids having extra frame in the call stack when we debug code and unnecessary redirection.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure that the node_api_create_external_sharedarraybuffer has the typical set of parameter checks as we do for other Node-API functions.

napi_env env,
void* external_data,
size_t byte_length,
void (*finalize_cb)(void* external_data, void* finalize_hint),
void* finalize_hint,
napi_value* result) {
struct FinalizerData {
void (*cb)(void* external_data, void* finalize_hint);
void* hint;
};
auto deleter = [](void* external_data, size_t length, void* deleter_data) {
if (auto fd = static_cast<FinalizerData*>(deleter_data)) {
fd->cb(external_data, fd->hint);
delete fd;
}
};
FinalizerData* deleter_data = nullptr;
if (finalize_cb != nullptr) {
deleter_data = new FinalizerData{finalize_cb, finalize_hint};
}
auto unique_backing_store = v8::SharedArrayBuffer::NewBackingStore(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to avoid auto for readability when possible.
IMHO, we should only use it for lambdas and iterators.
In other trivial cases like here using the real types can help readability and see the issues sooner like with the missing std::move below.

external_data,
byte_length,
deleter,
reinterpret_cast<void*>(deleter_data));
CHECK(!!unique_backing_store); // Cannot fail.
auto shared_backing_store =
std::shared_ptr<v8::BackingStore>(std::move(unique_backing_store));
auto shared_array_buffer =
v8::SharedArrayBuffer::New(env->isolate, shared_backing_store);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add std::move for the shared_backing_store to avoid extra ref counter interlocked increment and decrement.

CHECK_MAYBE_EMPTY(env, shared_array_buffer, napi_generic_failure);
*result = v8impl::JsValueFromV8LocalValue(shared_array_buffer);
return napi_clear_last_error(env);
}

class TrackedStringResource : private RefTracker {
public:
TrackedStringResource(napi_env env,
Expand Down Expand Up @@ -3134,6 +3170,17 @@ napi_create_external_arraybuffer(napi_env env,
env, buffer, nullptr, nullptr, nullptr, result, nullptr);
}

napi_status NAPI_CDECL napi_create_external_sharedarraybuffer(
napi_env env,
void* external_data,
size_t byte_length,
void (*finalize_cb)(void* external_data, void* finalize_hint),
void* finalize_hint,
napi_value* result) {
return v8impl::NewExternalSharedArrayBuffer(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this return napi_no_external_buffers_allowed if external buffers are not allowed, eg. when running in a sandboxed environment? Like napi_create_external_buffer does if V8_SANDBOX macro is defined.

env, external_data, byte_length, finalize_cb, finalize_hint, result);
}

napi_status NAPI_CDECL napi_get_arraybuffer_info(napi_env env,
napi_value arraybuffer,
void** data,
Expand Down
10 changes: 10 additions & 0 deletions test/node-api/test_buffer/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ const tick = require('util').promisify(require('../../common/tick'));
console.log('gc2');
assert.strictEqual(binding.getDeleterCallCount(), 2);

// Caveat emptor: it's indeterminate when the SharedArrayBuffer's backing
// store is reclaimed; at least some of the time it happens even before
// calling gc().
let sab = binding.newExternalSharedArrayBuffer();
sab = null; // eslint-disable-line no-unused-vars
global.gc();
await tick(10);
console.log('gc3');
assert.strictEqual(binding.getDeleterCallCount(), 3);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use const { gcUntil } = require('../../common/gc')? It would be more stable in tests on GC..


// To test this doesn't crash
binding.invalidObjectAsBuffer({});

Expand Down
26 changes: 26 additions & 0 deletions test/node-api/test_buffer/test_buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ static napi_value newExternalBuffer(napi_env env, napi_callback_info info) {
return theBuffer;
}

static char externalSharedArrayBufferData[1];

static void freeExternalSharedArrayBuffer(void* data, void* hint) {
(void)hint;
NODE_API_BASIC_ASSERT_RETURN_VOID(
data == (void*)externalSharedArrayBufferData,
"SharedArrayBuffer points to wrong data");
deleterCallCount++;
}

static napi_value newExternalSharedArrayBuffer(napi_env env,
napi_callback_info info) {
napi_value sab;
NODE_API_CALL(
env,
napi_create_external_sharedarraybuffer(env,
externalSharedArrayBufferData,
1,
freeExternalSharedArrayBuffer,
NULL,
&sab));
return sab;
}

static napi_value getDeleterCallCount(napi_env env, napi_callback_info info) {
napi_value callCount;
NODE_API_CALL(env, napi_create_int32(env, deleterCallCount, &callCount));
Expand Down Expand Up @@ -171,6 +195,8 @@ static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor methods[] = {
DECLARE_NODE_API_PROPERTY("newBuffer", newBuffer),
DECLARE_NODE_API_PROPERTY("newExternalBuffer", newExternalBuffer),
DECLARE_NODE_API_PROPERTY("newExternalSharedArrayBuffer",
newExternalSharedArrayBuffer),
DECLARE_NODE_API_PROPERTY("getDeleterCallCount", getDeleterCallCount),
DECLARE_NODE_API_PROPERTY("copyBuffer", copyBuffer),
DECLARE_NODE_API_PROPERTY("bufferHasInstance", bufferHasInstance),
Expand Down
Loading