diff --git a/change/react-native-windows-6a82bdc7-fdc0-4d57-bd42-cb297ef7a7ea.json b/change/react-native-windows-6a82bdc7-fdc0-4d57-bd42-cb297ef7a7ea.json new file mode 100644 index 00000000000..1a52644fae8 --- /dev/null +++ b/change/react-native-windows-6a82bdc7-fdc0-4d57-bd42-cb297ef7a7ea.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement SampleTurboModule", + "packageName": "react-native-windows", + "email": "jthysell@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.cpp b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.cpp index 9f6d4093a48..38bba3ed362 100644 --- a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.cpp +++ b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.cpp @@ -84,6 +84,8 @@ void ReactModuleBuilderMock::AddSyncMethod(hstring const &name, SyncMethodDelega m_syncMethods.emplace(name, method); } +void ReactModuleBuilderMock::AddEventEmitter(hstring const &, EventEmitterInitializerDelegate const &) noexcept {} + JSValueObject ReactModuleBuilderMock::GetConstants() noexcept { auto constantWriter = MakeJSValueTreeWriter(); constantWriter.WriteObjectBegin(); diff --git a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.h b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.h index 708a18c0030..a6e4b0d6a46 100644 --- a/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.h +++ b/vnext/Microsoft.ReactNative.Cxx.UnitTests/ReactModuleBuilderMock.h @@ -73,6 +73,7 @@ struct ReactModuleBuilderMock { 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; + void AddEventEmitter(hstring const &name, EventEmitterInitializerDelegate const &emitter) noexcept; private: MethodDelegate GetMethod0(std::wstring const &methodName) const noexcept; @@ -218,6 +219,7 @@ struct ReactModuleBuilderImpl : implements. +// It must be an instance field. +#define REACT_EVENT_EMITTER(/* field, [opt] eventEmitterName */...) \ + INTERNAL_REACT_MEMBER(__VA_ARGS__)(EventEmitterField, __VA_ARGS__) + // REACT_FUNCTION(field, [opt] functionName, [opt] moduleName) // Arguments: // - field (required) - the field name the macro is attached to. @@ -832,6 +842,35 @@ struct ModuleEventFieldInfo TModule::*> { } }; +template +struct ModuleEventEmitterFieldInfo; + +template class TFunc, class... TArgs> +struct ModuleEventEmitterFieldInfo TModule::*> { + using ModuleType = TModule; + using EventType = TFunc; + using FieldType = EventType TModule::*; + + static EventEmitterInitializerDelegate GetEventEmitterInitializer(void *module, FieldType field) noexcept { + return [module = static_cast(module), field](EmitEventSetterDelegate const &emitEventDelegate) { + module->*field = [emitEventDelegate](TArgs... args) noexcept { + emitEventDelegate([&args...]([[maybe_unused]] IJSValueWriter const &argWriter) noexcept { + (void)argWriter; // [[maybe_unused]] above does not work + (WriteValue(argWriter, args), ...); + }); + }; + }; + } + + static InitializerDelegate GetEventEmitterEmptyInitializer(void *module, FieldType field) noexcept { + return [module = static_cast(module), field](IReactContext const &reactContext) noexcept { + // Default emitter will do nothing + // This will be replaced with a method that will call the jsi EventEmitter when JS requests the emitter + module->*field = [](TArgs... args) noexcept {}; + }; + } +}; + template struct ModuleFunctionFieldInfo; @@ -898,6 +937,7 @@ enum class ReactMemberKind { ConstantStrongTypedMethod, ConstantField, EventField, + EventEmitterField, FunctionField, }; @@ -917,6 +957,7 @@ using ReactConstantMethodAttribute = ReactMemberAttribute; using ReactConstantFieldAttribute = ReactMemberAttribute; using ReactEventFieldAttribute = ReactMemberAttribute; +using ReactEventEmitterFieldAttribute = ReactMemberAttribute; using ReactFunctionFieldAttribute = ReactMemberAttribute; template @@ -941,7 +982,7 @@ struct ReactModuleBuilder { } void CompleteRegistration() noexcept { - // Add REACT_INIT initializers after REACT_EVENT and REACT_FUNCTION initializers. + // Add REACT_INIT initializers after REACT_EVENT, REACT_EVENT_EMITTER and REACT_FUNCTION initializers. // This way REACT_INIT method is invoked after event and function fields are initialized. for (auto &initializer : m_initializers) { m_moduleBuilder.AddInitializer(initializer); @@ -967,6 +1008,8 @@ struct ReactModuleBuilder { RegisterConstantField(member, attributeInfo.JSMemberName); } else if constexpr (std::is_same_v) { RegisterEventField(member, attributeInfo.JSMemberName, attributeInfo.JSModuleName); + } else if constexpr (std::is_same_v) { + RegisterEventEmitterField(member, attributeInfo.JSMemberName, attributeInfo.JSModuleName); } else if constexpr (std::is_same_v) { RegisterFunctionField(member, attributeInfo.JSMemberName, attributeInfo.JSModuleName); } @@ -1017,6 +1060,19 @@ struct ReactModuleBuilder { m_moduleBuilder.AddInitializer(eventHandlerInitializer); } + template + void RegisterEventEmitterField( + TField field, + std::wstring_view eventName, + std::wstring_view eventEmitterName = L"") noexcept { + auto eventHandlerEmptyInitializer = + ModuleEventEmitterFieldInfo::GetEventEmitterEmptyInitializer(m_module, field); + auto eventEmitterInitializer = ModuleEventEmitterFieldInfo::GetEventEmitterInitializer(m_module, field); + + m_moduleBuilder.AddInitializer(eventHandlerEmptyInitializer); + m_moduleBuilder.AddEventEmitter(eventName, eventEmitterInitializer); + } + template void RegisterFunctionField(TField field, std::wstring_view name, std::wstring_view moduleName = L"") noexcept { auto functionInitializer = ModuleFunctionFieldInfo::GetFunctionInitializer( diff --git a/vnext/Microsoft.ReactNative.Managed.UnitTests/ReactModuleBuilderMock.cs b/vnext/Microsoft.ReactNative.Managed.UnitTests/ReactModuleBuilderMock.cs index abb92b26ccf..d113464ce75 100644 --- a/vnext/Microsoft.ReactNative.Managed.UnitTests/ReactModuleBuilderMock.cs +++ b/vnext/Microsoft.ReactNative.Managed.UnitTests/ReactModuleBuilderMock.cs @@ -299,6 +299,8 @@ public void EmitJSEvent(string eventEmitterName, string eventName, JSValueArgWri writer.WriteArrayEnd(); m_jsEventHandler(eventEmitterName, eventName, writer.TakeValue()); } + + public void AddEventEmitter(string name, EventEmitterInitializerDelegate emitter) { } } class ReactSettingsSnapshot : IReactSettingsSnapshot diff --git a/vnext/Microsoft.ReactNative/IReactModuleBuilder.cpp b/vnext/Microsoft.ReactNative/IReactModuleBuilder.cpp index c7798550469..06f534b717d 100644 --- a/vnext/Microsoft.ReactNative/IReactModuleBuilder.cpp +++ b/vnext/Microsoft.ReactNative/IReactModuleBuilder.cpp @@ -129,6 +129,10 @@ void ReactModuleBuilder::AddSyncMethod(hstring const &name, SyncMethodDelegate c m_methods.push_back(std::move(cxxMethod)); } +void ReactModuleBuilder::AddEventEmitter(hstring const &name, EventEmitterInitializerDelegate const &emitter) noexcept { + // No-op. We only support auto generated EventEmitters in TurboModules +} + /*static*/ MethodResultCallback ReactModuleBuilder::MakeMethodResultCallback(CxxModule::Callback &&callback) noexcept { if (callback) { return [callback = std::move(callback)](const IJSValueWriter &outputWriter) noexcept { diff --git a/vnext/Microsoft.ReactNative/IReactModuleBuilder.h b/vnext/Microsoft.ReactNative/IReactModuleBuilder.h index 17914a0a1cb..f9e8154353f 100644 --- a/vnext/Microsoft.ReactNative/IReactModuleBuilder.h +++ b/vnext/Microsoft.ReactNative/IReactModuleBuilder.h @@ -15,6 +15,7 @@ struct ReactModuleBuilder : winrt::implements MakeCxxModule( diff --git a/vnext/Microsoft.ReactNative/IReactModuleBuilder.idl b/vnext/Microsoft.ReactNative/IReactModuleBuilder.idl index ca35014eb07..a80ea90880e 100644 --- a/vnext/Microsoft.ReactNative/IReactModuleBuilder.idl +++ b/vnext/Microsoft.ReactNative/IReactModuleBuilder.idl @@ -34,6 +34,12 @@ namespace Microsoft.ReactNative MethodResultCallback resolve, MethodResultCallback reject); + DOC_STRING("A delegate gets the arguments for an event emit on a EventEmitter.") + delegate void EmitEventSetterDelegate(JSValueArgWriter argWriter); + + DOC_STRING("A delegate to call a turbo module EventEmitter.") + delegate void EventEmitterInitializerDelegate(EmitEventSetterDelegate emitter); + DOC_STRING("A delegate to call native synchronous method.") delegate void SyncMethodDelegate(IJSValueReader inputReader, IJSValueWriter outputWriter); @@ -62,5 +68,8 @@ namespace Microsoft.ReactNative DOC_STRING("Adds a synchronous method to the native module. See @SyncMethodDelegate.") void AddSyncMethod(String name, SyncMethodDelegate method); + + DOC_STRING("Adds an EventEmitter to the turbo module. See @EventEmitterInitializerDelegate.") + void AddEventEmitter(String name, EventEmitterInitializerDelegate emitter); }; } // namespace Microsoft.ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/SampleTurboModule.cpp b/vnext/Microsoft.ReactNative/Modules/SampleTurboModule.cpp new file mode 100644 index 00000000000..0f9f79df318 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Modules/SampleTurboModule.cpp @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "SampleTurboModule.h" + +namespace Microsoft::ReactNative { + +void SampleTurboModule::Initialize(winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept { + m_reactContext = reactContext; +} + +ReactNativeSpecs::SampleTurboModuleSpec_Constants SampleTurboModule::GetConstants() noexcept { + ReactNativeSpecs::SampleTurboModuleSpec_Constants constants; + constants.const1 = true; + constants.const2 = 375; + constants.const3 = "something"; + return constants; +} + +void SampleTurboModule::voidFunc() noexcept { + onPress(); + onClick("click"); + + /* + SampleTurboModuleSpec_ObjectStruct will be added in a future version of SampleTurboModuleSpec + Uncomment below when we FI to that version + */ + /* + ReactNativeSpecs::SampleTurboModuleSpec_ObjectStruct obj; + obj.a = 1; + obj.b = "two"; + onChange(obj); + + ReactNativeSpecs::SampleTurboModuleSpec_ObjectStruct obj2; + obj2.a = 3; + obj2.b = "four"; + + auto writer = winrt::Microsoft::ReactNative::MakeJSValueTreeWriter(); + writer.WriteArrayBegin(); + winrt::Microsoft::ReactNative::WriteValue(writer, obj); + winrt::Microsoft::ReactNative::WriteValue(writer, obj2); + writer.WriteArrayEnd(); + onSubmit(winrt::Microsoft::ReactNative::TakeJSValue(writer).MoveArray()); + */ +} + +bool SampleTurboModule::getBool(bool arg) noexcept { + return arg; +} + +double SampleTurboModule::getEnum(double arg) noexcept { + return arg; +} + +double SampleTurboModule::getNumber(double arg) noexcept { + return arg; +} + +std::string SampleTurboModule::getString(std::string arg) noexcept { + return std::string(arg); +} + +::React::JSValueArray SampleTurboModule::getArray(::React::JSValueArray &&arg) noexcept { + return arg.Copy(); +} + +::React::JSValue SampleTurboModule::getObject(::React::JSValue &&arg) noexcept { + assert(arg.Type() == ::React::JSValueType::Object); + return arg.Copy(); +} + +::React::JSValue SampleTurboModule::getUnsafeObject(::React::JSValue &&arg) noexcept { + assert(arg.Type() == ::React::JSValueType::Object); + return arg.Copy(); +} + +double SampleTurboModule::getRootTag(double arg) noexcept { + // TODO: Proper impl + return arg; +} + +::React::JSValue SampleTurboModule::getValue(double x, std::string y, ::React::JSValue &&z) noexcept { + return ::React::JSValueObject{ + {"x", x}, + {"y", y}, + {"z", z.Copy()}, + }; +} + +void SampleTurboModule::getValueWithCallback(std::function const &callback) noexcept { + callback("value from callback!"); +} + +void SampleTurboModule::getValueWithPromise(bool error, ::React::ReactPromise &&result) noexcept { + if (error) { + result.Reject("intentional promise rejection"); + } else { + result.Resolve("result!"); + } +} + +void SampleTurboModule::voidFuncThrows() noexcept { + // TODO: Proper impl +} + +::React::JSValue SampleTurboModule::getObjectThrows(::React::JSValue &&arg) noexcept { + // TODO: Proper impl + return nullptr; +} + +void SampleTurboModule::promiseThrows(::React::ReactPromise &&result) noexcept { + // TODO: Proper impl +} + +void SampleTurboModule::voidFuncAssert() noexcept { + // TODO: Proper impl +} + +::React::JSValue SampleTurboModule::getObjectAssert(::React::JSValue &&arg) noexcept { + // TODO: Proper impl + return nullptr; +} + +void SampleTurboModule::promiseAssert(::React::ReactPromise &&result) noexcept { + // TODO: Proper impl +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/SampleTurboModule.h b/vnext/Microsoft.ReactNative/Modules/SampleTurboModule.h new file mode 100644 index 00000000000..77a4d09a623 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Modules/SampleTurboModule.h @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#include "codegen/NativeSampleTurboModuleSpec.g.h" +#include + +namespace Microsoft::ReactNative { + +REACT_MODULE(SampleTurboModule) +struct SampleTurboModule { + using ModuleSpec = ReactNativeSpecs::SampleTurboModuleSpec; + + REACT_INIT(Initialize) + void Initialize(winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept; + + REACT_EVENT_EMITTER(onPress) + std::function onPress; + + REACT_EVENT_EMITTER(onClick) + std::function onClick; + + /* + SampleTurboModuleSpec_ObjectStruct will be added in a future version of SampleTurboModuleSpec + Uncomment below when we FI to that version + */ + /* + REACT_EVENT_EMITTER(onChange) + std::function onChange; + + REACT_EVENT_EMITTER(onSubmit) + std::function onSubmit; + */ + + REACT_GET_CONSTANTS(GetConstants) + ReactNativeSpecs::SampleTurboModuleSpec_Constants GetConstants() noexcept; + + REACT_METHOD(voidFunc) + void voidFunc() noexcept; + + REACT_SYNC_METHOD(getBool) + bool getBool(bool arg) noexcept; + + REACT_SYNC_METHOD(getEnum) + double getEnum(double arg) noexcept; + + REACT_SYNC_METHOD(getNumber) + double getNumber(double arg) noexcept; + + REACT_SYNC_METHOD(getString) + std::string getString(std::string arg) noexcept; + + REACT_SYNC_METHOD(getArray) + ::React::JSValueArray getArray(::React::JSValueArray &&arg) noexcept; + + REACT_SYNC_METHOD(getObject) + ::React::JSValue getObject(::React::JSValue &&arg) noexcept; + + REACT_SYNC_METHOD(getUnsafeObject) + ::React::JSValue getUnsafeObject(::React::JSValue &&arg) noexcept; + + REACT_SYNC_METHOD(getRootTag) + double getRootTag(double arg) noexcept; + + REACT_SYNC_METHOD(getValue) + ::React::JSValue getValue(double x, std::string y, ::React::JSValue &&z) noexcept; + + REACT_METHOD(getValueWithCallback) + void getValueWithCallback(std::function const &callback) noexcept; + + REACT_METHOD(getValueWithPromise) + void getValueWithPromise(bool error, ::React::ReactPromise &&result) noexcept; + + REACT_METHOD(voidFuncThrows) + void voidFuncThrows() noexcept; + + REACT_SYNC_METHOD(getObjectThrows) + ::React::JSValue getObjectThrows(::React::JSValue &&arg) noexcept; + + REACT_METHOD(promiseThrows) + void promiseThrows(::React::ReactPromise &&result) noexcept; + + REACT_METHOD(voidFuncAssert) + void voidFuncAssert() noexcept; + + REACT_SYNC_METHOD(getObjectAssert) + ::React::JSValue getObjectAssert(::React::JSValue &&arg) noexcept; + + REACT_METHOD(promiseAssert) + void promiseAssert(::React::ReactPromise &&result) noexcept; + + private: + winrt::Microsoft::ReactNative::ReactContext m_reactContext; +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp index 12bfbb099c4..478654f4069 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp @@ -39,6 +39,7 @@ #include "Modules/ExceptionsManager.h" #include "Modules/PlatformConstantsWinModule.h" #include "Modules/ReactRootViewTagGenerator.h" +#include "Modules/SampleTurboModule.h" #include "Modules/SourceCode.h" #include "Modules/StatusBarManager.h" #include "Modules/Timing.h" @@ -411,6 +412,10 @@ void ReactInstanceWin::LoadModules( L"PlatformConstants", winrt::Microsoft::ReactNative::MakeTurboModuleProvider<::Microsoft::ReactNative::PlatformConstants>()); + registerTurboModule( + L"SampleTurboModule", + winrt::Microsoft::ReactNative::MakeTurboModuleProvider<::Microsoft::ReactNative::SampleTurboModule>()); + uint32_t hermesBytecodeVersion = 0; #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; diff --git a/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp b/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp index ae112ba8127..24292e540cc 100644 --- a/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp +++ b/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp @@ -8,7 +8,9 @@ #include "pch.h" #include "TurboModulesProvider.h" #include +#include #include "JSDispatcherWriter.h" +#include "JSValueWriter.h" #include "JsiApi.h" #include "JsiReader.h" #include "JsiWriter.h" @@ -51,6 +53,12 @@ struct TurboModuleBuilder : winrt::implements &EventEmitters() const noexcept { + return m_eventEmitters; + } + private: void EnsureMemberNotSet(const std::string &key, bool checkingMethod) noexcept { VerifyElseCrash(m_methods.find(key) == m_methods.end()); VerifyElseCrash(m_syncMethods.find(key) == m_syncMethods.end()); + VerifyElseCrash(m_eventEmitters.find(key) == m_eventEmitters.end()); if (checkingMethod && key == "getConstants") { VerifyElseCrash(m_constantProviders.size() == 0); } @@ -81,6 +94,7 @@ struct TurboModuleBuilder : winrt::implements m_eventEmitters; std::unordered_map m_methods; std::unordered_map m_syncMethods; std::vector m_constantProviders; @@ -373,6 +387,36 @@ class TurboModuleImpl : public facebook::react::TurboModule { } } + { + // try to find an event + auto it = m_moduleBuilder->EventEmitters().find(key); + if (it != m_moduleBuilder->EventEmitters().end()) { + // See if we have an existing eventEmitter + auto itEmitter = m_eventEmitters.find(key); + if (itEmitter == m_eventEmitters.end()) { + m_eventEmitters[key] = std::make_shared>(); + + itEmitter = m_eventEmitters.find(key); + + it->second([emitter = std::static_pointer_cast>( + itEmitter->second), + jsInvoker = jsInvoker_](const JSValueArgWriter &eventDelegate) { + auto argWriter = MakeJSValueTreeWriter(); + eventDelegate(argWriter); + emitter->emit( + [jsInvoker, eventDelegate, jsValue = std::make_shared(TakeJSValue(argWriter))]( + facebook::jsi::Runtime &rt) -> facebook::jsi::Value { + auto argWriter = winrt::make(rt); + WriteValue(argWriter, *jsValue); + return argWriter.as()->MoveResult(); + }); + }); + } + + return itEmitter->second->get(runtime, jsInvoker_); + } + } + // returns undefined if the expected member is not found return facebook::jsi::Value::undefined(); } @@ -408,6 +452,7 @@ class TurboModuleImpl : public facebook::react::TurboModule { IReactContext m_reactContext; winrt::com_ptr m_moduleBuilder; IInspectable m_providedModule; + std::unordered_map> m_eventEmitters; std::shared_ptr m_hostObjectWrapper; std::weak_ptr m_longLivedObjectCollection; }; diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index 806703860c7..788df25756d 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -481,6 +481,7 @@ + diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index 4a08c2e87d0..24e650996b0 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -304,6 +304,7 @@ + diff --git a/vnext/Shared/TurboModuleManager.cpp b/vnext/Shared/TurboModuleManager.cpp index 4e401ca253c..dbe3d968976 100644 --- a/vnext/Shared/TurboModuleManager.cpp +++ b/vnext/Shared/TurboModuleManager.cpp @@ -30,9 +30,6 @@ std::shared_ptr TurboModuleManager::getModule(const std::string &mo } } - if (moduleName.compare("SampleTurboModule") == 0) { - return m_modules.emplace(moduleName, std::make_shared(m_callInvoker)).first->second; - } return nullptr; }