From e44aca985ebf1b71a1d532a63c2ecaa61a34b736 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 28 Jun 2018 10:06:34 -0500 Subject: [PATCH] add bigint class --- doc/bigint.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++ napi-inl.h | 69 ++++++++++++++++++++++++++++++++++ napi.h | 59 ++++++++++++++++++++++++++++- test/bigint.cc | 76 +++++++++++++++++++++++++++++++++++++ test/bigint.js | 52 +++++++++++++++++++++++++ test/binding.cc | 2 + test/binding.gyp | 1 + test/index.js | 1 + test/typedarray.cc | 27 +++++++++++++ test/typedarray.js | 47 +++++++++++++++++++++++ 10 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 doc/bigint.md create mode 100644 test/bigint.cc create mode 100644 test/bigint.js diff --git a/doc/bigint.md b/doc/bigint.md new file mode 100644 index 000000000..6d3b0afc0 --- /dev/null +++ b/doc/bigint.md @@ -0,0 +1,94 @@ +# BigInt + +A JavaScript BigInt value. + +## Methods + +### New + +```cpp +static BigInt New(Napi::Env env, int64_t value); +static BigInt New(Napi::Env env, uint64_t value); +``` + + - `[in] env`: The environment in which to construct the `BigInt` object. + - `[in] value`: The value the JavaScript `BigInt` will contain + +These APIs convert the C `int64_t` and `uint64_t` types to the JavaScript +`BigInt` type. + +```cpp +static BigInt New(Napi::Env env, + int sign_bit, + size_t word_count, + const uint64_t* words); +``` + + - `[in] env`: The environment in which to construct the `BigInt` object. + - `[in] sign_bit`: Determines if the resulting `BigInt` will be positive or negative. + - `[in] word_count`: The length of the words array. + - `[in] words`: An array of `uint64_t` little-endian 64-bit words. + +This API converts an array of unsigned 64-bit words into a single `BigInt` +value. + +The resulting `BigInt` is calculated as: (–1)`sign_bit` (`words[0]` +× (264)0 + `words[1]` × (264)1 + …) + +Returns a new JavaScript `BigInt`. + +### Constructor + +```cpp +Napi::BigInt(); +``` + +Returns a new empty JavaScript `BigInt`. + +### Int64Value + +```cpp +int64_t Int64Value(bool* lossless) const; +``` + + - `[out] lossless`: Indicates whether the `BigInt` value was converted + losslessly. + +Returns the C `int64_t` primitive equivalent of the given JavaScript +`BigInt`. If needed it will truncate the value, setting lossless to false. + +### Uint64Value + +```cpp +uint64_t Uint64Value(bool* lossless) const; +``` + + - `[out] lossless`: Indicates whether the `BigInt` value was converted + losslessly. + +Returns the C `uint64_t` primitive equivalent of the given JavaScript +`BigInt`. If needed it will truncate the value, setting lossless to false. + +### WordCount + +```cpp +size_t WordCount() const; +``` + +Returns the number of words needed to store this `BigInt` value. + +### ToWords + +```cpp +void ToWords(size_t* word_count, int* sign_bit, uint64_t* words); +``` + + - `[out] sign_bit`: Integer representing if the JavaScript `BigInt` is positive + or negative. + - `[in/out] word_count`: Must be initialized to the length of the words array. + Upon return, it will be set to the actual number of words that would be + needed to store this `BigInt`. + - `[out] words`: Pointer to a pre-allocated 64-bit word array. + +Returns a single `BigInt` value into a sign bit, 64-bit little-endian array, +and the number of elements in the array. diff --git a/napi-inl.h b/napi-inl.h index 46ba2c0b9..c5ae15eb8 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -287,6 +287,12 @@ inline bool Value::IsNumber() const { return Type() == napi_number; } +#ifdef NAPI_EXPERIMENTAL +inline bool Value::IsBigInt() const { + return Type() == napi_bigint; +} +#endif // NAPI_EXPERIMENTAL + inline bool Value::IsString() const { return Type() == napi_string; } @@ -505,6 +511,69 @@ inline double Number::DoubleValue() const { return result; } +#ifdef NAPI_EXPERIMENTAL +//////////////////////////////////////////////////////////////////////////////// +// BigInt Class +//////////////////////////////////////////////////////////////////////////////// + +inline BigInt BigInt::New(napi_env env, int64_t val) { + napi_value value; + napi_status status = napi_create_bigint_int64(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, BigInt()); + return BigInt(env, value); +} + +inline BigInt BigInt::New(napi_env env, uint64_t val) { + napi_value value; + napi_status status = napi_create_bigint_uint64(env, val, &value); + NAPI_THROW_IF_FAILED(env, status, BigInt()); + return BigInt(env, value); +} + +inline BigInt BigInt::New(napi_env env, int sign_bit, size_t word_count, const uint64_t* words) { + napi_value value; + napi_status status = napi_create_bigint_words(env, sign_bit, word_count, words, &value); + NAPI_THROW_IF_FAILED(env, status, BigInt()); + return BigInt(env, value); +} + +inline BigInt::BigInt() : Value() { +} + +inline BigInt::BigInt(napi_env env, napi_value value) : Value(env, value) { +} + +inline int64_t BigInt::Int64Value(bool* lossless) const { + int64_t result; + napi_status status = napi_get_value_bigint_int64( + _env, _value, &result, lossless); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline uint64_t BigInt::Uint64Value(bool* lossless) const { + uint64_t result; + napi_status status = napi_get_value_bigint_uint64( + _env, _value, &result, lossless); + NAPI_THROW_IF_FAILED(_env, status, 0); + return result; +} + +inline size_t BigInt::WordCount() const { + size_t word_count; + napi_status status = napi_get_value_bigint_words( + _env, _value, nullptr, &word_count, nullptr); + NAPI_THROW_IF_FAILED(_env, status, 0); + return word_count; +} + +inline void BigInt::ToWords(int* sign_bit, size_t* word_count, uint64_t* words) { + napi_status status = napi_get_value_bigint_words( + _env, _value, sign_bit, word_count, words); + NAPI_THROW_IF_FAILED(_env, status); +} +#endif // NAPI_EXPERIMENTAL + //////////////////////////////////////////////////////////////////////////////// // Name class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index f9852968b..0f95d9a0c 100644 --- a/napi.h +++ b/napi.h @@ -52,6 +52,9 @@ namespace Napi { class Value; class Boolean; class Number; +#ifdef NAPI_EXPERIMENTAL + class BigInt; +#endif // NAPI_EXPERIMENTAL class String; class Object; class Array; @@ -72,6 +75,10 @@ namespace Napi { typedef TypedArrayOf Uint32Array; ///< Typed-array of unsigned 32-bit integers typedef TypedArrayOf Float32Array; ///< Typed-array of 32-bit floating-point values typedef TypedArrayOf Float64Array; ///< Typed-array of 64-bit floating-point values +#ifdef NAPI_EXPERIMENTAL + typedef TypedArrayOf BigInt64Array; ///< Typed array of signed 64-bit integers + typedef TypedArrayOf BigUint64Array; ///< Typed array of unsigned 64-bit integers +#endif // NAPI_EXPERIMENTAL /// Defines the signature of a N-API C++ module's registration callback (init) function. typedef Object (*ModuleRegisterCallback)(Env env, Object exports); @@ -171,6 +178,9 @@ namespace Napi { bool IsNull() const; ///< Tests if a value is a null JavaScript value. bool IsBoolean() const; ///< Tests if a value is a JavaScript boolean. bool IsNumber() const; ///< Tests if a value is a JavaScript number. +#ifdef NAPI_EXPERIMENTAL + bool IsBigInt() const; ///< Tests if a value is a JavaScript bigint. +#endif // NAPI_EXPERIMENTAL bool IsString() const; ///< Tests if a value is a JavaScript string. bool IsSymbol() const; ///< Tests if a value is a JavaScript symbol. bool IsArray() const; ///< Tests if a value is a JavaScript array. @@ -242,6 +252,47 @@ namespace Napi { double DoubleValue() const; ///< Converts a Number value to a 64-bit floating-point value. }; +#ifdef NAPI_EXPERIMENTAL + /// A JavaScript bigint value. + class BigInt : public Value { + public: + static BigInt New( + napi_env env, ///< N-API environment + int64_t value ///< Number value + ); + static BigInt New( + napi_env env, ///< N-API environment + uint64_t value ///< Number value + ); + + /// Creates a new BigInt object using a specified sign bit and a + /// specified list of digits/words. + /// The resulting number is calculated as: + /// (-1)^sign_bit * (words[0] * (2^64)^0 + words[1] * (2^64)^1 + ...) + static BigInt New( + napi_env env, ///< N-API environment + int sign_bit, ///< Sign bit. 1 if negative. + size_t word_count, ///< Number of words in array + const uint64_t* words ///< Array of words + ); + + BigInt(); ///< Creates a new _empty_ BigInt instance. + BigInt(napi_env env, napi_value value); ///< Wraps a N-API value primitive. + + int64_t Int64Value(bool* lossless) const; ///< Converts a BigInt value to a 64-bit signed integer value. + uint64_t Uint64Value(bool* lossless) const; ///< Converts a BigInt value to a 64-bit unsigned integer value. + + size_t WordCount() const; ///< The number of 64-bit words needed to store the result of ToWords(). + + /// Writes the contents of this BigInt to a specified memory location. + /// `sign_bit` must be provided and will be set to 1 if this BigInt is negative. + /// `*word_count` has to be initialized to the length of the `words` array. + /// Upon return, it will be set to the actual number of words that would + /// be needed to store this BigInt (i.e. the return value of `WordCount()`). + void ToWords(int* sign_bit, size_t* word_count, uint64_t* words); + }; +#endif // NAPI_EXPERIMENTAL + /// A JavaScript string or symbol value (that can be used as a property name). class Name : public Value { public: @@ -705,6 +756,10 @@ namespace Napi { : std::is_same::value ? napi_uint32_array : std::is_same::value ? napi_float32_array : std::is_same::value ? napi_float64_array +#ifdef NAPI_EXPERIMENTAL + : std::is_same::value ? napi_bigint64_array + : std::is_same::value ? napi_biguint64_array +#endif // NAPI_EXPERIMENTAL : unknown_array_type; } /// !endcond @@ -1551,9 +1606,9 @@ namespace Napi { std::string _error; }; - // Memory management. + // Memory management. class MemoryManagement { - public: + public: static int64_t AdjustExternalMemory(Env env, int64_t change_in_bytes); }; diff --git a/test/bigint.cc b/test/bigint.cc new file mode 100644 index 000000000..48e44ac18 --- /dev/null +++ b/test/bigint.cc @@ -0,0 +1,76 @@ +#define NAPI_EXPERIMENTAL +#include "napi.h" + +using namespace Napi; + +namespace { + +Value IsLossless(const CallbackInfo& info) { + Env env = info.Env(); + + BigInt big = info[0].As(); + bool is_signed = info[1].ToBoolean().Value(); + + bool lossless; + if (is_signed) { + big.Int64Value(&lossless); + } else { + big.Uint64Value(&lossless); + } + + return Boolean::New(env, lossless); +} + +Value TestInt64(const CallbackInfo& info) { + bool lossless; + int64_t input = info[0].As().Int64Value(&lossless); + + return BigInt::New(info.Env(), input); +} + +Value TestUint64(const CallbackInfo& info) { + bool lossless; + uint64_t input = info[0].As().Uint64Value(&lossless); + + return BigInt::New(info.Env(), input); +} + +Value TestWords(const CallbackInfo& info) { + BigInt big = info[0].As(); + + size_t expected_word_count = big.WordCount(); + + int sign_bit; + size_t word_count = 10; + uint64_t words[10]; + + big.ToWords(&sign_bit, &word_count, words); + + if (word_count != expected_word_count) { + Error::New(info.Env(), "word count did not match").ThrowAsJavaScriptException(); + return BigInt(); + } + + return BigInt::New(info.Env(), sign_bit, word_count, words); +} + +Value TestTooBigBigInt(const CallbackInfo& info) { + int sign_bit = 0; + size_t word_count = SIZE_MAX; + uint64_t words[10]; + + return BigInt::New(info.Env(), sign_bit, word_count, words); +} + +} // anonymous namespace + +Object InitBigInt(Env env) { + Object exports = Object::New(env); + exports["IsLossless"] = Function::New(env, IsLossless); + exports["TestInt64"] = Function::New(env, TestInt64); + exports["TestUint64"] = Function::New(env, TestUint64); + exports["TestWords"] = Function::New(env, TestWords); + exports["TestTooBigBigInt"] = Function::New(env, TestTooBigBigInt); + + return exports; +} diff --git a/test/bigint.js b/test/bigint.js new file mode 100644 index 000000000..e4255172c --- /dev/null +++ b/test/bigint.js @@ -0,0 +1,52 @@ +'use strict'; + +const buildType = process.config.target_defaults.default_configuration; +const assert = require('assert'); + +test(require(`./build/${buildType}/binding.node`)); +test(require(`./build/${buildType}/binding_noexcept.node`)); + +function test(binding) { + const { + TestInt64, + TestUint64, + TestWords, + IsLossless, + TestTooBigBigInt, + } = binding.bigint; + + [ + 0n, + -0n, + 1n, + -1n, + 100n, + 2121n, + -1233n, + 986583n, + -976675n, + 98765432213456789876546896323445679887645323232436587988766545658n, + -4350987086545760976737453646576078997096876957864353245245769809n, + ].forEach((num) => { + if (num > -(2n ** 63n) && num < 2n ** 63n) { + assert.strictEqual(TestInt64(num), num); + assert.strictEqual(IsLossless(num, true), true); + } else { + assert.strictEqual(IsLossless(num, true), false); + } + + if (num >= 0 && num < 2n ** 64n) { + assert.strictEqual(TestUint64(num), num); + assert.strictEqual(IsLossless(num, false), true); + } else { + assert.strictEqual(IsLossless(num, false), false); + } + + assert.strictEqual(num, TestWords(num)); + }); + + assert.throws(TestTooBigBigInt, { + name: 'RangeError', + message: 'Maximum BigInt size exceeded', + }); +} diff --git a/test/binding.cc b/test/binding.cc index 0032c8e30..b91367d30 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -6,6 +6,7 @@ Object InitArrayBuffer(Env env); Object InitAsyncWorker(Env env); Object InitBasicTypesNumber(Env env); Object InitBasicTypesValue(Env env); +Object InitBigInt(Env env); Object InitBuffer(Env env); Object InitDataView(Env env); Object InitDataViewReadWrite(Env env); @@ -25,6 +26,7 @@ Object Init(Env env, Object exports) { exports.Set("asyncworker", InitAsyncWorker(env)); exports.Set("basic_types_number", InitBasicTypesNumber(env)); exports.Set("basic_types_value", InitBasicTypesValue(env)); + exports.Set("bigint", InitBigInt(env)); exports.Set("buffer", InitBuffer(env)); exports.Set("dataview", InitDataView(env)); exports.Set("dataview_read_write", InitDataView(env)); diff --git a/test/binding.gyp b/test/binding.gyp index acbbc0a12..e81b00f60 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -5,6 +5,7 @@ 'asyncworker.cc', 'basic_types/number.cc', 'basic_types/value.cc', + 'bigint.cc', 'binding.cc', 'buffer.cc', 'dataview/dataview.cc', diff --git a/test/index.js b/test/index.js index c9d7fb0c2..1181dbf15 100644 --- a/test/index.js +++ b/test/index.js @@ -12,6 +12,7 @@ let testModules = [ 'asyncworker', 'basic_types/number', 'basic_types/value', + 'bigint', 'buffer', 'dataview/dataview', 'dataview/dataview_read_write', diff --git a/test/typedarray.cc b/test/typedarray.cc index b556a80a6..d231f69f6 100644 --- a/test/typedarray.cc +++ b/test/typedarray.cc @@ -1,3 +1,4 @@ +#define NAPI_EXPERIMENTAL #include "napi.h" using namespace Napi; @@ -64,6 +65,16 @@ Value CreateTypedArray(const CallbackInfo& info) { NAPI_TYPEDARRAY_NEW(Float64Array, info.Env(), length, napi_float64_array) : NAPI_TYPEDARRAY_NEW_BUFFER(Float64Array, info.Env(), length, buffer, bufferOffset, napi_float64_array); + } else if (arrayType == "bigint64") { + return buffer.IsUndefined() ? + NAPI_TYPEDARRAY_NEW(BigInt64Array, info.Env(), length, napi_bigint64_array) : + NAPI_TYPEDARRAY_NEW_BUFFER(BigInt64Array, info.Env(), length, buffer, bufferOffset, + napi_bigint64_array); + } else if (arrayType == "biguint64") { + return buffer.IsUndefined() ? + NAPI_TYPEDARRAY_NEW(BigUint64Array, info.Env(), length, napi_biguint64_array) : + NAPI_TYPEDARRAY_NEW_BUFFER(BigUint64Array, info.Env(), length, buffer, bufferOffset, + napi_biguint64_array); } else { Error::New(info.Env(), "Invalid typed-array type.").ThrowAsJavaScriptException(); return Value(); @@ -86,6 +97,8 @@ Value GetTypedArrayType(const CallbackInfo& info) { case napi_uint32_array: return String::New(info.Env(), "uint32"); case napi_float32_array: return String::New(info.Env(), "float32"); case napi_float64_array: return String::New(info.Env(), "float64"); + case napi_bigint64_array: return String::New(info.Env(), "bigint64"); + case napi_biguint64_array: return String::New(info.Env(), "biguint64"); default: return String::New(info.Env(), "invalid"); } } @@ -122,6 +135,10 @@ Value GetTypedArrayElement(const CallbackInfo& info) { return Number::New(info.Env(), array.As()[index]); case napi_float64_array: return Number::New(info.Env(), array.As()[index]); + case napi_bigint64_array: + return BigInt::New(info.Env(), array.As()[index]); + case napi_biguint64_array: + return BigInt::New(info.Env(), array.As()[index]); default: Error::New(info.Env(), "Invalid typed-array type.").ThrowAsJavaScriptException(); return Value(); @@ -160,6 +177,16 @@ void SetTypedArrayElement(const CallbackInfo& info) { case napi_float64_array: array.As()[index] = value.DoubleValue(); break; + case napi_bigint64_array: { + bool lossless; + array.As()[index] = value.As().Int64Value(&lossless); + break; + } + case napi_biguint64_array: { + bool lossless; + array.As()[index] = value.As().Uint64Value(&lossless); + break; + } default: Error::New(info.Env(), "Invalid typed-array type.").ThrowAsJavaScriptException(); } diff --git a/test/typedarray.js b/test/typedarray.js index 9aa880c16..680cf856e 100644 --- a/test/typedarray.js +++ b/test/typedarray.js @@ -64,6 +64,53 @@ function test(binding) { } }); + [ + ['bigint64', BigInt64Array], + ['biguint64', BigUint64Array], + ].forEach(([type, Constructor]) => { + try { + const length = 4; + const t = binding.typedarray.createTypedArray(type, length); + assert.ok(t instanceof Constructor); + assert.strictEqual(binding.typedarray.getTypedArrayType(t), type); + assert.strictEqual(binding.typedarray.getTypedArrayLength(t), length); + + t[3] = 11n; + assert.strictEqual(binding.typedarray.getTypedArrayElement(t, 3), 11n); + binding.typedarray.setTypedArrayElement(t, 3, 22n); + assert.strictEqual(binding.typedarray.getTypedArrayElement(t, 3), 22n); + assert.strictEqual(t[3], 22n); + + const b = binding.typedarray.getTypedArrayBuffer(t); + assert.ok(b instanceof ArrayBuffer); + } catch (e) { + console.log(type, Constructor); + throw e; + } + + try { + const length = 4; + const offset = 8; + const b = new ArrayBuffer(offset + 64 * 4); + + const t = binding.typedarray.createTypedArray(type, length, b, offset); + assert.ok(t instanceof Constructor); + assert.strictEqual(binding.typedarray.getTypedArrayType(t), type); + assert.strictEqual(binding.typedarray.getTypedArrayLength(t), length); + + t[3] = 11n; + assert.strictEqual(binding.typedarray.getTypedArrayElement(t, 3), 11n); + binding.typedarray.setTypedArrayElement(t, 3, 22n); + assert.strictEqual(binding.typedarray.getTypedArrayElement(t, 3), 22n); + assert.strictEqual(t[3], 22n); + + assert.strictEqual(binding.typedarray.getTypedArrayBuffer(t), b); + } catch (e) { + console.log(type, Constructor); + throw e; + } + }); + assert.throws(() => { binding.typedarray.createInvalidTypedArray(); }, /Invalid (pointer passed as )?argument/);