@exodus/patch-broken-hermes-typed-arrays
Version:
Fix broken Hermes engine TypedArray implementation for React Native
142 lines (116 loc) • 6.75 kB
JavaScript
// We do not attempt to use any functionality here that is not natively supported on Hermes, including
// block-scoped vars -- Hermes fakes them, let/const is just an alias for vsr there and break expectations
// This method is designed start throwing on an engine update that can cause it to stop working or to break something
// That is a safeguard to ensure that things don't end up in a more broken state than they were initially
// This doesn't work with ECMAScript 2024 resizable ArrayBuffers, but at the moment Hermes does not support them
// We detect ArrayBuffer.prototype.resize and stop in that case. This might be fixed at need
// This also is not designed to work with Symbol.species support, but at the moment Hermes doesn't support that, too,
// and Symbol.species might even end up being dropped from spec
;(function fixHermesTypedArrayBug() {
'use strict'
var areWeBroken = function () {
var called = 0
var ok = true
var TestArray = function (...args) {
called++
var buf = new Uint8Array(...args)
Object.setPrototypeOf(buf, TestArray.prototype)
return buf
}
Object.setPrototypeOf(TestArray.prototype, Uint8Array.prototype)
Object.setPrototypeOf(TestArray, Uint8Array)
var arr = new TestArray(1)
ok &&= called === 1
if (arr.subarray(0).constructor !== TestArray) ok = false
ok &&= called === 2
if (arr.map(() => 1).constructor !== TestArray) ok = false
ok &&= called === 3
if (arr.filter(() => true).constructor !== TestArray) ok = false
ok &&= called === 4
if (arr.slice(0).constructor !== TestArray) ok = false
ok &&= called === 5
if (ok) {
// These are expected to be fine, re-check if others are ok,
// but don't increase `called` in shouldPatch
if (TestArray.of(1, 2).constructor !== TestArray) ok = false
ok &&= called === 6
if (TestArray.from([0]).constructor !== TestArray) ok = false
ok &&= called === 7
}
var broken = !ok
// If `called` is of unexpected value for brokenness -- we can't be sure what is happening
// For Symbol.species and ArrayBuffer.prototype.resize checks, see patch logic for explanation
var shouldPatch = broken && called === 1 && !Symbol.species && !ArrayBuffer.prototype.resize
return { broken, shouldPatch }
}
var { broken, shouldPatch } = areWeBroken()
if (!broken) return
var prefix = '[@exodus/patch-broken-hermes-typed-arrays] TypedArray support looks broken, '
var reportNotice =
' Report this to https://github.com/ExodusMovement/patch-broken-hermes-typed-arrays/issues'
if (!shouldPatch) throw new Error(`${prefix}but we could not fix it.${reportNotice}`)
// Note: does not follow %Symbol.species%, but Hermes doesn't support it anyway
// Refs: https://tc39.es/ecma262/2024/#sec-get-%typedarray%-%symbol.species%
var TypedArray = Object.getPrototypeOf(Uint8Array)
var { subarray, map, filter, slice } = TypedArray.prototype
// This conforms to 2023 edition, but not 2024 edition with resizable ArrayBuffer instances
// This is why we are not safe if an engine (1) has broken TypedArrays and (2) implements ArrayBuffer.prototype.resize
// The behavior is identical though if the underlying ArrayBuffer is not a resizable one
// Refs: https://tc39.es/ecma262/2024/#sec-%typedarray%.prototype.subarray, steps 15-17
// NOTE: step 15 from 2024 edition is ignored and we don't call a 1-argument version
// 17. Return ? TypedArraySpeciesCreate(O, argumentsList).
// Refs: https://tc39.es/ecma262/2023/#sec-%typedarray%.prototype.subarray, steps 18-19
// 18. Let argumentsList be « buffer, 𝔽(beginByteOffset), 𝔽(newLength) ».
// 19. Return ? TypedArraySpeciesCreate(O, argumentsList).
TypedArray.prototype.subarray = function (...args) {
var arr = subarray.apply(this, args)
if (!this.constructor || arr.constructor === this.constructor) return arr
return new this.constructor(arr.buffer, arr.byteOffset, arr.length)
}
var callTypedArrayCreateCopyWithSizeFromTypedArray = function (instance, typed) {
if (!instance.constructor || instance.constructor === typed.constructor) return typed
var { constructor } = instance
// Fast path, non-spec hack for 'buffer' from https://www.npmjs.com/package/buffer
if (
instance._isBuffer &&
constructor.name === 'Buffer' &&
Object.hasOwn(constructor, 'TYPED_ARRAY_SUPPORT') && // should not be inherited
constructor.TYPED_ARRAY_SUPPORT === true
) {
// We are already operating on a just-created copy in `typed`, so we can avoid copying as long
// as the child implementation is fine with us calling a different version of the constructor
// than expected per spec
// We had to double-check that this is not something inheriting Buffer though
return new constructor(typed.buffer, typed.byteOffset, typed.length)
}
// Copies but this is the only proper way to call the constructor per spec here
var A = new constructor(typed.length)
var n
for (n = 0; n < typed.length; n++) A['' + n] = typed[n]
return A
}
// Refs: https://tc39.es/ecma262/2024/#sec-%typedarray%.prototype.map, step 5
// 5. Let A be ? TypedArraySpeciesCreate(O, « 𝔽(len) »).
TypedArray.prototype.map = function (...args) {
return callTypedArrayCreateCopyWithSizeFromTypedArray(this, map.apply(this, args))
}
// Refs: https://tc39.es/ecma262/2024/#sec-%typedarray%.prototype.filter, step 9
// 9. Let A be ? TypedArraySpeciesCreate(O, « 𝔽(captured) »).
TypedArray.prototype.filter = function (...args) {
return callTypedArrayCreateCopyWithSizeFromTypedArray(this, filter.apply(this, args))
}
// Refs: https://tc39.es/ecma262/2024/#sec-%typedarray%.prototype.slice, step 13
// 13. Let A be ? TypedArraySpeciesCreate(O, « 𝔽(countBytes) »).
TypedArray.prototype.slice = function (...args) {
// https://www.npmjs.com/package/buffer overrides this method, but let's still use a fast path
// TypedArray.prototype.slice can still be called on Buffer:
// e.g. Uint8Array.prototype.slice.call(Buffer.alloc(10), 2)
// should return an instance of a child class (i.e. Buffer in this example)
// _isBuffer fast path is included in the following call
return callTypedArrayCreateCopyWithSizeFromTypedArray(this, slice.apply(this, args))
}
// The four above methods cover all TypedArraySpeciesCreate calls in the spec for ECMAScript 2024
// TypedArray.from and TypedArray.of which call TypedArrayCreateFromConstructor are fine
// We recheck that just in case though
if (areWeBroken().broken) throw new Error(`${prefix}and patch failed to fix it!${reportNotice}`)
})()