feat: add support for SharedArrayBuffer in DataViews (#1714)
diff --git a/doc/dataview.md b/doc/dataview.md
index 66fb289..619ceec 100644
--- a/doc/dataview.md
+++ b/doc/dataview.md
@@ -6,6 +6,11 @@
[JavaScript `DataView`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView)
class.
+**NOTE**: The support for `Napi::DataView::New()` overloads accepting an
+`Napi::SharedArrayBuffer` parameter is only available when using
+`NAPI_EXPERIMENTAL` and building against Node.js headers that support this
+feature.
+
## Methods
### New
@@ -50,6 +55,48 @@
Returns a new `Napi::DataView` instance.
+### New
+
+Allocates a new `Napi::DataView` instance with a given `Napi::SharedArrayBuffer`.
+
+```cpp
+static Napi::DataView Napi::DataView::New(napi_env env, Napi::SharedArrayBuffer sharedArrayBuffer);
+```
+
+- `[in] env`: The environment in which to create the `Napi::DataView` instance.
+- `[in] sharedArrayBuffer` : `Napi::SharedArrayBuffer` underlying the `Napi::DataView`.
+
+Returns a new `Napi::DataView` instance.
+
+### New
+
+Allocates a new `Napi::DataView` instance with a given `Napi::SharedArrayBuffer`.
+
+```cpp
+static Napi::DataView Napi::DataView::New(napi_env env, Napi::SharedArrayBuffer sharedArrayBuffer, size_t byteOffset);
+```
+
+- `[in] env`: The environment in which to create the `Napi::DataView` instance.
+- `[in] sharedArrayBuffer` : `Napi::SharedArrayBuffer` underlying the `Napi::DataView`.
+- `[in] byteOffset` : The byte offset within the `Napi::SharedArrayBuffer` from which to start projecting the `Napi::DataView`.
+
+Returns a new `Napi::DataView` instance.
+
+### New
+
+Allocates a new `Napi::DataView` instance with a given `Napi::SharedArrayBuffer`.
+
+```cpp
+static Napi::DataView Napi::DataView::New(napi_env env, Napi::SharedArrayBuffer sharedArrayBuffer, size_t byteOffset, size_t byteLength);
+```
+
+- `[in] env`: The environment in which to create the `Napi::DataView` instance.
+- `[in] sharedArrayBuffer` : `Napi::SharedArrayBuffer` underlying the `Napi::DataView`.
+- `[in] byteOffset` : The byte offset within the `Napi::SharedArrayBuffer` from which to start projecting the `Napi::DataView`.
+- `[in] byteLength` : Number of elements in the `Napi::DataView`.
+
+Returns a new `Napi::DataView` instance.
+
### Constructor
Initializes an empty instance of the `Napi::DataView` class.
@@ -75,7 +122,22 @@
Napi::ArrayBuffer Napi::DataView::ArrayBuffer() const;
```
-Returns the backing array buffer.
+Returns the backing array buffer as an `Napi::ArrayBuffer`.
+
+**NOTE**: If the `Napi::DataView` is not backed by an `Napi::ArrayBuffer`, this
+method will terminate the process with a fatal error when using
+`NODE_ADDON_API_ENABLE_TYPE_CHECK_ON_AS` or exhibit undefined behavior
+otherwise. Use `Buffer()` instead to get the backing buffer without assuming its
+type.
+
+### Buffer
+
+```cpp
+Napi::Value Napi::DataView::Buffer() const;
+```
+
+Returns the backing array buffer as a generic `Napi::Value`, allowing optional
+type-checking with `Is*()` and type-casting with `As<>()` methods.
### ByteOffset
diff --git a/napi-inl.h b/napi-inl.h
index 3b82ec8..7ef2e64 100644
--- a/napi-inl.h
+++ b/napi-inl.h
@@ -2302,6 +2302,39 @@
return DataView(env, value);
}
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+inline DataView DataView::New(napi_env env,
+ Napi::SharedArrayBuffer arrayBuffer) {
+ return New(env, arrayBuffer, 0, arrayBuffer.ByteLength());
+}
+
+inline DataView DataView::New(napi_env env,
+ Napi::SharedArrayBuffer arrayBuffer,
+ size_t byteOffset) {
+ if (byteOffset > arrayBuffer.ByteLength()) {
+ NAPI_THROW(RangeError::New(
+ env, "Start offset is outside the bounds of the buffer"),
+ DataView());
+ }
+ return New(
+ env, arrayBuffer, byteOffset, arrayBuffer.ByteLength() - byteOffset);
+}
+
+inline DataView DataView::New(napi_env env,
+ Napi::SharedArrayBuffer arrayBuffer,
+ size_t byteOffset,
+ size_t byteLength) {
+ if (byteOffset + byteLength > arrayBuffer.ByteLength()) {
+ NAPI_THROW(RangeError::New(env, "Invalid DataView length"), DataView());
+ }
+ napi_value value;
+ napi_status status =
+ napi_create_dataview(env, byteLength, arrayBuffer, byteOffset, &value);
+ NAPI_THROW_IF_FAILED(env, status, DataView());
+ return DataView(env, value);
+}
+#endif // NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+
inline void DataView::CheckCast(napi_env env, napi_value value) {
NAPI_CHECK(value != nullptr, "DataView::CheckCast", "empty value");
@@ -2325,6 +2358,10 @@
}
inline Napi::ArrayBuffer DataView::ArrayBuffer() const {
+ return Buffer().As<Napi::ArrayBuffer>();
+}
+
+inline Napi::Value DataView::Buffer() const {
napi_value arrayBuffer;
napi_status status = napi_get_dataview_info(_env,
_value /* dataView */,
@@ -2332,8 +2369,8 @@
nullptr /* data */,
&arrayBuffer /* arrayBuffer */,
nullptr /* byteOffset */);
- NAPI_THROW_IF_FAILED(_env, status, Napi::ArrayBuffer());
- return Napi::ArrayBuffer(_env, arrayBuffer);
+ NAPI_THROW_IF_FAILED(_env, status, Napi::Value());
+ return Napi::Value(_env, arrayBuffer);
}
inline size_t DataView::ByteOffset() const {
diff --git a/napi.h b/napi.h
index d92702e..fd1e592 100644
--- a/napi.h
+++ b/napi.h
@@ -1456,13 +1456,37 @@
size_t byteOffset,
size_t byteLength);
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+ static DataView New(napi_env env, Napi::SharedArrayBuffer arrayBuffer);
+ static DataView New(napi_env env,
+ Napi::SharedArrayBuffer arrayBuffer,
+ size_t byteOffset);
+ static DataView New(napi_env env,
+ Napi::SharedArrayBuffer arrayBuffer,
+ size_t byteOffset,
+ size_t byteLength);
+#endif
+
static void CheckCast(napi_env env, napi_value value);
DataView(); ///< Creates a new _empty_ DataView instance.
DataView(napi_env env,
napi_value value); ///< Wraps a Node-API value primitive.
- Napi::ArrayBuffer ArrayBuffer() const; ///< Gets the backing array buffer.
+ // Gets the backing `ArrayBuffer`.
+ //
+ // If this `DataView` is not backed by an `ArrayBuffer`, this method will
+ // terminate the process with a fatal error when using
+ // `NODE_ADDON_API_ENABLE_TYPE_CHECK_ON_AS` or exhibit undefined behavior
+ // otherwise. Use `Buffer()` instead to get the backing buffer without
+ // assuming its type.
+ Napi::ArrayBuffer ArrayBuffer() const;
+
+ // Gets the backing buffer (an `ArrayBuffer` or `SharedArrayBuffer`).
+ //
+ // Use `IsArrayBuffer()` or `IsSharedArrayBuffer()` to check the type of the
+ // backing buffer prior to casting with `As<T>()`.
+ Napi::Value Buffer() const;
size_t ByteOffset()
const; ///< Gets the offset into the buffer where the array starts.
size_t ByteLength() const; ///< Gets the length of the array in bytes.
diff --git a/test/dataview/dataview.cc b/test/dataview/dataview.cc
index f055d95..cbd5933 100644
--- a/test/dataview/dataview.cc
+++ b/test/dataview/dataview.cc
@@ -2,24 +2,51 @@
using namespace Napi;
-static Value CreateDataView1(const CallbackInfo& info) {
+static Value CreateDataView(const CallbackInfo& info) {
ArrayBuffer arrayBuffer = info[0].As<ArrayBuffer>();
return DataView::New(info.Env(), arrayBuffer);
}
-static Value CreateDataView2(const CallbackInfo& info) {
+static Value CreateDataViewWithByteOffset(const CallbackInfo& info) {
ArrayBuffer arrayBuffer = info[0].As<ArrayBuffer>();
size_t byteOffset = info[1].As<Number>().Uint32Value();
return DataView::New(info.Env(), arrayBuffer, byteOffset);
}
-static Value CreateDataView3(const CallbackInfo& info) {
+static Value CreateDataViewWithByteOffsetAndByteLength(
+ const CallbackInfo& info) {
ArrayBuffer arrayBuffer = info[0].As<ArrayBuffer>();
size_t byteOffset = info[1].As<Number>().Uint32Value();
size_t byteLength = info[2].As<Number>().Uint32Value();
return DataView::New(info.Env(), arrayBuffer, byteOffset, byteLength);
}
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+static Value CreateDataViewOnSharedArrayBuffer(const CallbackInfo& info) {
+ SharedArrayBuffer arrayBuffer = info[0].As<SharedArrayBuffer>();
+ return DataView::New(info.Env(), arrayBuffer);
+}
+
+static Value CreateDataViewOnSharedArrayBufferWithByteOffset(
+ const CallbackInfo& info) {
+ SharedArrayBuffer arrayBuffer = info[0].As<SharedArrayBuffer>();
+ size_t byteOffset = info[1].As<Number>().Uint32Value();
+ return DataView::New(info.Env(), arrayBuffer, byteOffset);
+}
+
+static Value CreateDataViewOnSharedArrayBufferWithByteOffsetAndByteLength(
+ const CallbackInfo& info) {
+ SharedArrayBuffer arrayBuffer = info[0].As<SharedArrayBuffer>();
+ size_t byteOffset = info[1].As<Number>().Uint32Value();
+ size_t byteLength = info[2].As<Number>().Uint32Value();
+ return DataView::New(info.Env(), arrayBuffer, byteOffset, byteLength);
+}
+#endif
+
+static Value GetBuffer(const CallbackInfo& info) {
+ return info[0].As<DataView>().Buffer();
+}
+
static Value GetArrayBuffer(const CallbackInfo& info) {
return info[0].As<DataView>().ArrayBuffer();
}
@@ -37,10 +64,24 @@
Object InitDataView(Env env) {
Object exports = Object::New(env);
- exports["createDataView1"] = Function::New(env, CreateDataView1);
- exports["createDataView2"] = Function::New(env, CreateDataView2);
- exports["createDataView3"] = Function::New(env, CreateDataView3);
+ exports["createDataView"] = Function::New(env, CreateDataView);
+ exports["createDataViewWithByteOffset"] =
+ Function::New(env, CreateDataViewWithByteOffset);
+ exports["createDataViewWithByteOffsetAndByteLength"] =
+ Function::New(env, CreateDataViewWithByteOffsetAndByteLength);
+
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+ exports["createDataViewOnSharedArrayBuffer"] =
+ Function::New(env, CreateDataViewOnSharedArrayBuffer);
+ exports["createDataViewOnSharedArrayBufferWithByteOffset"] =
+ Function::New(env, CreateDataViewOnSharedArrayBufferWithByteOffset);
+ exports["createDataViewOnSharedArrayBufferWithByteOffsetAndByteLength"] =
+ Function::New(
+ env, CreateDataViewOnSharedArrayBufferWithByteOffsetAndByteLength);
+#endif
+
exports["getArrayBuffer"] = Function::New(env, GetArrayBuffer);
+ exports["getBuffer"] = Function::New(env, GetBuffer);
exports["getByteOffset"] = Function::New(env, GetByteOffset);
exports["getByteLength"] = Function::New(env, GetByteLength);
diff --git a/test/dataview/dataview.js b/test/dataview/dataview.js
index 5956128..5916f6b 100644
--- a/test/dataview/dataview.js
+++ b/test/dataview/dataview.js
@@ -3,12 +3,19 @@
const assert = require('assert');
module.exports = require('../common').runTest(test);
+let runSharedArrayBufferTests = true;
+
function test (binding) {
function testDataViewCreation (factory, arrayBuffer, offset, length) {
const view = factory(arrayBuffer, offset, length);
offset = offset || 0;
- assert.ok(dataview.getArrayBuffer(view) instanceof ArrayBuffer);
- assert.strictEqual(dataview.getArrayBuffer(view), arrayBuffer);
+ if (arrayBuffer instanceof ArrayBuffer) {
+ assert.ok(dataview.getArrayBuffer(view) instanceof ArrayBuffer);
+ assert.strictEqual(dataview.getArrayBuffer(view), arrayBuffer);
+ } else {
+ assert.ok(dataview.getBuffer(view) instanceof SharedArrayBuffer);
+ assert.strictEqual(dataview.getBuffer(view), arrayBuffer);
+ }
assert.strictEqual(dataview.getByteOffset(view), offset);
assert.strictEqual(dataview.getByteLength(view),
length || arrayBuffer.byteLength - offset);
@@ -20,16 +27,48 @@
}, RangeError);
}
- const dataview = binding.dataview;
- const arrayBuffer = new ArrayBuffer(10);
+ const { hasSharedArrayBuffer, dataview } = binding;
- testDataViewCreation(dataview.createDataView1, arrayBuffer);
- testDataViewCreation(dataview.createDataView2, arrayBuffer, 2);
- testDataViewCreation(dataview.createDataView2, arrayBuffer, 10);
- testDataViewCreation(dataview.createDataView3, arrayBuffer, 2, 4);
- testDataViewCreation(dataview.createDataView3, arrayBuffer, 10, 0);
+ {
+ const arrayBuffer = new ArrayBuffer(10);
- testInvalidRange(dataview.createDataView2, arrayBuffer, 11);
- testInvalidRange(dataview.createDataView3, arrayBuffer, 11, 0);
- testInvalidRange(dataview.createDataView3, arrayBuffer, 6, 5);
+ testDataViewCreation(dataview.createDataView, arrayBuffer);
+ testDataViewCreation(dataview.createDataViewWithByteOffset, arrayBuffer, 2);
+ testDataViewCreation(dataview.createDataViewWithByteOffset, arrayBuffer, 10);
+ testDataViewCreation(dataview.createDataViewWithByteOffsetAndByteLength, arrayBuffer, 2, 4);
+ testDataViewCreation(dataview.createDataViewWithByteOffsetAndByteLength, arrayBuffer, 10, 0);
+
+ testInvalidRange(dataview.createDataViewWithByteOffset, arrayBuffer, 11);
+ testInvalidRange(dataview.createDataViewWithByteOffsetAndByteLength, arrayBuffer, 11, 0);
+ testInvalidRange(dataview.createDataViewWithByteOffsetAndByteLength, arrayBuffer, 6, 5);
+ }
+
+ if (hasSharedArrayBuffer && runSharedArrayBufferTests) {
+ const sab = new SharedArrayBuffer(10);
+
+ try {
+ testDataViewCreation(dataview.createDataViewOnSharedArrayBuffer, sab);
+ } catch (ex) {
+ // The `napi_create_dataview` API does not have a valid `#define`
+ // preprocessor guard for SharedArrayBuffer support, so it is
+ // possible that the API is present but creating a DataView on
+ // SharedArrayBuffer is not supported in the current version of Node.js.
+ // In that case, we should skip the test instead of throwing.
+ if (ex.message === 'Invalid argument') {
+ console.warn(`The current version of Node.js (${process.version}) does not support creating DataViews on SharedArrayBuffers; skipping tests.`);
+ runSharedArrayBufferTests = false;
+ return;
+ }
+
+ throw ex;
+ }
+ testDataViewCreation(dataview.createDataViewOnSharedArrayBufferWithByteOffset, sab, 2);
+ testDataViewCreation(dataview.createDataViewOnSharedArrayBufferWithByteOffset, sab, 10);
+ testDataViewCreation(dataview.createDataViewOnSharedArrayBufferWithByteOffsetAndByteLength, sab, 2, 4);
+ testDataViewCreation(dataview.createDataViewOnSharedArrayBufferWithByteOffsetAndByteLength, sab, 10, 0);
+
+ testInvalidRange(dataview.createDataViewOnSharedArrayBufferWithByteOffset, sab, 11);
+ testInvalidRange(dataview.createDataViewOnSharedArrayBufferWithByteOffsetAndByteLength, sab, 11, 0);
+ testInvalidRange(dataview.createDataViewOnSharedArrayBufferWithByteOffsetAndByteLength, sab, 6, 5);
+ }
}