Skip to content

Commit

Permalink
Merge pull request #25 from timostamm/safari-bigint-fix
Browse files Browse the repository at this point in the history
Fix Safari 14 bigint detection
  • Loading branch information
timostamm authored Nov 13, 2020
2 parents 03507f4 + 7adbb2b commit 2cbed16
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 58 deletions.
83 changes: 61 additions & 22 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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();
```




Expand Down Expand Up @@ -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`,
Expand Down
124 changes: 88 additions & 36 deletions packages/runtime/src/pb-long.ts
Original file line number Diff line number Diff line change
@@ -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]+$/;
Expand Down Expand Up @@ -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) {

Expand All @@ -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),
);

}
Expand Down Expand Up @@ -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);
}

}
Expand All @@ -177,32 +228,32 @@ 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":
if (value == "0")
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
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

}

0 comments on commit 2cbed16

Please sign in to comment.