diff --git a/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/Shareables.cpp b/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/Shareables.cpp index f7862f14d117..2cd76457b7d8 100644 --- a/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/Shareables.cpp +++ b/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/Shareables.cpp @@ -1,4 +1,5 @@ #include +#include "WorkletStore.h" using namespace facebook; @@ -57,9 +58,11 @@ jsi::Value makeShareableClone( auto object = value.asObject(rt); jsi::PropNameID prop = workletCodePropName(rt); - if (object.hasProperty(rt, prop)) { - jsi::Value code = object.getProperty(rt, prop); - shareable = std::make_shared(code.asString(rt).utf8(rt)); + if (object.hasProperty(rt, prop)) { // Worklet function + auto code = object.getProperty(rt, prop).asString(rt).utf8(rt); + double hash = object.getProperty(rt,jsi::String::createFromUtf8(rt,"hash")).asNumber(); + WorkletStore::getInstance().set(hash, code); + shareable = std::make_shared(""); } else if (!object.getProperty(rt, "__workletHash").isUndefined()) { shareable = std::make_shared(rt, object); } else if (!object.getProperty(rt, "__init").isUndefined()) { diff --git a/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/WorkletStore.cpp b/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/WorkletStore.cpp new file mode 100644 index 000000000000..8c57cea5ba2d --- /dev/null +++ b/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/WorkletStore.cpp @@ -0,0 +1,70 @@ +// +// WorkletStore.cpp +// Pods +// +// Created by Alexander Pataridze on 29.03.25. +// + +#include "WorkletStore.h" // Include the header definition +#include // For potential use of std::move if needed + +// --- Singleton Implementation --- +// Provides the actual instance storage and retrieval logic. +// Meyers' Singleton pattern guarantees thread-safe initialization since C++11. +WorkletStore& WorkletStore::getInstance() { + // The 'static' variable is initialized only once, the first time this function is called. + static WorkletStore instance; + return instance; +} + +// --- Method Implementations --- + +void WorkletStore::set(double key, const std::string& value) { + // Acquire an exclusive lock that automatically releases when 'lock' goes out of scope. + std::lock_guard lock(storeMutex_); + // Insert or update the key-value pair in the map. + store_[key] = value; +} + +std::string WorkletStore::get(double key) const { + // Acquire a lock (needed even for reading to prevent data races with writes). + std::lock_guard lock(storeMutex_); + // Find the key in the map. + auto it = store_.find(key); + if (it != store_.end()) { + // Key found, return the value wrapped in std::optional. + return it->second; + } + // Key not found, return an empty std::optional. + return ""; +} + +bool WorkletStore::remove(double key) { + // Acquire an exclusive lock. + std::lock_guard lock(storeMutex_); + // Attempt to erase the key. std::unordered_map::erase returns the number + // of elements removed (0 or 1 for maps with unique keys). + return store_.erase(key) > 0; +} + +bool WorkletStore::contains(double key) const { + // Acquire a lock. + std::lock_guard lock(storeMutex_); + // Check if the key exists. map::count returns 1 if the key exists, 0 otherwise. + return store_.count(key) > 0; + // Alternative: return store_.find(key) != store_.end(); +} + +void WorkletStore::clear() { + // Acquire an exclusive lock. + std::lock_guard lock(storeMutex_); + // Remove all elements from the map. + store_.clear(); +} + +size_t WorkletStore::size() const { + // Acquire a lock. + std::lock_guard lock(storeMutex_); + // Return the current number of elements in the map. + return store_.size(); +} diff --git a/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/WorkletStore.h b/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/WorkletStore.h new file mode 100644 index 000000000000..151410b522b8 --- /dev/null +++ b/packages/react-native-reanimated/Common/cpp/worklets/SharedItems/WorkletStore.h @@ -0,0 +1,99 @@ +// +// WorkletMap.h +// Pods +// +// Created by Alexander Pataridze on 29.03.25. +// + +#pragma once // Prevents multiple inclusions of the header + +#include +#include // Efficient hash-based map +#include // For thread safety +#include // To safely return values that might not exist +#include // For potential read-write optimization (optional) + +// A thread-safe singleton class for storing key-value pairs (string -> string). +class WorkletStore { +public: + // --- Singleton Access --- + + /** + * @brief Gets the single instance of the WorkletStore. Thread-safe initialization. + * @return Reference to the WorkletStore instance. + */ + static WorkletStore& getInstance(); + + // --- Deleted Constructors/Assignments (Singleton Pattern) --- + // Prevent copying and moving to enforce single instance + WorkletStore(const WorkletStore&) = delete; + WorkletStore& operator=(const WorkletStore&) = delete; + WorkletStore(WorkletStore&&) = delete; + WorkletStore& operator=(WorkletStore&&) = delete; + + // --- Public Interface --- + + /** + * @brief Sets (inserts or updates) the value for a given key. Thread-safe. + * @param key The key to set. + * @param value The value to associate with the key. + */ + void set(double key, const std::string& value); + + /** + * @brief Gets the value associated with a given key. Thread-safe. + * @param key The key to look up. + * @return An std::optional containing the value if the key exists, + * otherwise std::nullopt. + */ + std::string get(double key) const; // const because it reads + + /** + * @brief Removes a key-value pair from the store. Thread-safe. + * @param key The key to remove. + * @return true if an element was removed, false otherwise. + */ + bool remove(double key); + + /** + * @brief Checks if the store contains a specific key. Thread-safe. + * @param key The key to check for. + * @return true if the key exists, false otherwise. + */ + bool contains(double key) const; // const because it reads + + /** + * @brief Removes all key-value pairs from the store. Thread-safe. + */ + void clear(); + + /** + * @brief Gets the number of key-value pairs currently in the store. Thread-safe. + * @return The number of elements. + */ + size_t size() const; // const because it reads + +private: + // --- Private Members --- + + // Private constructor: enforce singleton access via getInstance() + WorkletStore() = default; + + // Private destructor (can be defaulted if no special cleanup needed) + ~WorkletStore() = default; + + // The underlying map storing the data + std::unordered_map store_; + + // Mutex to protect access to the store_ map from concurrent threads. + // 'mutable' allows locking even in 'const' methods like get() and contains(). + mutable std::mutex storeMutex_; + + // --- Optional Optimization (Advanced) --- + // For high-contention scenarios with many more reads than writes, + // a std::shared_mutex can sometimes offer better performance. + // mutable std::shared_mutex storeSharedMutex_; + // Use std::lock_guard for writes (exclusive lock). + // Use std::shared_lock for reads (shared lock). + // For simplicity, we'll stick with the standard std::mutex here. +}; diff --git a/packages/react-native-reanimated/Common/cpp/worklets/WorkletRuntime/ReanimatedHermesRuntime.cpp b/packages/react-native-reanimated/Common/cpp/worklets/WorkletRuntime/ReanimatedHermesRuntime.cpp index f7b7b4127855..c9bbe371b861 100644 --- a/packages/react-native-reanimated/Common/cpp/worklets/WorkletRuntime/ReanimatedHermesRuntime.cpp +++ b/packages/react-native-reanimated/Common/cpp/worklets/WorkletRuntime/ReanimatedHermesRuntime.cpp @@ -1,4 +1,5 @@ #include +#include // Only include this file in Hermes-enabled builds as some platforms (like tvOS) // don't support hermes and it causes the compilation to fail. @@ -80,34 +81,33 @@ ReanimatedHermesRuntime::ReanimatedHermesRuntime( jsQueue->quitSynchronous(); #endif // HERMES_ENABLE_DEBUGGER -#ifndef NDEBUG - facebook::hermes::HermesRuntime *wrappedRuntime = runtime_.get(); - jsi::Value evalWithSourceMap = jsi::Function::createFromHostFunction( - *runtime_, - jsi::PropNameID::forAscii(*runtime_, "evalWithSourceMap"), - 3, - [wrappedRuntime]( - jsi::Runtime &rt, - const jsi::Value &thisValue, - const jsi::Value *args, - size_t count) -> jsi::Value { - auto code = std::make_shared( - args[0].asString(rt).utf8(rt)); - std::string sourceURL; - if (count > 1 && args[1].isString()) { - sourceURL = args[1].asString(rt).utf8(rt); - } - std::shared_ptr sourceMap; - if (count > 2 && args[2].isString()) { - sourceMap = std::make_shared( - args[2].asString(rt).utf8(rt)); - } - return wrappedRuntime->evaluateJavaScriptWithSourceMap( - code, sourceMap, sourceURL); - }); - runtime_->global().setProperty( - *runtime_, "evalWithSourceMap", evalWithSourceMap); -#endif // NDEBUG + +facebook::hermes::HermesRuntime *wrappedRuntime = runtime_.get(); +jsi::Value evalFromHashValue = jsi::Function::createFromHostFunction( + *runtime_, + jsi::PropNameID::forAscii(*runtime_, "evalFromHashValue"), + 3, + [wrappedRuntime]( + jsi::Runtime &rt, + const jsi::Value &thisValue, + const jsi::Value *args, + size_t count) -> jsi::Value { + auto code = std::make_shared("("+WorkletStore::getInstance().get(args[0].asNumber())+"\n)"); + + std::string sourceURL; + if (count > 1 && args[1].isString()) { + sourceURL = args[1].asString(rt).utf8(rt); + } + std::shared_ptr sourceMap; + if (count > 2 && args[2].isString()) { + sourceMap = std::make_shared( + args[2].asString(rt).utf8(rt)); + } + return wrappedRuntime->evaluateJavaScriptWithSourceMap( + code, sourceMap, sourceURL); + }); +runtime_->global().setProperty( + *runtime_, "evalFromHashValue", evalFromHashValue); } ReanimatedHermesRuntime::~ReanimatedHermesRuntime() { diff --git a/packages/react-native-reanimated/Common/cpp/worklets/WorkletRuntime/WorkletRuntime.cpp b/packages/react-native-reanimated/Common/cpp/worklets/WorkletRuntime/WorkletRuntime.cpp index 057b73544a2d..d93d8877f894 100644 --- a/packages/react-native-reanimated/Common/cpp/worklets/WorkletRuntime/WorkletRuntime.cpp +++ b/packages/react-native-reanimated/Common/cpp/worklets/WorkletRuntime/WorkletRuntime.cpp @@ -51,11 +51,6 @@ static std::shared_ptr makeRuntime( } } - -std::string getJsValueUnpackerString() { - return "(function valueUnpacker_Exodus_valueUnpackerJs1(objectToUnpack,category,remoteFunctionName){let workletsCache=global.__workletsCache;let handleCache=global.__handleCache;if(workletsCache===undefined){workletsCache=global.__workletsCache=new Map();handleCache=global.__handleCache=new WeakMap();}const workletHash=objectToUnpack.__workletHash;if(workletHash!==undefined){let workletFun=workletsCache.get(workletHash);if(workletFun===undefined){const initData=objectToUnpack.__initData;if(global.evalWithSourceMap){workletFun=global.evalWithSourceMap('('+initData.__reanimated_workletCodeWrapper+'\\n)',initData.location,initData.sourceMap);}else if(global.evalWithSourceUrl){workletFun=global.evalWithSourceUrl('('+initData.__reanimated_workletCodeWrapper+'\\n)',\"worklet_\"+workletHash);}else{workletFun=eval('('+initData.__reanimated_workletCodeWrapper+'\\n)');}workletsCache.set(workletHash,workletFun);}const functionInstance=workletFun.bind(objectToUnpack);objectToUnpack._recur=functionInstance;return functionInstance;}else if(objectToUnpack.__init!==undefined){let value=handleCache.get(objectToUnpack);if(value===undefined){value=objectToUnpack.__init();handleCache.set(objectToUnpack,value);}return value;}else if(category==='RemoteFunction'){const fun=function(){const label=remoteFunctionName?\"function \"+remoteFunctionName:'anonymous function';throw new Error(\"[Reanimated] Tried to synchronously call a non-worklet \"+label+\" on the UI thread.\\nSee https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-synchronously-call-a-non-worklet-function-on-the-ui-thread for more details.\");};fun.__remoteFunction=objectToUnpack;return fun;}else{throw new Error(\"[Reanimated] Data type in category \"+category+\" not recognized by value unpacker: \"+_toString(objectToUnpack)+\".\");}}\n)"; -} - WorkletRuntime::WorkletRuntime( jsi::Runtime &rnRuntime, const std::shared_ptr &jsQueue, @@ -78,7 +73,7 @@ WorkletRuntime::WorkletRuntime( WorkletRuntimeCollector::install(rt); WorkletRuntimeDecorator::decorate(rt, name, jsScheduler); - auto codeBuffer = std::make_shared(getJsValueUnpackerString()); + auto codeBuffer = std::make_shared("(" + valueUnpackerCode + "\n)"); auto valueUnpacker = rt.evaluateJavaScript(codeBuffer, "valueUnpacker") .asObject(rt) .asFunction(rt); diff --git a/packages/react-native-reanimated/plugin/index.js b/packages/react-native-reanimated/plugin/index.js index 7326d458638e..7502a972827d 100644 --- a/packages/react-native-reanimated/plugin/index.js +++ b/packages/react-native-reanimated/plugin/index.js @@ -461,9 +461,10 @@ var require_workletFactory = __commonJS({ const initDataId = pathForStringDefinitions.parentPath.scope.generateUidIdentifier(`worklet_${workletHash}_init_data`); const symbolForWorkletCode = (0, types_12.callExpression)((0, types_12.memberExpression)((0, types_12.identifier)("Symbol"), (0, types_12.identifier)("for")), [(0, types_12.stringLiteral)("__reanimated_workletCode")]); const workletCodeProperty = (0, types_12.objectProperty)(symbolForWorkletCode, (0, types_12.stringLiteral)(funString), true); - const reanimatedSecretCodeProperty = (0, types_12.objectProperty)((0, types_12.identifier)("__reanimated_workletCodeWrapper"), (0, types_12.objectExpression)([workletCodeProperty])); + const workletHashProperty = (0, types_12.objectProperty)((0, types_12.stringLiteral)("hash"), (0, types_12.numericLiteral)(workletHash)); + const reanimatedWorkletCodeProperty = (0, types_12.objectProperty)((0, types_12.identifier)("__reanimated_workletCodeWrapper"), (0, types_12.objectExpression)([workletCodeProperty, workletHashProperty])); const initDataObjectExpression = (0, types_12.objectExpression)([ - reanimatedSecretCodeProperty + reanimatedWorkletCodeProperty ]); const shouldInjectLocation = !(0, utils_1.isRelease)(); if (shouldInjectLocation) { diff --git a/packages/react-native-reanimated/plugin/src/workletFactory.ts b/packages/react-native-reanimated/plugin/src/workletFactory.ts index 465fa575c70b..f6976ed552f8 100644 --- a/packages/react-native-reanimated/plugin/src/workletFactory.ts +++ b/packages/react-native-reanimated/plugin/src/workletFactory.ts @@ -153,13 +153,18 @@ export function makeWorkletFactory( true ); - const reanimatedSecretCodeProperty = objectProperty( + const workletHashProperty = objectProperty( + stringLiteral('hash'), + numericLiteral(workletHash) + ); + + const reanimatedWorkletCodeProperty = objectProperty( identifier('__reanimated_workletCodeWrapper'), - objectExpression([workletCodeProperty]) + objectExpression([workletCodeProperty, workletHashProperty]) ); const initDataObjectExpression = objectExpression([ - reanimatedSecretCodeProperty, + reanimatedWorkletCodeProperty, ]); // When testing with jest I noticed that environment variables are set later diff --git a/packages/react-native-reanimated/src/NativeReanimated/NativeReanimated.ts b/packages/react-native-reanimated/src/NativeReanimated/NativeReanimated.ts index 437490906062..f577fb3193bb 100644 --- a/packages/react-native-reanimated/src/NativeReanimated/NativeReanimated.ts +++ b/packages/react-native-reanimated/src/NativeReanimated/NativeReanimated.ts @@ -7,6 +7,7 @@ import type { } from '../commonTypes'; import { jsVersion } from '../platform-specific/jsVersion'; import type { WorkletRuntime } from '../runtimes'; +import { getValueUnpackerCode } from '../valueUnpacker'; import { isFabric } from '../PlatformChecker'; import type React from 'react'; import { getShadowNodeWrapperFromRef } from '../fabricUtils'; @@ -85,7 +86,8 @@ export class NativeReanimated { } global._REANIMATED_VERSION_JS = jsVersion; if (global.__reanimatedModuleProxy === undefined) { - ReanimatedModule?.installTurboModule(''); + const valueUnpackerCode = getValueUnpackerCode(); + ReanimatedModule?.installTurboModule(valueUnpackerCode); } if (global.__reanimatedModuleProxy === undefined) { throw new ReanimatedError( diff --git a/packages/react-native-reanimated/src/privateGlobals.d.ts b/packages/react-native-reanimated/src/privateGlobals.d.ts index cccf3af9d36e..07b83b50a68c 100644 --- a/packages/react-native-reanimated/src/privateGlobals.d.ts +++ b/packages/react-native-reanimated/src/privateGlobals.d.ts @@ -32,9 +32,11 @@ declare global { var _REANIMATED_VERSION_JS: string | undefined; var __reanimatedModuleProxy: NativeReanimatedModule | undefined; var __callGuardDEV: typeof callGuardDEV | undefined; - var evalWithSourceMap: - | ((js: string, sourceURL: string, sourceMap: string) => any) - | undefined; + var evalFromHashValue: ( + js: string, + sourceURL?: string, + sourceMap?: string + ) => any; var evalWithSourceUrl: ((js: string, sourceURL: string) => any) | undefined; var _log: (value: unknown) => void; var _toString: (value: unknown) => string; diff --git a/packages/react-native-reanimated/src/valueUnpacker.ts b/packages/react-native-reanimated/src/valueUnpacker.ts new file mode 100644 index 000000000000..771eae144716 --- /dev/null +++ b/packages/react-native-reanimated/src/valueUnpacker.ts @@ -0,0 +1,51 @@ +/* eslint-disable reanimated/use-reanimated-error */ +'use strict'; + +const valueUnpackerCode = ` +function valueUnpacker(objectToUnpack, category, remoteFunctionName) { + 'worklet'; + + let workletsCache = global.__workletsCache; + let handleCache = global.__handleCache; + if (workletsCache === undefined) { + // init + workletsCache = global.__workletsCache = new Map(); + handleCache = global.__handleCache = new WeakMap(); + } + const workletHash = objectToUnpack.__workletHash; + if (workletHash !== undefined) { + let workletFun = workletsCache.get(workletHash); + if (workletFun === undefined) { + const initData = objectToUnpack.__initData; + if (initData.location && initData.sourceMap) { + workletFun = global.evalFromHashValue(workletHash, initData.location, initData.sourceMap); + }else { + workletFun = global.evalFromHashValue(workletHash); + } + workletsCache.set(workletHash, workletFun); + } + const functionInstance = workletFun.bind(objectToUnpack); + objectToUnpack._recur = functionInstance; + return functionInstance; + } else if (objectToUnpack.__init !== undefined) { + let value = handleCache.get(objectToUnpack); + if (value === undefined) { + value = objectToUnpack.__init(); + handleCache.set(objectToUnpack, value); + } + return value; + } else if (category === 'RemoteFunction') { + const fun = () => { + const label = remoteFunctionName ? "function " + remoteFunctionName : "anonymous function"; + throw new Error("[Reanimated] Tried to synchronously call a non-worklet" + label + " on the UI thread. See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-synchronously-call-a-non-worklet-function-on-the-ui-thread for more details."); + }; + fun.__remoteFunction = objectToUnpack; + return fun; + } else { + throw new Error("[Reanimated] Data type in category " + category + " not recognized by value unpacker: " + _toString(objectToUnpack) + "."); + } +}`; + +export function getValueUnpackerCode() { + return valueUnpackerCode; +}