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
89 changes: 76 additions & 13 deletions compiler/cpp/src/thrift/generate/t_js_generator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class t_js_generator : public t_oop_generator {
gen_es6_ = false;
gen_esm_ = false;
gen_episode_file_ = false;
// Default true: when generating for js:node, int64 values are emitted as
// native BigInt literals and the node-int64 import is omitted. Callers
// opt out with bigint=false to retain the legacy `new Int64(...)` output.
gen_bigint_ = true;

bool with_ns_ = false;

Expand All @@ -90,11 +94,27 @@ class t_js_generator : public t_oop_generator {
parse_imports(program, iter->second);
} else if (iter->first.compare("thrift_package_output_directory") == 0) {
parse_thrift_package_output_directory(iter->second);
} else if (iter->first.compare("bigint") == 0) {
if (iter->second.empty() || iter->second.compare("true") == 0) {
gen_bigint_ = true;
} else if (iter->second.compare("false") == 0) {
gen_bigint_ = false;
} else {
throw std::invalid_argument(
"invalid value for js:bigint, must be true or false");
}
} else {
throw std::invalid_argument("unknown option js:" + iter->first);
}
}

// BigInt-mode code generation is only meaningful for the node generator.
// Force it off for plain browser-JS so the default-true setting does not
// affect callers that pass plain `--gen js`.
if (!gen_node_) {
gen_bigint_ = false;
}

