BigInt on Tiny Hardware with XS

The JavaScript language is preparing to add built-in support for integers of arbitrary size with a new feature named BigInt. The built-in Number type is accurate to 53-bits of precision, which is insufficient for many tasks. By standardizing BigInt into the language, developers benefit from having a single API available in all environments and JavaScript engines can more easily optimize BigInt performance.

Moddable's XS JavaScript engine is focused on providing full JavaScript support on the smallest of devices, where memory and performance are limited. At first, BigInt might seem a poor fit for our goals with XS. However, the Moddable SDK has long provided equivalent functionality to BigInt in our Arith modules, which are part of our the cryptography libraries. We decided it would be worthwhile to explore implementing these using the new BigInt type.

Introducing BigInt

If you aren't yet familiar with BigInt, "BigInt: arbitrary-precision integers in JavaScript" by Mathias Bynens is excellent introduction. The ECMA-262 specification draft text for BigInt and an in-depth explainer by Daniel Ehrenberg are also available. At this writing, BigInt is at Stage 3 in the JavaScript language standardization process which means it is considered stable, but changes may be still be made prior to it becoming part of the JavaScript standard based on the experience of engine implementations.

Here's a simple example of using BigInt. BigInt literals have an "n" at the end to distinguish them from Number. That is because BigInt values cannot be mixed with the built-in JavaScript Number type. This restriction is in place to avoid problems that occur when performing operations that mix numeric types. That means the following code fails:

let big = 1000000000000000000000n;
big += 1;

The following succeeds because big is incremented by another BigInt.

let big = 1000000000000000000000n;
big += 1n;

The BigInt specification also extends TypedArray support with BigInt64Array and BigUint64Array. These allow efficient storage of arrays of 64-bit values. Values in the array are returned as BigInt values.

let a = new BigInt64Array(2);
a[0] = 123456781234567812345678n
a[1] = a[0] + 1n;

BigInt in XS

With today's release, XS fully implements the current draft of the BigInt specification. The implementation passes 100% of the BigInt tests in the test262 test suite used to measure conformance with the JavaScript language specification.

BigInt works on all targets. We've tested it on macOS, Windows, and Linux computers as well as ESP8266 and ESP32 microcontrollers. The only limit on the size of integers is available free memory.

The XS implementation of BigInt stores the BigInt values in the XS chunk heap, just as it stores strings. By storing BigInt values in the chunk heap, instead of in system memory, the BigInt values are automatically compacted during garbage collection which avoids memory fragmentation. This is important on microcontrollers with limited memory and no MMU.

BigInt support is useful in certain specialized scenarios. Still, many projects will not use it. The XS JavaScript engine's strip feature automatically eliminates many unused language features from the engine at build time. This means that the BigInt support in XS does not increase the size of your project unless you actually use it.

BigInt is supported in xsbug, so properties with BigInt values may be inspected. The values are displayed in hex notation rather than decimal.

Because BigInt is a new primitive type in JavaScript, the xsTypeOf macro in BigInt now can return xsBigIntType and xsBigIntXType (the latter is for BigInt values stored in read-only memory).

XS adds three functions to its BigInt implementation that are not part of the standard. These are not strictly necessary as they could be implemented from a script. To achieve better performance, it is beneficial to provide them in the engine.

  • BigInt.bitLength(value) -- a static function to return the bit length of a BigInt value. This functionality was considered for inclusion in the standard but deferred to a later release.
  • BigInt.fromArrayBuffer(buffer) -- a static function to convert an ArrayBuffer to a BigInt. This is used to deserialize BigInt values.
  • ArrayBuffer.fromBigInt(value) - a static function to convert a BigInt to an ArrayBuffer. This is used to serialize BigInt values.

The Moddable XS Test Engine (xst) binaries are up-to-date with the latest version of XS, and so now include BigInt support. The jsvu tool uses the xst binaries. Using jsvu you can compare the results of BigInt operations in XS, V8, and SpiderMonkey.

Use of BigInt in Moddable SDK

BigInt has fully replaced the previous "big number" support in the Moddable SDK used to implement the cryptography libraries. The result has been smaller and simpler code.

BigInt manages memory using the XS chunk heap, where as the "big number" module it replaces manages memory using a stack in the system heap. Both implementations are fast, but the BigInt approach is able to more aggressively garbage collect unused values, which can lower the overall memory requirements of some operations. For example, the TLS handshake, which is the most memory intensive part of establishing a TLS connection, has reduced its peak memory use by between 6 and 8 KB. That's a huge saving when you recall that this code is running on devices with as little as 45 KB of memory available to the JavaScript engine.

Looking Ahead

This initial implementation of BigInt has language conformance and size as priorities. The performance is as good or better than the Big Number implementation in the Moddable SDK that it replaces. Opportunities remain to optimize performance further.

The XS in C interface that bridges between JavaScript and native C code does not fully support BigInt yet. The primitives are in place and are used by the cryptographic implementation. The usual xs* macros in xs.h remain.

Conclusion

BigInt is another example of the JavaScript language expanding to standardize common functionality previously provided by scripts. Developers benefit from being able to use the same APIs across engines and environments, and engines have the potential to optimize the performance of these functions further. The XS engine continues to take on the challenge of providing these leading-edge JavaScript features on the smallest device. This gives developers working in embedded JavaScript a more powerful host to build their products on.