From 40352bb2c8545a5831873a51b05b24999c545f7d Mon Sep 17 00:00:00 2001 From: azim Date: Sun, 8 Dec 2024 21:08:36 +0500 Subject: [PATCH] chore: fix crash due to invalid component reference --- cpp/module/SLModuleSpec/SLCommitHook.cpp | 111 +-------------------- cpp/module/SLModuleSpec/SLKeyExtractor.cpp | 50 ++++++++++ cpp/module/SLModuleSpec/SLKeyExtractor.h | 13 +++ cpp/module/SLModuleSpec/SLModuleJSI.cpp | 74 ++++++++++++++ cpp/module/SLModuleSpec/SLModuleJSI.h | 10 ++ cpp/module/SLModuleSpec/SLTemplate.cpp | 61 +++++++++++ cpp/module/SLModuleSpec/SLTemplate.h | 16 +++ example/src/App.tsx | 2 +- ios/SLModule.mm | 65 +----------- src/SLContainer.tsx | 37 +++++-- src/SLContainerNativeComponent.ts | 5 + src/SLElement.tsx | 37 +++++-- src/SLElementNativeComponent.ts | 5 + src/Shadowlist.tsx | 9 +- 14 files changed, 305 insertions(+), 190 deletions(-) create mode 100644 cpp/module/SLModuleSpec/SLKeyExtractor.cpp create mode 100644 cpp/module/SLModuleSpec/SLKeyExtractor.h create mode 100644 cpp/module/SLModuleSpec/SLModuleJSI.cpp create mode 100644 cpp/module/SLModuleSpec/SLModuleJSI.h create mode 100644 cpp/module/SLModuleSpec/SLTemplate.cpp create mode 100644 cpp/module/SLModuleSpec/SLTemplate.h diff --git a/cpp/module/SLModuleSpec/SLCommitHook.cpp b/cpp/module/SLModuleSpec/SLCommitHook.cpp index 978ceec..af62a7b 100644 --- a/cpp/module/SLModuleSpec/SLCommitHook.cpp +++ b/cpp/module/SLModuleSpec/SLCommitHook.cpp @@ -3,117 +3,12 @@ #include "SLCommitHook.h" #include "SLContainerShadowNode.h" #include "SLElementShadowNode.h" - +#include "SLTemplate.h" using namespace facebook::react; namespace facebook::react { -int nextFamilyTag = -2; - -class KeyExtractor { -public: - static std::optional extractKey(const std::string& input) { - if (input.length() < 4) { - return std::nullopt; - } - - if (input.substr(0, 2) != "{{" || input.substr(input.length() - 2) != "}}") { - return std::nullopt; - } - - std::string key = input.substr(2, input.length() - 4); - - if (key.find('{') != std::string::npos || key.find('}') != std::string::npos) { - return std::nullopt; - } - - if (key.empty()) { - return std::nullopt; - } - - return key; - } - - static std::vector extractAllKeys(const std::string& input) { - std::vector keys; - size_t pos = 0; - - while (pos < input.length()) { - size_t start = input.find("{{", pos); - if (start == std::string::npos) break; - - size_t end = input.find("}}", start); - if (end == std::string::npos) break; - - std::string potential_key = input.substr(start, end - start + 2); - auto key = extractKey(potential_key); - - if (key) { - keys.push_back(*key); - } - - pos = end + 2; - } - - return keys; - } -}; - -auto adjustFamilyTag = [](int tag) { - const int MIN_TAG_VALUE = -2e9; - const int CLAMPED_TAG = -2; - return tag < MIN_TAG_VALUE ? CLAMPED_TAG : tag - 2; -}; - -ShadowNode::Shared cloneShadowNodeTree(jsi::Runtime *runtime, nlohmann::json* elementData, const ShadowNode::Shared& shadowNode) -{ - auto const &componentDescriptor = shadowNode->getComponentDescriptor(); - PropsParserContext propsParserContext{shadowNode->getSurfaceId(), *componentDescriptor.getContextContainer().get()}; - - nextFamilyTag = adjustFamilyTag(nextFamilyTag); - - InstanceHandle::Shared instanceHandle = std::make_shared( - *runtime, - shadowNode->getInstanceHandle(*runtime), - shadowNode->getTag()); - auto const fragment = ShadowNodeFamilyFragment{nextFamilyTag, shadowNode->getSurfaceId(), instanceHandle}; - auto const family = componentDescriptor.createFamily(fragment); - auto const props = componentDescriptor.cloneProps(propsParserContext, shadowNode->getProps(), {}); - auto const state = componentDescriptor.createInitialState(props, family); - auto const nextShadowNode = componentDescriptor.createShadowNode( - ShadowNodeFragment{props, ShadowNodeFragment::childrenPlaceholder(), state}, family); - - RawTextShadowNode::ConcreteProps* interpolatedProps; - if (shadowNode->getComponentName() == std::string("RawText")) { - const Props::Shared& nextprops = nextShadowNode->getProps(); - interpolatedProps = const_cast( - static_cast(nextprops.get()) - ); - - try { - if (!KeyExtractor::extractKey(interpolatedProps->text).has_value()) { - throw false; - } - - auto elementDataPointer = nlohmann::json::json_pointer( - "/" + KeyExtractor::extractKey(interpolatedProps->text).value() - ); - - if ((*elementData).contains(elementDataPointer) && (*elementData)[elementDataPointer].is_string()) { - interpolatedProps->text = (*elementData)[elementDataPointer].get(); - } - } catch (...) {} - } - - for (const auto &childShadowNode : shadowNode->getChildren()) { - auto const clonedChildShadowNode = cloneShadowNodeTree(runtime, elementData, childShadowNode); - componentDescriptor.appendChild(nextShadowNode, clonedChildShadowNode); - } - - return nextShadowNode; -} - class SLYogaLayoutableShadowNode : public facebook::react::YogaLayoutableShadowNode { public: facebook::yoga::Node& getYogaNode() { @@ -171,7 +66,9 @@ RootShadowNode::Unshared SLCommitHook::shadowTreeWillCommit( for (int i = 0; i < (*containerData).size(); ++i) { auto elementDataPointer = nlohmann::json::json_pointer("/" + std::to_string(i)); auto* elementData = &(*containerData)[elementDataPointer]; - rootShadowNodeChildren->push_back(cloneShadowNodeTree(runtime_, elementData, elementNode.second)); + rootShadowNodeChildren->push_back( + SLTemplate::cloneShadowNodeTree(runtime_, elementData, elementNode.second) + ); } } else { // rootShadowNodeChildren->push_back(cloneShadowNodeTree(runtime_, elementData, elementNode.second)); diff --git a/cpp/module/SLModuleSpec/SLKeyExtractor.cpp b/cpp/module/SLModuleSpec/SLKeyExtractor.cpp new file mode 100644 index 0000000..6daf7c0 --- /dev/null +++ b/cpp/module/SLModuleSpec/SLKeyExtractor.cpp @@ -0,0 +1,50 @@ +#include "SLKeyExtractor.h" + +namespace facebook::react { + +std::optional SLKeyExtractor::extractKey(const std::string& input) { + if (input.length() < 4) { + return std::nullopt; + } + + if (input.substr(0, 2) != "{{" || input.substr(input.length() - 2) != "}}") { + return std::nullopt; + } + + std::string key = input.substr(2, input.length() - 4); + + if (key.find('{') != std::string::npos || key.find('}') != std::string::npos) { + return std::nullopt; + } + + if (key.empty()) { + return std::nullopt; + } + + return key; +} + +std::vector SLKeyExtractor::extractAllKeys(const std::string& input) { + std::vector keys; + size_t pos = 0; + + while (pos < input.length()) { + size_t start = input.find("{{", pos); + if (start == std::string::npos) break; + + size_t end = input.find("}}", start); + if (end == std::string::npos) break; + + std::string potential_key = input.substr(start, end - start + 2); + auto key = extractKey(potential_key); + + if (key) { + keys.push_back(*key); + } + + pos = end + 2; + } + + return keys; +} +} diff --git a/cpp/module/SLModuleSpec/SLKeyExtractor.h b/cpp/module/SLModuleSpec/SLKeyExtractor.h new file mode 100644 index 0000000..22cfec5 --- /dev/null +++ b/cpp/module/SLModuleSpec/SLKeyExtractor.h @@ -0,0 +1,13 @@ +#include +#include +#include + +namespace facebook::react { + +class SLKeyExtractor { + public: + static std::optional extractKey(const std::string& input); + static std::vector extractAllKeys(const std::string& input); +}; + +} diff --git a/cpp/module/SLModuleSpec/SLModuleJSI.cpp b/cpp/module/SLModuleSpec/SLModuleJSI.cpp new file mode 100644 index 0000000..69e3f28 --- /dev/null +++ b/cpp/module/SLModuleSpec/SLModuleJSI.cpp @@ -0,0 +1,74 @@ +#include "SLModuleJSI.h" + +namespace facebook::react { + +using namespace facebook; + +void SLModuleJSI::install(facebook::jsi::Runtime &runtime, std::shared_ptr &commitHook) { + auto registerContainerFamily = facebook::jsi::Function::createFromHostFunction( + runtime, + facebook::jsi::PropNameID::forAscii(runtime, "__NATIVE_registerContainerNode"), + 1, + [&](facebook::jsi::Runtime &runtime, + const facebook::jsi::Value &thisValue, + const facebook::jsi::Value *arguments, + size_t count) -> facebook::jsi::Value + { + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); + commitHook->registerContainerNode(shadowNode); + + return facebook::jsi::Value::undefined(); + }); + runtime.global().setProperty(runtime, "__NATIVE_registerContainerNode", std::move(registerContainerFamily)); + + auto unregisterContainerFamily = facebook::jsi::Function::createFromHostFunction( + runtime, + facebook::jsi::PropNameID::forAscii(runtime, "__NATIVE_unregisterContainerNode"), + 1, + [&](facebook::jsi::Runtime &runtime, + const facebook::jsi::Value &thisValue, + const facebook::jsi::Value *arguments, + size_t count) -> facebook::jsi::Value + { + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); + commitHook->unregisterContainerNode(shadowNode); + + return facebook::jsi::Value::undefined(); + }); + runtime.global().setProperty(runtime, "__NATIVE_unregisterContainerNode", std::move(unregisterContainerFamily)); + + auto registerElementFamily = facebook::jsi::Function::createFromHostFunction( + runtime, + facebook::jsi::PropNameID::forAscii(runtime, "__NATIVE_registerElementNode"), + 1, + [&](facebook::jsi::Runtime &runtime, + const facebook::jsi::Value &thisValue, + const facebook::jsi::Value *arguments, + size_t count) -> facebook::jsi::Value + { + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); + commitHook->registerElementNode(shadowNode); + + return facebook::jsi::Value::undefined(); + }); + runtime.global().setProperty(runtime, "__NATIVE_registerElementNode", std::move(registerElementFamily)); + + auto unregisterElementFamily = facebook::jsi::Function::createFromHostFunction( + runtime, + facebook::jsi::PropNameID::forAscii(runtime, "__NATIVE_unregisterElementNode"), + 1, + [&](facebook::jsi::Runtime &runtime, + const facebook::jsi::Value &thisValue, + const facebook::jsi::Value *arguments, + size_t count) -> facebook::jsi::Value + { + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); + commitHook->unregisterElementNode(shadowNode); + + return facebook::jsi::Value::undefined(); + }); + runtime.global().setProperty(runtime, "__NATIVE_unregisterElementNode", std::move(unregisterElementFamily)); +} + +} + diff --git a/cpp/module/SLModuleSpec/SLModuleJSI.h b/cpp/module/SLModuleSpec/SLModuleJSI.h new file mode 100644 index 0000000..303f8eb --- /dev/null +++ b/cpp/module/SLModuleSpec/SLModuleJSI.h @@ -0,0 +1,10 @@ +#include "SLCommitHook.h" + +namespace facebook::react { + +class SLModuleJSI { + public: + static void install(facebook::jsi::Runtime &runtime, std::shared_ptr &commitHook); +}; + +} diff --git a/cpp/module/SLModuleSpec/SLTemplate.cpp b/cpp/module/SLModuleSpec/SLTemplate.cpp new file mode 100644 index 0000000..eae38f2 --- /dev/null +++ b/cpp/module/SLModuleSpec/SLTemplate.cpp @@ -0,0 +1,61 @@ +#include "SLTemplate.h" + +namespace facebook::react { + +int nextFamilyTag = -2; + +auto adjustFamilyTag = [](int tag) { + const int MIN_TAG_VALUE = -2e9; + const int CLAMPED_TAG = -2; + return tag < MIN_TAG_VALUE ? CLAMPED_TAG : tag - 2; +}; + +ShadowNode::Shared SLTemplate::cloneShadowNodeTree(jsi::Runtime *runtime, nlohmann::json* elementData, const ShadowNode::Shared& shadowNode) +{ + auto const &componentDescriptor = shadowNode->getComponentDescriptor(); + PropsParserContext propsParserContext{shadowNode->getSurfaceId(), *componentDescriptor.getContextContainer().get()}; + + nextFamilyTag = adjustFamilyTag(nextFamilyTag); + + InstanceHandle::Shared instanceHandle = std::make_shared( + *runtime, + shadowNode->getInstanceHandle(*runtime), + shadowNode->getTag()); + auto const fragment = ShadowNodeFamilyFragment{nextFamilyTag, shadowNode->getSurfaceId(), instanceHandle}; + auto const family = componentDescriptor.createFamily(fragment); + auto const props = componentDescriptor.cloneProps(propsParserContext, shadowNode->getProps(), {}); + auto const state = componentDescriptor.createInitialState(props, family); + auto const nextShadowNode = componentDescriptor.createShadowNode( + ShadowNodeFragment{props, ShadowNodeFragment::childrenPlaceholder(), state}, family); + + RawTextShadowNode::ConcreteProps* interpolatedProps; + if (shadowNode->getComponentName() == std::string("RawText")) { + const Props::Shared& nextprops = nextShadowNode->getProps(); + interpolatedProps = const_cast( + static_cast(nextprops.get()) + ); + + try { + if (!SLKeyExtractor::extractKey(interpolatedProps->text).has_value()) { + throw false; + } + + auto elementDataPointer = nlohmann::json::json_pointer( + "/" + SLKeyExtractor::extractKey(interpolatedProps->text).value() + ); + + if ((*elementData).contains(elementDataPointer) && (*elementData)[elementDataPointer].is_string()) { + interpolatedProps->text = (*elementData)[elementDataPointer].get(); + } + } catch (...) {} + } + + for (const auto &childShadowNode : shadowNode->getChildren()) { + auto const clonedChildShadowNode = cloneShadowNodeTree(runtime, elementData, childShadowNode); + componentDescriptor.appendChild(nextShadowNode, clonedChildShadowNode); + } + + return nextShadowNode; +} + +} diff --git a/cpp/module/SLModuleSpec/SLTemplate.h b/cpp/module/SLModuleSpec/SLTemplate.h new file mode 100644 index 0000000..a3f4348 --- /dev/null +++ b/cpp/module/SLModuleSpec/SLTemplate.h @@ -0,0 +1,16 @@ +#include +#include +#include "json.hpp" +#include "SLKeyExtractor.h" + +namespace facebook::react { + +class SLTemplate { + public: + static ShadowNode::Shared cloneShadowNodeTree( + jsi::Runtime *runtime, + nlohmann::json* elementData, + const ShadowNode::Shared& shadowNode); +}; + +} diff --git a/example/src/App.tsx b/example/src/App.tsx index 66b46d8..8524ea1 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -12,7 +12,7 @@ import { import useData from './useData'; import Element from './Element'; -const ITEMS_COUNT = 5; +const ITEMS_COUNT = 500; const IS_INVERTED = false; const IS_HORIZONTAL = false; const INITIAL_SCROLL_INDEX = 0; diff --git a/ios/SLModule.mm b/ios/SLModule.mm index 9bca57e..ab81959 100644 --- a/ios/SLModule.mm +++ b/ios/SLModule.mm @@ -1,6 +1,7 @@ #import "SLModule.h" #import "SLCommitHook.h" #import "SLModuleSpecJSI.h" +#import "SLModuleJSI.h" #import #import #import @@ -24,69 +25,7 @@ - (void)setSurfacePresenter:(id)surfacePresenter - (void)installJSIBindingsWithRuntime:(facebook::jsi::Runtime &)runtime { self->runtime_ = &runtime; - - auto registerContainerFamily = facebook::jsi::Function::createFromHostFunction( - runtime, - facebook::jsi::PropNameID::forAscii(runtime, "__NATIVE_registerContainerNode"), - 1, - [=](facebook::jsi::Runtime &runtime, - const facebook::jsi::Value &thisValue, - const facebook::jsi::Value *arguments, - size_t count) -> facebook::jsi::Value - { - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - self->commitHook_->registerContainerNode(shadowNode); - - return facebook::jsi::Value::undefined(); - }); - runtime.global().setProperty(runtime, "__NATIVE_registerContainerNode", std::move(registerContainerFamily)); - - auto unregisterContainerFamily = facebook::jsi::Function::createFromHostFunction( - runtime, - facebook::jsi::PropNameID::forAscii(runtime, "__NATIVE_unregisterContainerNode"), - 1, - [=](facebook::jsi::Runtime &runtime, - const facebook::jsi::Value &thisValue, - const facebook::jsi::Value *arguments, - size_t count) -> facebook::jsi::Value - { - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - self->commitHook_->unregisterContainerNode(shadowNode); - - return facebook::jsi::Value::undefined(); - }); - runtime.global().setProperty(runtime, "__NATIVE_unregisterContainerNode", std::move(unregisterContainerFamily)); - - auto registerElementFamily = facebook::jsi::Function::createFromHostFunction( - runtime, - facebook::jsi::PropNameID::forAscii(runtime, "__NATIVE_registerElementNode"), - 1, - [=](facebook::jsi::Runtime &runtime, - const facebook::jsi::Value &thisValue, - const facebook::jsi::Value *arguments, - size_t count) -> facebook::jsi::Value - { - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - self->commitHook_->registerElementNode(shadowNode); - - return facebook::jsi::Value::undefined(); - }); - runtime.global().setProperty(runtime, "__NATIVE_registerElementNode", std::move(registerElementFamily)); - auto unregisterElementFamily = facebook::jsi::Function::createFromHostFunction( - runtime, - facebook::jsi::PropNameID::forAscii(runtime, "__NATIVE_unregisterElementNode"), - 1, - [=](facebook::jsi::Runtime &runtime, - const facebook::jsi::Value &thisValue, - const facebook::jsi::Value *arguments, - size_t count) -> facebook::jsi::Value - { - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - self->commitHook_->unregisterElementNode(shadowNode); - - return facebook::jsi::Value::undefined(); - }); - runtime.global().setProperty(runtime, "__NATIVE_unregisterElementNode", std::move(unregisterContainerFamily)); + SLModuleJSI::install(runtime, commitHook_); } - (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params diff --git a/src/SLContainer.tsx b/src/SLContainer.tsx index e884520..39de2ae 100644 --- a/src/SLContainer.tsx +++ b/src/SLContainer.tsx @@ -28,14 +28,8 @@ const SLContainerWrapper = ( props: Omit & SLContainerWrapperProps, forwardedRef: React.Ref> ) => { - const instanceRef = React.useRef(null); - - React.useLayoutEffect(() => { - // @ts-ignore - global.__NATIVE_registerContainerNode( - ReactNativeInterface.getNodeFromPublicInstance(instanceRef.current) - ); - }, []); + const [_registered, setRegistered] = React.useState(false); + const instanceRef = React.useRef(null); React.useImperativeHandle, SLContainerRef>( forwardedRef, @@ -61,12 +55,37 @@ const SLContainerWrapper = ( ? styles.containerHorizontal : styles.containerVertical; + const nextRef = (ref: SLContainerInstance) => { + if (ref) { + global.__NATIVE_registerContainerNode( + ReactNativeInterface.getNodeFromPublicInstance(ref) + ); + instanceRef.current = ref; + setRegistered(true); + } else { + global.__NATIVE_unregisterContainerNode( + ReactNativeInterface.getNodeFromPublicInstance(instanceRef.current) + ); + instanceRef.current = null; + setRegistered(true); + } + + if (forwardedRef) { + if (typeof forwardedRef === 'function') { + // @ts-ignore + forwardedRef(ref); + } else { + (forwardedRef as React.MutableRefObject).current = ref; + } + } + }; + return ( {props.children} diff --git a/src/SLContainerNativeComponent.ts b/src/SLContainerNativeComponent.ts index 76f3255..bd50c08 100644 --- a/src/SLContainerNativeComponent.ts +++ b/src/SLContainerNativeComponent.ts @@ -6,6 +6,11 @@ import type { DirectEventHandler, } from 'react-native/Libraries/Types/CodegenTypes'; +declare global { + var __NATIVE_registerContainerNode: (node: any) => void; + var __NATIVE_unregisterContainerNode: (node: any) => void; +} + export type ScrollToIndexOptions = { index: number; animated?: boolean; diff --git a/src/SLElement.tsx b/src/SLElement.tsx index a78e37c..9c8e728 100644 --- a/src/SLElement.tsx +++ b/src/SLElement.tsx @@ -14,19 +14,38 @@ const SLElementWrapper = ( props: SLElementNativeProps & SLElementWrapperProps, forwardedRef: React.Ref<{}> ) => { - const instanceRef = React.useRef(null); - - React.useLayoutEffect(() => { - // @ts-ignore - global.__NATIVE_registerElementNode( - ReactNativeInterface.getNodeFromPublicInstance(instanceRef.current) - ); - }, []); + const [_registered, setRegistered] = React.useState(false); + const instanceRef = React.useRef(null); React.useImperativeHandle(forwardedRef, () => ({})); + const nextRef = (ref: SLElementInstance) => { + if (ref) { + global.__NATIVE_registerElementNode( + ReactNativeInterface.getNodeFromPublicInstance(ref) + ); + instanceRef.current = ref; + setRegistered(true); + } else { + global.__NATIVE_unregisterElementNode( + ReactNativeInterface.getNodeFromPublicInstance(instanceRef.current) + ); + instanceRef.current = null; + setRegistered(false); + } + + if (forwardedRef) { + if (typeof forwardedRef === 'function') { + // @ts-ignore + forwardedRef(ref); + } else { + (forwardedRef as React.MutableRefObject).current = ref; + } + } + }; + return ( - + {props.children} ); diff --git a/src/SLElementNativeComponent.ts b/src/SLElementNativeComponent.ts index c699a10..7243595 100644 --- a/src/SLElementNativeComponent.ts +++ b/src/SLElementNativeComponent.ts @@ -2,6 +2,11 @@ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNati import type { ViewProps } from 'react-native'; import type { Int32 } from 'react-native/Libraries/Types/CodegenTypes'; +declare global { + var __NATIVE_registerElementNode: (node: any) => void; + var __NATIVE_unregisterElementNode: (node: any) => void; +} + export interface SLElementNativeProps extends ViewProps { index: Int32; uniqueId: string; diff --git a/src/Shadowlist.tsx b/src/Shadowlist.tsx index 367b49a..34fb038 100644 --- a/src/Shadowlist.tsx +++ b/src/Shadowlist.tsx @@ -43,6 +43,7 @@ export const Shadowlist = React.forwardRef( {invoker(props.ListHeaderComponent)} @@ -56,6 +57,7 @@ export const Shadowlist = React.forwardRef( {invoker(props.ListFooterComponent)} @@ -69,6 +71,7 @@ export const Shadowlist = React.forwardRef( {invoker(props.ListEmptyComponent)} @@ -79,7 +82,11 @@ export const Shadowlist = React.forwardRef( * ListChildrenComponent */ const ListChildrenComponent = ( - + {props.renderItem()} );