From 7adbb2b90263e3c961a70157b93f57c7daebe12c Mon Sep 17 00:00:00 2001 From: Timo Stamm Date: Fri, 13 Nov 2020 11:47:46 +0100 Subject: [PATCH] Fix Safari 14 bigint detection. Safari 14 does have BigInt, but is missing the DataView bigint methods. - Changed detection to check for all required methods. - Added meaningful error message. - Updated MANUAL.md --- MANUAL.md | 83 +++++++++++++++------ packages/runtime/src/pb-long.ts | 124 ++++++++++++++++++++++---------- 2 files changed, 149 insertions(+), 58 deletions(-) diff --git a/MANUAL.md b/MANUAL.md index c08f8a72..9c722998 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -523,28 +523,35 @@ if (message.oneofKind === "value") { ## BigInt support -Protocol buffers have signed and unsigned 64 bit integral types, which cannot -be represented reliably by the JavaScript `number` primitive. `protobuf-ts` +Protocol buffers have signed and unsigned 64 bit integral types. `protobuf-ts` gives you the following options to represent those `.proto` types in TypeScript: 1. `bigint` - Enabled by default. Lets you use the standard JavaScript operators. - - > **Note:** bigint is not available in Safari / WebKit as of August 2020. - - > **Note:** Your tsconfig.json has to target ES2020 and you need Node.js - > 14.5.0 or higher. - + Enabled by default. Lets you use the standard JavaScript operators. 2. `string` Enabled by setting the option `[jstype = JS_STRING]` on a field , or by setting the [plugin parameter](#the-protoc-plugin) "long_type_string". - Works in all Web Browsers. 3. `number` Enabled by setting the field option `[jstype = JS_NUMBER]`. - Not recommended. `protobuf-ts` will try to detect overflows when - converting to/from `number` and raise an error. + + +> **Note:** Use the `string` representation if you target browsers. +> BigInt is still not fully supported in Safari as of November 2020. +> Safari 14 adds BigInt support, but its DataView implementation is missing +> the necessary BigInt methods. + +> **Note:** Using `number` is not recommended. +> JavaScript numbers do not cover the range of all possible 64 bit integral +> values. + +> **Note:** `bigint` requires target ES2020 in your tsconfig.json and you +> need Node.js 14.5.0 or higher. + + + +#### Changing the long representation For example, the following .proto: @@ -560,17 +567,48 @@ Generates the following TypeScript: ```typescript interface LongTypes { - normal: bigint; // will be `string` with `--ts_opt long_type_string` + normal: bigint; // `bigint` is the "normal" representation string: string; number: number; } -``` +``` -Internally, `protobuf-ts` uses the two classes `PbLong` and `PbUlong` to -convert between the different representations and the wire format. +If you set the plugin option "long_type_string", the following TypeScript is generated: -For arithmetic, you need a third party library like the excellent -[long.js](https://github.com/dcodeIO/Long.js/) if you cannot use `bigint`. +```typescript +interface LongTypes { + normal: string; // changed from `bigint` to `string` by --ts_opt long_type_string + string: string; + number: number; // not affected by --ts_opt long_type_string +} +``` + + +#### Arithmetics + +For arithmetic across browsers, you need a third party library like the +excellent [long.js](https://github.com/dcodeIO/Long.js/) or [JSBI](https://github.com/GoogleChromeLabs/jsbi). + +You should use the `string` representation, for example with the plugin +option "long_type_string". You can then read the string values, make your +operations and set a string value back on the field: + +```typescript +const myMessage = LongTypes.create({ + string: "9223372036854770000" +}); + +// using long.js: +let a = Long.fromString(myMessage.string) +let b = a.add(123); +myMessage.string = b.toString(); + +// using JSBI: +let c = JSBI.BigInt(myMessage.value) +let d = c.add(123); +myMessage.string = d.toString(); +``` + @@ -1129,10 +1167,11 @@ several code generators: | generator | version | optimize for | webpack output size | |-------------------------|----------------:|-------------------|--------------------:| -| protobuf-ts | 1.0.4 | size | 42,353 b | -| protobuf-ts | 1.0.4 | speed | 72,558 b | -| ts-proto | 1.26.0 | | 111.825 b | -| google-protobuf | 3.12.2 | | 396.934 b | +| pbf | 3.2.1 | | 22,132 b | +| protobuf-ts | 1.0.7 | size | 42,728 b | +| protobuf-ts | 1.0.7 | speed | 73,230 b | +| ts-proto | 1.26.0 | | 111,825 b | +| google-protobuf | 3.12.2 | | 396,934 b | The file sizes are calculated by compiling `google/protobuf/descriptor.proto`, diff --git a/packages/runtime/src/pb-long.ts b/packages/runtime/src/pb-long.ts index 063cfa8e..078f702d 100644 --- a/packages/runtime/src/pb-long.ts +++ b/packages/runtime/src/pb-long.ts @@ -1,16 +1,68 @@ -// factory for BigInt, is `undefined` when unavailable import {int64fromString, int64toString} from "./goog-varint"; -const biCtor: undefined | ((v: number | string | bigint) => bigint) = globalThis.BigInt; -// min / max values in bigint format -const LONG_MIN = biCtor ? biCtor("-9223372036854775808") : undefined; -const LONG_MAX = biCtor ? biCtor("9223372036854775807") : undefined; -const ULONG_MIN = biCtor ? biCtor("0") : undefined; -const ULONG_MAX = biCtor ? biCtor("18446744073709551615") : undefined; +/** + * API for supported BigInt on current platform. + */ +interface BiSupport { + + /** + * Minimum signed value. + */ + MIN: bigint; + + /** + * Maximum signed value. + */ + MAX: bigint; + + /** + * Minimum unsigned value. + */ + UMIN: bigint; + + /** + * Maximum unsigned value. + */ + UMAX: bigint; + + /** + * A data view that is guaranteed to have the methods + * - getBigInt64 + * - getBigUint64 + * - setBigInt64 + * - setBigUint64 + */ + V: DataView; -// used to convert bigint from / to bits -const VIEW64 = new DataView(new ArrayBuffer(8)); + /** + * The BigInt constructor function. + */ + C(v: number | string | bigint): bigint; +} + +function detectBi(): BiSupport | undefined { + const dv = new DataView(new ArrayBuffer(8)); + const ok = globalThis.BigInt !== undefined + && typeof dv.getBigInt64 === "function" + && typeof dv.getBigUint64 === "function" + && typeof dv.setBigInt64 === "function" + && typeof dv.setBigUint64 === "function"; + return ok ? { + MIN: BigInt("-9223372036854775808"), + MAX: BigInt("9223372036854775807"), + UMIN: BigInt("0"), + UMAX: BigInt("18446744073709551615"), + C: BigInt, + V: dv, + } : undefined; +} + +const BI = detectBi(); + +function assertBi(bi: BiSupport | undefined): asserts bi is BiSupport { + if (!bi) throw new Error("BigInt unavailable, see https://github.com/timostamm/protobuf-ts/blob/v1.0.8/MANUAL.md#bigint-support"); +} // used to validate from(string) input (when bigint is unavailable) const RE_DECIMAL_STR = /^-?[0-9]+$/; @@ -84,7 +136,7 @@ export class PbULong extends SharedPbLong { * Create instance from a `string`, `number` or `bigint`. */ static from(value: string | number | bigint): PbULong { - if (biCtor) + if (BI) // noinspection FallThroughInSwitchStatementJS switch (typeof value) { @@ -93,24 +145,24 @@ export class PbULong extends SharedPbLong { return this.ZERO; if (value == "") throw new Error('string is no integer'); - value = biCtor(value); + value = BI.C(value); case "number": if (value === 0) return this.ZERO; - value = biCtor(value); + value = BI.C(value); case "bigint": if (!value) return this.ZERO; - if (value < ULONG_MIN!) + if (value < BI.UMIN) throw new Error('signed value for ulong'); - if (value > ULONG_MAX!) + if (value > BI.UMAX) throw new Error('ulong too large'); - VIEW64.setBigUint64(0, value, true); + BI.V.setBigUint64(0, value, true); return new PbULong( - VIEW64.getInt32(0, true), - VIEW64.getInt32(4, true), + BI.V.getInt32(0, true), + BI.V.getInt32(4, true), ); } @@ -145,18 +197,17 @@ export class PbULong extends SharedPbLong { * Convert to decimal string. */ toString() { - if (biCtor) - return this.toBigInt().toString() - return int64toString(this.lo, this.hi); + return BI ? this.toBigInt().toString() : int64toString(this.lo, this.hi); } /** * Convert to native bigint. */ toBigInt(): bigint { - VIEW64.setInt32(0, this.lo, true); - VIEW64.setInt32(4, this.hi, true); - return VIEW64.getBigUint64(0, true); + assertBi(BI); + BI.V.setInt32(0, this.lo, true); + BI.V.setInt32(4, this.hi, true); + return BI.V.getBigUint64(0, true); } } @@ -177,7 +228,7 @@ export class PbLong extends SharedPbLong { * Create instance from a `string`, `number` or `bigint`. */ static from(value: string | number | bigint): PbLong { - if (biCtor) + if (BI) // noinspection FallThroughInSwitchStatementJS switch (typeof value) { case "string": @@ -185,24 +236,24 @@ export class PbLong extends SharedPbLong { return this.ZERO; if (value == "") throw new Error('string is no integer'); - value = biCtor(value); + value = BI.C(value); case "number": if (value === 0) return this.ZERO; - value = biCtor(value); + value = BI.C(value); case "bigint": if (!value) return this.ZERO; - if (value < LONG_MIN!) + if (value < BI.MIN) throw new Error('ulong too small'); - if (value > LONG_MAX!) + if (value > BI.MAX) throw new Error('ulong too large'); - VIEW64.setBigInt64(0, value, true); + BI.V.setBigInt64(0, value, true); return new PbLong( - VIEW64.getInt32(0, true), - VIEW64.getInt32(4, true), + BI.V.getInt32(0, true), + BI.V.getInt32(4, true), ); } else @@ -254,8 +305,8 @@ export class PbLong extends SharedPbLong { * Convert to decimal string. */ toString() { - if (biCtor) - return this.toBigInt().toString() + if (BI) + return this.toBigInt().toString(); if (this.isNegative()) { let n = this.negate(); return '-' + int64toString(n.lo, n.hi); @@ -267,9 +318,10 @@ export class PbLong extends SharedPbLong { * Convert to native bigint. */ toBigInt(): bigint { - VIEW64.setInt32(0, this.lo, true); - VIEW64.setInt32(4, this.hi, true); - return VIEW64.getBigInt64(0, true); + assertBi(BI); + BI.V.setInt32(0, this.lo, true); + BI.V.setInt32(4, this.hi, true); + return BI.V.getBigInt64(0, true); } }