diff --git a/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts b/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts index 364a1126da49..3ff0ef987279 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts @@ -6,6 +6,7 @@ import type { LayoutAnimationBatchItem, IReanimatedModule, IWorkletsModule, + WorkletFunction, } from '../commonTypes'; import { checkCppVersion } from '../platform-specific/checkCppVersion'; import { jsVersion } from '../platform-specific/jsVersion'; @@ -165,7 +166,7 @@ See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooti } subscribeForKeyboardEvents( - handler: ShareableRef, + handler: ShareableRef, isStatusBarTranslucent: boolean, isNavigationBarTranslucent: boolean ) { diff --git a/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts b/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts index f54963b6171e..e4fa3b8a008f 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts @@ -12,6 +12,7 @@ import type { ShareableRef, Value3D, ValueRotation, + WorkletFunction, } from '../../commonTypes'; import type { WebSensor } from './WebSensor'; import { mockedRequestAnimationFrame } from '../../mockedRequestAnimationFrame'; @@ -210,7 +211,7 @@ class JSReanimated implements IReanimatedModule { } } - subscribeForKeyboardEvents(_: ShareableRef): number { + subscribeForKeyboardEvents(_: ShareableRef): number { if (isWeb()) { logger.warn('useAnimatedKeyboard is not available on web yet.'); } else if (isJest()) { diff --git a/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts b/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts index cff70e423a70..5d334fb98979 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts @@ -6,6 +6,7 @@ import type { Value3D, ValueRotation, LayoutAnimationBatchItem, + WorkletFunction, } from '../commonTypes'; import type { WorkletRuntime } from '../runtimes'; @@ -53,7 +54,7 @@ export interface ReanimatedModuleProxy { configureProps(uiProps: string[], nativeProps: string[]): void; subscribeForKeyboardEvents( - handler: ShareableRef, + handler: ShareableRef, isStatusBarTranslucent: boolean, isNavigationBarTranslucent: boolean ): number; diff --git a/packages/react-native-reanimated/src/Sensor.ts b/packages/react-native-reanimated/src/Sensor.ts index af0c1d2f552d..64dc9dd6e926 100644 --- a/packages/react-native-reanimated/src/Sensor.ts +++ b/packages/react-native-reanimated/src/Sensor.ts @@ -6,6 +6,7 @@ import type { Value3D, ValueRotation, ShareableRef, + WorkletFunction, } from './commonTypes'; import { SensorType } from './commonTypes'; import { makeMutable } from './mutables'; @@ -56,7 +57,7 @@ export default class Sensor { sensorType, config.interval === 'auto' ? -1 : config.interval, config.iosReferenceFrame, - eventHandler + eventHandler as ShareableRef ); return this.sensorId !== -1; } diff --git a/packages/react-native-reanimated/src/commonTypes.ts b/packages/react-native-reanimated/src/commonTypes.ts index f50717436d8f..725436df1d89 100644 --- a/packages/react-native-reanimated/src/commonTypes.ts +++ b/packages/react-native-reanimated/src/commonTypes.ts @@ -327,10 +327,22 @@ interface WorkletBaseDev extends WorkletBaseCommon { __stackDetails?: WorkletStackDetails; } +export type WorkletFunctionDev< + Args extends unknown[] = unknown[], + ReturnValue = unknown, +> = ((...args: Args) => ReturnValue) & WorkletBaseDev; + +type WorkletFunctionRelease< + Args extends unknown[] = unknown[], + ReturnValue = unknown, +> = ((...args: Args) => ReturnValue) & WorkletBaseRelease; + export type WorkletFunction< Args extends unknown[] = unknown[], ReturnValue = unknown, -> = ((...args: Args) => ReturnValue) & (WorkletBaseRelease | WorkletBaseDev); +> = + | WorkletFunctionDev + | WorkletFunctionRelease; /** * This function allows you to determine if a given function is a worklet. It diff --git a/packages/react-native-reanimated/src/core.ts b/packages/react-native-reanimated/src/core.ts index dbe927731204..c913d948c455 100644 --- a/packages/react-native-reanimated/src/core.ts +++ b/packages/react-native-reanimated/src/core.ts @@ -9,6 +9,7 @@ import type { SharedValue, Value3D, ValueRotation, + WorkletFunction, } from './commonTypes'; import { makeShareableCloneRecursive } from './shareables'; import { initializeUIRuntime } from './initializers'; @@ -85,7 +86,9 @@ export function registerEventHandler( global.__frameTimestamp = undefined; } return ReanimatedModule.registerEventHandler( - makeShareableCloneRecursive(handleAndFlushAnimationFrame), + makeShareableCloneRecursive( + handleAndFlushAnimationFrame as WorkletFunction + ), eventName, emitterReactTag ); @@ -110,7 +113,9 @@ export function subscribeForKeyboardEvents( global.__frameTimestamp = undefined; } return ReanimatedModule.subscribeForKeyboardEvents( - makeShareableCloneRecursive(handleAndFlushAnimationFrame), + makeShareableCloneRecursive( + handleAndFlushAnimationFrame as WorkletFunction + ), options.isStatusBarTranslucentAndroid ?? false, options.isNavigationBarTranslucentAndroid ?? false ); @@ -132,7 +137,7 @@ export function registerSensor( return sensorContainer.registerSensor( sensorType, config, - makeShareableCloneRecursive(eventHandler) + makeShareableCloneRecursive(eventHandler as WorkletFunction) ); } diff --git a/packages/react-native-reanimated/src/shareables.ts b/packages/react-native-reanimated/src/shareables.ts index 23070fe7de99..ca99961133e9 100644 --- a/packages/react-native-reanimated/src/shareables.ts +++ b/packages/react-native-reanimated/src/shareables.ts @@ -4,6 +4,7 @@ import type { ShareableRef, FlatShareableRef, WorkletFunction, + WorkletFunctionDev, } from './commonTypes'; import { shouldBeUseWeb } from './PlatformChecker'; import { ReanimatedError, registerWorkletStackDetails } from './errors'; @@ -32,10 +33,19 @@ function isHostObject(value: NonNullable) { return MAGIC_KEY in value; } -function isPlainJSObject(object: object) { +function isPlainJSObject(object: object): object is Record { return Object.getPrototypeOf(object) === Object.prototype; } +function getFromCache(value: object) { + const cached = shareableMappingCache.get(value); + if (cached === shareableMappingFlag) { + // This means that `value` was already a clone and we should return it as is. + return value; + } + return cached; +} + // The below object is used as a replacement for objects that cannot be transferred // as shareable values. In makeShareableCloneRecursive we detect if an object is of // a plain Object.prototype and only allow such objects to be transferred. This lets @@ -100,14 +110,72 @@ const DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD = 30; // We use it to check if later on the function reenters with the same object let processedObjectAtThresholdDepth: unknown; -export function makeShareableCloneRecursive( - value: any, +function makeShareableCloneRecursiveWeb(value: T): ShareableRef { + return value as ShareableRef; +} + +function makeShareableCloneRecursiveNative( + value: T, shouldPersistRemote = false, depth = 0 ): ShareableRef { - if (SHOULD_BE_USE_WEB) { - return value; + detectCyclicObject(value, depth); + + const isObject = typeof value === 'object'; + const isFunction = typeof value === 'function'; + + if ((!isObject && !isFunction) || value === null) { + return clonePrimitive(value, shouldPersistRemote); + } + + const cached = getFromCache(value); + if (cached !== undefined) { + return cached as ShareableRef; + } + + if (Array.isArray(value)) { + return cloneArray(value, shouldPersistRemote, depth); + } + if (isFunction && !isWorkletFunction(value)) { + return cloneRemoteFunction(value, shouldPersistRemote); } + if (isHostObject(value)) { + return cloneHostObject(value, shouldPersistRemote); + } + if (isPlainJSObject(value) && value.__workletContextObjectFactory) { + return cloneContextObject(value); + } + if ((isPlainJSObject(value) || isFunction) && isWorkletFunction(value)) { + return cloneWorklet(value, shouldPersistRemote, depth); + } + if (isPlainJSObject(value) || isFunction) { + return clonePlainJSObject(value, shouldPersistRemote, depth); + } + if (value instanceof RegExp) { + return cloneRegExp(value); + } + if (value instanceof Error) { + return cloneError(value); + } + if (value instanceof ArrayBuffer) { + return cloneArrayBuffer(value, shouldPersistRemote); + } + if (ArrayBuffer.isView(value)) { + // typed array (e.g. Int32Array, Uint8ClampedArray) or DataView + return cloneArrayBufferView(value); + } + return inaccessibleObject(value); +} + +interface MakeShareableClone { + (value: T, shouldPersistRemote?: boolean, depth?: number): ShareableRef; +} + +export const makeShareableCloneRecursive: MakeShareableClone = SHOULD_BE_USE_WEB + ? makeShareableCloneRecursiveWeb + : makeShareableCloneRecursiveNative; + +function detectCyclicObject(value: unknown, depth: number) { if (depth >= DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) { // if we reach certain recursion depth we suspect that we are dealing with a cyclic object. // this type of objects are not supported and cannot be transferred as shareable, so we @@ -124,176 +192,243 @@ export function makeShareableCloneRecursive( } else { processedObjectAtThresholdDepth = undefined; } - // This one actually may be worth to be moved to c++, we also need similar logic to run on the UI thread - const type = typeof value; - const isTypeObject = type === 'object'; - const isTypeFunction = type === 'function'; - if ((isTypeObject || isTypeFunction) && value !== null) { - const cached = shareableMappingCache.get(value); - if (cached === shareableMappingFlag) { - return value; - } else if (cached !== undefined) { - return cached as ShareableRef; - } else { - let toAdapt: any; - if (Array.isArray(value)) { - toAdapt = value.map((element) => - makeShareableCloneRecursive(element, shouldPersistRemote, depth + 1) - ); - freezeObjectIfDev(value); - } else if (isTypeFunction && !isWorkletFunction(value)) { - // this is a remote function - toAdapt = value; - freezeObjectIfDev(value); - } else if (isHostObject(value)) { - // for host objects we pass the reference to the object as shareable and - // then recreate new host object wrapping the same instance on the UI thread. - // there is no point of iterating over keys as we do for regular objects. - toAdapt = value; - } else if ( - isPlainJSObject(value) && - value.__workletContextObjectFactory - ) { - const workletContextObjectFactory = value.__workletContextObjectFactory; - const handle = makeShareableCloneRecursive({ - __init: () => { - 'worklet'; - return workletContextObjectFactory(); - }, - }); - shareableMappingCache.set(value, handle); - return handle as ShareableRef; - } else if (isPlainJSObject(value) || isTypeFunction) { - toAdapt = {}; - if (isWorkletFunction(value)) { - if (__DEV__) { - const babelVersion = value.__initData.version; - if (babelVersion !== undefined && babelVersion !== jsVersion) { - throw new ReanimatedError(`Mismatch between JavaScript code version and Reanimated Babel plugin version (${jsVersion} vs. ${babelVersion}). +} + +function clonePrimitive( + value: T, + shouldPersistRemote: boolean +): ShareableRef { + return WorkletsModule.makeShareableClone(value, shouldPersistRemote); +} + +function cloneArray( + value: T, + shouldPersistRemote: boolean, + depth: number +): ShareableRef { + const clonedElements = value.map((element) => + makeShareableCloneRecursive(element, shouldPersistRemote, depth + 1) + ); + const clone = WorkletsModule.makeShareableClone( + clonedElements, + shouldPersistRemote + ) as ShareableRef; + shareableMappingCache.set(value, clone); + shareableMappingCache.set(clone); + + freezeObjectInDev(value); + return clone; +} + +function cloneRemoteFunction( + value: T, + shouldPersistRemote: boolean +): ShareableRef { + const clone = WorkletsModule.makeShareableClone(value, shouldPersistRemote); + shareableMappingCache.set(value, clone); + shareableMappingCache.set(clone); + + freezeObjectInDev(value); + return clone; +} + +function cloneHostObject( + value: T, + shouldPersistRemote: boolean +): ShareableRef { + // for host objects we pass the reference to the object as shareable and + // then recreate new host object wrapping the same instance on the UI thread. + // there is no point of iterating over keys as we do for regular objects. + const clone = WorkletsModule.makeShareableClone(value, shouldPersistRemote); + shareableMappingCache.set(value, clone); + shareableMappingCache.set(clone); + + return clone; +} + +function cloneWorklet( + value: T, + shouldPersistRemote: boolean, + depth: number +): ShareableRef { + if (__DEV__) { + const babelVersion = (value as WorkletFunctionDev).__initData.version; + if (babelVersion !== undefined && babelVersion !== jsVersion) { + throw new ReanimatedError(`[Reanimated] Mismatch between JavaScript code version and Reanimated Babel plugin version (${jsVersion} vs. ${babelVersion}). See \`https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#mismatch-between-javascript-code-version-and-reanimated-babel-plugin-version\` for more details. Offending code was: \`${getWorkletCode(value)}\``); - } - registerWorkletStackDetails( - value.__workletHash, - value.__stackDetails! - ); - } - if (value.__stackDetails) { - // `Error` type of value cannot be copied to the UI thread, so we - // remove it after we handled it in dev mode or delete it to ignore it in production mode. - // Not removing this would cause an infinite loop in production mode and it just - // seems more elegant to handle it this way. - delete value.__stackDetails; - } - // to save on transferring static __initData field of worklet structure - // we request shareable value to persist its UI counterpart. This means - // that the __initData field that contains long strings representing the - // worklet code, source map, and location, will always be - // serialized/deserialized once. - toAdapt.__initData = makeShareableCloneRecursive( - value.__initData, - true, - depth + 1 - ); - } - - for (const [key, element] of Object.entries(value)) { - if (key === '__initData' && toAdapt.__initData !== undefined) { - continue; - } - toAdapt[key] = makeShareableCloneRecursive( - element, - shouldPersistRemote, - depth + 1 - ); - } - freezeObjectIfDev(value); - } else if (value instanceof RegExp) { - const pattern = value.source; - const flags = value.flags; - const handle = makeShareableCloneRecursive({ - __init: () => { - 'worklet'; - return new RegExp(pattern, flags); - }, - }); - shareableMappingCache.set(value, handle); - return handle as ShareableRef; - } else if (value instanceof Error) { - const { name, message, stack } = value; - const handle = makeShareableCloneRecursive({ - __init: () => { - 'worklet'; - // eslint-disable-next-line reanimated/use-reanimated-error - const error = new Error(); - error.name = name; - error.message = message; - error.stack = stack; - return error; - }, - }); - shareableMappingCache.set(value, handle); - return handle as ShareableRef; - } else if (value instanceof ArrayBuffer) { - toAdapt = value; - } else if (ArrayBuffer.isView(value)) { - // typed array (e.g. Int32Array, Uint8ClampedArray) or DataView - const buffer = value.buffer; - const typeName = value.constructor.name; - const handle = makeShareableCloneRecursive({ - __init: () => { - 'worklet'; - if (!VALID_ARRAY_VIEWS_NAMES.includes(typeName)) { - throw new ReanimatedError( - `Invalid array view name \`${typeName}\`.` - ); - } - const constructor = global[typeName as keyof typeof global]; - if (constructor === undefined) { - throw new ReanimatedError( - `Constructor for \`${typeName}\` not found.` - ); - } - return new constructor(buffer); - }, - }); - shareableMappingCache.set(value, handle); - return handle as ShareableRef; - } else { - // This is reached for object types that are not of plain Object.prototype. - // We don't support such objects from being transferred as shareables to - // the UI runtime and hence we replace them with "inaccessible object" - // which is implemented as a Proxy object that throws on any attempt - // of accessing its fields. We argue that such objects can sometimes leak - // as attributes of objects being captured by worklets but should never - // be used on the UI runtime regardless. If they are being accessed, the user - // will get an appropriate error message. - const inaccessibleObject = - makeShareableCloneRecursive(INACCESSIBLE_OBJECT); - shareableMappingCache.set(value, inaccessibleObject); - return inaccessibleObject; - } - const adapted = WorkletsModule.makeShareableClone( - toAdapt, - shouldPersistRemote, - value - ); - shareableMappingCache.set(value, adapted); - shareableMappingCache.set(adapted); - return adapted; } + registerWorkletStackDetails( + value.__workletHash, + (value as WorkletFunctionDev).__stackDetails! + ); + } + if ((value as WorkletFunctionDev).__stackDetails) { + // `Error` type of value cannot be copied to the UI thread, so we + // remove it after we handled it in dev mode or delete it to ignore it in production mode. + // Not removing this would cause an infinite loop in production mode and it just + // seems more elegant to handle it this way. + delete (value as WorkletFunctionDev).__stackDetails; } - return WorkletsModule.makeShareableClone( - value, - shouldPersistRemote, - undefined + // to save on transferring static __initData field of worklet structure + // we request shareable value to persist its UI counterpart. This means + // that the __initData field that contains long strings represeting the + // worklet code, source map, and location, will always be + // serialized/deserialized once. + const clonedProps: Record = {}; + clonedProps.__initData = makeShareableCloneRecursive( + value.__initData, + true, + depth + 1 ); + + for (const [key, element] of Object.entries(value)) { + if (key === '__initData' && clonedProps.__initData !== undefined) { + continue; + } + clonedProps[key] = makeShareableCloneRecursive( + element, + shouldPersistRemote, + depth + 1 + ); + } + const clone = WorkletsModule.makeShareableClone( + clonedProps, + shouldPersistRemote + ) as ShareableRef; + shareableMappingCache.set(value, clone); + shareableMappingCache.set(clone); + + freezeObjectInDev(value); + return clone; +} + +function cloneContextObject(value: T): ShareableRef { + const workletContextObjectFactory = (value as Record) + .__workletContextObjectFactory as () => T; + const handle = makeShareableCloneRecursive({ + __init: () => { + 'worklet'; + return workletContextObjectFactory(); + }, + }); + shareableMappingCache.set(value, handle); + return handle as ShareableRef; +} + +function clonePlainJSObject( + value: T, + shouldPersistRemote: boolean, + depth: number +): ShareableRef { + const clonedProps: Record = {}; + for (const [key, element] of Object.entries(value)) { + if (key === '__initData' && clonedProps.__initData !== undefined) { + continue; + } + clonedProps[key] = makeShareableCloneRecursive( + element, + shouldPersistRemote, + depth + 1 + ); + } + const clone = WorkletsModule.makeShareableClone( + clonedProps, + shouldPersistRemote + ) as ShareableRef; + shareableMappingCache.set(value, clone); + shareableMappingCache.set(clone); + + freezeObjectInDev(value); + return clone; +} + +function cloneRegExp(value: T): ShareableRef { + const pattern = value.source; + const flags = value.flags; + const handle = makeShareableCloneRecursive({ + __init: () => { + 'worklet'; + return new RegExp(pattern, flags); + }, + }) as unknown as ShareableRef; + shareableMappingCache.set(value, handle); + + return handle; +} + +function cloneError(value: T): ShareableRef { + const { name, message, stack } = value; + const handle = makeShareableCloneRecursive({ + __init: () => { + 'worklet'; + // eslint-disable-next-line reanimated/use-reanimated-error + const error = new Error(); + error.name = name; + error.message = message; + error.stack = stack; + return error; + }, + }); + shareableMappingCache.set(value, handle); + return handle as unknown as ShareableRef; +} + +function cloneArrayBuffer( + value: T, + shouldPersistRemote: boolean +): ShareableRef { + const clone = WorkletsModule.makeShareableClone(value, shouldPersistRemote); + shareableMappingCache.set(value, clone); + shareableMappingCache.set(clone); + + return clone; +} + +function cloneArrayBufferView( + value: T +): ShareableRef { + const buffer = value.buffer; + const typeName = value.constructor.name; + const handle = makeShareableCloneRecursive({ + __init: () => { + 'worklet'; + if (!VALID_ARRAY_VIEWS_NAMES.includes(typeName)) { + throw new ReanimatedError( + `[Reanimated] Invalid array view name \`${typeName}\`.` + ); + } + const constructor = global[typeName as keyof typeof global]; + if (constructor === undefined) { + throw new ReanimatedError( + `[Reanimated] Constructor for \`${typeName}\` not found.` + ); + } + return new constructor(buffer); + }, + }) as unknown as ShareableRef; + shareableMappingCache.set(value, handle); + + return handle; +} + +function inaccessibleObject(value: T): ShareableRef { + // This is reached for object types that are not of plain Object.prototype. + // We don't support such objects from being transferred as shareables to + // the UI runtime and hence we replace them with "inaccessible object" + // which is implemented as a Proxy object that throws on any attempt + // of accessing its fields. We argue that such objects can sometimes leak + // as attributes of objects being captured by worklets but should never + // be used on the UI runtime regardless. If they are being accessed, the user + // will get an appropriate error message. + const clone = makeShareableCloneRecursive(INACCESSIBLE_OBJECT as T); + shareableMappingCache.set(value, clone); + return clone; } const WORKLET_CODE_THRESHOLD = 255; function getWorkletCode(value: WorkletFunction) { - // @ts-ignore this is fine const code = value?.__initData?.code; if (!code) { return 'unknown'; @@ -329,7 +464,7 @@ function isRemoteFunction(value: { * the UI thread. If the user really wants some objects to be mutable they * should use shared values instead. */ -function freezeObjectIfDev(value: T) { +function freezeObjectInDev(value: T) { if (!__DEV__) { return; }