How to go between JS BigInts and TypedArrays
Published 2018-12-13We now have native BigInt
s in JavaScript (not just 64-bit integers,
but any arbitrary precision of integer with no pre-defined bit-width). Yay!
However, BigInts have a sordid past... and JavaScript has a sordid past,
so the new BigInt
primitives are shimmed in and both hold to old, unhelpful
JS conventions as well as break some new conventions because, well, it's just
how the cookie crumbled.
TL;DR:
For the impatient, this is what we're building up to:
bnToBuf(bn)
:
function bnToBuf(bn) {
var hex = BigInt(bn).toString(16);
if (hex.length % 2) { hex = '0' + hex; }
var len = hex.length / 2;
var u8 = new Uint8Array(len);
var i = 0;
var j = 0;
while (i < len) {
u8[i] = parseInt(hex.slice(j, j+2), 16);
i += 1;
j += 2;
}
return u8;
}
bufToBn(buf)
:
function bufToBn(buf) {
var hex = [];
u8 = Uint8Array.from(buf);
u8.forEach(function (i) {
var h = i.toString(16);
if (h.length % 2) { h = '0' + h; }
hex.push(h);
});
return BigInt('0x' + hex.join(''));
}
Usage:
bnToBuf('1234567890123456789012345678901234567890');
// Uint8Array [ 3, 160, 201, 32, 117, 192, 219, 243,
184, 172, 188, 95, 150, 206, 63, 10, 210 ]
bufToBn([ 3, 160, 201, 32, 117, 192, 219, 243,
184, 172, 188, 95, 150, 206, 63, 10, 210 ]);
// 1234567890123456789012345678901234567890n
And if you need to handle negative numbers, you'll need to take a look at How to Convert JS Signed BigInts to Hex
BigInt64Arrays are not exactly TypedArrays
When you're glancing over the almost non-existent documentation for BigInt
you'll likely
come across BigInt64Array
and BigUint64Array
, which are mostly unrelated.
As the name implies, these ints are not true BigInt
s, but rather capped at 64-bits wide.
However, they aren't just Number
s either.
Number
s can only survive normal bitwise
operations up to 31-bits. With a little triple-shift trickery they can be coaxed into
behaving well will a full 32 bits, but the full width 52-bit integers is not supported.
These BigInt64Array
elements will work with many bitwise operations
(though signed / negative numbers are still problematic for some operations).
Also, they're not compatible with other TypedArray
s.
This snippet, for example, won't work:
var bns = BigInt64Array.from([ BigInt(1), BigInt(2), BigInt(3), BigInt(4) ]);
var u8 = Uint8Array.from(bns);
It throws an exception.
And despite being able to access it's ArrayBuffer
you can't use that either:
var u8 = Uint8Array.from(bns.buffer);
However, it turns out that if you do it just right, you can get access to
the bytes by calling new
:
var u8 = new Uint8Array(bns.buffer);
Likewise you can ever-so-carefully convince it to go into a DataView
as well:
var dv = new DataView(bns.buffer);
Here's the rub:
Even then you're capped to 64-bits. You can't even convert from a BigInt
as you might suspect:
BigInt64Array.from(BigInt('12345678901234567890123456781234567783456')); // BigInt64Array []
BigInt64Array.from([ BigInt('12345678901234567890123456781234567783456') ]);
// BigInt64Array [ -4657930579192962016 ]
Converting from BigInts to Arrays/Buffers
So, ultimately, if you want to get a BigInt
that's larger than 64-bits wide into indidivual bytes,
you have to do it the old fashioned way - with hex (or binary strings):
function bnToBuf(bn) {
// The handy-dandy `toString(base)` works!!
var hex = BigInt(bn).toString(16);
// But it still follows the old behavior of giving
// invalid hex strings (due to missing padding),
// but we can easily add that back
if (hex.length % 2) { hex = '0' + hex; }
// The byteLength will be half of the hex string length
var len = hex.length / 2;
var u8 = new Uint8Array(len);
// And then we can iterate each element by one
// and each hex segment by two
var i = 0;
var j = 0;
while (i < len) {
u8[i] = parseInt(hex.slice(j, j+2), 16);
i += 1;
j += 2;
}
// Tada!!
return u8;
}
And thus we can handle integers of arbirary length in buffers, yay!
bnToBuf('1234567890123456789012345678901234567890')
// Uint8Array [ 3, 160, 201, 32, 117, 192, 219, 243,
184, 172, 188, 95, 150, 206, 63, 10, 210 ]
Converting an ArrayBuffer to BigInt
By comparison, this is reverse is super simple.
function bufToBn(buf) {
var hex = [];
u8 = Uint8Array.from(buf);
u8.forEach(function (i) {
var h = i.toString(16);
if (h.length % 2) { h = '0' + h; }
hex.push(h);
});
return BigInt('0x' + hex.join(''));
}
There aren't any real gotchas - unless you're working with signed BigInts, in which case you'll want to read up on that specifically.
I cover negative number encoding and decoding in the artciles on converting between hex and decimal listed below:
Looking for more?
If you found this useful, and you're interested in solving related problems, take a look at these as well:
- BigInts land in Chrome and node.js
- Convert Hex to Decimal strings with JS BigInts
- Convert Decimal strings to Hex with JS BigInts
- BigInts and Base64 in JavaScript
- A Primer on Big Int Encoding
References
- https://developers.google.com/web/updates/2018/05/bigint
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
By AJ ONeal
Did I make your day?
Buy me a coffee
(you can learn about the bigger picture I'm working towards on my patreon page )