From 40222e91ce798ae448980fa0cf61baf476957ed0 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Fri, 24 Jul 2026 19:06:50 -0700 Subject: [PATCH] Compile out `RCTGetModuleClasses`/`RCTRegisterModule` when both legacy interops are removed (#57670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Changelog: [iOS][Changed] - Compile out `RCTGetModuleClasses`/`RCTRegisterModule` and skip static module registration when both `RCT_REMOVE_LEGACY_MODULE_INTEROP` and `RCT_REMOVE_LEGACY_COMPONENT_INTEROP` are defined `RCTGetModuleClasses()` and the `RCTModuleClasses` registry it reads (populated by `RCTRegisterModule`) exist solely to support the two legacy interop layers. Every reader of the registry is already gated behind either `RCT_REMOVE_LEGACY_MODULE_INTEROP` (TurboModule ⇄ legacy NativeModule interop) or `RCT_REMOVE_LEGACY_COMPONENT_INTEROP` (Fabric ⇄ legacy ViewManager interop). When an app defines **both** macros, the registry has no readers and the function, its backing statics, and `RCTRegisterModule` are dead code. This diff compiles them out in that configuration: - `React/Base/RCTBridge.mm`: guard `RCTModuleClasses`/`RCTModuleClassesSyncQueue`, `RCTGetModuleClasses()`, `getCoreModuleClasses()`, and `RCTRegisterModule()` behind `#if !defined(RCT_REMOVE_LEGACY_MODULE_INTEROP) || !defined(RCT_REMOVE_LEGACY_COMPONENT_INTEROP)`. The old-arch warning helpers (`getModulesLoadedWithOldArch` et al.) stay compiled in — they have an external caller in `RCTInstance.mm` — and simply observe an empty list once the registry's only writer is gone. - `React/Base/RCTBridge+Private.h`: guard the matching `RCT_EXTERN` declarations. - `React/Base/RCTBridgeModule.h`: add a "both-removed" variant of `RCT_EXPORT_MODULE` / `RCT_EXPORT_MODULE_NO_LOAD` that emits only `+moduleName` and omits the `RCTRegisterModule` call, so native modules still link when the symbol is gone. - `RCTLegacyUIManagerConstantsProvider.{h,mm}`: wrap the file body in `#ifndef RCT_REMOVE_LEGACY_COMPONENT_INTEROP`, matching the sibling `LegacyViewManagerInterop*` files. It was the one caller of `RCTGetModuleClasses` whose file body was not already guarded. - `fbobjc/Libraries/FBReactModuleRegistration/FBReactModuleRegistration.mm`: the FIT/new-arch path (`RCT_REMOVE_LEGACY_ARCH`) hand-calls `RCTRegisterModule`. On `cove-ios`, which sets all three macros, that symbol is now compiled out, so the call is additionally gated to only run when `RCTRegisterModule` still exists. The registry has no readers in that config, so this is a no-op. The default build (neither/one macro set) is behavior-unchanged: all guarded code is still compiled in. Differential Revision: D113555374 --- .../react-native/React/Base/RCTBridge+Private.h | 2 ++ packages/react-native/React/Base/RCTBridge.mm | 4 ++++ packages/react-native/React/Base/RCTBridgeModule.h | 14 ++++++++++++++ .../RCTLegacyUIManagerConstantsProvider.h | 4 ++++ .../RCTLegacyUIManagerConstantsProvider.mm | 4 ++++ 5 files changed, 28 insertions(+) diff --git a/packages/react-native/React/Base/RCTBridge+Private.h b/packages/react-native/React/Base/RCTBridge+Private.h index 3a5141fe47a7..85e8a2762b9c 100644 --- a/packages/react-native/React/Base/RCTBridge+Private.h +++ b/packages/react-native/React/Base/RCTBridge+Private.h @@ -11,8 +11,10 @@ @class RCTModuleData; @protocol RCTJavaScriptExecutor; +#if !defined(RCT_REMOVE_LEGACY_MODULE_INTEROP) || !defined(RCT_REMOVE_LEGACY_COMPONENT_INTEROP) RCT_EXTERN NSArray *RCTGetModuleClasses(void); RCT_EXTERN void RCTRegisterModule(Class); +#endif // !defined(RCT_REMOVE_LEGACY_MODULE_INTEROP) || !defined(RCT_REMOVE_LEGACY_COMPONENT_INTEROP) @interface RCTBridge () diff --git a/packages/react-native/React/Base/RCTBridge.mm b/packages/react-native/React/Base/RCTBridge.mm index b46e05c066ac..b75b728b8648 100644 --- a/packages/react-native/React/Base/RCTBridge.mm +++ b/packages/react-native/React/Base/RCTBridge.mm @@ -31,6 +31,7 @@ #import "RCTReloadCommand.h" #import "RCTUtils.h" +#if !defined(RCT_REMOVE_LEGACY_MODULE_INTEROP) || !defined(RCT_REMOVE_LEGACY_COMPONENT_INTEROP) static NSMutableArray *RCTModuleClasses; static dispatch_queue_t RCTModuleClassesSyncQueue; NSArray *RCTGetModuleClasses(void) @@ -115,6 +116,7 @@ return coreModuleClasses; } +#endif // !defined(RCT_REMOVE_LEGACY_MODULE_INTEROP) || !defined(RCT_REMOVE_LEGACY_COMPONENT_INTEROP) static NSMutableArray *modulesLoadedWithOldArch; void addModuleLoadedWithOldArch(NSString * /*moduleName*/); @@ -133,6 +135,7 @@ void addModuleLoadedWithOldArch(NSString *moduleName) return modulesLoadedWithOldArch; } +#if !defined(RCT_REMOVE_LEGACY_MODULE_INTEROP) || !defined(RCT_REMOVE_LEGACY_COMPONENT_INTEROP) /** * Register the given class as a bridge module. All modules must be registered * prior to the first bridge initialization. @@ -161,6 +164,7 @@ void RCTRegisterModule(Class moduleClass) [RCTModuleClasses addObject:moduleClass]; }); } +#endif // !defined(RCT_REMOVE_LEGACY_MODULE_INTEROP) || !defined(RCT_REMOVE_LEGACY_COMPONENT_INTEROP) /** * This function returns the module name for a given class. diff --git a/packages/react-native/React/Base/RCTBridgeModule.h b/packages/react-native/React/Base/RCTBridgeModule.h index 20b943538c87..6f2e8115f79f 100644 --- a/packages/react-native/React/Base/RCTBridgeModule.h +++ b/packages/react-native/React/Base/RCTBridgeModule.h @@ -69,6 +69,18 @@ RCT_EXTERN_C_END * will be used as the JS module name. If omitted, the JS module name will * match the Objective-C class name. */ +#if defined(RCT_REMOVE_LEGACY_MODULE_INTEROP) && defined(RCT_REMOVE_LEGACY_COMPONENT_INTEROP) + +// With both legacy interop layers removed, RCTRegisterModule is compiled out, so +// static module registration is a no-op: only the JS module name is emitted. +#define RCT_EXPORT_MODULE(js_name) \ + +(NSString *)moduleName \ + { \ + return @ #js_name; \ + } + +#else + #ifndef RCT_DISABLE_STATIC_MODULE_REGISTRATION #define RCT_EXPORT_MODULE(js_name) \ RCT_EXTERN void RCTRegisterModule(Class); \ @@ -108,6 +120,8 @@ RCT_EXTERN_C_END RCTRegisterModule([objc_name class]); \ } +#endif // defined(RCT_REMOVE_LEGACY_MODULE_INTEROP) && defined(RCT_REMOVE_LEGACY_COMPONENT_INTEROP) + // Implemented by RCT_EXPORT_MODULE + (NSString *)moduleName; diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTLegacyUIManagerConstantsProvider.h b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTLegacyUIManagerConstantsProvider.h index 9359df209c04..8711745df841 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTLegacyUIManagerConstantsProvider.h +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTLegacyUIManagerConstantsProvider.h @@ -7,6 +7,8 @@ #pragma once +#ifndef RCT_REMOVE_LEGACY_COMPONENT_INTEROP + #include namespace facebook::react { @@ -17,3 +19,5 @@ namespace facebook::react { */ void installLegacyUIManagerConstantsProviderBinding(jsi::Runtime &runtime); } // namespace facebook::react + +#endif // RCT_REMOVE_LEGACY_COMPONENT_INTEROP diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTLegacyUIManagerConstantsProvider.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTLegacyUIManagerConstantsProvider.mm index 7d55ee995b81..5734c971960a 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTLegacyUIManagerConstantsProvider.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTLegacyUIManagerConstantsProvider.mm @@ -7,6 +7,8 @@ #include "RCTLegacyUIManagerConstantsProvider.h" +#ifndef RCT_REMOVE_LEGACY_COMPONENT_INTEROP + #import #import #import @@ -41,3 +43,5 @@ void installLegacyUIManagerConstantsProviderBinding(jsi::Runtime &runtime) LegacyUIManagerConstantsProviderBinding::install(runtime, "getConstants", getConstants); } } // namespace facebook::react + +#endif // RCT_REMOVE_LEGACY_COMPONENT_INTEROP