From 4f1e1eb7c91ca11e8601dc034c90398dd543eadf Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Mon, 9 Mar 2026 17:04:18 -0700 Subject: [PATCH 1/4] Implement RCT_MODULE_NO_SELF_LOAD --- packages/react-native/React/Base/RCTBridge.mm | 171 +++++++++++------- .../react-native/React/Base/RCTBridgeModule.h | 17 +- packages/react-native/React/Base/RCTDefines.h | 9 + 3 files changed, 123 insertions(+), 74 deletions(-) diff --git a/packages/react-native/React/Base/RCTBridge.mm b/packages/react-native/React/Base/RCTBridge.mm index 1155428bafc8..9f377430436c 100644 --- a/packages/react-native/React/Base/RCTBridge.mm +++ b/packages/react-native/React/Base/RCTBridge.mm @@ -32,14 +32,113 @@ #import "RCTReloadCommand.h" #import "RCTUtils.h" +/** + * List of core React Native modules. + * + * When RCT_MODULE_NO_SELF_LOAD is set to non-zero, module self-registration via +load is disabled. + * Instead, RCTBridge will register these modules at initialization time. + */ +static NSArray *moduleClassNames = @[ + @"RCTViewManager", + @"RCTActivityIndicatorViewManager", + @"RCTDebuggingOverlayManager", + @"RCTModalHostViewManager", + @"RCTModalManager", + @"RCTRefreshControlManager", + @"RCTSafeAreaViewManager", + @"RCTScrollContentViewManager", + @"RCTScrollViewManager", + @"RCTSwitchManager", + @"RCTUIManager", + @"RCTAccessibilityManager", + @"RCTActionSheetManager", + @"RCTAlertManager", + @"RCTAppearance", + @"RCTAppState", + @"RCTClipboard", + @"RCTDeviceInfo", + @"RCTDevLoadingView", + @"RCTDevMenu", + @"RCTDevSettings", + @"RCTDevToolsRuntimeSettingsModule", + @"RCTEventDispatcher", + @"RCTExceptionsManager", + @"RCTI18nManager", + @"RCTKeyboardObserver", + @"RCTLogBox", + @"RCTPerfMonitor", + @"RCTPlatform", + @"RCTRedBox", + @"RCTSourceCode", + @"RCTStatusBarManager", + @"RCTTiming", + @"RCTWebSocketModule", + @"RCTNativeAnimatedModule", + @"RCTNativeAnimatedTurboModule", + @"RCTBlobManager", + @"RCTFileReaderModule", + @"RCTBundleAssetImageLoader", + @"RCTGIFImageDecoder", + @"RCTImageEditingManager", + @"RCTImageLoader", + @"RCTImageStoreManager", + @"RCTImageViewManager", + @"RCTLocalAssetImageLoader", + @"RCTLinkingManager", + @"RCTDataRequestHandler", + @"RCTFileRequestHandler", + @"RCTHTTPRequestHandler", + @"RCTNetworking", + @"RCTPushNotificationManager", + @"RCTSettingsManager", + @"RCTBaseTextViewManager", + @"RCTBaseTextInputViewManager", + @"RCTInputAccessoryViewManager", + @"RCTMultilineTextInputViewManager", + @"RCTRawTextViewManager", + @"RCTSinglelineTextInputViewManager", + @"RCTTextViewManager", + @"RCTVirtualTextViewManager", + @"RCTVibration", +]; + static NSMutableArray *RCTModuleClasses; static dispatch_queue_t RCTModuleClassesSyncQueue; + +/** + * Make sure ModuleClassesSyncQueue is initialized before any referring functions are called. + */ +static void RCTEnsureModuleClassesInitialized(void) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + RCTModuleClasses = [NSMutableArray new]; + RCTModuleClassesSyncQueue = + dispatch_queue_create("com.facebook.react.ModuleClassesSyncQueue", DISPATCH_QUEUE_CONCURRENT); + }); +} + NSArray *RCTGetModuleClasses(void) { + RCTEnsureModuleClassesInitialized(); __block NSArray *result; dispatch_sync(RCTModuleClassesSyncQueue, ^{ result = [RCTModuleClasses copy]; }); + +#if RCT_MODULE_NO_SELF_LOAD + // When RCT_MODULE_NO_SELF_LOAD is enabled, modules don't self-register via +load + // Add core React Native modules here instead + NSMutableArray *validClasses = [NSMutableArray arrayWithArray:result]; + for (NSString *className in moduleClassNames) { + Class cls = NSClassFromString(className); + if (cls != nil) { + [validClasses addObject:cls]; + } + } + result = [validClasses copy]; +#endif //RCT_MODULE_NO_SELF_LOAD + return result; } @@ -49,69 +148,7 @@ static NSSet *coreModuleClasses = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - coreModuleClasses = [NSSet setWithArray:@[ - @"RCTViewManager", - @"RCTActivityIndicatorViewManager", - @"RCTDebuggingOverlayManager", - @"RCTModalHostViewManager", - @"RCTModalManager", - @"RCTRefreshControlManager", - @"RCTSafeAreaViewManager", - @"RCTScrollContentViewManager", - @"RCTScrollViewManager", - @"RCTSwitchManager", - @"RCTUIManager", - @"RCTAccessibilityManager", - @"RCTActionSheetManager", - @"RCTAlertManager", - @"RCTAppearance", - @"RCTAppState", - @"RCTClipboard", - @"RCTDeviceInfo", - @"RCTDevLoadingView", - @"RCTDevMenu", - @"RCTDevSettings", - @"RCTDevToolsRuntimeSettingsModule", - @"RCTEventDispatcher", - @"RCTExceptionsManager", - @"RCTI18nManager", - @"RCTKeyboardObserver", - @"RCTLogBox", - @"RCTPerfMonitor", - @"RCTPlatform", - @"RCTRedBox", - @"RCTSourceCode", - @"RCTStatusBarManager", - @"RCTTiming", - @"RCTWebSocketModule", - @"RCTNativeAnimatedModule", - @"RCTNativeAnimatedTurboModule", - @"RCTBlobManager", - @"RCTFileReaderModule", - @"RCTBundleAssetImageLoader", - @"RCTGIFImageDecoder", - @"RCTImageEditingManager", - @"RCTImageLoader", - @"RCTImageStoreManager", - @"RCTImageViewManager", - @"RCTLocalAssetImageLoader", - @"RCTLinkingManager", - @"RCTDataRequestHandler", - @"RCTFileRequestHandler", - @"RCTHTTPRequestHandler", - @"RCTNetworking", - @"RCTPushNotificationManager", - @"RCTSettingsManager", - @"RCTBaseTextViewManager", - @"RCTBaseTextInputViewManager", - @"RCTInputAccessoryViewManager", - @"RCTMultilineTextInputViewManager", - @"RCTRawTextViewManager", - @"RCTSinglelineTextInputViewManager", - @"RCTTextViewManager", - @"RCTVirtualTextViewManager", - @"RCTVibration", - ]]; + coreModuleClasses = [NSSet setWithArray:moduleClassNames]; }); return coreModuleClasses; @@ -146,12 +183,8 @@ void RCTRegisterModule(Class moduleClass) ![getCoreModuleClasses() containsObject:[moduleClass description]]) { addModuleLoadedWithOldArch([moduleClass description]); } - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - RCTModuleClasses = [NSMutableArray new]; - RCTModuleClassesSyncQueue = - dispatch_queue_create("com.facebook.react.ModuleClassesSyncQueue", DISPATCH_QUEUE_CONCURRENT); - }); + + RCTEnsureModuleClassesInitialized(); RCTAssert( [moduleClass conformsToProtocol:@protocol(RCTBridgeModule)], diff --git a/packages/react-native/React/Base/RCTBridgeModule.h b/packages/react-native/React/Base/RCTBridgeModule.h index fc86dc806f76..44820146b1de 100644 --- a/packages/react-native/React/Base/RCTBridgeModule.h +++ b/packages/react-native/React/Base/RCTBridgeModule.h @@ -63,22 +63,29 @@ RCT_EXTERN_C_END */ @protocol RCTBridgeModule +#if RCT_MODULE_NO_SELF_LOAD +#define RCT_EXPORT_MODULE_LOAD +#else +#define RCT_EXPORT_MODULE_LOAD \ + +(void)load \ + { \ + RCTRegisterModule(self); \ + } +#endif + /** * Place this macro in your class implementation to automatically register * your module with the bridge when it loads. The optional js_name argument * will be used as the JS module name. If omitted, the JS module name will * match the Objective-C class name. */ -#define RCT_EXPORT_MODULE(js_name) \ +#define RCT_EXPORT_MODULE(js_name) \ RCT_EXTERN void RCTRegisterModule(Class); \ +(NSString *)moduleName \ { \ return @ #js_name; \ } \ - +(void)load \ - { \ - RCTRegisterModule(self); \ - } +RCT_EXPORT_MODULE_LOAD /** * Same as RCT_EXPORT_MODULE, but uses __attribute__((constructor)) for module diff --git a/packages/react-native/React/Base/RCTDefines.h b/packages/react-native/React/Base/RCTDefines.h index 228d92c2960a..86f49fed34d5 100644 --- a/packages/react-native/React/Base/RCTDefines.h +++ b/packages/react-native/React/Base/RCTDefines.h @@ -22,6 +22,15 @@ #define RCT_EXTERN_C_END #endif +/** + * The RCT_MODULE_NO_SELF_LOAD macro can be used to disable module self-registration + * via +load methods. When enabled, modules are registered by RCTBridge instead. + * This can improve performance during module lazy-loading. + */ +#ifndef RCT_MODULE_NO_SELF_LOAD +#define RCT_MODULE_NO_SELF_LOAD 0 +#endif + /** * The RCT_DEBUG macro can be used to exclude error checking and logging code * from release builds to improve performance and reduce binary size. From 9908903340e967285ac7b3da641515b652d5d5ff Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Mon, 9 Mar 2026 17:29:05 -0700 Subject: [PATCH 2/4] Add check for external unregistered modules --- packages/react-native/React/Base/RCTBridge.mm | 87 +++++++++++++++++-- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/packages/react-native/React/Base/RCTBridge.mm b/packages/react-native/React/Base/RCTBridge.mm index 9f377430436c..006b62f31e46 100644 --- a/packages/react-native/React/Base/RCTBridge.mm +++ b/packages/react-native/React/Base/RCTBridge.mm @@ -118,28 +118,99 @@ static void RCTEnsureModuleClassesInitialized(void) }); } +/** + * Checks for unregistered modules that conform to RCTBridgeModule protocol. + * This detects misconfiguration where external modules are compiled with + * RCT_MODULE_NO_SELF_LOAD=1 but aren't in the moduleClassNames list. + */ +static void RCTCheckForUnregisteredModules(NSArray *registeredClasses) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableSet *registeredSet = [NSMutableSet setWithArray:registeredClasses]; + + // Get all loaded classes + int numClasses = objc_getClassList(NULL, 0); + if (numClasses <= 0) { + return; + } + + Class *classes = (Class *)malloc(sizeof(Class) * numClasses); + numClasses = objc_getClassList(classes, numClasses); + + NSMutableArray *unregisteredModules = [NSMutableArray new]; + + // Check each class that conforms to RCTBridgeModule + for (int i = 0; i < numClasses; i++) { + Class cls = classes[i]; + + // Skip if already registered + if ([registeredSet containsObject:cls]) { + continue; + } + + // Check if class conforms to RCTBridgeModule protocol + if (class_conformsToProtocol(cls, @protocol(RCTBridgeModule))) { + // Skip if it's a core module that will be added + NSString *className = NSStringFromClass(cls); + if ([moduleClassNames containsObject:className]) { + continue; + } + + [unregisteredModules addObject:className]; + } + } + + free(classes); + + // Log warning if unregistered modules found + if ([unregisteredModules count] > 0) { + RCTLogWarn(@"⚠️ Detected unregistered RCTBridgeModule classes: %@\n" + @"These modules may have been compiled with RCT_MODULE_NO_SELF_LOAD=1 " + @"but are not in the core moduleClassNames list.\n" + @"To fix: Either compile all modules with RCT_MODULE_NO_SELF_LOAD=0, " + @"or add external modules to moduleClassNames in RCTBridge.mm", + [unregisteredModules componentsJoinedByString:@", "]); + } + }); +} + NSArray *RCTGetModuleClasses(void) { RCTEnsureModuleClassesInitialized(); - __block NSArray *result; - dispatch_sync(RCTModuleClassesSyncQueue, ^{ - result = [RCTModuleClasses copy]; - }); #if RCT_MODULE_NO_SELF_LOAD // When RCT_MODULE_NO_SELF_LOAD is enabled, modules don't self-register via +load // Add core React Native modules here instead - NSMutableArray *validClasses = [NSMutableArray arrayWithArray:result]; + __block NSMutableArray *result; + dispatch_sync(RCTModuleClassesSyncQueue, ^{ + result = [RCTModuleClasses mutableCopy]; + }); + for (NSString *className in moduleClassNames) { Class cls = NSClassFromString(className); if (cls != nil) { - [validClasses addObject:cls]; + [result addObject:cls]; } } - result = [validClasses copy]; -#endif //RCT_MODULE_NO_SELF_LOAD + + NSArray *finalResult = [result copy]; + + // Check for misconfigured external modules + RCTCheckForUnregisteredModules(finalResult); + + return finalResult; +#else + __block NSArray *result; + dispatch_sync(RCTModuleClassesSyncQueue, ^{ + result = [RCTModuleClasses copy]; + }); + + // Check for misconfigured external modules + RCTCheckForUnregisteredModules(result); return result; +#endif //RCT_MODULE_NO_SELF_LOAD } NSSet *getCoreModuleClasses(void); From 2264b85499dc6db72f3227b7f96e2db175f55ab6 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Mon, 9 Mar 2026 17:43:29 -0700 Subject: [PATCH 3/4] Add missing spaces --- packages/react-native/React/Base/RCTBridgeModule.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/React/Base/RCTBridgeModule.h b/packages/react-native/React/Base/RCTBridgeModule.h index 44820146b1de..0a2a6e0a667a 100644 --- a/packages/react-native/React/Base/RCTBridgeModule.h +++ b/packages/react-native/React/Base/RCTBridgeModule.h @@ -79,7 +79,7 @@ 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. */ -#define RCT_EXPORT_MODULE(js_name) \ +#define RCT_EXPORT_MODULE(js_name) \ RCT_EXTERN void RCTRegisterModule(Class); \ +(NSString *)moduleName \ { \ From db048467f6cd44bd19ad406a78720218b36773c7 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Mon, 9 Mar 2026 18:42:16 -0700 Subject: [PATCH 4/4] Add [macOS] tags --- packages/react-native/React/Base/RCTBridge.mm | 14 ++++++++++++-- packages/react-native/React/Base/RCTBridgeModule.h | 4 +++- packages/react-native/React/Base/RCTDefines.h | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/react-native/React/Base/RCTBridge.mm b/packages/react-native/React/Base/RCTBridge.mm index 006b62f31e46..f3fc0d2dee1e 100644 --- a/packages/react-native/React/Base/RCTBridge.mm +++ b/packages/react-native/React/Base/RCTBridge.mm @@ -32,6 +32,7 @@ #import "RCTReloadCommand.h" #import "RCTUtils.h" +// [macOS /** * List of core React Native modules. * @@ -101,10 +102,12 @@ @"RCTVirtualTextViewManager", @"RCTVibration", ]; +// macOS] static NSMutableArray *RCTModuleClasses; static dispatch_queue_t RCTModuleClassesSyncQueue; +// [macOS /** * Make sure ModuleClassesSyncQueue is initialized before any referring functions are called. */ @@ -174,9 +177,11 @@ static void RCTCheckForUnregisteredModules(NSArray *registeredClasses) } }); } +// macOS] NSArray *RCTGetModuleClasses(void) { + // [macOS RCTEnsureModuleClassesInitialized(); #if RCT_MODULE_NO_SELF_LOAD @@ -201,16 +206,21 @@ static void RCTCheckForUnregisteredModules(NSArray *registeredClasses) return finalResult; #else + // macOS] __block NSArray *result; dispatch_sync(RCTModuleClassesSyncQueue, ^{ result = [RCTModuleClasses copy]; }); + // [macOS // Check for misconfigured external modules RCTCheckForUnregisteredModules(result); + // macOS] return result; + // [macOS #endif //RCT_MODULE_NO_SELF_LOAD + // macOS] } NSSet *getCoreModuleClasses(void); @@ -219,7 +229,7 @@ static void RCTCheckForUnregisteredModules(NSArray *registeredClasses) static NSSet *coreModuleClasses = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - coreModuleClasses = [NSSet setWithArray:moduleClassNames]; + coreModuleClasses = [NSSet setWithArray:moduleClassNames]; // [macOS] }); return coreModuleClasses; @@ -255,7 +265,7 @@ void RCTRegisterModule(Class moduleClass) addModuleLoadedWithOldArch([moduleClass description]); } - RCTEnsureModuleClassesInitialized(); + RCTEnsureModuleClassesInitialized(); // [macOS] RCTAssert( [moduleClass conformsToProtocol:@protocol(RCTBridgeModule)], diff --git a/packages/react-native/React/Base/RCTBridgeModule.h b/packages/react-native/React/Base/RCTBridgeModule.h index 0a2a6e0a667a..a14beb424106 100644 --- a/packages/react-native/React/Base/RCTBridgeModule.h +++ b/packages/react-native/React/Base/RCTBridgeModule.h @@ -63,6 +63,7 @@ RCT_EXTERN_C_END */ @protocol RCTBridgeModule +// [macOS #if RCT_MODULE_NO_SELF_LOAD #define RCT_EXPORT_MODULE_LOAD #else @@ -72,6 +73,7 @@ RCT_EXTERN_C_END RCTRegisterModule(self); \ } #endif +// macOS] /** * Place this macro in your class implementation to automatically register @@ -85,7 +87,7 @@ RCT_EXTERN_C_END { \ return @ #js_name; \ } \ -RCT_EXPORT_MODULE_LOAD +RCT_EXPORT_MODULE_LOAD // [macOS] /** * Same as RCT_EXPORT_MODULE, but uses __attribute__((constructor)) for module diff --git a/packages/react-native/React/Base/RCTDefines.h b/packages/react-native/React/Base/RCTDefines.h index 86f49fed34d5..0cf7b7124588 100644 --- a/packages/react-native/React/Base/RCTDefines.h +++ b/packages/react-native/React/Base/RCTDefines.h @@ -22,6 +22,7 @@ #define RCT_EXTERN_C_END #endif +// [macOS /** * The RCT_MODULE_NO_SELF_LOAD macro can be used to disable module self-registration * via +load methods. When enabled, modules are registered by RCTBridge instead. @@ -30,6 +31,7 @@ #ifndef RCT_MODULE_NO_SELF_LOAD #define RCT_MODULE_NO_SELF_LOAD 0 #endif +// macOS] /** * The RCT_DEBUG macro can be used to exclude error checking and logging code