if (gen_es6_ && gen_jquery_) {
throw std::invalid_argument("invalid switch: [-gen js:es6,jquery] options not compatible");
}
Expand Down Expand Up @@ -383,6 +403,13 @@ class t_js_generator : public t_oop_generator {
*/
bool gen_esm_;

/**
* True (default) if int64 values should be generated as native BigInt
* literals. False emits the legacy `new Int64(...)` from node-int64.
* Only meaningful when gen_node_ is true.
*/
bool gen_bigint_;

/**
* True if we will generate an episode file.
*/
Expand Down Expand Up @@ -538,10 +565,14 @@ string t_js_generator::js_includes() {
}
}
if (gen_esm_) {
result += "import Int64 from 'node-int64';\n";
if (!gen_bigint_) {
result += "import Int64 from 'node-int64';\n";
}
result += "import { v4 as uuid } from 'uuid';";
} else {
result += js_const_type_ + "Int64 = require('node-int64');\n";
if (!gen_bigint_) {
result += js_const_type_ + "Int64 = require('node-int64');\n";
}
result += js_const_type_ + "uuid = require('uuid').v4;\n";
}
return result;
Expand All @@ -556,13 +587,17 @@ string t_js_generator::js_includes() {
*/
string t_js_generator::ts_includes() {
if (gen_node_) {
return string(
string result =
"import thrift = require('thrift');\n"
"import Thrift = thrift.Thrift;\n"
"import Q = thrift.Q;\n"
"import Int64 = require('node-int64');\n"
"import Q = thrift.Q;\n";
if (!gen_bigint_) {
result += "import Int64 = require('node-int64');\n";
}
result +=
"import { v4 as uuid } from 'uuid';\n"
"type uuid = string;");
"type uuid = string;";
return result;
}
return string(
"import Int64 = require('node-int64');\n"
Expand All @@ -575,11 +610,14 @@ string t_js_generator::ts_includes() {
*/
string t_js_generator::ts_service_includes() {
if (gen_node_) {
return string(
string result =
"import thrift = require('thrift');\n"
"import Thrift = thrift.Thrift;\n"
"import Q = thrift.Q;\n"
"import Int64 = require('node-int64');");
"import Q = thrift.Q;";
if (!gen_bigint_) {
result += "\nimport Int64 = require('node-int64');";
}
return result;
}
return string("import Int64 = require('node-int64');");
}
Expand Down Expand Up @@ -779,7 +817,10 @@ string t_js_generator::render_const_value(t_type* type, t_const_value* value) {
case t_base_type::TYPE_I64:
{
int64_t const& integer_value = value->get_integer();
if (integer_value <= max_safe_integer && integer_value >= min_safe_integer) {
if (gen_bigint_) {
// Native BigInt literal — handles the full signed 64-bit range.
out << integer_value << "n";
} else if (integer_value <= max_safe_integer && integer_value >= min_safe_integer) {
out << "new Int64(" << integer_value << ")";
} else {
out << "new Int64('" << std::hex << integer_value << std::dec << "')";
Expand Down Expand Up @@ -2335,7 +2376,18 @@ void t_js_generator::generate_deserialize_field(ostream& out,
} else if (type->is_container()) {
generate_deserialize_container(out, type, name);
} else if (type->is_base_type() || type->is_enum()) {
indent(out) << name << " = input.";
// In bigint mode the protocol still returns a node-int64 Int64; wrap
// the read site in `thrift.toBigInt(...)` so the assigned value matches
// the generated `bigint` type. gen_bigint_ is only true when gen_node_
// is true, so the `.value` branch below is never reached in this mode.
bool wrap_i64_bigint = gen_bigint_ && type->is_base_type() &&
((t_base_type*)type)->get_base() == t_base_type::TYPE_I64;

indent(out) << name << " = ";
if (wrap_i64_bigint) {
out << "thrift.toBigInt(";
}
out << "input.";

if (type->is_base_type()) {
t_base_type::t_base tbase = ((t_base_type*)type)->get_base();
Expand Down Expand Up @@ -2374,6 +2426,10 @@ void t_js_generator::generate_deserialize_field(ostream& out,
out << "readI32()";
}

if (wrap_i64_bigint) {
out << ")";
}

if (!gen_node_) {
out << ".value";
}
Expand Down Expand Up @@ -2565,7 +2621,14 @@ void t_js_generator::generate_serialize_field(ostream& out, t_field* tfield, str
out << "writeI32(" << name << ")";
break;
case t_base_type::TYPE_I64:
out << "writeI64(" << name << ")";
// In bigint mode the generated field holds a `bigint`; convert
// back to a node-int64 Int64 before handing to `writeI64`, which
// expects either an Int64 or a Number.
if (gen_bigint_) {
out << "writeI64(thrift.fromBigInt(" << name << "))";
} else {
out << "writeI64(" << name << ")";
}
break;
case t_base_type::TYPE_DOUBLE:
out << "writeDouble(" << name << ")";
Expand Down Expand Up @@ -2866,7 +2929,7 @@ string t_js_generator::ts_get_type(t_type* type) {
ts_type = "number";
break;
case t_base_type::TYPE_I64:
ts_type = "Int64";
ts_type = gen_bigint_ ? "bigint" : "Int64";
break;
case t_base_type::TYPE_VOID:
ts_type = "void";
Expand Down
2 changes: 1 addition & 1 deletion lib/nodejs/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# We call npm twice to work around npm issues

stubs: $(top_srcdir)/test/ThriftTest.thrift
$(THRIFT) --gen js:node -o test/ $(top_srcdir)/test/ThriftTest.thrift
$(THRIFT) --gen js:node,bigint=false -o test/ $(top_srcdir)/test/ThriftTest.thrift

deps-root: $(top_srcdir)/package.json
$(NPM) install $(top_srcdir)/ || $(NPM) install $(top_srcdir)/
Expand Down
45 changes: 44 additions & 1 deletion lib/nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ under the License.

## Compatibility

node version 6 or later is required
Node.js 10.18 or later is required, matching the `engines` field in the
package's [`package.json`](../../package.json).

## Install

Expand Down Expand Up @@ -65,6 +66,48 @@ client.get_slice("Keyspace", "key", new ttypes.ColumnParent({column_family: "Exa

Since JavaScript represents all numbers as doubles, int64 values cannot be accurately represented naturally. To solve this, int64 values in responses will be wrapped with Thrift.Int64 objects. The Int64 implementation used is [broofa/node-int64](https://github.com/broofa/node-int64).

### BigInt mode (`js:bigint`, default `true`)

Native `BigInt` has been available since Node 10.4, so every supported runtime
can use it. The js generator emits BigInt-aware code by default:

```sh
thrift --gen js:node MyService.thrift # bigint=true is the default
thrift --gen js:node,bigint=false MyService.thrift # legacy node-int64 output
```

When `bigint=true` (the default), code generated by `--gen js:node` (and the
`ts`, `es6`, `esm` variants):

- Emits native `bigint` literals (e.g. `42n`, `9223372036854775807n`) for
int64 constants instead of `new Int64(42)`.
- Drops the `require('node-int64')` / `import Int64 from 'node-int64'`
preamble.
- Declares int64 fields as `bigint` in `.d.ts` files instead of `Int64`.
- Wraps `readI64()` calls with `thrift.toBigInt(...)` so deserialized
fields land in the generated struct as `bigint`, and `writeI64(...)`
calls with `thrift.fromBigInt(...)` so consumers can pass plain
`bigint` values into their structs.

The flag is only meaningful when `node` is in the option list; for plain
`--gen js` (browser JS) it is silently forced to `false`.

`TBinaryProtocol` itself is **unchanged** — `readI64` still returns a
`Thrift.Int64` and `writeI64` still expects one. The BigInt boundary lives
entirely in the JS layer, in the two helpers exported from the `thrift`
package:

```js
const thrift = require("thrift");

const big = thrift.toBigInt(prot.readI64()); // Int64 -> bigint
prot.writeI64(thrift.fromBigInt(big)); // bigint -> Int64
```

`thrift.fromBigInt` wraps values to the signed 64-bit range
(`BigInt.asIntN(64, ...)`). Existing code that imports `Thrift.Int64` or
calls `prot.readI64()` directly keeps working with no changes.

## Client and server examples

Several example clients and servers are included in the thrift/lib/nodejs/examples folder and the cross language tutorial thrift/tutorial/nodejs folder.
Expand Down
68 changes: 68 additions & 0 deletions lib/nodejs/lib/thrift/bigint_compat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// Buffer#readBigInt64BE / writeBigInt64BE were added in Node 12.0.0. The
// published package targets `engines: >= 10.18.0`, and browser-side Buffer
// polyfills may also lag, so we feature-detect and fall back to
// readInt32BE / writeUInt32BE plus BigInt arithmetic when the native
// methods are missing.

const HAS_NATIVE =
typeof Buffer !== "undefined" &&
typeof Buffer.prototype.readBigInt64BE === "function" &&
typeof Buffer.prototype.writeBigInt64BE === "function";

function readBigInt64BEFallback(buf, offset) {
// Signed high 32 bits, unsigned low 32 bits — recombined with a BigInt
// shift+OR. Negative `hi` produces an arbitrarily-negative BigInt with
// ones in all bits above 32, which OR-merges correctly with the
// unsigned low half.
const hi = BigInt(buf.readInt32BE(offset));
const lo = BigInt(buf.readUInt32BE(offset + 4));
return (hi << 32n) | lo;
}

function writeBigInt64BEFallback(buf, value, offset) {
const v = BigInt.asIntN(64, value);
const hi = Number(BigInt.asIntN(32, v >> 32n));
const lo = Number(BigInt.asUintN(32, v));
buf.writeInt32BE(hi, offset);
buf.writeUInt32BE(lo, offset + 4);
}

exports.readBigInt64BE = HAS_NATIVE
? function (buf, offset) {
return buf.readBigInt64BE(offset);
}
: readBigInt64BEFallback;

exports.writeBigInt64BE = HAS_NATIVE
? function (buf, value, offset) {
// Native `writeBigInt64BE` rejects values outside [-2^63, 2^63 - 1];
// wrap explicitly so callers (e.g. `fromBigInt`) can hand in any
// bigint and get two's-complement truncation, matching the fallback.
buf.writeBigInt64BE(BigInt.asIntN(64, value), offset);
}
: writeBigInt64BEFallback;

// Exposed so tests can exercise the fallback path on runtimes that have
// the native methods.
exports._readBigInt64BEFallback = readBigInt64BEFallback;
exports._writeBigInt64BEFallback = writeBigInt64BEFallback;
exports._hasNativeBigInt64 = HAS_NATIVE;
2 changes: 1 addition & 1 deletion lib/nodejs/lib/thrift/binary_protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ TBinaryProtocol.prototype.skip = function (type, depth) {
if (depth > 64) {
throw new Thrift.TProtocolException(
Thrift.TProtocolExceptionType.DEPTH_LIMIT,
"Maximum skip depth exceeded"
"Maximum skip depth exceeded",
);
}
switch (type) {
Expand Down
30 changes: 30 additions & 0 deletions lib/nodejs/lib/thrift/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ exports.createClient = require("./create_client");
exports.Int64 = require("node-int64");
exports.Q = require("q");

const bigIntCompat = require("./bigint_compat");

/**
* Convert a `node-int64` Int64 to a native `bigint`. Used by code generated
* with `js:bigint=true`. Feature-detects `Buffer#readBigInt64BE` (Node 12+
* and modern buffer polyfills) and falls back to a `readInt32BE` /
* `readUInt32BE` + BigInt composition when the native method is absent.
*
* @param {Int64} i64
* @returns {bigint}
*/
exports.toBigInt = function (i64) {
return bigIntCompat.readBigInt64BE(i64.buffer, i64.offset || 0);
};

/**
* Convert a native `bigint` to a `node-int64` Int64 for `writeI64`. Values
* outside the signed 64-bit range are wrapped (`BigInt.asIntN(64, ...)`).
* Uses the same feature-detected fallback as `toBigInt`.
*
* @param {bigint} value
* @returns {Int64}
*/
exports.fromBigInt = function (value) {
const Int64 = exports.Int64;
const buf = Buffer.allocUnsafe(8);
bigIntCompat.writeBigInt64BE(buf, value, 0);
return new Int64(buf);
};
Comment thread
jimexist marked this conversation as resolved.

var mpxProtocol = require("./multiplexed_protocol");
exports.Multiplexer = mpxProtocol.Multiplexer;

Expand Down
Loading
Loading