Skip to content

Commit 25ee70a

Browse files
chrfalchmeta-codesync[bot]
authored andcommitted
fix(iOS): gate prebuilt headers on Swift C++-interop importability (#57606)
Summary: Adds a generator-time gate so a Swift-C++-interop-hostile C++ type can never silently ship in the prebuilt iOS core headers again. **The class this guards:** the prebuilt core headers ship as real clang modules. A C++ value type reachable from a shipped module whose *implicit copy constructor is declared but ill-formed on instantiation* — e.g. an implicit-copy struct with a `std::vector<move-only-T>` member — compiles fine in C++ but hard-errors when a Swift target with `-cxx-interoperability-mode=default` imports the module and its generated bridging uses the type as a copyable value. This is invisible to C++ CI (plain C++ never instantiates the copy), so it only surfaced as a broken community-library nightly (react-native-unistyles, via `TraceRecordingState` / `HostTracingProfile` — fixed in #57605). **The gate:** a fourth compile gate (stage 3d) in `headers-verify.js`'s `runCompileGates`, compiling a Swift TU with `-cxx-interoperability-mode=default` that imports every module declared by the composed `ReactNativeHeaders` module map (parsed at runtime, not hardcoded), plus a probe that forces the ClangImporter copyability path for a small explicit list of value types (`SWIFT_CXX_INTEROP_PROBE_TYPES`). Because a bare `import` does not eagerly instantiate copy constructors on the current toolchain, the probe reproduces what a real interop consumer's generated code does: for each listed type it conditionally instantiates a copy, which fails exactly as the real consumer fails if the type regresses to an implicit copy. Adding future coverage is a one-line list edit. One module (`React_RCTAppDelegate`) is excluded as non-importable under interop by design (ObjC bootstrap module with C++-only factory conformances); the inspector C++ graph it would reach is covered directly by the probe instead. The exclusion is documented in-source, and new exclusions are required to carry a justification. Independent review confirmed the probe reproduces the real failure for the right reason (the dangerous shape reports `is_copy_constructible_v == true`, so the probe enters the branch and forces the ill-formed body instantiation — mirroring ClangImporter, not masking the bug), and that both offender types are covered. ⚠️ **Land after #57605 — the gate runs against the composed artifacts, which are built from source; until the move-only type fixes are in, the gate correctly fails the prebuild on the unfixed types. ## Changelog: [INTERNAL] [ADDED] - Prebuild gate that fails composed iOS headers unusable from a Swift C++-interop consumer Pull Request resolved: #57606 Test Plan: Against a freshly composed Debug artifact (`node scripts/ios-prebuild -c -f Debug`): - **Green**: `node scripts/ios-prebuild/headers-verify.js --flavor Debug` passes the new gate with both probe types (`TraceRecordingState`, `HostTracingProfile`) fixed in the artifact. - **Red, TraceRecordingState**: restoring the unfixed `TraceRecordingState.h` into the composed artifact fails the gate with the exact `__construct_at` / implicit-copy diagnostic; restoring the fix → green. - **Red, HostTracingProfile** (proves coverage isn't accidental): same protocol with `HostTracingProfile` unfixed (TraceRecordingState left fixed) → gate fails naming `HostTracingProfile` and its probe specialization; restore → green. - `node --check`, `prettier --check`, `flow focus-check` clean. Gate is skipped by `--skip-compile` like the other compile gates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed By: cortinico Differential Revision: D113011048 Pulled By: cipolleschi fbshipit-source-id: 51cfd9fde67c60bce9d13e490fe966ed059432c2
1 parent a7ba4ce commit 25ee70a

1 file changed

Lines changed: 178 additions & 2 deletions

File tree

packages/react-native/scripts/ios-prebuild/headers-verify.js

Lines changed: 178 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
* every R9 textual Fabric header against ReactNativeHeaders.
3232
* c. A Swift TU: `import React` + `RCTBridge.moduleRegistry` — the Expo
3333
* Swift case, proving the R9 modular header is module-visible.
34+
* d. Swift C++-interop TUs that import React plus every importable module
35+
* declared by the composed ReactNativeHeaders module map, then the
36+
* shipped inspector umbrella as an isolated probe module, forcing
37+
* ClangImporter to instantiate imported C++ value-type special members.
3438
*
3539
* Usage:
3640
* node scripts/ios-prebuild/headers-verify.js [--flavor Debug|Release]
@@ -64,6 +68,18 @@ const FOLLY_DEFINES = [
6468
];
6569
const SIM_TARGET = 'arm64-apple-ios15.0-simulator';
6670

71+
// This Objective-C app-bootstrap module is not importable with Swift C++
72+
// interop: its __cplusplus-only factory conformances cross-import React and
73+
// ReactCommon module members, which ClangImporter rejects for hidden
74+
// declarations and for re-reading the unguarded RCTComponentViewProtocol.h.
75+
// Any new exclusion must have its own explicit justification comment.
76+
const SWIFT_CXX_INTEROP_EXCLUDED_MODULES = new Set(['React_RCTAppDelegate']);
77+
const SWIFT_CXX_INTEROP_PROBE_MODULE = 'ReactNativeHeaders_CxxInteropProbe';
78+
const SWIFT_CXX_INTEROP_PROBE_TYPES = [
79+
'facebook::react::jsinspector_modern::tracing::TraceRecordingState',
80+
'facebook::react::jsinspector_modern::tracing::HostTracingProfile',
81+
];
82+
6783
function log(msg /*: string */) {
6884
console.log(`[headers-verify] ${msg}`);
6985
}
@@ -304,12 +320,49 @@ func _headersVerifyProbe(_ bridge: RCTBridge) {
304320
`;
305321
}
306322

323+
/** Swift C++-interop fixture: every module shipped by ReactNativeHeaders. */
324+
function renderSwiftCxxInteropFixture(
325+
moduleNames /*: Array<string> */,
326+
) /*: string */ {
327+
return [
328+
'import React',
329+
...moduleNames.map(name => `import ${name}`),
330+
'',
331+
].join('\n');
332+
}
333+
334+
function renderSwiftCxxInteropProbeFixture() /*: string */ {
335+
return `import ${SWIFT_CXX_INTEROP_PROBE_MODULE}
336+
func _headersVerifyCxxValueProbe(
337+
_ state: borrowing facebook.react.jsinspector_modern.tracing.TraceRecordingState
338+
) {
339+
_ = state.mode
340+
}
341+
`;
342+
}
343+
344+
function parseModuleNames(moduleMapPath /*: string */) /*: Array<string> */ {
345+
const moduleMap = fs.readFileSync(moduleMapPath, 'utf8');
346+
// Dotted submodule names may not be standalone-importable if the composed
347+
// module map ever grows explicit submodules.
348+
return Array.from(
349+
moduleMap.matchAll(
350+
/^\s*(?:explicit\s+)?(?:framework\s+)?module\s+([A-Za-z_][A-Za-z0-9_.]*)\s*\{/gm,
351+
),
352+
match => match[1],
353+
);
354+
}
355+
307356
function xcrun(args /*: Array<string> */, what /*: string */) {
308357
try {
309-
execFileSync('xcrun', args, {stdio: ['ignore', 'pipe', 'pipe']});
358+
execFileSync('xcrun', args, {
359+
stdio: ['ignore', 'pipe', 'pipe'],
360+
maxBuffer: 64 * 1024 * 1024,
361+
});
310362
} catch (e) {
363+
const stdout = e.stdout != null ? String(e.stdout) : '';
311364
const stderr = e.stderr != null ? String(e.stderr) : String(e);
312-
throw new Error(`${what} FAILED:\n${stderr}`);
365+
throw new Error(`${what} FAILED:\n${stdout}${stderr}`);
313366
}
314367
}
315368

@@ -406,6 +459,129 @@ function runCompileGates(
406459
'Swift gate (import React + RCTBridge.moduleRegistry)',
407460
);
408461
log('compile: Swift moduleRegistry fixture OK.');
462+
463+
const moduleMap = path.join(rnhHeaders, 'module.modulemap');
464+
const moduleNames = parseModuleNames(moduleMap);
465+
if (moduleNames.length === 0) {
466+
throw new Error(
467+
`No modules declared in composed module map: ${moduleMap}`,
468+
);
469+
}
470+
const importableModuleNames = moduleNames.filter(
471+
name => !SWIFT_CXX_INTEROP_EXCLUDED_MODULES.has(name),
472+
);
473+
// React_RCTAppDelegate is the only composed module that reaches the
474+
// inspector C++ graph, but it cannot itself be imported in C++-interop
475+
// mode (see the exclusion comment above). Wrap the shipped inspector
476+
// umbrella in a temporary module so ClangImporter still checks that graph.
477+
const cxxInteropProbeHeaders = path.join(tmp, 'rnh-cxx-interop-probe');
478+
fs.cpSync(rnhHeaders, cxxInteropProbeHeaders, {
479+
recursive: true,
480+
filter: source => path.basename(source) !== 'module.modulemap',
481+
});
482+
const cxxInteropProbeHeader = path.join(tmp, 'cxx-interop-probe.h');
483+
const cxxInteropCopyProbes = SWIFT_CXX_INTEROP_PROBE_TYPES.map(
484+
(typeName, index) =>
485+
`inline void probeCopy${index}(const ${typeName} &value) {\n` +
486+
` instantiateCopy(value);\n` +
487+
`}\n`,
488+
).join('');
489+
fs.writeFileSync(
490+
cxxInteropProbeHeader,
491+
`#include <jsinspector-modern/ReactCdp.h>\n` +
492+
`#include <type_traits>\n` +
493+
`namespace rn_headers_verify {\n` +
494+
`template <typename T> void instantiateCopy(const T &value) {\n` +
495+
` if constexpr (std::is_copy_constructible_v<T>) {\n` +
496+
` T copy(value);\n` +
497+
` (void)copy;\n` +
498+
` }\n` +
499+
`}\n` +
500+
cxxInteropCopyProbes +
501+
`}\n`,
502+
);
503+
const cxxInteropProbeModuleMap = path.join(
504+
tmp,
505+
'module-cxx-interop-probe.modulemap',
506+
);
507+
fs.writeFileSync(
508+
cxxInteropProbeModuleMap,
509+
`module ${SWIFT_CXX_INTEROP_PROBE_MODULE} {\n` +
510+
` header ${JSON.stringify(cxxInteropProbeHeader)}\n` +
511+
` export *\n` +
512+
`}\n`,
513+
);
514+
const swiftCxxInterop = path.join(tmp, 'gate-swift-cxx-interop.swift');
515+
fs.writeFileSync(
516+
swiftCxxInterop,
517+
renderSwiftCxxInteropFixture(importableModuleNames),
518+
);
519+
xcrun(
520+
[
521+
'swiftc',
522+
'-typecheck',
523+
'-cxx-interoperability-mode=default',
524+
'-sdk',
525+
sdk,
526+
'-target',
527+
SIM_TARGET,
528+
'-module-cache-path',
529+
path.join(tmp, 'mc-swift-cxx-interop'),
530+
'-F',
531+
reactSlice,
532+
'-I',
533+
rnhHeaders,
534+
'-I',
535+
depsHeaders,
536+
'-Xcc',
537+
'-std=c++20',
538+
'-Xcc',
539+
'-w',
540+
'-Xcc',
541+
`-fmodule-map-file=${moduleMap}`,
542+
...FOLLY_DEFINES.flatMap(flag => ['-Xcc', flag]),
543+
swiftCxxInterop,
544+
],
545+
'Swift C++-interop gate (React + importable ReactNativeHeaders modules)',
546+
);
547+
548+
const swiftCxxInteropProbe = path.join(
549+
tmp,
550+
'gate-swift-cxx-interop-probe.swift',
551+
);
552+
fs.writeFileSync(swiftCxxInteropProbe, renderSwiftCxxInteropProbeFixture());
553+
xcrun(
554+
[
555+
'swiftc',
556+
'-typecheck',
557+
'-cxx-interoperability-mode=default',
558+
'-sdk',
559+
sdk,
560+
'-target',
561+
SIM_TARGET,
562+
'-module-cache-path',
563+
path.join(tmp, 'mc-swift-cxx-interop-probe'),
564+
'-I',
565+
cxxInteropProbeHeaders,
566+
'-I',
567+
depsHeaders,
568+
'-Xcc',
569+
'-std=c++20',
570+
'-Xcc',
571+
'-w',
572+
'-Xcc',
573+
`-fmodule-map-file=${cxxInteropProbeModuleMap}`,
574+
...FOLLY_DEFINES.flatMap(flag => ['-Xcc', flag]),
575+
swiftCxxInteropProbe,
576+
],
577+
'Swift C++-interop gate (inspector value types)',
578+
);
579+
log(
580+
`compile: Swift C++ interop React + ${importableModuleNames.length} ` +
581+
`ReactNativeHeaders modules + inspector probes ` +
582+
`[${SWIFT_CXX_INTEROP_PROBE_TYPES.join(', ')}] OK ` +
583+
`(${moduleNames.length - importableModuleNames.length} excluded).`,
584+
);
409585
} finally {
410586
fs.rmSync(tmp, {recursive: true, force: true});
411587
}

0 commit comments

Comments
 (0)