diff --git a/packages/react-native/scripts/ios-prebuild/headers-verify.js b/packages/react-native/scripts/ios-prebuild/headers-verify.js index 79bebc800348..37357bfb483a 100644 --- a/packages/react-native/scripts/ios-prebuild/headers-verify.js +++ b/packages/react-native/scripts/ios-prebuild/headers-verify.js @@ -31,6 +31,10 @@ * every R9 textual Fabric header against ReactNativeHeaders. * c. A Swift TU: `import React` + `RCTBridge.moduleRegistry` — the Expo * Swift case, proving the R9 modular header is module-visible. + * d. Swift C++-interop TUs that import React plus every importable module + * declared by the composed ReactNativeHeaders module map, then the + * shipped inspector umbrella as an isolated probe module, forcing + * ClangImporter to instantiate imported C++ value-type special members. * * Usage: * node scripts/ios-prebuild/headers-verify.js [--flavor Debug|Release] @@ -63,6 +67,18 @@ const FOLLY_DEFINES = [ ]; const SIM_TARGET = 'arm64-apple-ios15.0-simulator'; +// This Objective-C app-bootstrap module is not importable with Swift C++ +// interop: its __cplusplus-only factory conformances cross-import React and +// ReactCommon module members, which ClangImporter rejects for hidden +// declarations and for re-reading the unguarded RCTComponentViewProtocol.h. +// Any new exclusion must have its own explicit justification comment. +const SWIFT_CXX_INTEROP_EXCLUDED_MODULES = new Set(['React_RCTAppDelegate']); +const SWIFT_CXX_INTEROP_PROBE_MODULE = 'ReactNativeHeaders_CxxInteropProbe'; +const SWIFT_CXX_INTEROP_PROBE_TYPES = [ + 'facebook::react::jsinspector_modern::tracing::TraceRecordingState', + 'facebook::react::jsinspector_modern::tracing::HostTracingProfile', +]; + function log(msg /*: string */) { console.log(`[headers-verify] ${msg}`); } @@ -303,12 +319,49 @@ func _headersVerifyProbe(_ bridge: RCTBridge) { `; } +/** Swift C++-interop fixture: every module shipped by ReactNativeHeaders. */ +function renderSwiftCxxInteropFixture( + moduleNames /*: Array */, +) /*: string */ { + return [ + 'import React', + ...moduleNames.map(name => `import ${name}`), + '', + ].join('\n'); +} + +function renderSwiftCxxInteropProbeFixture() /*: string */ { + return `import ${SWIFT_CXX_INTEROP_PROBE_MODULE} +func _headersVerifyCxxValueProbe( + _ state: borrowing facebook.react.jsinspector_modern.tracing.TraceRecordingState +) { + _ = state.mode +} +`; +} + +function parseModuleNames(moduleMapPath /*: string */) /*: Array */ { + const moduleMap = fs.readFileSync(moduleMapPath, 'utf8'); + // Dotted submodule names may not be standalone-importable if the composed + // module map ever grows explicit submodules. + return Array.from( + moduleMap.matchAll( + /^\s*(?:explicit\s+)?(?:framework\s+)?module\s+([A-Za-z_][A-Za-z0-9_.]*)\s*\{/gm, + ), + match => match[1], + ); +} + function xcrun(args /*: Array */, what /*: string */) { try { - execFileSync('xcrun', args, {stdio: ['ignore', 'pipe', 'pipe']}); + execFileSync('xcrun', args, { + stdio: ['ignore', 'pipe', 'pipe'], + maxBuffer: 64 * 1024 * 1024, + }); } catch (e) { + const stdout = e.stdout != null ? String(e.stdout) : ''; const stderr = e.stderr != null ? String(e.stderr) : String(e); - throw new Error(`${what} FAILED:\n${stderr}`); + throw new Error(`${what} FAILED:\n${stdout}${stderr}`); } } @@ -405,6 +458,129 @@ function runCompileGates( 'Swift gate (import React + RCTBridge.moduleRegistry)', ); log('compile: Swift moduleRegistry fixture OK.'); + + const moduleMap = path.join(rnhHeaders, 'module.modulemap'); + const moduleNames = parseModuleNames(moduleMap); + if (moduleNames.length === 0) { + throw new Error( + `No modules declared in composed module map: ${moduleMap}`, + ); + } + const importableModuleNames = moduleNames.filter( + name => !SWIFT_CXX_INTEROP_EXCLUDED_MODULES.has(name), + ); + // React_RCTAppDelegate is the only composed module that reaches the + // inspector C++ graph, but it cannot itself be imported in C++-interop + // mode (see the exclusion comment above). Wrap the shipped inspector + // umbrella in a temporary module so ClangImporter still checks that graph. + const cxxInteropProbeHeaders = path.join(tmp, 'rnh-cxx-interop-probe'); + fs.cpSync(rnhHeaders, cxxInteropProbeHeaders, { + recursive: true, + filter: source => path.basename(source) !== 'module.modulemap', + }); + const cxxInteropProbeHeader = path.join(tmp, 'cxx-interop-probe.h'); + const cxxInteropCopyProbes = SWIFT_CXX_INTEROP_PROBE_TYPES.map( + (typeName, index) => + `inline void probeCopy${index}(const ${typeName} &value) {\n` + + ` instantiateCopy(value);\n` + + `}\n`, + ).join(''); + fs.writeFileSync( + cxxInteropProbeHeader, + `#include \n` + + `#include \n` + + `namespace rn_headers_verify {\n` + + `template void instantiateCopy(const T &value) {\n` + + ` if constexpr (std::is_copy_constructible_v) {\n` + + ` T copy(value);\n` + + ` (void)copy;\n` + + ` }\n` + + `}\n` + + cxxInteropCopyProbes + + `}\n`, + ); + const cxxInteropProbeModuleMap = path.join( + tmp, + 'module-cxx-interop-probe.modulemap', + ); + fs.writeFileSync( + cxxInteropProbeModuleMap, + `module ${SWIFT_CXX_INTEROP_PROBE_MODULE} {\n` + + ` header ${JSON.stringify(cxxInteropProbeHeader)}\n` + + ` export *\n` + + `}\n`, + ); + const swiftCxxInterop = path.join(tmp, 'gate-swift-cxx-interop.swift'); + fs.writeFileSync( + swiftCxxInterop, + renderSwiftCxxInteropFixture(importableModuleNames), + ); + xcrun( + [ + 'swiftc', + '-typecheck', + '-cxx-interoperability-mode=default', + '-sdk', + sdk, + '-target', + SIM_TARGET, + '-module-cache-path', + path.join(tmp, 'mc-swift-cxx-interop'), + '-F', + reactSlice, + '-I', + rnhHeaders, + '-I', + depsHeaders, + '-Xcc', + '-std=c++20', + '-Xcc', + '-w', + '-Xcc', + `-fmodule-map-file=${moduleMap}`, + ...FOLLY_DEFINES.flatMap(flag => ['-Xcc', flag]), + swiftCxxInterop, + ], + 'Swift C++-interop gate (React + importable ReactNativeHeaders modules)', + ); + + const swiftCxxInteropProbe = path.join( + tmp, + 'gate-swift-cxx-interop-probe.swift', + ); + fs.writeFileSync(swiftCxxInteropProbe, renderSwiftCxxInteropProbeFixture()); + xcrun( + [ + 'swiftc', + '-typecheck', + '-cxx-interoperability-mode=default', + '-sdk', + sdk, + '-target', + SIM_TARGET, + '-module-cache-path', + path.join(tmp, 'mc-swift-cxx-interop-probe'), + '-I', + cxxInteropProbeHeaders, + '-I', + depsHeaders, + '-Xcc', + '-std=c++20', + '-Xcc', + '-w', + '-Xcc', + `-fmodule-map-file=${cxxInteropProbeModuleMap}`, + ...FOLLY_DEFINES.flatMap(flag => ['-Xcc', flag]), + swiftCxxInteropProbe, + ], + 'Swift C++-interop gate (inspector value types)', + ); + log( + `compile: Swift C++ interop React + ${importableModuleNames.length} ` + + `ReactNativeHeaders modules + inspector probes ` + + `[${SWIFT_CXX_INTEROP_PROBE_TYPES.join(', ')}] OK ` + + `(${moduleNames.length - importableModuleNames.length} excluded).`, + ); } finally { fs.rmSync(tmp, {recursive: true, force: true}); }