Skip to content

Commit 8d0b4bf

Browse files
authored
Levrage BigInt to represent int64/uint64 (#1030)
Due to [double precision floating point format](https://en.wikipedia.org/wiki/Double_precision_floating-point_format), JavaScript can only safely represent integers [between](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) -(2^53 – 1) to 2^53 – 1, meanwhile, `Number.MAX_SAFE_INTEGER` is defined to represent the maximum safe integer in JavaScript, which is 9007199254740991. Per ROS2 message types, it has [int64](https://github.com/ros2/common_interfaces/blob/rolling/std_msgs/msg/Int64.msg)/[uint64](https://github.com/ros2/common_interfaces/blob/rolling/std_msgs/msg/UInt64.msg), which may be out of the range above. For current implementation, we leverage [ref](https://github.com/node-ffi-napi/ref-napi) to read/write values from `int64_t` and `uint64_t` data of C++, which can be type of: 1. `number`: when value in [`-Number.MAX_SAFE_INTEGER` `Number.MAX_SAFE_INTEGER`]. 2. `string`: otherwise. This brings the problem that the type of int64/uint64 be ununified in JavaScript. Along with [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) introduced, it becomes possible to make the type consistent. This patch implements to leverage `BigInt` to represent int64/uint64, including: 1. Update `message.dot` to use `BigInt` when hitting int64/uint64. 2. Update `rosidl_parser.js` to convert int64/uint64 to string in JSON object. 3. Update `message_translator.js` to add `bigint` as primitive. 4. Update `parameter.js` to use `BigInt` for integer and its `ts` definition `parameter.d.ts`. 5. Update tests targeting on messages that include int64/uint64. Fix: #836, #1025
1 parent 6295ef8 commit 8d0b4bf

23 files changed

+264
-296
lines changed

lib/parameter.js

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const rclnodejs = require('bindings')('rclnodejs');
3434
const DEFAULT_NUMERIC_RANGE_TOLERANCE = 1e-6;
3535

3636
const PARAMETER_SEPARATOR = '.';
37+
const PARAMETER_BYTE = 10;
3738

3839
/**
3940
* Enum for ParameterType
@@ -125,7 +126,9 @@ class Parameter {
125126
constructor(name, type, value) {
126127
this._name = name;
127128
this._type = type;
128-
this._value = value;
129+
// Convert to bigint if it's type of `PARAMETER_INTEGER`.
130+
this._value =
131+
this._type == ParameterType.PARAMETER_INTEGER ? BigInt(value) : value;
129132
this._isDirty = true;
130133

131134
this.validate();
@@ -240,10 +243,10 @@ class Parameter {
240243
msg.double_array_value = this.value;
241244
break;
242245
case ParameterType.PARAMETER_INTEGER:
243-
msg.integer_value = Math.trunc(this.value);
246+
msg.integer_value = this.value;
244247
break;
245248
case ParameterType.PARAMETER_INTEGER_ARRAY:
246-
msg.integer_array_value = this.value.map((val) => Math.trunc(val));
249+
msg.integer_array_value = this.value;
247250
break;
248251
case ParameterType.PARAMETER_STRING:
249252
msg.string_value = this.value;
@@ -537,10 +540,10 @@ class Range {
537540

538541
/**
539542
* Determine if a value is within this range.
540-
* A TypeError is thrown when value is not a number.
543+
* A TypeError is thrown when value is not a number or bigint.
541544
* Subclasses should override and call this method for basic type checking.
542545
*
543-
* @param {number} value - The number to check.
546+
* @param {number|bigint} value - The number or bigint to check.
544547
* @return {boolean} - True if value satisfies the range; false otherwise.
545548
*/
546549
inRange(value) {
@@ -550,8 +553,8 @@ class Range {
550553
(inRange, val) => inRange && this.inRange(val),
551554
true
552555
);
553-
} else if (typeof value !== 'number') {
554-
throw new TypeError('Value must be a number');
556+
} else if (typeof value !== 'number' && typeof value !== 'bigint') {
557+
throw new TypeError('Value must be a number or bigint');
555558
}
556559

557560
return true;
@@ -652,27 +655,16 @@ class FloatingPointRange extends Range {
652655
* Defines a range for integer values.
653656
* @class
654657
*/
655-
class IntegerRange extends FloatingPointRange {
658+
class IntegerRange extends Range {
656659
/**
657660
* Create a new instance.
658661
* @constructor
659-
* @param {number} fromValue - The lowest inclusive value in range
660-
* @param {number} toValue - The highest inclusive value in range
661-
* @param {number} step - The internal unit size.
662-
* @param {number} tolerance - The plus/minus tolerance for number equivalence.
662+
* @param {bigint} fromValue - The lowest inclusive value in range
663+
* @param {bigint} toValue - The highest inclusive value in range
664+
* @param {bigint} step - The internal unit size.
663665
*/
664-
constructor(
665-
fromValue,
666-
toValue,
667-
step = 1,
668-
tolerance = DEFAULT_NUMERIC_RANGE_TOLERANCE
669-
) {
670-
super(
671-
Math.trunc(fromValue),
672-
Math.trunc(toValue),
673-
Math.trunc(step),
674-
tolerance
675-
);
666+
constructor(fromValue, toValue, step = 1n) {
667+
super(fromValue, toValue, step);
676668
}
677669

678670
/**
@@ -683,12 +675,23 @@ class IntegerRange extends FloatingPointRange {
683675
*/
684676
isValidType(parameterType) {
685677
const result =
686-
parameterType === ParameterType.PARAMETER_BYTE ||
687-
parameterType === ParameterType.PARAMETER_BYTE_ARRAY ||
688678
parameterType === ParameterType.PARAMETER_INTEGER ||
689679
parameterType === ParameterType.PARAMETER_INTEGER_ARRAY;
690680
return result;
691681
}
682+
683+
inRange(value) {
684+
const min = this.fromValue;
685+
const max = this.toValue;
686+
if (value < min || value > max) {
687+
return false;
688+
}
689+
690+
if (this.step != 0n && (value - min) % this.step !== 0n) {
691+
return false;
692+
}
693+
return true;
694+
}
692695
}
693696

694697
/**
@@ -763,10 +766,13 @@ function validValue(value, type) {
763766
case ParameterType.PARAMETER_STRING:
764767
result = typeof value === 'string';
765768
break;
766-
case ParameterType.PARAMETER_INTEGER:
767769
case ParameterType.PARAMETER_DOUBLE:
770+
case PARAMETER_BYTE:
768771
result = typeof value === 'number';
769772
break;
773+
case ParameterType.PARAMETER_INTEGER:
774+
result = typeof value === 'bigint';
775+
break;
770776
case ParameterType.PARAMETER_BOOL_ARRAY:
771777
case ParameterType.PARAMETER_BYTE_ARRAY:
772778
case ParameterType.PARAMETER_INTEGER_ARRAY:
@@ -789,7 +795,7 @@ function _validArray(values, type) {
789795
if (type === ParameterType.PARAMETER_BOOL_ARRAY) {
790796
arrayElementType = ParameterType.PARAMETER_BOOL;
791797
} else if (type === ParameterType.PARAMETER_BYTE_ARRAY) {
792-
arrayElementType = ParameterType.PARAMETER_INTEGER;
798+
arrayElementType = PARAMETER_BYTE;
793799
}
794800
if (type === ParameterType.PARAMETER_INTEGER_ARRAY) {
795801
arrayElementType = ParameterType.PARAMETER_INTEGER;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"dot": "^1.1.3",
7676
"dtslint": "^4.2.1",
7777
"fs-extra": "^11.2.0",
78+
"json-bigint": "^1.0.0",
7879
"int64-napi": "^1.0.2",
7980
"is-close": "^1.3.3",
8081
"mkdirp": "^3.0.1",

rosidl_gen/message_translator.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ function copyMsgObject(msg, obj) {
2727
for (let i in obj) {
2828
if (msg.hasMember(i)) {
2929
const type = typeof obj[i];
30-
if (type === 'string' || type === 'number' || type === 'boolean') {
30+
if (
31+
type === 'string' ||
32+
type === 'number' ||
33+
type === 'boolean' ||
34+
type === 'bigint'
35+
) {
3136
// A primitive-type value
3237
msg[i] = obj[i];
3338
} else if (Array.isArray(obj[i]) || isTypedArray(obj[i])) {
@@ -80,17 +85,20 @@ function verifyMessage(message, obj) {
8085
case 'char':
8186
case 'int16':
8287
case 'int32':
83-
case 'int64':
8488
case 'byte':
8589
case 'uint16':
8690
case 'uint32':
87-
case 'uint64':
8891
case 'float32':
8992
case 'float64':
9093
if (typeof obj[name] != 'number') {
9194
return false;
9295
}
9396
break;
97+
case 'int64':
98+
case 'uint64':
99+
if (typeof obj[name] != 'bigint') {
100+
return false;
101+
}
94102
case 'bool':
95103
if (typeof obj[name] != 'boolean') {
96104
return false;
@@ -171,7 +179,12 @@ function toROSMessage(TypeClass, obj) {
171179

172180
function constructFromPlanObject(msg, obj) {
173181
const type = typeof obj;
174-
if (type === 'string' || type === 'number' || type === 'boolean') {
182+
if (
183+
type === 'string' ||
184+
type === 'number' ||
185+
type === 'boolean' ||
186+
type === 'bigint'
187+
) {
175188
msg.data = obj;
176189
} else if (type === 'object') {
177190
copyMsgObject(msg, obj);

rosidl_gen/packages.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,7 @@ async function generateMsgForSrv(filePath, interfaceInfo, pkgMap) {
161161
async function addInterfaceInfos(filePath, dir, pkgMap) {
162162
const interfaceInfo = grabInterfaceInfo(filePath, true);
163163
const ignore = pkgFilters.matchesAny(interfaceInfo);
164-
if (ignore) {
165-
console.log('Omitting filtered interface: ', interfaceInfo);
166-
} else {
164+
if (!ignore) {
167165
if (path.extname(filePath) === '.msg') {
168166
// Some .msg files were generated prior to 0.3.2 for .action files,
169167
// which has been disabled. So these files should be ignored here.
@@ -232,9 +230,7 @@ async function findPackagesInDirectory(dir) {
232230
amentExecuted
233231
);
234232
const ignore = pkgFilters.matchesAny(interfaceInfo);
235-
if (ignore) {
236-
console.log('Omitting filtered interface: ', interfaceInfo);
237-
} else {
233+
if (!ignore) {
238234
if (path.extname(file.name) === '.msg') {
239235
// Some .msg files were generated prior to 0.3.2 for .action files,
240236
// which has been disabled. So these files should be ignored here.

rosidl_gen/templates/message.dot

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ function isTypedArrayType(type) {
157157
return typedArrayType.indexOf(type.type.toLowerCase()) !== -1;
158158
}
159159

160+
function isBigInt(type) {
161+
return ['int64', 'uint64'].indexOf(type.type.toLowerCase()) !== -1;
162+
}
163+
160164
const willUseTypedArray = isTypedArrayType(it.spec.baseType);
161165
const currentTypedArray = getTypedArrayName(it.spec.baseType);
162166
const currentTypedArrayElementType = getTypedArrayElementName(it.spec.baseType);
@@ -303,7 +307,13 @@ class {{=objectWrapper}} {
303307
this._refObject.{{=field.name}} = {{=field.default_value}};
304308
{{?}}
305309
{{?? field.type.isPrimitiveType && !isTypedArrayType(field.type) && field.default_value}}
306-
this._{{=field.name}}Array = {{=JSON.stringify(field.default_value)}};
310+
{{? isBigInt(field.type)}}
311+
{{/* For non-TypedArray like int64/uint64. */}}
312+
this._{{=field.name}}Array = {{=JSON.stringify(field.default_value)}}.map(num => BigInt(num));
313+
{{??}}
314+
{{/* For non-TypedArray like bool. */}}
315+
this._{{=field.name}}Array = {{=JSON.stringify(field.default_value)}};
316+
{{?}}
307317
{{?? field.type.isPrimitiveType && isTypedArrayType(field.type) && field.default_value}}
308318
this._wrapperFields.{{=field.name}}.fill({{=getTypedArrayName(field.type)}}.from({{=JSON.stringify(field.default_value)}}));
309319
{{?}}
@@ -376,8 +386,11 @@ class {{=objectWrapper}} {
376386
}
377387
}
378388
}
389+
{{?? isBigInt(field.type)}}
390+
{{/* For non-TypedArray like int64/uint64. */}}
391+
this._refObject.{{=field.name}} = this._{{=field.name}}Array.map(num => num.toString());
379392
{{??}}
380-
{{/* For non-TypedArray like int64/uint64/bool. */}}
393+
{{/* For non-TypedArray like bool. */}}
381394
this._refObject.{{=field.name}} = this._{{=field.name}}Array;
382395
{{?}}
383396
{{?? field.type.isArray && field.type.isPrimitiveType && isTypedArrayType(field.type) && field.type.isFixedSizeArray}}
@@ -527,6 +540,8 @@ class {{=objectWrapper}} {
527540
return this._wrapperFields.{{=field.name}};
528541
{{?? !field.type.isArray && field.type.type === 'string' && it.spec.msgName !== 'String'}}
529542
return this._wrapperFields.{{=field.name}}.data;
543+
{{?? isBigInt(field.type)}}
544+
return BigInt(this._refObject.{{=field.name}});
530545
{{??}}
531546
return this._refObject.{{=field.name}};
532547
{{?}}
@@ -559,6 +574,11 @@ class {{=objectWrapper}} {
559574
}
560575
{{?? !field.type.isArray && field.type.type === 'string' && it.spec.msgName !== 'String'}}
561576
this._wrapperFields.{{=field.name}}.data = value;
577+
{{?? isBigInt(field.type)}}
578+
if (typeof value !== "bigint") {
579+
throw new TypeError('{{=field.name}} must be type of bigint');
580+
}
581+
this._refObject.{{=field.name}} = value.toString();
562582
{{??}}
563583
{{? it.spec.msgName === 'String'}}
564584
this._refObject.size = Buffer.byteLength(value);

rosidl_parser/rosidl_parser.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,20 @@
1414

1515
'use strict';
1616

17+
const compareVersions = require('compare-versions');
1718
const path = require('path');
1819
const execFile = require('child_process').execFile;
1920

2021
const pythonExecutable = require('./py_utils').getPythonExecutable('python3');
2122

23+
const contextSupportedVersion = '21.0.0.0';
24+
const currentVersion = process.version;
25+
const isContextSupported = compareVersions.compare(
26+
currentVersion.substring(1, currentVersion.length),
27+
contextSupportedVersion,
28+
'>='
29+
);
30+
2231
const rosidlParser = {
2332
parseMessageFile(packageName, filePath) {
2433
return this._parseFile('parse_message_file', packageName, filePath);
@@ -32,6 +41,25 @@ const rosidlParser = {
3241
return this._parseFile('parse_action_file', packageName, filePath);
3342
},
3443

44+
_parseJSONObject(str) {
45+
// For nodejs >= `contextSupportedVersion`, we leverage context parameter to
46+
// convert unsafe integer to string, otherwise, json-bigint is used.
47+
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
48+
if (isContextSupported) {
49+
return JSON.parse(str, (key, value, context) => {
50+
if (
51+
Number.isInteger(value) &&
52+
!Number.isSafeInteger(Number(context.source))
53+
) {
54+
return context.source;
55+
}
56+
return value;
57+
});
58+
}
59+
const JSONbigString = require('json-bigint')({ storeAsString: true });
60+
return JSONbigString.parse(str);
61+
},
62+
3563
_parseFile(command, packageName, filePath) {
3664
return new Promise((resolve, reject) => {
3765
const args = [
@@ -54,7 +82,7 @@ const rosidlParser = {
5482
)
5583
);
5684
} else {
57-
resolve(JSON.parse(stdout));
85+
resolve(this._parseJSONObject(stdout));
5886
}
5987
}
6088
);

rostsd_gen/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,6 @@ function primitiveType2JSName(type) {
478478
case 'int8':
479479
case 'int16':
480480
case 'int32':
481-
case 'int64':
482481

483482
// signed explicit float types
484483
case 'float32':
@@ -488,7 +487,6 @@ function primitiveType2JSName(type) {
488487
case 'uint8':
489488
case 'uint16':
490489
case 'uint32':
491-
case 'uint64':
492490
jsName = 'number';
493491
break;
494492
case 'bool':
@@ -499,6 +497,10 @@ function primitiveType2JSName(type) {
499497
case 'wstring':
500498
jsName = 'string';
501499
break;
500+
case 'int64':
501+
case 'uint64':
502+
jsName = 'bigint';
503+
break;
502504
}
503505

504506
return jsName;

test/client_setup.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ rclnodejs
2424
const Int8 = 'std_msgs/msg/Int8';
2525
var client = node.createClient(AddTwoInts, 'add_two_ints');
2626
const request = {
27-
a: 1,
28-
b: 2,
27+
a: 1n,
28+
b: 2n,
2929
};
3030
var publisher = node.createPublisher(Int8, 'back_add_two_ints');
3131
client.waitForService().then(() => {
3232
client.sendRequest(request, (response) => {
33-
publisher.publish(response.sum);
33+
publisher.publish(Number(response.sum));
3434
});
3535
});
3636

test/publisher_msg.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const rclnodejs = require('../index.js');
1919
var rclType = process.argv[2];
2020
var rclValue = eval(process.argv[3]);
2121

22+
if (['int64', 'uint64'].indexOf(rclType.toLowerCase()) !== -1) {
23+
rclValue = BigInt(rclValue);
24+
}
25+
2226
rclnodejs
2327
.init()
2428
.then(() => {

0 commit comments

Comments
 (0)