From c3c0c7cd1548eed20bb5f2b597a6f2df99596d1c Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Fri, 18 Jul 2025 16:06:56 +0100 Subject: [PATCH 1/2] [RN][iOS]Add basic support for TM in Swift - Codegen Disabled --- .../core/platform/ios/ReactCommon/RCTModule.h | 11 ++++ .../ios/ReactCommon/RCTSwiftTurboModule.h | 13 +++++ .../platform/ios/ReactCommon/RCTTurboModule.h | 14 +++++- .../ios/ReactCommon/RCTTurboModule.mm | 3 +- .../ios/ReactCommon/RCTTurboModuleManager.mm | 50 ++++++++++++------- ...eactAppDependencyProvider.podspec.template | 2 +- private/helloworld/scripts/metro.js | 2 +- 7 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTModule.h create mode 100644 packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTSwiftTurboModule.h diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTModule.h new file mode 100644 index 000000000000..19ba1cbf9250 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTModule.h @@ -0,0 +1,11 @@ +// +// RCTModule.mm +// Pods +// +// Created by Riccardo Cipolleschi on 18/07/2025. +// + +@protocol RCTModule + +@end + diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTSwiftTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTSwiftTurboModule.h new file mode 100644 index 000000000000..51ae3ed4bd4b --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTSwiftTurboModule.h @@ -0,0 +1,13 @@ +// +// RCTSwiftTurboModule.h +// Pods +// +// Created by Riccardo Cipolleschi on 18/07/2025. +// + +#import "RCTModule.h" + +@protocol RCTSwiftTurboModule + +@end + diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h index 179ebf7d9dcc..5de3ccd6ac41 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h @@ -19,8 +19,10 @@ #import #import +#import "RCTModule.h" + #define RCT_IS_TURBO_MODULE_CLASS(klass) \ - ((RCTTurboModuleEnabled() && [(klass) conformsToProtocol:@protocol(RCTTurboModule)])) + ((RCTTurboModuleEnabled() && [(klass) conformsToProtocol:@protocol(RCTModule)])) #define RCT_IS_TURBO_MODULE_INSTANCE(module) RCT_IS_TURBO_MODULE_CLASS([(module) class]) namespace facebook::react { @@ -186,13 +188,20 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { */ - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params; + +@optional +/** + * Return an instance of an Apple Module + */ +- (Class)getAppleModule; + @end /** * Protocol that objects can inherit to conform to be treated as turbomodules. * It inherits from RCTTurboModuleProvider, meaning that a TurboModule can create itself */ -@protocol RCTTurboModule +@protocol RCTTurboModule @optional - (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper; @@ -210,3 +219,4 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { - (std::shared_ptr)decorateNativeMethodCallInvoker: (std::shared_ptr)nativeMethodCallInvoker; @end + diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm index 2a677974ca8a..afae804a4571 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm @@ -847,7 +847,7 @@ TraceSection s( void ObjCTurboModule::setEventEmitterCallback(EventEmitterCallback eventEmitterCallback) { - if ([instance_ conformsToProtocol:@protocol(RCTTurboModule)] && + if ([instance_ conformsToProtocol:@protocol(RCTModule)] && [instance_ respondsToSelector:@selector(setEventEmitterCallback:)]) { EventEmitterCallbackWrapper *wrapper = [EventEmitterCallbackWrapper new]; wrapper->_eventEmitterCallback = std::move(eventEmitterCallback); @@ -859,3 +859,4 @@ TraceSection s( @implementation EventEmitterCallbackWrapper @end + diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm index 5168bbf415d1..a9dade00ea80 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm @@ -159,7 +159,7 @@ void invokeSync(const std::string &methodName, std::function &&work) ove bool isTurboModuleClass(Class cls) { - return [cls conformsToProtocol:@protocol(RCTTurboModule)]; + return [cls conformsToProtocol:@protocol(RCTModule)]; } bool isTurboModuleInstance(id module) @@ -339,18 +339,24 @@ - (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy /** * Step 2: Look for platform-specific modules. */ - id module = [self _moduleProviderForName:moduleName]; + id moduleProvider = [self _moduleProviderForName:moduleName]; TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName); // If we request that a TurboModule be created, its respective ObjC class must exist // If the class doesn't exist, then _provideObjCModule returns nil - if (!module) { + if (!moduleProvider) { return nullptr; } + + id module = nullptr; + if ([moduleProvider respondsToSelector:@selector(getAppleModule)]) { + module = (id)[self _provideObjCModule:moduleName moduleProvider:moduleProvider]; + } + id moduleOrProvider = module ? module : moduleProvider; std::shared_ptr nativeMethodCallInvoker = nullptr; - dispatch_queue_t methodQueue = (dispatch_queue_t)objc_getAssociatedObject(module, &kAssociatedMethodQueueKey); + dispatch_queue_t methodQueue = (dispatch_queue_t)objc_getAssociatedObject(moduleOrProvider, &kAssociatedMethodQueueKey); if (methodQueue) { /** * Step 2c: Create and native CallInvoker from the TurboModule's method queue. @@ -370,11 +376,11 @@ - (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy * Step 2d: If the moduleClass is a legacy CxxModule, return a TurboCxxModule instance that * wraps CxxModule. */ - Class moduleClass = [module class]; + Class moduleClass = [moduleOrProvider class]; if ([moduleClass isSubclassOfClass:RCTCxxModule.class]) { // Use TurboCxxModule compat class to wrap the CxxModule instance. // This is only for migration convenience, despite less performant. - auto turboModule = std::make_shared([((RCTCxxModule *)module) createModule], _jsInvoker); + auto turboModule = std::make_shared([((RCTCxxModule *)moduleOrProvider) createModule], _jsInvoker); _turboModuleCache.insert({moduleName, turboModule}); return turboModule; } @@ -385,27 +391,27 @@ - (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy * Use respondsToSelector: below to infer conformance to @protocol(RCTTurboModule). Using conformsToProtocol: is * expensive. */ - if ([module respondsToSelector:@selector(getTurboModule:)]) { + if ([moduleProvider respondsToSelector:@selector(getTurboModule:)]) { ObjCTurboModule::InitParams params = { .moduleName = moduleName, - .instance = (id)module, + .instance = (id)moduleOrProvider, .jsInvoker = _jsInvoker, .nativeMethodCallInvoker = nativeMethodCallInvoker, .isSyncModule = methodQueue == RCTJSThread, .shouldVoidMethodsExecuteSync = (bool)RCTTurboModuleSyncVoidMethodsEnabled(), }; - auto turboModule = [(id)module getTurboModule:params]; + auto turboModule = [(id)moduleProvider getTurboModule:params]; if (turboModule == nullptr) { RCTLogError(@"TurboModule \"%@\"'s getTurboModule: method returned nil.", moduleClass); } _turboModuleCache.insert({moduleName, turboModule}); - if ([module respondsToSelector:@selector(installJSIBindingsWithRuntime:callInvoker:)]) { - [(id)module installJSIBindingsWithRuntime:*runtime callInvoker:_jsInvoker]; - } else if ([module respondsToSelector:@selector(installJSIBindingsWithRuntime:)]) { + if ([moduleOrProvider respondsToSelector:@selector(installJSIBindingsWithRuntime:callInvoker:)]) { + [(id)moduleOrProvider installJSIBindingsWithRuntime:*runtime callInvoker:_jsInvoker]; + } else if ([moduleOrProvider respondsToSelector:@selector(installJSIBindingsWithRuntime:)]) { // Old API without CallInvoker (deprecated) - [(id)module installJSIBindingsWithRuntime:*runtime]; + [(id)moduleOrProvider installJSIBindingsWithRuntime:*runtime]; } return turboModule; } @@ -496,16 +502,16 @@ - (BOOL)_isLegacyModuleClass:(Class)moduleClass if ([_delegate respondsToSelector:@selector(getModuleProvider:)]) { moduleProvider = [_delegate getModuleProvider:moduleName]; } - + if (RCTTurboModuleInteropEnabled() && ![self _isTurboModule:moduleName] && !moduleProvider) { return nil; } - + if (moduleProvider) { - if ([moduleProvider conformsToProtocol:@protocol(RCTTurboModule)]) { - // moduleProvider is also a TM, we need to initialize objectiveC properties, like the dispatch queue + if ([moduleProvider conformsToProtocol:@protocol(RCTModule)]) { return (id)[self _provideObjCModule:moduleName moduleProvider:moduleProvider]; } + // module is Cxx module return moduleProvider; } @@ -585,7 +591,12 @@ - (ModuleHolder *)_getOrCreateModuleHolder:(const char *)moduleName /** * Step 2a: Resolve platform-specific class. */ - Class moduleClass = moduleProvider ? [moduleProvider class] : [self _getModuleClassFromName:moduleName]; + Class moduleClass = moduleProvider ? + ([moduleProvider respondsToSelector:@selector(getAppleModule)] ? + [moduleProvider getAppleModule] : + [moduleProvider class]) : + [self _getModuleClassFromName:moduleName]; + __block id module = nil; @@ -640,7 +651,7 @@ - (BOOL)_shouldCreateObjCModule:(Class)moduleClass return [moduleClass conformsToProtocol:@protocol(RCTBridgeModule)]; } - return [moduleClass conformsToProtocol:@protocol(RCTTurboModule)]; + return [moduleClass conformsToProtocol:@protocol(RCTModule)]; } /** @@ -1104,3 +1115,4 @@ - (void)_invalidateModules } @end + diff --git a/packages/react-native/scripts/codegen/templates/ReactAppDependencyProvider.podspec.template b/packages/react-native/scripts/codegen/templates/ReactAppDependencyProvider.podspec.template index 327bc9c102e3..c977f45e579b 100644 --- a/packages/react-native/scripts/codegen/templates/ReactAppDependencyProvider.podspec.template +++ b/packages/react-native/scripts/codegen/templates/ReactAppDependencyProvider.podspec.template @@ -22,7 +22,7 @@ Pod::Spec.new do |s| s.author = "Meta Platforms, Inc. and its affiliates" s.platforms = min_supported_versions s.source = source - s.source_files = "**/RCTAppDependencyProvider.{h,mm}" + s.source_files = ["**/RCTAppDependencyProvider.{h,mm}", "**/*-Swift.{h,mm}"] # This guard prevent to install the dependencies when we run `pod install` in the old architecture. s.pod_target_xcconfig = { diff --git a/private/helloworld/scripts/metro.js b/private/helloworld/scripts/metro.js index b100f15ae21b..3e24e27f5517 100644 --- a/private/helloworld/scripts/metro.js +++ b/private/helloworld/scripts/metro.js @@ -23,7 +23,7 @@ program android: {}, }, root: path.join(__dirname, '../'), - reactNativePath: path.join(__dirname, '../../react-native'), + reactNativePath: path.join(__dirname, '../../../packages/react-native'), }, { experimentalDebugger: false, From 09fddfb56e958ba396691479fad5f971db004e63 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Sat, 19 Jul 2025 20:32:15 +0100 Subject: [PATCH 2/2] update codege --- .../modules/GenerateModuleObjCpp/index.js | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js index defe5722f29a..629976a74c4f 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js @@ -22,6 +22,20 @@ const {StructCollector} = require('./StructCollector'); type FilesOutput = Map; +const SwiftCompatibleDeclarationTemplate = ({ + hasteModuleName, + protocolMethods, +}: $ReadOnly<{ + hasteModuleName: string, + protocolMethods: string, +}>) => ` +@protocol ${hasteModuleName}Spec + +${protocolMethods} + +@end +` + const ModuleDeclarationTemplate = ({ hasteModuleName, structDeclarations, @@ -58,6 +72,51 @@ namespace facebook::react { }; } // namespace facebook::react`; +const SwiftCompatibleHeaderFileTemplate = ({ + headerFileName, + moduleDeclarations, + assumeNonnull, +}: $ReadOnly<{ + headerFileName: string, + moduleDeclarations: string, + assumeNonnull: boolean, +}>) => { + const headerFileNameWithNoExt = headerFileName + .replace(/\.h$/, '') + .replace(/-/, ''); + + return ( + `/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * ${'@'}generated by codegen project: GenerateModuleObjCpp + * + * We create an umbrella header (and corresponding implementation) here since + * Cxx compilation in BUCK has a limitation: source-code producing genrule()s + * must have a single output. More files => more genrule()s => slower builds. + */ + +// Avoid multiple includes of ${headerFileNameWithNoExt} symbols +#ifndef ${headerFileNameWithNoExt}_H +#define ${headerFileNameWithNoExt}_H + +#import +#import +#import + +` + + (assumeNonnull ? '\nNS_ASSUME_NONNULL_BEGIN\n' : '') + + moduleDeclarations + + '\n' + + (assumeNonnull ? '\nNS_ASSUME_NONNULL_END\n' : '\n') + + `#endif // ${headerFileNameWithNoExt}_H` + + '\n' + ); +}; + const HeaderFileTemplate = ({ headerFileName, moduleDeclarations, @@ -150,6 +209,7 @@ module.exports = { const nativeModules = getModules(schema); const moduleDeclarations: Array = []; + const swiftCompatibleModulesDeclarations: Array = []; const structInlineMethods: Array = []; const moduleImplementations: Array = []; @@ -209,6 +269,15 @@ module.exports = { }), ); + swiftCompatibleModulesDeclarations.push( + SwiftCompatibleDeclarationTemplate({ + hasteModuleName: hasteModuleName, + protocolMethods: methodSerializations + .map(({protocolMethod}) => protocolMethod) + .join('\n'), + }), + ); + structInlineMethods.push(methodStrs.join('\n')); moduleImplementations.push( @@ -232,6 +301,13 @@ module.exports = { assumeNonnull, }); + const swiftCompatibleHeaderFileName = `${libraryName}-Swift.h`; + const swiftHeaderFile = SwiftCompatibleHeaderFileTemplate({ + headerFileName: swiftCompatibleHeaderFileName, + moduleDeclarations: swiftCompatibleModulesDeclarations.join('\n'), + assumeNonnull, + }); + const sourceFileName = `${libraryName}-generated.mm`; const sourceFile = SourceFileTemplate({ headerFileName, @@ -240,6 +316,7 @@ module.exports = { return new Map([ [headerFileName, headerFile], + [swiftCompatibleHeaderFileName, swiftHeaderFile], [sourceFileName, sourceFile], ]); },