diff --git a/change/react-native-windows-8c0a3d4a-27a0-4b81-810c-1578749610a8.json b/change/react-native-windows-8c0a3d4a-27a0-4b81-810c-1578749610a8.json new file mode 100644 index 00000000000..5a87e5bed5f --- /dev/null +++ b/change/react-native-windows-8c0a3d4a-27a0-4b81-810c-1578749610a8.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Remove usage of JSDispatcher in various built-in modules", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop/React.Windows.Desktop.vcxproj b/vnext/Desktop/React.Windows.Desktop.vcxproj index 52387dbd75f..74b8151709c 100644 --- a/vnext/Desktop/React.Windows.Desktop.vcxproj +++ b/vnext/Desktop/React.Windows.Desktop.vcxproj @@ -243,9 +243,6 @@ ..\Microsoft.ReactNative\IJSValueWriter.idl - - ..\Microsoft.ReactNative\IJSValueWriter.idl - ..\Microsoft.ReactNative\ReactInstanceSettings.idl diff --git a/vnext/Folly/Folly.vcxproj b/vnext/Folly/Folly.vcxproj index c483305b1eb..d98cb6d6b0e 100644 --- a/vnext/Folly/Folly.vcxproj +++ b/vnext/Folly/Folly.vcxproj @@ -50,6 +50,7 @@ StaticLibrary + diff --git a/vnext/Folly/ThreadNameStub.cpp b/vnext/Folly/ThreadNameStub.cpp new file mode 100644 index 00000000000..d4216832bb1 --- /dev/null +++ b/vnext/Folly/ThreadNameStub.cpp @@ -0,0 +1,10 @@ +#include + +// Avoid bringing in a bunch of folly threading just for setThreadName +namespace folly { + bool setThreadName(StringPiece) + { + return false; + } + } + \ No newline at end of file diff --git a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactContextTest.cpp b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactContextTest.cpp index 059a6f83ccf..f677674bfaa 100644 --- a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactContextTest.cpp +++ b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactContextTest.cpp @@ -28,6 +28,10 @@ struct ReactContextStub : implements { VerifyElseCrashSz(false, "Not implemented"); } + CallInvoker CallInvoker() noexcept { + VerifyElseCrashSz(false, "Not implemented"); + } + IInspectable JSRuntime() noexcept { VerifyElseCrashSz(false, "Not implemented"); } diff --git a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.cpp b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.cpp index 38bba3ed362..ca554de7174 100644 --- a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.cpp +++ b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.cpp @@ -69,6 +69,10 @@ void ReactModuleBuilderMock::AddInitializer(InitializerDelegate const &initializ m_initializers.push_back(initializer); } +void ReactModuleBuilderMock::AddJsiInitializer(JsiInitializerDelegate const &initializer) noexcept { + m_jsiinitializers.push_back(initializer); +} + void ReactModuleBuilderMock::AddConstantProvider(ConstantProviderDelegate const &constantProvider) noexcept { m_constantProviders.push_back(constantProvider); } diff --git a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.h b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.h index a6e4b0d6a46..dae87e5a71f 100644 --- a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.h +++ b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.h @@ -70,6 +70,7 @@ struct ReactModuleBuilderMock { public: // IReactModuleBuilder void AddInitializer(InitializerDelegate const &initializer) noexcept; + void AddJsiInitializer(JsiInitializerDelegate const &initializer) noexcept; void AddConstantProvider(ConstantProviderDelegate const &constantProvider) noexcept; void AddMethod(hstring const &name, MethodReturnType returnType, MethodDelegate const &method) noexcept; void AddSyncMethod(hstring const &name, SyncMethodDelegate const &method) noexcept; @@ -100,6 +101,7 @@ struct ReactModuleBuilderMock { private: IReactContext m_reactContext{nullptr}; std::vector m_initializers; + std::vector m_jsiinitializers; std::vector m_constantProviders; std::map> m_methods; std::map m_syncMethods; @@ -132,6 +134,10 @@ struct ReactContextMock : implements { VerifyElseCrashSz(false, "Not implemented"); } + CallInvoker CallInvoker() noexcept { + VerifyElseCrashSz(false, "Not implemented"); + } + IInspectable JSRuntime() noexcept { VerifyElseCrashSz(false, "Not implemented"); } @@ -216,6 +222,7 @@ struct ReactModuleBuilderImpl : implements +#include "ReactContext.h" #include "ReactNonAbiValue.h" #include "winrt/Windows.Foundation.Collections.h" @@ -14,6 +15,16 @@ using namespace facebook::jsi; namespace winrt::Microsoft::ReactNative { +namespace Details { +// Try to get JSI Runtime for the ReactContext +// If it is not found, then create it based on context JSI runtime and store it in the context.Properties(). +// The function returns nullptr if the current context does not have JSI runtime. +// It makes sure that the JSI runtime holder is removed when the instance is unloaded. +JsiAbiRuntime *TryGetOrCreateContextRuntime( + winrt::Microsoft::ReactNative::ReactContext const &context, + JsiRuntime const &runtimeHandle) noexcept; +} // namespace Details + // The macro to simplify recording JSI exceptions. // It looks strange to keep the normal structure of the try/catch in code. #define JSI_RUNTIME_SET_ERROR(runtime) \ @@ -132,6 +143,52 @@ std::shared_ptr const &JsiHostObjectWrapper::HostObje return m_hostObject; } +//=========================================================================== +// JsiHostObjectGetOrCreateWrapper implementation +//=========================================================================== + +JsiHostObjectGetOrCreateWrapper::JsiHostObjectGetOrCreateWrapper( + const winrt::Microsoft::ReactNative::IReactContext &context, + std::shared_ptr &&hostObject) noexcept + : m_hostObject(std::move(hostObject)), m_context(context) {} + +JsiValueRef JsiHostObjectGetOrCreateWrapper::GetProperty(JsiRuntime const &runtime, JsiPropertyIdRef const &name) try { + JsiAbiRuntime *rt{Details::TryGetOrCreateContextRuntime(m_context, runtime)}; + JsiAbiRuntime::PropNameIDRef nameRef{name}; + return JsiAbiRuntime::DetachJsiValueRef(m_hostObject->get(*rt, nameRef)); +} catch (JSI_RUNTIME_SET_ERROR(runtime)) { + throw; +} + +void JsiHostObjectGetOrCreateWrapper::SetProperty( + JsiRuntime const &runtime, + JsiPropertyIdRef const &name, + JsiValueRef const &value) try { + JsiAbiRuntime *rt{Details::TryGetOrCreateContextRuntime(m_context, runtime)}; + m_hostObject->set(*rt, JsiAbiRuntime::PropNameIDRef{name}, JsiAbiRuntime::ValueRef(value)); +} catch (JSI_RUNTIME_SET_ERROR(runtime)) { + throw; +} + +winrt::Windows::Foundation::Collections::IVector JsiHostObjectGetOrCreateWrapper::GetPropertyIds( + JsiRuntime const &runtime) try { + JsiAbiRuntime *rt{Details::TryGetOrCreateContextRuntime(m_context, runtime)}; + auto names = m_hostObject->getPropertyNames(*rt); + std::vector result; + result.reserve(names.size()); + for (auto &name : names) { + result.push_back(JsiAbiRuntime::DetachJsiPropertyIdRef(std::move(name))); + } + + return winrt::single_threaded_vector(std::move(result)); +} catch (JSI_RUNTIME_SET_ERROR(runtime)) { + throw; +} + +std::shared_ptr const &JsiHostObjectGetOrCreateWrapper::HostObjectSharedPtr() noexcept { + return m_hostObject; +} + //=========================================================================== // JsiHostFunctionWrapper implementation //=========================================================================== @@ -162,38 +219,41 @@ JsiValueRef JsiHostFunctionWrapper::operator()( // JsiAbiRuntime implementation //=========================================================================== -// The tls_jsiAbiRuntimeMap map allows us to associate JsiAbiRuntime with JsiRuntime. -// The association is thread-specific and DLL-specific. -// It is thread specific because we want to have the safe access only in JS thread. -// It is DLL-specific because JsiAbiRuntime is not ABI-safe and each module DLL will -// have their own JsiAbiRuntime instance. -static thread_local std::map *tls_jsiAbiRuntimeMap{nullptr}; +// The s_jsiAbiRuntimeMap map allows us to associate JsiAbiRuntime with JsiRuntime. +// The association is DLL-specific because JsiAbiRuntime is not ABI-safe and each module DLL will have their own +// JsiAbiRuntime instance. +static std::map *s_jsiAbiRuntimeMap{nullptr}; +static std::recursive_mutex s_jsiRuntimeMapMutex; JsiAbiRuntime::JsiAbiRuntime(JsiRuntime const &runtime) noexcept : m_runtime{runtime} { VerifyElseCrashSz(runtime, "JSI runtime is null"); - VerifyElseCrashSz( - GetFromJsiRuntime(runtime) == nullptr, - "We can have only one instance of JsiAbiRuntime for JsiRuntime in the thread."); - if (!tls_jsiAbiRuntimeMap) { - tls_jsiAbiRuntimeMap = new std::map(); + + { + std::lock_guard guard(s_jsiRuntimeMapMutex); + VerifyElseCrashSz( + GetFromJsiRuntime(runtime) == nullptr, "We can have only one instance of JsiAbiRuntime for a JsiRuntime."); + + if (!s_jsiAbiRuntimeMap) { + s_jsiAbiRuntimeMap = new std::map(); + } + s_jsiAbiRuntimeMap->try_emplace(get_abi(runtime), this); } - tls_jsiAbiRuntimeMap->try_emplace(get_abi(runtime), this); } JsiAbiRuntime::~JsiAbiRuntime() { - VerifyElseCrashSz( - GetFromJsiRuntime(m_runtime) != nullptr, "JsiAbiRuntime must be called in the same thread where it was created."); - tls_jsiAbiRuntimeMap->erase(get_abi(m_runtime)); - if (tls_jsiAbiRuntimeMap->empty()) { - delete tls_jsiAbiRuntimeMap; - tls_jsiAbiRuntimeMap = nullptr; + std::lock_guard guard(s_jsiRuntimeMapMutex); + s_jsiAbiRuntimeMap->erase(get_abi(m_runtime)); + if (s_jsiAbiRuntimeMap->empty()) { + delete s_jsiAbiRuntimeMap; + s_jsiAbiRuntimeMap = nullptr; } } /*static*/ JsiAbiRuntime *JsiAbiRuntime::GetFromJsiRuntime(JsiRuntime const &runtime) noexcept { - if (tls_jsiAbiRuntimeMap && runtime) { - auto it = tls_jsiAbiRuntimeMap->find(get_abi(runtime)); - if (it != tls_jsiAbiRuntimeMap->end()) { + std::lock_guard guard(s_jsiRuntimeMapMutex); + if (s_jsiAbiRuntimeMap && runtime) { + auto it = s_jsiAbiRuntimeMap->find(get_abi(runtime)); + if (it != s_jsiAbiRuntimeMap->end()) { return it->second; } } diff --git a/vnext/Microsoft.ReactNative.Cxx/JSI/JsiAbiApi.h b/vnext/Microsoft.ReactNative.Cxx/JSI/JsiAbiApi.h index 314bb0f3807..6f3230727a2 100644 --- a/vnext/Microsoft.ReactNative.Cxx/JSI/JsiAbiApi.h +++ b/vnext/Microsoft.ReactNative.Cxx/JSI/JsiAbiApi.h @@ -54,6 +54,25 @@ struct JsiHostObjectWrapper : implements { std::shared_ptr m_hostObject; }; +// An ABI-safe wrapper for facebook::jsi::HostObject, similar to JsiHostObjectWrapper, +// but uses GetOrCreate for the AbiRuntime to ensure its created on first use +// This is important for use with TurboModules, which may not go through the ReactContext.CallInvoker +struct JsiHostObjectGetOrCreateWrapper : implements { + JsiHostObjectGetOrCreateWrapper( + const winrt::Microsoft::ReactNative::IReactContext &context, + std::shared_ptr &&hostObject) noexcept; + + JsiValueRef GetProperty(JsiRuntime const &runtime, JsiPropertyIdRef const &name); + void SetProperty(JsiRuntime const &runtime, JsiPropertyIdRef const &name, JsiValueRef const &value); + winrt::Windows::Foundation::Collections::IVector GetPropertyIds(JsiRuntime const &runtime); + + std::shared_ptr const &HostObjectSharedPtr() noexcept; + + private: + std::shared_ptr m_hostObject; + winrt::Microsoft::ReactNative::IReactContext m_context; +}; + // The function object that wraps up the facebook::jsi::HostFunctionType struct JsiHostFunctionWrapper { // We only support new and move constructors. @@ -226,6 +245,7 @@ struct JsiAbiRuntime : facebook::jsi::Runtime { // Allow access to the helper function friend struct JsiByteBufferWrapper; friend struct JsiHostObjectWrapper; + friend struct JsiHostObjectGetOrCreateWrapper; friend struct JsiHostFunctionWrapper; friend struct AbiJSError; friend struct AbiJSINativeException; diff --git a/vnext/Microsoft.ReactNative.Cxx/JSI/JsiApiContext.cpp b/vnext/Microsoft.ReactNative.Cxx/JSI/JsiApiContext.cpp index aa75c348fcf..46e00403166 100644 --- a/vnext/Microsoft.ReactNative.Cxx/JSI/JsiApiContext.cpp +++ b/vnext/Microsoft.ReactNative.Cxx/JSI/JsiApiContext.cpp @@ -10,20 +10,12 @@ extern "C" IMAGE_DOS_HEADER __ImageBase; namespace winrt::Microsoft::ReactNative { -// Try to get JSI Runtime for the current JS dispatcher thread. +namespace Details { +// Try to get JSI Runtime for the ReactContext // If it is not found, then create it based on context JSI runtime and store it in the context.Properties(). // The function returns nullptr if the current context does not have JSI runtime. // It makes sure that the JSI runtime holder is removed when the instance is unloaded. -facebook::jsi::Runtime *TryGetOrCreateContextRuntime(ReactContext const &context) noexcept { - ReactDispatcher jsDispatcher = context.JSDispatcher(); - VerifyElseCrashSz(jsDispatcher.HasThreadAccess(), "Must be in JS thread"); - - // The JSI runtime is not available if we do Web debugging when JS is running in web browser. - JsiRuntime abiJsiRuntime = context.Handle().JSRuntime().as(); - if (!abiJsiRuntime) { - return nullptr; - } - +JsiAbiRuntime *TryGetOrCreateContextRuntime(ReactContext const &context, JsiRuntime const &abiJsiRuntime) noexcept { // See if the JSI runtime was previously created. JsiAbiRuntime *runtime = JsiAbiRuntime::GetFromJsiRuntime(abiJsiRuntime); if (!runtime) { @@ -57,6 +49,48 @@ facebook::jsi::Runtime *TryGetOrCreateContextRuntime(ReactContext const &context return runtime; } +} // namespace Details + +facebook::jsi::Runtime *TryGetOrCreateContextRuntime( + ReactContext const &context, + winrt::Windows::Foundation::IInspectable const &runtimeHandle) noexcept { + if (!runtimeHandle) { + return nullptr; + } + + // The JSI runtime is not available if we do Web debugging when JS is running in web browser. + JsiRuntime abiJsiRuntime = runtimeHandle.as(); + if (!abiJsiRuntime) { + return nullptr; + } + + return Details::TryGetOrCreateContextRuntime(context, abiJsiRuntime); +} + +// Note: deprecated in favor of TryGetOrCreateContextRuntime with Handle parameter +facebook::jsi::Runtime *TryGetOrCreateContextRuntime(ReactContext const &context) noexcept { +#ifdef DEBUG + ReactDispatcher jsDispatcher = context.JSDispatcher(); + VerifyElseCrashSz(jsDispatcher.HasThreadAccess(), "Must be in JS thread"); +#endif + + if (auto runtimeHandle = context.Handle().JSRuntime()) { + return TryGetOrCreateContextRuntime(context, runtimeHandle); + } + + return nullptr; +} + +// Calls TryGetOrCreateContextRuntime to get JSI runtime. +// It crashes when TryGetOrCreateContextRuntime returns null. +[[deprecated]] facebook::jsi::Runtime &GetOrCreateContextRuntime( + ReactContext const &context, + winrt::Windows::Foundation::IInspectable const &runtimeHandle) noexcept { + facebook::jsi::Runtime *runtime = TryGetOrCreateContextRuntime(context, runtimeHandle); + VerifyElseCrashSz(runtime, "JSI runtime is not available"); + return *runtime; +} + // Calls TryGetOrCreateContextRuntime to get JSI runtime. // It crashes when TryGetOrCreateContextRuntime returns null. // Note: deprecated in favor of TryGetOrCreateContextRuntime. diff --git a/vnext/Microsoft.ReactNative.Cxx/JSI/JsiApiContext.h b/vnext/Microsoft.ReactNative.Cxx/JSI/JsiApiContext.h index caf6ad2bb3d..d0fdc92c848 100644 --- a/vnext/Microsoft.ReactNative.Cxx/JSI/JsiApiContext.h +++ b/vnext/Microsoft.ReactNative.Cxx/JSI/JsiApiContext.h @@ -11,10 +11,15 @@ namespace winrt::Microsoft::ReactNative { +facebook::jsi::Runtime &GetOrCreateContextRuntime( + ReactContext const &context, + winrt::Windows::Foundation::IInspectable const &runtimeHandle) noexcept; + // Try to get JSI Runtime for the current JS dispatcher thread. // If it is not found, then create it based on context JSI runtime and store it in the context.Properties(). // The function returns nullptr if the current context does not have JSI runtime. // It makes sure that the JSI runtime holder is removed when the instance is unloaded. +// Deprecated -- will remove once we remove the JSRuntime property from ReactContext facebook::jsi::Runtime *TryGetOrCreateContextRuntime(ReactContext const &context) noexcept; // Calls TryGetOrCreateContextRuntime to get JSI runtime. @@ -25,6 +30,7 @@ facebook::jsi::Runtime *TryGetOrCreateContextRuntime(ReactContext const &context // Call provided lambda with the facebook::jsi::Runtime& parameter. // For example: ExecuteJsi(context, [](facebook::jsi::Runtime& runtime){...}) // The code is executed synchronously if it is already in JSDispatcher, or asynchronously otherwise. +// New Arch should use ReactContext.CallInvoker instead template void ExecuteJsi(ReactContext const &context, TCodeWithRuntime const &code, ReactPromise *callStatus = nullptr) { ReactDispatcher jsDispatcher = context.JSDispatcher(); diff --git a/vnext/Microsoft.ReactNative.Cxx/NativeModules.h b/vnext/Microsoft.ReactNative.Cxx/NativeModules.h index 4e2fe9f1e5d..50f8c199899 100644 --- a/vnext/Microsoft.ReactNative.Cxx/NativeModules.h +++ b/vnext/Microsoft.ReactNative.Cxx/NativeModules.h @@ -7,6 +7,7 @@ #pragma once #include #include +#include "JSI/JsiApiContext.h" #include "JSValueReader.h" #include "JSValueWriter.h" #include "ModuleRegistration.h" @@ -500,6 +501,7 @@ template struct ModuleInitMethodInfo { using ModuleType = TModule; using MethodType = void (TModule::*)(ReactContext const &) noexcept; + using JsiMethodType = void (TModule::*)(ReactContext const &, facebook::jsi::Runtime &) noexcept; static InitializerDelegate GetInitializer(void *module, MethodType method) noexcept { return [module = static_cast(module), method](ReactContext const &reactContext) noexcept { @@ -508,6 +510,23 @@ struct ModuleInitMethodInfo { } }; +template +struct ModuleJsiInitMethodInfo; + +template +struct ModuleJsiInitMethodInfo { + using ModuleType = TModule; + using MethodType = void (TModule::*)(ReactContext const &, facebook::jsi::Runtime &) noexcept; + + static JsiInitializerDelegate GetJsiInitializer(void *module, MethodType method) noexcept { + return + [module = static_cast(module), method]( + ReactContext const &reactContext, winrt::Windows::Foundation::IInspectable const &runtimeHandle) noexcept { + (module->*method)(reactContext, GetOrCreateContextRuntime(reactContext, runtimeHandle)); + }; + } +}; + // ==== MakeCallbackSignatures ================================================= template @@ -1041,6 +1060,9 @@ struct ReactModuleBuilder { for (auto &initializer : m_initializers) { m_moduleBuilder.AddInitializer(initializer); } + for (auto &initializer : m_jsiinitializers) { + m_moduleBuilder.AddJsiInitializer(initializer); + } } template @@ -1069,8 +1091,14 @@ struct ReactModuleBuilder { template void RegisterInitMethod(TMethod method) noexcept { - auto initializer = ModuleInitMethodInfo::GetInitializer(m_module, method); - m_initializers.push_back(std::move(initializer)); + if constexpr (ModuleMethodInfo::ArgCount == 1) { + auto initializer = ModuleInitMethodInfo::GetInitializer(m_module, method); + m_initializers.push_back(std::move(initializer)); + } else { + static_assert(ModuleMethodInfo::ArgCount == 2); + auto jsiinitializer = ModuleJsiInitMethodInfo::GetJsiInitializer(m_module, method); + m_jsiinitializers.push_back(std::move(jsiinitializer)); + } } template @@ -1129,6 +1157,7 @@ struct ReactModuleBuilder { std::wstring_view m_moduleName{L""}; std::wstring_view m_eventEmitterName{L""}; std::vector m_initializers; + std::vector m_jsiinitializers; }; struct VerificationResult { diff --git a/vnext/Microsoft.ReactNative.Cxx/ReactContext.h b/vnext/Microsoft.ReactNative.Cxx/ReactContext.h index 9023a39b3f1..1c521771e59 100644 --- a/vnext/Microsoft.ReactNative.Cxx/ReactContext.h +++ b/vnext/Microsoft.ReactNative.Cxx/ReactContext.h @@ -11,6 +11,7 @@ #if !defined(CORE_ABI) && !defined(__APPLE__) #include #endif +#include #include #include "JSValueWriter.h" #include "ReactNotificationService.h" @@ -18,6 +19,8 @@ namespace winrt::Microsoft::ReactNative { +std::shared_ptr MakeAbiCallInvoker(IReactContext const &context) noexcept; + // Represents a context of execution for the Native Module. // It wraps up the IReactContext and adds convenience methods for // working with C++ types. @@ -50,6 +53,10 @@ struct ReactContext { return ReactDispatcher{m_handle.JSDispatcher()}; } + std::shared_ptr CallInvoker() const noexcept { + return MakeAbiCallInvoker(m_handle); + } + // Call methodName JS function of module with moduleName. // args are either function arguments or a single lambda with 'IJSValueWriter const&' argument. template diff --git a/vnext/Microsoft.ReactNative.Cxx/TurboModuleProvider.cpp b/vnext/Microsoft.ReactNative.Cxx/TurboModuleProvider.cpp index bc57f6711ff..bfeb719adaa 100644 --- a/vnext/Microsoft.ReactNative.Cxx/TurboModuleProvider.cpp +++ b/vnext/Microsoft.ReactNative.Cxx/TurboModuleProvider.cpp @@ -4,28 +4,26 @@ #include "pch.h" #include "TurboModuleProvider.h" +#include "JSI/JsiApiContext.h" + namespace winrt::Microsoft::ReactNative { // CallInvoker implementation based on JSDispatcher. struct AbiCallInvoker final : facebook::react::CallInvoker { AbiCallInvoker(IReactContext const &context) : m_context(context) {} - void invokeAsync(std::function &&func) noexcept override { - m_context.JSDispatcher().Post([func = std::move(func)]() { func(); }); - } - void invokeAsync(facebook::react::CallFunc &&func) noexcept override { - // If async is allowed to run sync if already on the JS thread, this could be replaced with just the ExecuteJsi call - m_context.JSDispatcher().Post([context = m_context, func = std::move(func)]() { - winrt::Microsoft::ReactNative::ExecuteJsi(context, std::move(func)); - }); + m_context.CallInvoker().InvokeAsync( + [context = m_context, func = std::move(func)](const winrt::Windows::Foundation::IInspectable &runtimeHandle) { + func(GetOrCreateContextRuntime(context, runtimeHandle)); + }); } - virtual void invokeSync(facebook::react::CallFunc &&func) override { - // Throwing an exception in this method matches the behavior of - // Instance::JSCallInvoker::invokeSync in react-native\ReactCommon\cxxreact\Instance.cpp - UNREFERENCED_PARAMETER(func); - throw std::runtime_error("Synchronous native -> JS calls are currently not supported."); + void invokeSync(facebook::react::CallFunc &&func) override { + m_context.CallInvoker().InvokeSync( + [context = m_context, func = std::move(func)](const winrt::Windows::Foundation::IInspectable &runtimeHandle) { + func(GetOrCreateContextRuntime(context, runtimeHandle)); + }); } private: diff --git a/vnext/Microsoft.ReactNative.Cxx/TurboModuleProvider.h b/vnext/Microsoft.ReactNative.Cxx/TurboModuleProvider.h index 3308662a74c..465b3a2133f 100644 --- a/vnext/Microsoft.ReactNative.Cxx/TurboModuleProvider.h +++ b/vnext/Microsoft.ReactNative.Cxx/TurboModuleProvider.h @@ -22,10 +22,9 @@ void AddTurboModuleProvider(IReactPackageBuilder const &packageBuilder, std::wst IJsiHostObject abiTurboModule{nullptr}; // We expect the initializer to be called immediately for TurboModules moduleBuilder.AddInitializer([&abiTurboModule](IReactContext const &context) mutable { - TryGetOrCreateContextRuntime(ReactContext{context}); // Ensure the JSI runtime is created. - auto callInvoker = MakeAbiCallInvoker(context); + auto callInvoker = ReactContext{context}.CallInvoker(); auto turboModule = std::make_shared(callInvoker); - abiTurboModule = winrt::make(std::move(turboModule)); + abiTurboModule = winrt::make(context, std::move(turboModule)); }); return abiTurboModule.as(); }); diff --git a/vnext/Microsoft.ReactNative.IntegrationTests/ExecuteJsiTests.cpp b/vnext/Microsoft.ReactNative.IntegrationTests/ExecuteJsiTests.cpp index e58a23de143..df2bfb761a3 100644 --- a/vnext/Microsoft.ReactNative.IntegrationTests/ExecuteJsiTests.cpp +++ b/vnext/Microsoft.ReactNative.IntegrationTests/ExecuteJsiTests.cpp @@ -27,7 +27,8 @@ struct TestExecuteJsiModule { REACT_METHOD(TestHostFunction, L"testHostFunction") void TestHostFunction() noexcept { TestEventService::LogEvent("testHostFunction started", nullptr); - ExecuteJsi(m_reactContext, [](Runtime &rt) { + + m_reactContext.CallInvoker()->invokeAsync([](Runtime &rt) { Function hostGreeter = Function::createFromHostFunction( rt, PropNameID::forAscii(rt, "hostGreeter"), @@ -51,7 +52,7 @@ struct TestExecuteJsiModule { REACT_METHOD(TestHostObject, L"testHostObject") void TestHostObject() noexcept { TestEventService::LogEvent("testHostObject started", nullptr); - ExecuteJsi(m_reactContext, [](Runtime &rt) { + m_reactContext.CallInvoker()->invokeAsync([](Runtime &rt) { class GreeterHostObject : public HostObject { Value get(Runtime &rt, const PropNameID &) override { return String::createFromAscii(rt, "Hello"); @@ -80,8 +81,8 @@ struct TestExecuteJsiModule { // The JSI executed synchronously here because we are in JS thread. TestEventService::LogEvent("testSameJsiRuntime started", nullptr); Runtime *jsiRuntime{}; - ExecuteJsi(m_reactContext, [&jsiRuntime](Runtime &rt) { jsiRuntime = &rt; }); - ExecuteJsi(m_reactContext, [&jsiRuntime](Runtime &rt) { + m_reactContext.CallInvoker()->invokeAsync([&jsiRuntime](Runtime &rt) { jsiRuntime = &rt; }); + m_reactContext.CallInvoker()->invokeAsync([&jsiRuntime](Runtime &rt) { TestCheckEqual(jsiRuntime, &rt); TestEventService::LogEvent("testSameJsiRuntime completed", nullptr); }); @@ -97,10 +98,10 @@ struct TestExecuteJsiModule { [](ReactError const &error) noexcept { TestEventService::LogEvent("testExecuteJsiPromise promise failed", error.Message.c_str()); }); - ExecuteJsi( - m_reactContext, - [](Runtime &) { TestEventService::LogEvent("testExecuteJsiPromise completed", nullptr); }, - &callResult); + m_reactContext.CallInvoker()->invokeAsync([callResult](Runtime &) { + TestEventService::LogEvent("testExecuteJsiPromise completed", nullptr); + callResult.Resolve(); + }); } private: @@ -133,12 +134,12 @@ TEST_CLASS (ExecuteJsiTests) { TestEventService::ObserveEvents({ TestEvent{"initialize", nullptr}, TestEvent{"testHostFunction started", nullptr}, - TestEvent{"testHostFunction completed", nullptr}, TestEvent{"testHostObject started", nullptr}, - TestEvent{"testHostObject completed", nullptr}, TestEvent{"testSameJsiRuntime started", nullptr}, - TestEvent{"testSameJsiRuntime completed", nullptr}, TestEvent{"testExecuteJsiPromise started", nullptr}, + TestEvent{"testHostFunction completed", nullptr}, + TestEvent{"testHostObject completed", nullptr}, + TestEvent{"testSameJsiRuntime completed", nullptr}, TestEvent{"testExecuteJsiPromise completed", nullptr}, TestEvent{"testExecuteJsiPromise promise succeeded", nullptr}, }); @@ -146,12 +147,12 @@ TEST_CLASS (ExecuteJsiTests) { TestEventService::ObserveEvents({ TestEvent{"OnInstanceDestroyed started", nullptr}, - TestEvent{"OnInstanceDestroyed promise failed", "No JSI runtime"}, + TestEvent{"OnInstanceDestroyed promise failed", "Promise destroyed."}, }); } static void OnInstanceDestroyed(ReactContext const &reactContext) { - // See that ExecuteJsi failed to execute + // See that Jsi failed to execute TestEventService::LogEvent("OnInstanceDestroyed started", nullptr); ReactPromise callResult( @@ -159,10 +160,8 @@ TEST_CLASS (ExecuteJsiTests) { [](ReactError const &error) noexcept { TestEventService::LogEvent("OnInstanceDestroyed promise failed", error.Message.c_str()); }); - ExecuteJsi( - reactContext, - [](Runtime &) { TestEventService::LogEvent("OnInstanceDestroyed completed", nullptr); }, - &callResult); + reactContext.CallInvoker()->invokeAsync( + [](Runtime &) { TestEventService::LogEvent("OnInstanceDestroyed completed", nullptr); }); } }; diff --git a/vnext/Microsoft.ReactNative.IntegrationTests/JsiSimpleTurboModuleTests.cpp b/vnext/Microsoft.ReactNative.IntegrationTests/JsiSimpleTurboModuleTests.cpp index a698d84bde3..e033b9267fc 100644 --- a/vnext/Microsoft.ReactNative.IntegrationTests/JsiSimpleTurboModuleTests.cpp +++ b/vnext/Microsoft.ReactNative.IntegrationTests/JsiSimpleTurboModuleTests.cpp @@ -47,25 +47,37 @@ TEST_CLASS (JsiSimpleTurboModuleTests) { TEST_METHOD(TestInstanceReload) { TestEventService::Initialize(); + std::thread::id jsThreadId{}; + auto reactNativeHost = TestReactNativeHostHolder(L"JsiSimpleTurboModuleTests", [&](ReactNativeHost const &host) noexcept { host.PackageProviders().Append(winrt::make()); // See that all events are raised in JSDispatcher thread. host.InstanceSettings().InstanceCreated( - [](IInspectable const & /*sender*/, InstanceCreatedEventArgs const &args) { + [&](IInspectable const & /*sender*/, InstanceCreatedEventArgs const &args) { TestEventService::LogEvent("Instance created event", nullptr); + // Save this thread as the js thread + jsThreadId = std::this_thread::get_id(); +#if !USE_FABRIC TestCheck(ReactContext(args.Context()).JSDispatcher().HasThreadAccess()); +#endif }); host.InstanceSettings().InstanceLoaded( - [](IInspectable const & /*sender*/, InstanceLoadedEventArgs const &args) { + [&](IInspectable const & /*sender*/, InstanceLoadedEventArgs const &args) { TestEventService::LogEvent("Instance loaded event", nullptr); + TestCheck(jsThreadId == std::this_thread::get_id()); +#if !USE_FABRIC TestCheck(ReactContext(args.Context()).JSDispatcher().HasThreadAccess()); +#endif }); host.InstanceSettings().InstanceDestroyed( [&](IInspectable const & /*sender*/, InstanceDestroyedEventArgs const &args) { TestEventService::LogEvent("Instance destroyed event", nullptr); + TestCheck(jsThreadId == std::this_thread::get_id()); +#if !USE_FABRIC TestCheck(ReactContext(args.Context()).JSDispatcher().HasThreadAccess()); +#endif }); }); diff --git a/vnext/Microsoft.ReactNative.IntegrationTests/ReactNativeHostTests.cpp b/vnext/Microsoft.ReactNative.IntegrationTests/ReactNativeHostTests.cpp index 2e5b0b9a7b9..c7ccf3bb689 100644 --- a/vnext/Microsoft.ReactNative.IntegrationTests/ReactNativeHostTests.cpp +++ b/vnext/Microsoft.ReactNative.IntegrationTests/ReactNativeHostTests.cpp @@ -131,6 +131,29 @@ TEST_CLASS (ReactNativeHostTests) { TestEventService::ObserveEvents({TestEvent{"InstanceLoaded::Success", nullptr}}); } + TEST_METHOD(LoadInstance_FiresInstanceCreatedHasJsiAccess) { + TestEventService::Initialize(); + + auto options = TestReactNativeHostHolder::Options{}; + auto reactNativeHost = TestReactNativeHostHolder(L"ReactNativeHostTests", [](ReactNativeHost const &host) noexcept { + host.InstanceSettings().InstanceCreated( + [](auto const &, winrt::Microsoft::ReactNative::IInstanceCreatedEventArgs args) noexcept { + facebook::jsi::Runtime &rt = + winrt::Microsoft::ReactNative::GetOrCreateContextRuntime(args.Context(), args.RuntimeHandle()); + + facebook::jsi::Object result(rt); + result.setProperty(rt, "const1", facebook::jsi::Value(true)); + result.setProperty(rt, "const2", facebook::jsi::Value(375)); + result.setProperty(rt, "const3", facebook::jsi::String::createFromUtf8(rt, "something")); + rt.global().setProperty(rt, "setFromInstanceCreated", result); + + TestEventService::LogEvent("InstanceCreated", nullptr); + }); + }); + + TestEventService::ObserveEvents({TestEvent{"InstanceCreated", nullptr}}); + } + TEST_METHOD(LoadBundleWithError_FiresInstanceLoaded_Failed) { TestEventService::Initialize(); @@ -169,7 +192,11 @@ TEST_CLASS (ReactNativeHostTests) { }, std::move(options)); +#if USE_FABRIC + TestEventService::ObserveEvents({TestEvent{"InstanceLoaded::Failed", nullptr}}); +#else TestEventService::ObserveEvents({TestEvent{"InstanceLoaded::Canceled", nullptr}}); +#endif } }; diff --git a/vnext/Microsoft.ReactNative.IntegrationTests/TurboModuleTests.cpp b/vnext/Microsoft.ReactNative.IntegrationTests/TurboModuleTests.cpp index 2486e8fdba1..287104d97f8 100644 --- a/vnext/Microsoft.ReactNative.IntegrationTests/TurboModuleTests.cpp +++ b/vnext/Microsoft.ReactNative.IntegrationTests/TurboModuleTests.cpp @@ -650,7 +650,9 @@ TEST_CLASS (TurboModuleTests) { TestEventService::Initialize(); TestNotificationService::Initialize(); - auto reactNativeHost = TestReactNativeHostHolder(L"TurboModuleTests", [](ReactNativeHost const &host) noexcept { + CallInvoker callInvoker{nullptr}; + + auto reactNativeHost = TestReactNativeHostHolder(L"TurboModuleTests", [&](ReactNativeHost const &host) noexcept { host.PackageProviders().Append(winrt::make()); ReactPropertyBag(host.InstanceSettings().Properties()) .Set(CppTurboModule::TestName, L"JSDispatcherAfterInstanceUnload"); @@ -658,6 +660,10 @@ TEST_CLASS (TurboModuleTests) { [&](IInspectable const & /*sender*/, InstanceDestroyedEventArgs const & /*args*/) { TestNotificationService::Set("Instance destroyed event"); }); + host.InstanceSettings().InstanceCreated( + [&](IInspectable const & /*sender*/, InstanceCreatedEventArgs const &args) { + callInvoker = args.Context().CallInvoker(); + }); }); TestEventService::ObserveEvents({ @@ -667,19 +673,17 @@ TEST_CLASS (TurboModuleTests) { reactNativeHost.Host().UnloadInstance(); TestNotificationService::Wait("Instance destroyed event"); - // JSDispatcher must not process any callbacks - auto jsDispatcher = reactNativeHost.Host() - .InstanceSettings() - .Properties() - .Get(ReactDispatcherHelper::JSDispatcherProperty()) - .as(); struct CallbackData { ~CallbackData() { TestNotificationService::Set("CallbackData destroyed"); } }; bool callbackIsCalled{false}; - jsDispatcher.Post([&callbackIsCalled, data = std::make_shared()] { callbackIsCalled = true; }); + + // callInvoker must not process any callbacks + callInvoker.InvokeAsync( + [&callbackIsCalled, data = std::make_shared()]( + const winrt::Windows::Foundation::IInspectable & /*runtimeHandle*/) { callbackIsCalled = true; }); TestNotificationService::Wait("CallbackData destroyed"); TestCheck(!callbackIsCalled); } diff --git a/vnext/Microsoft.ReactNative.Managed.UnitTests/ReactModuleBuilderMock.cs b/vnext/Microsoft.ReactNative.Managed.UnitTests/ReactModuleBuilderMock.cs index 87aa0a1e046..53a072a15a8 100644 --- a/vnext/Microsoft.ReactNative.Managed.UnitTests/ReactModuleBuilderMock.cs +++ b/vnext/Microsoft.ReactNative.Managed.UnitTests/ReactModuleBuilderMock.cs @@ -18,6 +18,7 @@ namespace Microsoft.ReactNative.Managed.UnitTests class ReactModuleBuilderMock : IReactModuleBuilder { private readonly List m_initializers = new List(); + private readonly List m_jsiinitializers = new List(); private readonly Dictionary> m_methods = new Dictionary>(); private readonly Dictionary m_syncMethods = @@ -55,6 +56,11 @@ public void AddInitializer(InitializerDelegate initializer) m_initializers.Add(initializer); } + public void AddJsiInitializer(JsiInitializerDelegate initializer) + { + m_jsiinitializers.Add(initializer); + } + public void AddConstantProvider(ConstantProviderDelegate constantProvider) { m_constantProviders.Add(constantProvider); @@ -354,7 +360,11 @@ public ReactContextMock(ReactModuleBuilderMock builder) public IReactDispatcher UIDispatcher => Properties.Get(ReactDispatcherHelper.UIDispatcherProperty) as IReactDispatcher; +#pragma warning disable 612, 618 // Deprecated public IReactDispatcher JSDispatcher => Properties.Get(ReactDispatcherHelper.JSDispatcherProperty) as IReactDispatcher; +#pragma warning restore 612, 618 + + public CallInvoker CallInvoker => throw new NotImplementedException(); public Object JSRuntime => throw new NotImplementedException(); diff --git a/vnext/Microsoft.ReactNative.Managed/ReactContext.cs b/vnext/Microsoft.ReactNative.Managed/ReactContext.cs index ac8c52e9023..4fbf17810b8 100644 --- a/vnext/Microsoft.ReactNative.Managed/ReactContext.cs +++ b/vnext/Microsoft.ReactNative.Managed/ReactContext.cs @@ -24,8 +24,10 @@ public struct ReactContext public ReactDispatcher UIDispatcher => new ReactDispatcher(Handle.UIDispatcher); + #pragma warning disable 612, 618 // Deprecated public ReactDispatcher JSDispatcher => new ReactDispatcher(Handle.JSDispatcher); - + #pragma warning restore 612, 618 + public LoadingState LoadingState => Handle.LoadingState; public ReactSettingsSnapshot SettingsSnapshot => new ReactSettingsSnapshot(Handle.SettingsSnapshot); diff --git a/vnext/Microsoft.ReactNative/CallInvoker.cpp b/vnext/Microsoft.ReactNative/CallInvoker.cpp new file mode 100644 index 00000000000..7a51a1d30b7 --- /dev/null +++ b/vnext/Microsoft.ReactNative/CallInvoker.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "CallInvoker.h" + +#include "CallInvoker.g.cpp" +#include "JsiApi.h" + +namespace winrt::Microsoft::ReactNative::implementation { + +static const ReactPropertyId &CallInvokerPropertyId() noexcept { + static const ReactPropertyId prop{L"ReactNative", L"CallInvoker"}; + return prop; +} + +CallInvoker::CallInvoker( + Mso::React::IReactContext &reactContext, + std::shared_ptr callInvoker) noexcept + : m_callInvoker(callInvoker), m_context(&reactContext) {} + +void CallInvoker::InvokeAsync(CallFunc func) noexcept { + m_callInvoker->invokeAsync( + [reactContext = m_context, func](facebook::jsi::Runtime & /*runtime*/) { func(reactContext->JsiRuntime()); }); +} + +void CallInvoker::InvokeSync(CallFunc func) noexcept { + m_callInvoker->invokeSync( + [reactContext = m_context, func](facebook::jsi::Runtime & /*runtime*/) { func(reactContext->JsiRuntime()); }); +} + +winrt::Microsoft::ReactNative::CallInvoker CallInvoker::FromProperties( + const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept { + return properties.Get(CallInvokerPropertyId()); +} + +void CallInvoker::SetProperties( + const winrt::Microsoft::ReactNative::ReactPropertyBag &properties, + winrt::Microsoft::ReactNative::CallInvoker invoker) noexcept { + properties.Set(CallInvokerPropertyId(), invoker); +} + +} // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/CallInvoker.h b/vnext/Microsoft.ReactNative/CallInvoker.h new file mode 100644 index 00000000000..898b06b945b --- /dev/null +++ b/vnext/Microsoft.ReactNative/CallInvoker.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include "ReactHost/React.h" +#include "winrt/Microsoft.ReactNative.h" + +#include "CallInvoker.g.h" + +namespace winrt::Microsoft::ReactNative::implementation { + +struct CallInvoker : CallInvokerT { + CallInvoker( + Mso::React::IReactContext &reactContext, + std::shared_ptr callInvoker) noexcept; + + void InvokeAsync(CallFunc func) noexcept; + void InvokeSync(CallFunc func) noexcept; + + static winrt::Microsoft::ReactNative::CallInvoker FromProperties( + const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept; + static void SetProperties( + const winrt::Microsoft::ReactNative::ReactPropertyBag &properties, + winrt::Microsoft::ReactNative::CallInvoker invoker) noexcept; + + private: + Mso::CntPtr m_context; + std::shared_ptr m_callInvoker; +}; + +} // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/CallInvokerWriter.cpp b/vnext/Microsoft.ReactNative/CallInvokerWriter.cpp index d7c1b24ee09..35cbdc98d0f 100644 --- a/vnext/Microsoft.ReactNative/CallInvokerWriter.cpp +++ b/vnext/Microsoft.ReactNative/CallInvokerWriter.cpp @@ -8,22 +8,6 @@ namespace winrt::Microsoft::ReactNative { -// Special IJSValueWriter that does nothing. -// We use it instead of JsiWriter when JSI runtime is not available anymore. -struct JSNoopWriter : winrt::implements { - public: // IJSValueWriter - void WriteNull() noexcept; - void WriteBoolean(bool value) noexcept; - void WriteInt64(int64_t value) noexcept; - void WriteDouble(double value) noexcept; - void WriteString(const winrt::hstring &value) noexcept; - void WriteObjectBegin() noexcept; - void WritePropertyName(const winrt::hstring &name) noexcept; - void WriteObjectEnd() noexcept; - void WriteArrayBegin() noexcept; - void WriteArrayEnd() noexcept; -}; - //=========================================================================== // CallInvokerWriter implementation //=========================================================================== @@ -145,4 +129,4 @@ void JSNoopWriter::WriteObjectEnd() noexcept {} void JSNoopWriter::WriteArrayBegin() noexcept {} void JSNoopWriter::WriteArrayEnd() noexcept {} -} // namespace winrt::Microsoft::ReactNative +} // namespace winrt::Microsoft::ReactNative \ No newline at end of file diff --git a/vnext/Microsoft.ReactNative/CallInvokerWriter.h b/vnext/Microsoft.ReactNative/CallInvokerWriter.h index fdbcc96b7b1..f9372105f66 100644 --- a/vnext/Microsoft.ReactNative/CallInvokerWriter.h +++ b/vnext/Microsoft.ReactNative/CallInvokerWriter.h @@ -45,4 +45,20 @@ struct CallInvokerWriter : winrt::implements IJSValueWriter m_writer; }; -} // namespace winrt::Microsoft::ReactNative +// Special IJSValueWriter that does nothing. +// We use it instead of JsiWriter when JSI runtime is not available anymore. +struct JSNoopWriter : winrt::implements { + public: // IJSValueWriter + void WriteNull() noexcept; + void WriteBoolean(bool value) noexcept; + void WriteInt64(int64_t value) noexcept; + void WriteDouble(double value) noexcept; + void WriteString(const winrt::hstring &value) noexcept; + void WriteObjectBegin() noexcept; + void WritePropertyName(const winrt::hstring &name) noexcept; + void WriteObjectEnd() noexcept; + void WriteArrayBegin() noexcept; + void WriteArrayEnd() noexcept; +}; + +} // namespace winrt::Microsoft::ReactNative \ No newline at end of file diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h index be9cfa44b79..899d2ffa6bc 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h @@ -14,7 +14,6 @@ #include #include #include -#include "Utils/BatchingEventEmitter.h" namespace winrt { using namespace Windows::UI; diff --git a/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp b/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp index 586926a0621..790fd270389 100644 --- a/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp @@ -470,6 +470,19 @@ void FabricUIManager::Initialize(winrt::Microsoft::ReactNative::ReactContext con m_context.Properties().Set(FabicUIManagerProperty(), shared_from_this()); + auto destroyInstanceNotificationId{ + winrt::Microsoft::ReactNative::ReactNotificationId{ + L"ReactNative.InstanceSettings", L"InstanceDestroyed"}}; + reactContext.Notifications().Subscribe( + destroyInstanceNotificationId, + [reactContext]( + winrt::Windows::Foundation::IInspectable const & /*sender*/, + winrt::Microsoft::ReactNative::ReactNotificationArgs< + winrt::Microsoft::ReactNative::InstanceDestroyedEventArgs> const &args) noexcept { + reactContext.Properties().Remove(FabicUIManagerProperty()); + args.Subscription().Unsubscribe(); // Unsubscribe after we handle the notification. + }); + /* EventBeatManager eventBeatManager = new EventBeatManager(mReactApplicationContext); UIManagerModule nativeModule = diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/MessageQueueThreadImpl.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/MessageQueueThreadImpl.cpp new file mode 100644 index 00000000000..a23a40422c8 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/MessageQueueThreadImpl.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [Windows] Brought forward from react-native - switch to using react-native/ReactCxxPlatform/react/threading version +// once we integrate that far + +#include "MessageQueueThreadImpl.h" + +#include + +namespace facebook::react { + +void MessageQueueThreadImpl::runOnQueue(std::function &&runnable) { + if (!taskDispatchThread_.isRunning()) { + return; + } + taskDispatchThread_.runAsync([runnable = std::move(runnable)]() noexcept { runnable(); }); +} + +void MessageQueueThreadImpl::runOnQueueSync(std::function &&runnable) { + if (!taskDispatchThread_.isRunning()) { + return; + } + if (taskDispatchThread_.isOnThread()) { + runnable(); + } else { + taskDispatchThread_.runSync([runnable = std::move(runnable)]() noexcept { runnable(); }); + } +} + +void MessageQueueThreadImpl::quitSynchronous() { + taskDispatchThread_.quit(); +} + +} // namespace facebook::react diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/MessageQueueThreadImpl.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/MessageQueueThreadImpl.h new file mode 100644 index 00000000000..0f4bdf0f85d --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/MessageQueueThreadImpl.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [Windows] Brought forward from react-native - switch to using react-native/ReactCxxPlatform/react/threading version +// once we integrate that far + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +using MessageQueueThreadFactory = std::function()>; + +constexpr char MessageQueueThreadFactoryKey[] = "MessageQueueThreadFactoryKey"; + +/** + * MessageQueueThread implementation that uses a TaskDispatchThread for + * queueing and threading logic + */ +class MessageQueueThreadImpl : public MessageQueueThread { + public: + MessageQueueThreadImpl() noexcept = default; + explicit MessageQueueThreadImpl(int priorityOffset) noexcept : taskDispatchThread_("MessageQueue", priorityOffset) {} + + ~MessageQueueThreadImpl() noexcept override = default; + + /** Add a job to the queue asynchronously */ + void runOnQueue(std::function &&runnable) override; + + /** + * Add a job to the queue synchronously - call won't return until runnable + * has completed. Will run immediately if called from the looper thread. + * Should only be used for initialization. + */ + void runOnQueueSync(std::function &&runnable) override; + + /** + * Stop the message queue thread. Should only be used for cleanup - once it + * returns, no further work should run on the queue. + */ + void quitSynchronous() override; + + private: + TaskDispatchThread taskDispatchThread_{"MessageQueue"}; +}; + +} // namespace facebook::react diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.cpp new file mode 100644 index 00000000000..9c7318103bd --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [Windows] Brought forward from react-native - switch to using react-native/ReactCxxPlatform/react/threading version +// once we integrate that far + +#include "TaskDispatchThread.h" + +#include +#include +#include +#include +#include + +#include + +#ifdef ANDROID +#include +#include +#endif + +namespace facebook::react { + +TaskDispatchThread::TaskDispatchThread(std::string threadName, int priorityOffset) noexcept + : threadName_(std::move(threadName)) { +#ifdef ANDROID + // Attaches the thread to JVM just in case anything calls out to Java + thread_ = std::thread([&]() { + facebook::jni::ThreadScope::WithClassLoader([&]() { + int result = setpriority(PRIO_PROCESS, static_cast(::syscall(SYS_gettid)), priorityOffset); + + if (result != 0) { + LOG(INFO) << " setCurrentThreadPriority failed with pri errno: " << errno; + } + + loop(); + }); + }); + +#else + thread_ = std::thread(&TaskDispatchThread::loop, this); +#endif +} + +TaskDispatchThread::~TaskDispatchThread() noexcept { + quit(); +} + +bool TaskDispatchThread::isOnThread() noexcept { + return std::this_thread::get_id() == thread_.get_id(); +} + +bool TaskDispatchThread::isRunning() noexcept { + return running_; +} + +void TaskDispatchThread::runAsync(TaskFn &&task, std::chrono::milliseconds delayMs) noexcept { + if (!running_) { + return; + } + std::lock_guard guard(queueLock_); + auto dispatchTime = std::chrono::system_clock::now() + delayMs; + queue_.emplace(dispatchTime, std::move(task)); + loopCv_.notify_one(); +} + +void TaskDispatchThread::runSync(TaskFn &&task) noexcept { + std::promise promise; + runAsync([&]() { + if (running_) { + task(); + } + promise.set_value(); + }); + promise.get_future().wait(); +} + +void TaskDispatchThread::quit() noexcept { + if (!running_) { + return; + } + running_ = false; + loopCv_.notify_one(); + if (thread_.joinable()) { + if (!isOnThread()) { + thread_.join(); + } else { + thread_.detach(); + } + } +} + +void TaskDispatchThread::loop() noexcept { + if (!threadName_.empty()) { + folly::setThreadName(threadName_); + } + while (running_) { + std::unique_lock lock(queueLock_); + loopCv_.wait(lock, [&]() { return !running_ || !queue_.empty(); }); + while (!queue_.empty()) { + auto task = queue_.top(); + auto now = std::chrono::system_clock::now(); + if (task.dispatchTime > now) { + if (running_) { + loopCv_.wait_until(lock, task.dispatchTime); + } else { + // Shutting down, skip all the delayed tasks that are not to be + // executed yet + queue_.pop(); + } + continue; + } + + queue_.pop(); + lock.unlock(); + task.fn(); + lock.lock(); + } + } +} + +} // namespace facebook::react diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.h new file mode 100644 index 00000000000..1f161ca6997 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [Windows] Brought forward from react-native - switch to using react-native/ReactCxxPlatform/react/threading version +// once we integrate that far + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +/** + * Representation of a thread looper which can add tasks to a queue and handle + * the synchronization of callers. + */ +class TaskDispatchThread { + public: + using TaskFn = std::function; + using TimePoint = std::chrono::time_point; + + TaskDispatchThread(std::string threadName = "", int priorityOffset = 0) noexcept; + + ~TaskDispatchThread() noexcept; + + /** Return true if the current thread is the same as this looper's thread. */ + bool isOnThread() noexcept; + + /** Return true until TaskDispatchThread.quit() is called */ + bool isRunning() noexcept; + + /** Add task to the queue and return immediately. */ + void runAsync(TaskFn &&task, std::chrono::milliseconds delayMs = std::chrono::milliseconds::zero()) noexcept; + + /** Add task to the queue and wait until it has completed. */ + void runSync(TaskFn &&task) noexcept; + + /** Shut down and clean up the thread. */ + void quit() noexcept; + + protected: + struct Task { + TimePoint dispatchTime; + TaskFn fn; + + Task(TimePoint dispatchTime, TaskFn &&fn) : dispatchTime(dispatchTime), fn(std::move(fn)) {} + + bool operator<(const Task &other) const { + // Have the earliest tasks be at the front of the queue. + return dispatchTime > other.dispatchTime; + } + }; + + void loop() noexcept; + + std::mutex queueLock_; + std::condition_variable loopCv_; + std::priority_queue queue_; + std::atomic running_{true}; + std::string threadName_; + std::thread thread_; +}; + +} // namespace facebook::react diff --git a/vnext/Microsoft.ReactNative/IReactContext.cpp b/vnext/Microsoft.ReactNative/IReactContext.cpp index 8e79b0fa2c7..1e106b61ba5 100644 --- a/vnext/Microsoft.ReactNative/IReactContext.cpp +++ b/vnext/Microsoft.ReactNative/IReactContext.cpp @@ -8,6 +8,9 @@ #include "XamlUIService.h" #endif +#include "CallInvoker.h" +#include "Utils/Helpers.h" + namespace winrt::Microsoft::ReactNative::implementation { //============================================================================= @@ -97,10 +100,24 @@ IReactDispatcher ReactContext::UIDispatcher() noexcept { } IReactDispatcher ReactContext::JSDispatcher() noexcept { +#if defined(DEBUG) && defined(USE_FABRIC) + VerifyElseCrashSz( + !::Microsoft::ReactNative::IsFabricEnabled(Properties()), + "ReactContext.JSRuntime is not supported on new arch, use ReactContext.CallInvoker instead."); +#endif return Properties().Get(ReactDispatcherHelper::JSDispatcherProperty()).try_as(); } +winrt::Microsoft::ReactNative::CallInvoker ReactContext::CallInvoker() noexcept { + return winrt::Microsoft::ReactNative::implementation::CallInvoker::FromProperties(ReactPropertyBag(Properties())); +} + winrt::Windows::Foundation::IInspectable ReactContext::JSRuntime() noexcept { +#if defined(DEBUG) && defined(USE_FABRIC) + VerifyElseCrashSz( + !::Microsoft::ReactNative::IsFabricEnabled(Properties()), + "ReactContext.JSRuntime is not supported on new arch, use ReactContext.CallInvoker instead."); +#endif return m_context->JsiRuntime(); } diff --git a/vnext/Microsoft.ReactNative/IReactContext.h b/vnext/Microsoft.ReactNative/IReactContext.h index e22e0cf6f03..796f2faa492 100644 --- a/vnext/Microsoft.ReactNative/IReactContext.h +++ b/vnext/Microsoft.ReactNative/IReactContext.h @@ -43,6 +43,7 @@ struct ReactContext : winrt::implements { IReactNotificationService Notifications() noexcept; IReactDispatcher UIDispatcher() noexcept; IReactDispatcher JSDispatcher() noexcept; + winrt::Microsoft::ReactNative::CallInvoker CallInvoker() noexcept; IInspectable JSRuntime() noexcept; LoadingState LoadingState() noexcept; diff --git a/vnext/Microsoft.ReactNative/IReactContext.idl b/vnext/Microsoft.ReactNative/IReactContext.idl index 2aebfa210d5..2c497a6a8b4 100644 --- a/vnext/Microsoft.ReactNative/IReactContext.idl +++ b/vnext/Microsoft.ReactNative/IReactContext.idl @@ -14,6 +14,18 @@ import "IReactPropertyBag.idl"; namespace Microsoft.ReactNative { + DOC_STRING("Function that acts on a JsiRuntime, provided as the argument to the function. ABI safe version of facebook::react::CallFunc in CallInvoker.h. Most direct usage of this should be avoided by using ReactContext.CallInvoker.") + delegate void CallFunc(Object runtime); + + [webhosthidden] + [default_interface] + DOC_STRING("CallInvoker used to access the jsi runtime. Most direct usage of this should be avoided by using ReactContext.CallInvoker.") + runtimeclass CallInvoker + { + void InvokeAsync(CallFunc func); + void InvokeSync(CallFunc func); + }; + DOC_STRING( "Used to represent the state of the React Native JavaScript instance") enum LoadingState @@ -173,6 +185,7 @@ namespace Microsoft.ReactNative "It is a shortcut for the @ReactDispatcherHelper.UIDispatcherProperty from the @.Properties property bag.") IReactDispatcher UIDispatcher { get; }; + [deprecated("Use @IReactContext.CallInvoker instead", deprecate, 1)] DOC_STRING( "Gets the JavaScript engine thread dispatcher.\n" "It is a shortcut for the @ReactDispatcherHelper.JSDispatcherProperty from the @.Properties property bag.") @@ -182,9 +195,13 @@ namespace Microsoft.ReactNative "Gets the JavaScript runtime for the running React instance.\n" "It can be null if Web debugging is used.\n" "**Note: do not use this property directly. " - "It is an experimental property that may be removed or changed in a future version.") + "It is an experimental property will be removed in a future version.\n" + "Deprecated for new Arch: Use @IReactContext.CallInvoker instead.") Object JSRuntime { get; }; + DOC_STRING("used to schedule work on the JS runtime. Most direct usage of this should be avoided by using ReactContext.CallInvoker.") + CallInvoker CallInvoker { get; }; + #if !defined(CORE_ABI) && !defined(USE_FABRIC) [deprecated("Use @XamlUIService.DispatchEvent instead", deprecate, 1)] DOC_STRING("Deprecated property. Use @XamlUIService.DispatchEvent instead. It will be removed in a future version.") diff --git a/vnext/Microsoft.ReactNative/IReactDispatcher.idl b/vnext/Microsoft.ReactNative/IReactDispatcher.idl index b6e522bee53..5db40c06c81 100644 --- a/vnext/Microsoft.ReactNative/IReactDispatcher.idl +++ b/vnext/Microsoft.ReactNative/IReactDispatcher.idl @@ -48,6 +48,7 @@ namespace Microsoft.ReactNative "a specific React instance.") static IReactPropertyName UIDispatcherProperty { get; }; + [deprecated("Use @IReactContext.CallInvoker instead", deprecate, 1)] DOC_STRING( "Gets name of the `JSDispatcher` property for the @IReactPropertyBag.\n" "Generally you can use @IReactContext.JSDispatcher to get the value of this property for " diff --git a/vnext/Microsoft.ReactNative/IReactModuleBuilder.cpp b/vnext/Microsoft.ReactNative/IReactModuleBuilder.cpp index 06403da41b8..4911757ba98 100644 --- a/vnext/Microsoft.ReactNative/IReactModuleBuilder.cpp +++ b/vnext/Microsoft.ReactNative/IReactModuleBuilder.cpp @@ -74,6 +74,10 @@ void ReactModuleBuilder::AddInitializer(InitializerDelegate const &initializer) m_initializers.push_back(initializer); } +void ReactModuleBuilder::AddJsiInitializer(JsiInitializerDelegate const &initializer) noexcept { + m_jsiinitializers.push_back(initializer); +} + void ReactModuleBuilder::AddConstantProvider(ConstantProviderDelegate const &constantProvider) noexcept { m_constantProviders.push_back(constantProvider); } diff --git a/vnext/Microsoft.ReactNative/IReactModuleBuilder.h b/vnext/Microsoft.ReactNative/IReactModuleBuilder.h index 6fba03b3a71..17fdf22ecca 100644 --- a/vnext/Microsoft.ReactNative/IReactModuleBuilder.h +++ b/vnext/Microsoft.ReactNative/IReactModuleBuilder.h @@ -12,6 +12,7 @@ struct ReactModuleBuilder : winrt::implements m_initializers; + std::vector m_jsiinitializers; std::vector m_constantProviders; std::vector m_methods; }; diff --git a/vnext/Microsoft.ReactNative/IReactModuleBuilder.idl b/vnext/Microsoft.ReactNative/IReactModuleBuilder.idl index a80ea90880e..d422b3f2514 100644 --- a/vnext/Microsoft.ReactNative/IReactModuleBuilder.idl +++ b/vnext/Microsoft.ReactNative/IReactModuleBuilder.idl @@ -15,6 +15,12 @@ namespace Microsoft.ReactNative "Experimental code uses it to initialize TurboModule `CallInvoker`.") delegate void InitializerDelegate(IReactContext reactContext); + DOC_STRING( + "A delegate that sets `reactContext` for a module.\n" + "We use it for a stand-alone initialize method, strongly typed JS events and functions.\n" + "Experimental code uses it to initialize TurboModule `CallInvoker`.") + delegate void JsiInitializerDelegate(IReactContext reactContext, Object runtimeHandle); + DOC_STRING("Native method return type.") enum MethodReturnType { @@ -59,6 +65,8 @@ namespace Microsoft.ReactNative "There can be multiple initializer methods which are called in the order they were registered.") void AddInitializer(InitializerDelegate initializer); + void AddJsiInitializer(JsiInitializerDelegate initializer); + DOC_STRING( "Adds a constant provider method to define constants for the native module. See @ConstantProviderDelegate.") void AddConstantProvider(ConstantProviderDelegate constantProvider); diff --git a/vnext/Microsoft.ReactNative/JsiApi.cpp b/vnext/Microsoft.ReactNative/JsiApi.cpp index eee8ea21a28..d074fff39f7 100644 --- a/vnext/Microsoft.ReactNative/JsiApi.cpp +++ b/vnext/Microsoft.ReactNative/JsiApi.cpp @@ -482,8 +482,8 @@ facebook::jsi::JSError const &jsError) { \ jsiRuntime->evaluateJavaScript(jsiPalBuffer, "Form_JSI_API_not_a_real_file"); ReactNative::JsiRuntime abiJsiResult{make(Mso::Copy(jsiRuntimeHolder), Mso::Copy(jsiRuntime))}; std::scoped_lock lock{s_mutex}; - s_jsiRuntimeMap.try_emplace(reinterpret_cast(jsiRuntime.get()), abiJsiResult); - return abiJsiResult; + auto it = s_jsiRuntimeMap.try_emplace(reinterpret_cast(jsiRuntime.get()), abiJsiResult); + return it.first->second.get(); } ReactNative::JsiRuntime JsiRuntime::MakeChakraRuntime() { diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index 02c9a6d4d98..ef968d81f40 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -201,9 +201,6 @@ IJSValueWriter.idl - - IJSValueWriter.idl - IReactDispatcher.idl diff --git a/vnext/Microsoft.ReactNative/Modules/AccessibilityInfoModule.cpp b/vnext/Microsoft.ReactNative/Modules/AccessibilityInfoModule.cpp index af25b74353d..9e287550eee 100644 --- a/vnext/Microsoft.ReactNative/Modules/AccessibilityInfoModule.cpp +++ b/vnext/Microsoft.ReactNative/Modules/AccessibilityInfoModule.cpp @@ -22,12 +22,11 @@ void AccessibilityInfo::Initialize(winrt::Microsoft::ReactNative::ReactContext c } void AccessibilityInfo::isReduceMotionEnabled(std::function const &onSuccess) noexcept { - auto jsDispatcher = m_context.JSDispatcher(); - m_context.UIDispatcher().Post([weakThis = weak_from_this(), jsDispatcher, onSuccess] { + m_context.UIDispatcher().Post([weakThis = weak_from_this(), onSuccess] { if (auto strongThis = weakThis.lock()) { winrt::Windows::UI::ViewManagement::UISettings uiSettings; auto animationsEnabled = uiSettings.AnimationsEnabled(); - jsDispatcher.Post([animationsEnabled, onSuccess] { onSuccess(!animationsEnabled); }); + onSuccess(!animationsEnabled); } }); } diff --git a/vnext/Microsoft.ReactNative/Modules/AlertModule.cpp b/vnext/Microsoft.ReactNative/Modules/AlertModule.cpp index 60eb8fb5dd1..1b8f8509064 100644 --- a/vnext/Microsoft.ReactNative/Modules/AlertModule.cpp +++ b/vnext/Microsoft.ReactNative/Modules/AlertModule.cpp @@ -44,7 +44,6 @@ void Alert::ProcessPendingAlertRequestsXaml() noexcept { const auto &pendingAlert = pendingAlerts.front(); const auto &args = pendingAlert.args; const auto &result = pendingAlert.result; - auto jsDispatcher = m_context.JSDispatcher(); xaml::Controls::ContentDialog dialog{}; xaml::Controls::TextBlock titleTextBlock; @@ -110,7 +109,7 @@ void Alert::ProcessPendingAlertRequestsXaml() noexcept { } else if (IsXamlIsland()) { // We cannot show a ContentDialog in a XAML Island unless it is assigned a // XamlRoot instance. In such cases, we just treat the alert as dismissed. - jsDispatcher.Post([result, this] { result(m_constants.dismissed, m_constants.buttonNeutral); }); + result(m_constants.dismissed, m_constants.buttonNeutral); pendingAlerts.pop(); ProcessPendingAlertRequests(); return; @@ -138,19 +137,17 @@ void Alert::ProcessPendingAlertRequestsXaml() noexcept { const auto hasCloseButton = dialog.CloseButtonText().size() > 0; auto asyncOp = dialog.ShowAsync(); asyncOp.Completed( - [hasCloseButton, jsDispatcher, result, this]( + [hasCloseButton, result, this]( const winrt::IAsyncOperation &asyncOp, winrt::AsyncStatus status) { switch (asyncOp.GetResults()) { case xaml::Controls::ContentDialogResult::Primary: - jsDispatcher.Post([result, this] { result(m_constants.buttonClicked, m_constants.buttonPositive); }); + result(m_constants.buttonClicked, m_constants.buttonPositive); break; case xaml::Controls::ContentDialogResult::Secondary: - jsDispatcher.Post([result, this] { result(m_constants.buttonClicked, m_constants.buttonNegative); }); + result(m_constants.buttonClicked, m_constants.buttonNegative); break; case xaml::Controls::ContentDialogResult::None: - jsDispatcher.Post([hasCloseButton, result, this] { - result(hasCloseButton ? m_constants.buttonClicked : m_constants.dismissed, m_constants.buttonNeutral); - }); + result(hasCloseButton ? m_constants.buttonClicked : m_constants.dismissed, m_constants.buttonNeutral); break; default: break; @@ -164,7 +161,6 @@ void Alert::ProcessPendingAlertRequestsMessageDialog() noexcept { const auto &pendingAlert = pendingAlerts.front(); const auto &args = pendingAlert.args; const auto &result = pendingAlert.result; - auto jsDispatcher = m_context.JSDispatcher(); auto cancelable = args.cancelable.value_or(true); auto messageDialog = winrt::Windows::UI::Popups::MessageDialog( @@ -208,11 +204,10 @@ void Alert::ProcessPendingAlertRequestsMessageDialog() noexcept { auto asyncOp = messageDialog.ShowAsync(); asyncOp.Completed( - [jsDispatcher, result, this]( + [result, this]( const winrt::IAsyncOperation &asyncOp, winrt::AsyncStatus status) { auto uicommand = asyncOp.GetResults(); - jsDispatcher.Post( - [id = uicommand.Id(), result, this] { result(m_constants.buttonClicked, winrt::unbox_value(id)); }); + result(m_constants.buttonClicked, winrt::unbox_value(uicommand.Id())); pendingAlerts.pop(); ProcessPendingAlertRequests(); }); diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedModule.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedModule.cpp index 94766cfed17..62a433b65e0 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedModule.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedModule.cpp @@ -56,9 +56,7 @@ void NativeAnimatedModule::getValue(double tag, std::function cons callback = std::move(saveValueCallback)]() { if (auto pThis = wkThis.lock()) { pThis->m_nodesManager->GetValue( - tag, [context = pThis->m_context, callback = std::move(callback)](double value) { - context.JSDispatcher().Post([callback = std::move(callback), value]() { callback(value); }); - }); + tag, [context = pThis->m_context, callback = std::move(callback)](double value) { callback(value); }); } }); } @@ -131,11 +129,9 @@ void NativeAnimatedModule::startAnimatingNode( nodeTag, animationConfig.AsObject(), [context = pThis->m_context, endCallback = std::move(endCallback)](bool finished) { - context.JSDispatcher().Post([finished, endCallback = std::move(endCallback)]() { - ReactNativeSpecs::AnimatedModuleSpec_EndResult result; - result.finished = finished; - endCallback(std::move(result)); - }); + ReactNativeSpecs::AnimatedModuleSpec_EndResult result; + result.finished = finished; + endCallback(std::move(result)); }, pThis->m_nodesManager); } diff --git a/vnext/Microsoft.ReactNative/Modules/AppStateModule.cpp b/vnext/Microsoft.ReactNative/Modules/AppStateModule.cpp index 185b31c22d4..c6ac8d0b539 100644 --- a/vnext/Microsoft.ReactNative/Modules/AppStateModule.cpp +++ b/vnext/Microsoft.ReactNative/Modules/AppStateModule.cpp @@ -96,13 +96,13 @@ void AppState::SetDeactivated(bool deactivated) noexcept { if (winrt::Microsoft::ReactNative::implementation::QuirkSettings::GetMapWindowDeactivatedToAppStateInactive( m_context.Properties())) { m_deactivated = deactivated; - m_context.JSDispatcher().Post([this]() { AppStateDidChange({GetAppState()}); }); + AppStateDidChange({GetAppState()}); } } void AppState::SetEnteredBackground(bool enteredBackground) noexcept { m_enteredBackground = enteredBackground; - m_context.JSDispatcher().Post([this]() { AppStateDidChange({GetAppState()}); }); + AppStateDidChange({GetAppState()}); } std::string AppState::GetAppState() noexcept { diff --git a/vnext/Microsoft.ReactNative/Modules/ClipboardModule.cpp b/vnext/Microsoft.ReactNative/Modules/ClipboardModule.cpp index 5b155aa3364..b4765792738 100644 --- a/vnext/Microsoft.ReactNative/Modules/ClipboardModule.cpp +++ b/vnext/Microsoft.ReactNative/Modules/ClipboardModule.cpp @@ -16,27 +16,25 @@ void Clipboard::Initialize(winrt::Microsoft::ReactNative::ReactContext const &re } void Clipboard::getString(React::ReactPromise result) noexcept { - auto jsDispatcher = m_reactContext.JSDispatcher(); - m_reactContext.UIDispatcher().Post([jsDispatcher, result] { + m_reactContext.UIDispatcher().Post([result] { auto data = DataTransfer::Clipboard::GetContent(); auto asyncOp = data.GetTextAsync(); // unfortunately, lambda captures doesn't work well with winrt::fire_and_forget and co_await here // call asyncOp.Completed explicitly - asyncOp.Completed([jsDispatcher, result](const IAsyncOperation &asyncOp, AsyncStatus status) { + asyncOp.Completed([result](const IAsyncOperation &asyncOp, AsyncStatus status) { switch (status) { case AsyncStatus::Completed: { auto text = std::wstring(asyncOp.GetResults()); - jsDispatcher.Post( - [result, text] { result.Resolve(std::string{Microsoft::Common::Unicode::Utf16ToUtf8(text)}); }); + result.Resolve(std::string{Microsoft::Common::Unicode::Utf16ToUtf8(text)}); break; } case AsyncStatus::Canceled: { - jsDispatcher.Post([result] { result.Reject(React::ReactError()); }); + result.Reject(React::ReactError()); break; } case AsyncStatus::Error: { auto message = std::wstring(winrt::hresult_error(asyncOp.ErrorCode()).message()); - jsDispatcher.Post([result, message] { result.Reject(message.c_str()); }); + result.Reject(message.c_str()); break; } case AsyncStatus::Started: { diff --git a/vnext/Microsoft.ReactNative/Modules/ImageViewManagerModule.cpp b/vnext/Microsoft.ReactNative/Modules/ImageViewManagerModule.cpp index 1eb3e98e638..bf403ea1e49 100644 --- a/vnext/Microsoft.ReactNative/Modules/ImageViewManagerModule.cpp +++ b/vnext/Microsoft.ReactNative/Modules/ImageViewManagerModule.cpp @@ -109,14 +109,10 @@ void ImageLoader::getSize(std::string uri, React::ReactPromise{width, height}); - }); + [result](double width, double height) noexcept { + result.Resolve(std::vector{width, height}); }, - [result, context]() noexcept { - context.JSDispatcher().Post([result = std::move(result)]() noexcept { result.Reject("Failed"); }); - } + [result]() noexcept { result.Reject("Failed"); } #ifdef USE_FABRIC , IsFabricEnabled(context.Properties().Handle()) @@ -138,15 +134,10 @@ void ImageLoader::getSizeWithHeaders( context.Properties().Handle(), std::move(uri), std::move(headers), - [result, context](double width, double height) noexcept { - context.JSDispatcher().Post([result = std::move(result), width, height]() noexcept { - result.Resolve( - Microsoft::ReactNativeSpecs::ImageLoaderIOSSpec_getSizeWithHeaders_returnType{width, height}); - }); + [result](double width, double height) noexcept { + result.Resolve(Microsoft::ReactNativeSpecs::ImageLoaderIOSSpec_getSizeWithHeaders_returnType{width, height}); }, - [result, context]() noexcept { - context.JSDispatcher().Post([result = std::move(result)]() noexcept { result.Reject("Failed"); }); - } + [result]() noexcept { result.Reject("Failed"); } #ifdef USE_FABRIC , IsFabricEnabled(context.Properties().Handle()) diff --git a/vnext/Microsoft.ReactNative/Modules/NativeUIManager.cpp b/vnext/Microsoft.ReactNative/Modules/NativeUIManager.cpp index 02079637721..0a4d876bba9 100644 --- a/vnext/Microsoft.ReactNative/Modules/NativeUIManager.cpp +++ b/vnext/Microsoft.ReactNative/Modules/NativeUIManager.cpp @@ -998,14 +998,14 @@ void NativeUIManager::measure( auto feView = view.try_as(); if (feView == nullptr) { - m_context.JSDispatcher().Post([callback = std::move(callback)]() { callback(0, 0, 0, 0, 0, 0); }); + callback(0, 0, 0, 0, 0, 0); return; } // Retrieve the XAML element for the root view containing this view auto feRootView = static_cast(shadowRoot).GetView().try_as(); if (feRootView == nullptr) { - m_context.JSDispatcher().Post([callback = std::move(callback)]() { callback(0, 0, 0, 0, 0, 0); }); + callback(0, 0, 0, 0, 0, 0); return; } @@ -1015,9 +1015,7 @@ void NativeUIManager::measure( // this is exactly, but it is not used anyway. // Either codify this non-use or determine if and how we can send the needed // data. - m_context.JSDispatcher().Post([callback = std::move(callback), react = rectInParentCoords]() { - callback(0, 0, react.Width, react.Height, react.X, react.Y); - }); + callback(0, 0, rectInParentCoords.Width, rectInParentCoords.Height, rectInParentCoords.X, rectInParentCoords.Y); } void NativeUIManager::measureInWindow( @@ -1033,14 +1031,11 @@ void NativeUIManager::measureInWindow( auto windowTransform = view.TransformToVisual(nullptr); auto positionInWindow = windowTransform.TransformPoint({0, 0}); - m_context.JSDispatcher().Post( - [callback = std::move(callback), pos = positionInWindow, w = view.ActualWidth(), h = view.ActualHeight()]() { - callback(pos.X, pos.Y, w, h); - }); + callback(positionInWindow.X, positionInWindow.Y, view.ActualWidth(), view.ActualHeight()); return; } - m_context.JSDispatcher().Post([callback = std::move(callback)]() { callback(0, 0, 0, 0); }); + callback(0, 0, 0, 0); } void NativeUIManager::measureLayout( @@ -1060,15 +1055,11 @@ void NativeUIManager::measureLayout( const auto height = static_cast(targetElement.ActualHeight()); const auto transformedBounds = ancestorTransform.TransformBounds(winrt::Rect(0, 0, width, height)); - m_context.JSDispatcher().Post([callback = std::move(callback), rect = transformedBounds]() { - callback(rect.X, rect.Y, rect.Width, rect.Height); - }); + callback(transformedBounds.X, transformedBounds.Y, transformedBounds.Width, transformedBounds.Height); } catch (winrt::hresult_error const &e) { - m_context.JSDispatcher().Post([errorCallback = std::move(errorCallback), msg = e.message()]() { - auto writer = React::MakeJSValueTreeWriter(); - writer.WriteString(msg); - errorCallback(React::TakeJSValue(writer)); - }); + auto writer = React::MakeJSValueTreeWriter(); + writer.WriteString(e.message()); + errorCallback(React::TakeJSValue(writer)); } } @@ -1082,7 +1073,7 @@ void NativeUIManager::findSubviewIn( auto rootUIView = view.try_as(); if (rootUIView == nullptr) { - m_context.JSDispatcher().Post([callback = std::move(callback)]() { callback(0, 0, 0, 0, 0); }); + callback(0, 0, 0, 0, 0); return; } @@ -1111,14 +1102,12 @@ void NativeUIManager::findSubviewIn( } if (foundElement == nullptr) { - m_context.JSDispatcher().Post([callback = std::move(callback)]() { callback(0, 0, 0, 0, 0); }); + callback(0, 0, 0, 0, 0); return; } - m_context.JSDispatcher().Post( - [callback = std::move(callback), foundTag, box = GetRectOfElementInParentCoords(foundElement, rootUIView)]() { - callback(static_cast(foundTag), box.X, box.Y, box.Width, box.Height); - }); + auto box = GetRectOfElementInParentCoords(foundElement, rootUIView); + callback(static_cast(foundTag), box.X, box.Y, box.Width, box.Height); } void NativeUIManager::focus(int64_t reactTag) { diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp index 057beb22bbe..7941f125f45 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp @@ -4,6 +4,7 @@ #include "ReactInstanceWin.h" #include +#include #include #include #include @@ -64,6 +65,7 @@ #include #include #include +#include #endif #if !defined(CORE_ABI) && !defined(USE_FABRIC) @@ -641,15 +643,10 @@ void ReactInstanceWin::InitializeBridgeless() noexcept { // null moduleProvider since native modules are not supported in bridgeless LoadModules(devSettings, nullptr, m_options.TurboModuleProvider); - auto jsDispatchQueue = - Mso::DispatchQueue::MakeLooperQueue(CreateDispatchQueueSettings(m_reactContext->Notifications())); - auto jsDispatcher = - winrt::make(Mso::Copy(jsDispatchQueue)); - m_options.Properties.Set(ReactDispatcherHelper::JSDispatcherProperty(), jsDispatcher); - m_jsMessageThread.Exchange(std::make_shared( - jsDispatchQueue, Mso::MakeWeakMemberFunctor(this, &ReactInstanceWin::OnError))); + auto jsMessageThread = std::make_shared(); + m_jsMessageThread.Exchange(jsMessageThread); - m_jsDispatchQueue.Exchange(std::move(jsDispatchQueue)); + std::shared_ptr callInvoker; m_jsMessageThread.Load()->runOnQueueSync([&]() { SetJSThreadDescription(); @@ -672,11 +669,11 @@ void ReactInstanceWin::InitializeBridgeless() noexcept { } m_jsiRuntimeHolder = std::make_shared( - devSettings, m_jsMessageThread.Load(), CreatePreparedScriptStore()); + devSettings, jsMessageThread, CreatePreparedScriptStore()); auto jsRuntime = std::make_unique(m_jsiRuntimeHolder); jsRuntime->getRuntime(); m_bridgelessReactInstance = std::make_unique( - std::move(jsRuntime), m_jsMessageThread.Load(), timerManager, jsErrorHandlingFunc); + std::move(jsRuntime), jsMessageThread, timerManager, jsErrorHandlingFunc); auto bufferedRuntimeExecutor = m_bridgelessReactInstance->getBufferedRuntimeExecutor(); timerManager->setRuntimeExecutor(bufferedRuntimeExecutor); @@ -684,6 +681,14 @@ void ReactInstanceWin::InitializeBridgeless() noexcept { Microsoft::ReactNative::SchedulerSettings::SetRuntimeScheduler( winrt::Microsoft::ReactNative::ReactPropertyBag(m_options.Properties), m_bridgelessReactInstance->getRuntimeScheduler()); + + callInvoker = std::make_shared( + m_bridgelessReactInstance->getRuntimeScheduler()); + + winrt::Microsoft::ReactNative::implementation::CallInvoker::SetProperties( + ReactPropertyBag(m_options.Properties), + winrt::make( + *m_reactContext, std::shared_ptr(callInvoker))); }); m_options.TurboModuleProvider->SetReactContext( @@ -701,10 +706,8 @@ void ReactInstanceWin::InitializeBridgeless() noexcept { }; facebook::react::bindNativeLogger(runtime, logger); - auto turboModuleManager = std::make_shared( - m_options.TurboModuleProvider, - std::make_shared( - m_bridgelessReactInstance->getRuntimeScheduler())); + auto turboModuleManager = + std::make_shared(m_options.TurboModuleProvider, callInvoker); auto binding = [turboModuleManager](const std::string &name) -> std::shared_ptr { @@ -1087,7 +1090,6 @@ Mso::Future ReactInstanceWin::Destroy() noexcept { #ifdef USE_FABRIC if (m_bridgelessReactInstance) { - auto jsDispatchQueue = m_jsDispatchQueue.Exchange(nullptr); if (auto jsMessageThread = m_jsMessageThread.Exchange(nullptr)) { jsMessageThread->runOnQueueSync([&]() noexcept { { @@ -1099,9 +1101,6 @@ Mso::Future ReactInstanceWin::Destroy() noexcept { } this->m_bridgelessReactInstance = nullptr; jsMessageThread->quitSynchronous(); - if (jsDispatchQueue) { - jsDispatchQueue.Shutdown(PendingTaskAction::Complete); - } m_whenDestroyed.SetValue(); }); } @@ -1122,13 +1121,19 @@ ReactInstanceState ReactInstanceWin::State() const noexcept { void ReactInstanceWin::InitJSMessageThread() noexcept { m_instance.Exchange(std::make_shared()); + auto callInvoker = m_instance.Load()->getJSCallInvoker(); auto scheduler = Mso::MakeJSCallInvokerScheduler( CreateDispatchQueueSettings(m_reactContext->Notifications()), - m_instance.Load()->getJSCallInvoker(), + std::shared_ptr(callInvoker), Mso::MakeWeakMemberFunctor(this, &ReactInstanceWin::OnError), Mso::Copy(m_whenDestroyed)); auto jsDispatchQueue = Mso::DispatchQueue::MakeCustomQueue(Mso::CntPtr(scheduler)); + winrt::Microsoft::ReactNative::implementation::CallInvoker::SetProperties( + ReactPropertyBag(m_options.Properties), + winrt::make( + *m_reactContext, std::shared_ptr(callInvoker))); + auto jsDispatcher = winrt::make(Mso::Copy(jsDispatchQueue)); m_options.Properties.Set(ReactDispatcherHelper::JSDispatcherProperty(), jsDispatcher); diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h index 69a00df3de5..36c153df4e9 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h @@ -187,7 +187,7 @@ class ReactInstanceWin final : public Mso::ActiveObject mutable std::mutex m_mutex; // !Bridgeless - const Mso::ActiveReadableField m_jsDispatchQueue{Queue(), m_mutex}; + const Mso::ActiveReadableField m_jsDispatchQueue{nullptr, Queue(), m_mutex}; const Mso::ActiveReadableField> m_jsMessageThread{ Queue(), m_mutex}; diff --git a/vnext/Microsoft.ReactNative/ReactInstanceSettings.cpp b/vnext/Microsoft.ReactNative/ReactInstanceSettings.cpp index ce0b63b82f4..943def44914 100644 --- a/vnext/Microsoft.ReactNative/ReactInstanceSettings.cpp +++ b/vnext/Microsoft.ReactNative/ReactInstanceSettings.cpp @@ -20,6 +20,12 @@ winrt::Microsoft::ReactNative::IReactContext InstanceCreatedEventArgs::Context() return m_context; } +winrt::Windows::Foundation::IInspectable InstanceCreatedEventArgs::RuntimeHandle() noexcept { + return winrt::get_self(m_context) + ->GetInner() + .JsiRuntime(); +} + InstanceLoadedEventArgs::InstanceLoadedEventArgs(Mso::CntPtr &&context, bool failed) : m_context(winrt::make(std::move(context))), m_failed(failed) {} @@ -31,6 +37,12 @@ winrt::Microsoft::ReactNative::IReactContext InstanceLoadedEventArgs::Context() return m_context; } +winrt::Windows::Foundation::IInspectable InstanceLoadedEventArgs::RuntimeHandle() noexcept { + return winrt::get_self(m_context) + ->GetInner() + .JsiRuntime(); +} + InstanceDestroyedEventArgs::InstanceDestroyedEventArgs(Mso::CntPtr &&context) : m_context(winrt::make(std::move(context))) {} diff --git a/vnext/Microsoft.ReactNative/ReactInstanceSettings.h b/vnext/Microsoft.ReactNative/ReactInstanceSettings.h index 042b5cdc896..43740c8a630 100644 --- a/vnext/Microsoft.ReactNative/ReactInstanceSettings.h +++ b/vnext/Microsoft.ReactNative/ReactInstanceSettings.h @@ -19,6 +19,7 @@ struct InstanceCreatedEventArgs : InstanceCreatedEventArgsT &&context); winrt::Microsoft::ReactNative::IReactContext Context() noexcept; + winrt::Windows::Foundation::IInspectable RuntimeHandle() noexcept; private: winrt::Microsoft::ReactNative::IReactContext m_context; @@ -29,6 +30,7 @@ struct InstanceLoadedEventArgs : InstanceLoadedEventArgsT &&context, bool failed); winrt::Microsoft::ReactNative::IReactContext Context() noexcept; + winrt::Windows::Foundation::IInspectable RuntimeHandle() noexcept; bool Failed() noexcept; private: diff --git a/vnext/Microsoft.ReactNative/ReactInstanceSettings.idl b/vnext/Microsoft.ReactNative/ReactInstanceSettings.idl index f3703cdc038..be1cdb8afb8 100644 --- a/vnext/Microsoft.ReactNative/ReactInstanceSettings.idl +++ b/vnext/Microsoft.ReactNative/ReactInstanceSettings.idl @@ -45,6 +45,9 @@ namespace Microsoft.ReactNative { DOC_STRING("Gets the @IReactContext for the React instance that was just created.") IReactContext Context { get; }; + + DOC_STRING("Provides access to the jsi::Runtime for synchronous access using GetOrCreateContextRuntime") + Object RuntimeHandle { get; }; } [webhosthidden] @@ -57,6 +60,9 @@ namespace Microsoft.ReactNative DOC_STRING("Returns `true` if the JavaScript bundle failed to load.") Boolean Failed { get; }; + + DOC_STRING("Provides access to the jsi::Runtime for synchronous access using GetOrCreateContextRuntime") + Object RuntimeHandle { get; }; } [webhosthidden] diff --git a/vnext/Microsoft.ReactNative/SchedulerSettings.cpp b/vnext/Microsoft.ReactNative/SchedulerSettings.cpp index 605af5a3829..f4c842ae66c 100644 --- a/vnext/Microsoft.ReactNative/SchedulerSettings.cpp +++ b/vnext/Microsoft.ReactNative/SchedulerSettings.cpp @@ -49,17 +49,17 @@ facebook::react::RuntimeExecutor &GetRuntimeExecutor( } winrt::Microsoft::ReactNative::ReactPropertyId< - winrt::Microsoft::ReactNative::ReactNonAbiValue>> + winrt::Microsoft::ReactNative::ReactNonAbiValue>> RuntimeSchedulerProperty() noexcept { winrt::Microsoft::ReactNative::ReactPropertyId< - winrt::Microsoft::ReactNative::ReactNonAbiValue>> + winrt::Microsoft::ReactNative::ReactNonAbiValue>> propId{L"ReactNative", L"RuntimeScheduler"}; return propId; } void SetRuntimeScheduler( winrt::Microsoft::ReactNative::ReactPropertyBag properties, - const std::shared_ptr &runtimeScheduler) noexcept { + const std::weak_ptr &runtimeScheduler) noexcept { properties.Set(RuntimeSchedulerProperty(), runtimeScheduler); } @@ -68,7 +68,7 @@ std::shared_ptr RuntimeSchedulerFromPropertie auto runtimeScheduler = properties.Get(RuntimeSchedulerProperty()); if (!runtimeScheduler) return nullptr; - return runtimeScheduler.Value(); + return runtimeScheduler.Value().lock(); } } // namespace Microsoft::ReactNative::SchedulerSettings diff --git a/vnext/Microsoft.ReactNative/SchedulerSettings.h b/vnext/Microsoft.ReactNative/SchedulerSettings.h index dec7b296438..f92dd98df59 100644 --- a/vnext/Microsoft.ReactNative/SchedulerSettings.h +++ b/vnext/Microsoft.ReactNative/SchedulerSettings.h @@ -27,7 +27,7 @@ facebook::react::RuntimeExecutor &GetRuntimeExecutor( void SetRuntimeScheduler( winrt::Microsoft::ReactNative::ReactPropertyBag properties, - const std::shared_ptr &runtimeScheduler) noexcept; + const std::weak_ptr &runtimeScheduler) noexcept; std::shared_ptr RuntimeSchedulerFromProperties( winrt::Microsoft::ReactNative::ReactPropertyBag properties) noexcept; diff --git a/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp b/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp index 294e48b4f55..a89031616e9 100644 --- a/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp +++ b/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp @@ -7,6 +7,7 @@ #include "pch.h" #include "TurboModulesProvider.h" +#include #include #include #include "CallInvokerWriter.h" @@ -42,6 +43,14 @@ struct TurboModuleBuilder : winrt::implements(m_reactContext) + ->GetInner() + .JsiRuntime()); + } + void AddConstantProvider(ConstantProviderDelegate const &constantProvider) noexcept { EnsureMemberNotSet("getConstants", false); m_constantProviders.push_back(constantProvider); @@ -119,6 +128,10 @@ class TurboModuleImpl : public facebook::react::TurboModule { m_moduleBuilder(winrt::make_self(reactContext)), m_providedModule(reactModuleProvider(m_moduleBuilder.as())) { if (auto hostObject = m_providedModule.try_as()) { + // Force ABI runtime creation if it hasn't already been created + winrt::get_self(m_reactContext) + ->GetInner() + .JsiRuntime(); m_hostObjectWrapper = std::make_shared(hostObject); } } diff --git a/vnext/PropertySheets/React.Cpp.props b/vnext/PropertySheets/React.Cpp.props index fe6b132e839..d5ca9ebe3df 100644 --- a/vnext/PropertySheets/React.Cpp.props +++ b/vnext/PropertySheets/React.Cpp.props @@ -155,6 +155,7 @@ $(ReactNativeDir)\ReactCommon\react\bridging; $(ReactNativeDir)\ReactCommon\reactperflogger; $(ReactNativeDir)\ReactCommon\runtimeexecutor; + $(ReactNativeDir)\ReactCxxPlatform; %(AdditionalIncludeDirectories); diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jserrorhandler/JsErrorHandler.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jserrorhandler/JsErrorHandler.cpp new file mode 100644 index 00000000000..fc5ff6cfabc --- /dev/null +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jserrorhandler/JsErrorHandler.cpp @@ -0,0 +1,429 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "JsErrorHandler.h" +#include +#include +#include +#include +#include +#include "StackTraceParser.h" + +using namespace facebook; + +namespace { +std::string quote(const std::string& view) { + return "\"" + view + "\""; +} + +int nextExceptionId() { + static int exceptionId = 0; + return exceptionId++; +} + +bool isLooselyNull(const jsi::Value& value) { + return value.isNull() || value.isUndefined(); +} + +bool isEqualTo( + jsi::Runtime& runtime, + const jsi::Value& value, + const std::string& str) { + return jsi::Value::strictEquals( + runtime, value, jsi::String::createFromUtf8(runtime, str)); +} + +std::string stringifyToCpp(jsi::Runtime& runtime, const jsi::Value& value) { + return value.toString(runtime).utf8(runtime); +} + +bool isTruthy(jsi::Runtime& runtime, const jsi::Value& value) { + auto Boolean = runtime.global().getPropertyAsFunction(runtime, "Boolean"); + return Boolean.call(runtime, value).getBool(); +} + +void objectAssign( + jsi::Runtime& runtime, + jsi::Object& target, + const jsi::Object& value) { + auto Object = runtime.global().getPropertyAsObject(runtime, "Object"); + auto assign = Object.getPropertyAsFunction(runtime, "assign"); + assign.callWithThis(runtime, Object, target, value); +} + +jsi::Object wrapInErrorIfNecessary( + jsi::Runtime& runtime, + const jsi::Value& value) { + auto Error = runtime.global().getPropertyAsFunction(runtime, "Error"); + auto isError = + value.isObject() && value.asObject(runtime).instanceOf(runtime, Error); + auto error = isError + ? value.getObject(runtime) + : Error.callAsConstructor(runtime, value).getObject(runtime); + return error; +} + +class SetFalseOnDestruct { + std::shared_ptr _value; + + public: + SetFalseOnDestruct(const SetFalseOnDestruct&) = delete; + SetFalseOnDestruct& operator=(const SetFalseOnDestruct&) = delete; + SetFalseOnDestruct(SetFalseOnDestruct&&) = delete; + SetFalseOnDestruct& operator=(SetFalseOnDestruct&&) = delete; + explicit SetFalseOnDestruct(std::shared_ptr value) + : _value(std::move(value)) {} + ~SetFalseOnDestruct() { + *_value = false; + } +}; + +void logErrorWhileReporting( + std::string message, + jsi::JSError& error, + jsi::JSError& originalError) { + LOG(ERROR) << "JsErrorHandler::" << message << std::endl + << "Js error message: " << error.getMessage() << std::endl + << "Original js error message: " << originalError.getMessage() + << std::endl; +} + +jsi::Value getBundleMetadata(jsi::Runtime& runtime, jsi::JSError& error) { + auto jsGetBundleMetadataValue = + runtime.global().getProperty(runtime, "__getBundleMetadata"); + + if (!jsGetBundleMetadataValue.isObject() || + !jsGetBundleMetadataValue.asObject(runtime).isFunction(runtime)) { + return jsi::Value::null(); + } + + auto jsGetBundleMetadataValueFn = + jsGetBundleMetadataValue.asObject(runtime).asFunction(runtime); + + try { + auto bundleMetadataValue = jsGetBundleMetadataValueFn.call(runtime); + if (bundleMetadataValue.isObject()) { + return bundleMetadataValue; + } + return bundleMetadataValue; + } catch (jsi::JSError& ex) { + logErrorWhileReporting( + "getBundleMetadata(): Error raised while calling __getBundleMetadata(). Returning null.", + ex, + error); + } + + return jsi::Value::null(); +} +} // namespace + +namespace facebook::react { + +template <> +struct Bridging { + static jsi::Value toJs( + jsi::Runtime& runtime, + const JsErrorHandler::ProcessedError::StackFrame& frame) { + auto stackFrame = jsi::Object(runtime); + auto file = bridging::toJs(runtime, frame.file, nullptr); + auto lineNumber = bridging::toJs(runtime, frame.lineNumber, nullptr); + auto column = bridging::toJs(runtime, frame.column, nullptr); + + stackFrame.setProperty(runtime, "file", file); + stackFrame.setProperty(runtime, "methodName", frame.methodName); + stackFrame.setProperty(runtime, "lineNumber", lineNumber); + stackFrame.setProperty(runtime, "column", column); + return stackFrame; + } +}; + +template <> +struct Bridging { + static jsi::Value toJs( + jsi::Runtime& runtime, + const JsErrorHandler::ProcessedError& error) { + auto data = jsi::Object(runtime); + data.setProperty(runtime, "message", error.message); + data.setProperty( + runtime, + "originalMessage", + bridging::toJs(runtime, error.originalMessage, nullptr)); + data.setProperty( + runtime, "name", bridging::toJs(runtime, error.name, nullptr)); + data.setProperty( + runtime, + "componentStack", + bridging::toJs(runtime, error.componentStack, nullptr)); + + auto stack = jsi::Array(runtime, error.stack.size()); + for (size_t i = 0; i < error.stack.size(); i++) { + auto& frame = error.stack[i]; + stack.setValueAtIndex(runtime, i, bridging::toJs(runtime, frame)); + } + + data.setProperty(runtime, "stack", stack); + data.setProperty(runtime, "id", error.id); + data.setProperty(runtime, "isFatal", error.isFatal); + data.setProperty(runtime, "extraData", error.extraData); + return data; + } +}; + +std::ostream& operator<<( + std::ostream& os, + const JsErrorHandler::ProcessedError::StackFrame& frame) { + auto file = frame.file ? quote(*frame.file) : "nil"; + auto methodName = quote(frame.methodName); + auto lineNumber = + frame.lineNumber ? std::to_string(*frame.lineNumber) : "nil"; + auto column = frame.column ? std::to_string(*frame.column) : "nil"; + + os << "StackFrame { .file = " << file << ", .methodName = " << methodName + << ", .lineNumber = " << lineNumber << ", .column = " << column << " }"; + return os; +} +std::ostream& operator<<( + std::ostream& os, + const JsErrorHandler::ProcessedError& error) { + auto message = quote(error.message); + auto originalMessage = + error.originalMessage ? quote(*error.originalMessage) : "nil"; + auto name = error.name ? quote(*error.name) : "nil"; + auto componentStack = + error.componentStack ? quote(*error.componentStack) : "nil"; + auto id = std::to_string(error.id); + auto isFatal = std::to_string(static_cast(error.isFatal)); + auto extraData = "jsi::Object{ } "; + + os << "ProcessedError {\n" + << " .message = " << message << "\n" + << " .originalMessage = " << originalMessage << "\n" + << " .name = " << name << "\n" + << " .componentStack = " << componentStack << "\n" + << " .stack = [\n"; + + for (const auto& frame : error.stack) { + os << " " << frame << ", \n"; + } + os << " ]\n" + << " .id = " << id << "\n" + << " .isFatal " << isFatal << "\n" + << " .extraData = " << extraData << "\n" + << "}"; + return os; +} + +JsErrorHandler::JsErrorHandler(JsErrorHandler::OnJsError onJsError) + : _onJsError(std::move(onJsError)), + _inErrorHandler(std::make_shared(false)){ + + }; + +JsErrorHandler::~JsErrorHandler() {} + +void JsErrorHandler::handleError( + jsi::Runtime& runtime, + jsi::JSError& error, + bool isFatal, + bool logToConsole) { + // TODO: Current error parsing works and is stable. Can investigate using + // REGEX_HERMES to get additional Hermes data, though it requires JS setup + + if (!ReactNativeFeatureFlags::useAlwaysAvailableJSErrorHandling() && + _isRuntimeReady) { + try { + handleJSError(runtime, error, isFatal); + return; + } catch (jsi::JSError& ex) { + logErrorWhileReporting( + "handleError(): Error raised while reporting using js pipeline. Using c++ pipeline instead.", + ex, + error); + + // Re-try reporting using the c++ pipeline + _hasHandledFatalError = false; + } + } + + handleErrorWithCppPipeline(runtime, error, isFatal, logToConsole); +} + +void JsErrorHandler::handleErrorWithCppPipeline( + jsi::Runtime& runtime, + jsi::JSError& error, + bool isFatal, + bool logToConsole) { + *_inErrorHandler = true; + SetFalseOnDestruct temp{_inErrorHandler}; + + auto message = error.getMessage(); + auto errorObj = wrapInErrorIfNecessary(runtime, error.value()); + auto componentStackValue = errorObj.getProperty(runtime, "componentStack"); + if (!isLooselyNull(componentStackValue)) { + message += "\n" + stringifyToCpp(runtime, componentStackValue); + } + + auto nameValue = errorObj.getProperty(runtime, "name"); + auto name = (isLooselyNull(nameValue) || isEqualTo(runtime, nameValue, "")) + ? std::nullopt + : std::optional(stringifyToCpp(runtime, nameValue)); + + if (name && !message.starts_with(*name + ": ")) { + message = *name + ": " + message; + } + + auto jsEngineValue = errorObj.getProperty(runtime, "jsEngine"); + + if (!isLooselyNull(jsEngineValue)) { + message += ", js engine: " + stringifyToCpp(runtime, jsEngineValue); + } + + auto extraDataKey = jsi::PropNameID::forUtf8(runtime, "RN$ErrorExtraDataKey"); + auto extraDataValue = errorObj.getProperty(runtime, extraDataKey); + + auto extraData = jsi::Object(runtime); + if (extraDataValue.isObject()) { + objectAssign(runtime, extraData, extraDataValue.asObject(runtime)); + } + + auto isDEV = + isTruthy(runtime, runtime.global().getProperty(runtime, "__DEV__")); + + extraData.setProperty(runtime, "jsEngine", jsEngineValue); + extraData.setProperty(runtime, "rawStack", error.getStack()); + extraData.setProperty(runtime, "__DEV__", isDEV); + extraData.setProperty( + runtime, "bundleMetadata", getBundleMetadata(runtime, error)); + + auto cause = errorObj.getProperty(runtime, "cause"); + if (cause.isObject()) { + auto causeObj = cause.asObject(runtime); + // TODO: Consider just forwarding all properties. For now, just forward the + // stack properties to maintain symmetry with js pipeline + auto stackSymbols = causeObj.getProperty(runtime, "stackSymbols"); + extraData.setProperty(runtime, "stackSymbols", stackSymbols); + + auto stackReturnAddresses = + causeObj.getProperty(runtime, "stackReturnAddresses"); + extraData.setProperty( + runtime, "stackReturnAddresses", stackReturnAddresses); + + auto stackElements = causeObj.getProperty(runtime, "stackElements"); + extraData.setProperty(runtime, "stackElements", stackElements); + } + + auto originalMessage = message == error.getMessage() + ? std::nullopt + : std::optional(error.getMessage()); + + auto componentStack = !componentStackValue.isString() + ? std::nullopt + : std::optional(componentStackValue.asString(runtime).utf8(runtime)); + + auto isHermes = runtime.global().hasProperty(runtime, "HermesInternal"); + auto stackFrames = StackTraceParser::parse(isHermes, error.getStack()); + + auto id = nextExceptionId(); + + ProcessedError processedError = { + .message = + _isRuntimeReady ? message : ("[runtime not ready]: " + message), + .originalMessage = originalMessage, + .name = name, + .componentStack = componentStack, + .stack = stackFrames, + .id = id, + .isFatal = isFatal, + .extraData = std::move(extraData), + }; + + auto data = bridging::toJs(runtime, processedError).asObject(runtime); + + auto isComponentError = + isTruthy(runtime, errorObj.getProperty(runtime, "isComponentError")); + data.setProperty(runtime, "isComponentError", isComponentError); + + if (logToConsole && runtime.global().hasProperty(runtime, "console")) { // [Windows] Added hasProperty check + auto console = runtime.global().getPropertyAsObject(runtime, "console"); + auto errorFn = console.getPropertyAsFunction(runtime, "error"); + auto finalMessage = + jsi::String::createFromUtf8(runtime, processedError.message); + errorFn.callWithThis(runtime, console, finalMessage); + } + + std::shared_ptr shouldPreventDefault = std::make_shared(false); + auto preventDefault = jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "preventDefault"), + 0, + [shouldPreventDefault]( + jsi::Runtime& /*rt*/, + const jsi::Value& /*thisVal*/, + const jsi::Value* /*args*/, + size_t /*count*/) { + *shouldPreventDefault = true; + return jsi::Value::undefined(); + }); + + data.setProperty(runtime, "preventDefault", preventDefault); + + for (auto& errorListener : _errorListeners) { + try { + errorListener(runtime, jsi::Value(runtime, data)); + } catch (jsi::JSError& ex) { + logErrorWhileReporting( + "handleErrorWithCppPipeline(): Error raised inside an error listener. Executing next listener.", + ex, + error); + } + } + + if (*shouldPreventDefault) { + return; + } + + auto errorType = errorObj.getProperty(runtime, "type"); + auto isWarn = isEqualTo(runtime, errorType, "warn"); + + if (isFatal || !isWarn) { + if (isFatal) { + if (_hasHandledFatalError) { + return; + } + _hasHandledFatalError = true; + } + + _onJsError(runtime, processedError); + } +} + +void JsErrorHandler::registerErrorListener( + const std::function& errorListener) { + _errorListeners.push_back(errorListener); +} + +bool JsErrorHandler::hasHandledFatalError() { + return _hasHandledFatalError; +} + +void JsErrorHandler::setRuntimeReady() { + _isRuntimeReady = true; +} + +bool JsErrorHandler::isRuntimeReady() { + return _isRuntimeReady; +} + +void JsErrorHandler::notifyOfFatalError() { + _hasHandledFatalError = true; +} + +bool JsErrorHandler::inErrorHandler() { + return *_inErrorHandler; +} + +} // namespace facebook::react diff --git a/vnext/Shared/Modules/BlobModule.cpp b/vnext/Shared/Modules/BlobModule.cpp index d54220cdb61..d0754c15db1 100644 --- a/vnext/Shared/Modules/BlobModule.cpp +++ b/vnext/Shared/Modules/BlobModule.cpp @@ -35,28 +35,26 @@ namespace Microsoft::React { #pragma region BlobTurboModule -void BlobTurboModule::Initialize(msrn::ReactContext const &reactContext) noexcept { +void BlobTurboModule::Initialize(msrn::ReactContext const &reactContext, facebook::jsi::Runtime &runtime) noexcept { m_resource = IBlobResource::Make(reactContext.Properties().Handle()); m_resource->Callbacks().OnError = [&reactContext](string &&errorText) { Modules::SendEvent(reactContext, L"blobFailed", {errorText}); }; namespace jsi = facebook::jsi; - msrn::ExecuteJsi(reactContext, [resource = m_resource](jsi::Runtime &runtime) { - runtime.global().setProperty( - runtime, - "__blobCollectorProvider", - jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "__blobCollectorProvider"), - 1, - [resource](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { - auto blobId = args[0].asString(rt).utf8(rt); - auto collector = std::make_shared(blobId, resource); - - return jsi::Object::createFromHostObject(rt, collector); - })); - }); + runtime.global().setProperty( + runtime, + "__blobCollectorProvider", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "__blobCollectorProvider"), + 1, + [resource = m_resource](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { + auto blobId = args[0].asString(rt).utf8(rt); + auto collector = std::make_shared(blobId, resource); + + return jsi::Object::createFromHostObject(rt, collector); + })); } ReactNativeSpecs::BlobModuleSpec_Constants BlobTurboModule::GetConstants() noexcept { diff --git a/vnext/Shared/Modules/BlobModule.h b/vnext/Shared/Modules/BlobModule.h index 382129d202d..a541c8f7752 100644 --- a/vnext/Shared/Modules/BlobModule.h +++ b/vnext/Shared/Modules/BlobModule.h @@ -24,7 +24,9 @@ struct BlobTurboModule { using ModuleSpec = ReactNativeSpecs::BlobModuleSpec; REACT_INIT(Initialize) - void Initialize(winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept; + void Initialize( + winrt::Microsoft::ReactNative::ReactContext const &reactContext, + facebook::jsi::Runtime &runtime) noexcept; REACT_GET_CONSTANTS(GetConstants) ReactNativeSpecs::BlobModuleSpec_Constants GetConstants() noexcept; diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index 6b4703eb94e..ae3676e070b 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -342,6 +342,7 @@ $(MSBuildThisFileDirectory)..\Microsoft.ReactNative\JsiApi.idl + true @@ -672,6 +673,8 @@ + + diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index 9a6c83c196c..1bc977f8e60 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -342,6 +342,7 @@ +