Skip to content

Navigation Menu

Sign in
Sign up

Make +moduleName optional so New Arch modules can drop RCT_EXPORT_MODULE WIP - #58417

Open
christophpurrer wants to merge 1 commit into
react:main from
christophpurrer:export-D116325661
Open

Make +moduleName optional so New Arch modules can drop RCT_EXPORT_MODULE WIP #58417
christophpurrer wants to merge 1 commit into
react:main from
christophpurrer:export-D116325661

Conversation

@christophpurrer

@christophpurrer christophpurrer commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary:
Changelog:
[iOS][Breaking] - +moduleName is now optional on RCTBridgeModule instead of required, so a TurboModule resolved via a module provider need not declare it

Investigating whether ObjC Turbo Modules still need RCT_EXPORT_MODULE when only
the New Architecture is supported. The macro does two things, and they have
diverged.

Registration is already dead code in a new-arch-only build. When both
RCT_REMOVE_LEGACY_MODULE_INTEROP and RCT_REMOVE_LEGACY_COMPONENT_INTEROP are
set, RCT_EXPORT_MODULE already reduces to a bare name stub
(RCTBridgeModule.h:72-80), and RCTRegisterModule / RCTGetModuleClasses are
compiled out of RCTBridge.mm entirely. Every reader of that registry sits behind
the same guards. Discovery instead runs through RCTTurboModuleManagerDelegate:
RCTTurboModulePluginClassProvider over RctTurboModuleProviderSocket (populated
by react_module_plugin_providers in BUCK) internally, and generated
RCTModuleProviders.mm plus the NSClassFromString fallback in OSS. Neither path
consults +moduleName.

But +moduleName was still load-bearing, which is what actually blocked
removing the macro. It was required on RCTBridgeModule, and
RCTBridgeModuleNameForClass called it unguarded from
RCTTurboModuleManager.mm:778 — the hot path for every ObjC Turbo Module
instantiation. Deleting RCT_EXPORT_MODULE therefore produced an
unrecognized-selector crash, not graceful degradation.

This diff makes absence safe, without touching any macro:

  • RCTBridgeModule.h — move + (NSString *)moduleName below optional. It was
    already effectively optional; RCTLogBox, RCTRedBox and RCTDevLoadingView
    return nil today.
  • RCTBridge.mm and RCTComponentData.mm — guard the call with
    respondsToSelector:, letting the pre-existing
    name.length == 0 -> NSStringFromClass fallback handle absence.

No behavior change for any module that declares +moduleName. This only unblocks
removal; it does not remove the macro anywhere.

Why this is tagged Breaking. RCTBridgeModule is public API, and the
required -> optional move changes its published contract. Existing
implementors are unaffected — an implementation that satisfies a required member
also satisfies an optional one — but any code that consumed the old guarantee,
i.e. called [cls moduleName] on an arbitrary id<RCTBridgeModule> without a
respondsToSelector: check, is no longer guaranteed a responder once modules
start dropping the macro. Third-party callers doing that need the same guard this
diff adds to the two in-tree call sites.

Cxx / ObjC API snapshot: verified unchanged, no regeneration needed. The
scripts/cxx-api snapshot format does not encode ObjC required / optional,
and members are sorted alphabetically rather than by declaration order, so neither
half of this edit is observable in the .api output. Verified empirically rather
than assumed — see the test plan.

Two caveats worth recording for whoever does the removal:

  • It is not safe to drop the macro everywhere yet. In apps without
    react-remove-legacy-module-interop[enabled] (per-app, rollout incomplete) the
    RCTGetModuleClasses() scan at RCTTurboModuleManager.mm:761 is live and is the
    only resolver for a custom-JS-named module absent from the plugin socket;
    removal there silently yields nil for NativeModules.Foo. Same for OSS
    third-party libs where ObjC class name != JS name — both removal flags are
    opt-in and off by default. Do not drop it from view managers.
  • Pre-existing latent break, unrelated to this diff:
    RCT_EXPORT_MODULE_NO_LOAD sits at RCTBridgeModule.h:112-121, inside the
    #else branch, so RCT_EXTERN_MODULE (Swift) is undefined under
    interop-removal.

facebook
https://www.internalfb.com/agent-home?session_id=dmh-b3fdfe1a-fc40-4b19-9f59-aff69a033625

Differential Revision: D116325661

...MODULE` WIP
Summary:
Changelog:
[iOS][Breaking] - `+moduleName` is now optional on `RCTBridgeModule` instead of required, so a TurboModule resolved via a module provider need not declare it
Investigating whether ObjC Turbo Modules still need `RCT_EXPORT_MODULE` when only
the New Architecture is supported. The macro does two things, and they have
diverged.
**Registration is already dead code in a new-arch-only build.** When both
`RCT_REMOVE_LEGACY_MODULE_INTEROP` and `RCT_REMOVE_LEGACY_COMPONENT_INTEROP` are
set, `RCT_EXPORT_MODULE` already reduces to a bare name stub
(`RCTBridgeModule.h:72-80`), and `RCTRegisterModule` / `RCTGetModuleClasses` are
compiled out of `RCTBridge.mm` entirely. Every reader of that registry sits behind
the same guards. Discovery instead runs through `RCTTurboModuleManagerDelegate`:
`RCTTurboModulePluginClassProvider` over `RctTurboModuleProviderSocket` (populated
by `react_module_plugin_providers` in BUCK) internally, and generated
`RCTModuleProviders.mm` plus the `NSClassFromString` fallback in OSS. Neither path
consults `+moduleName`.
**But `+moduleName` was still load-bearing**, which is what actually blocked
removing the macro. It was `required` on `RCTBridgeModule`, and
`RCTBridgeModuleNameForClass` called it unguarded from
`RCTTurboModuleManager.mm:778` — the hot path for every ObjC Turbo Module
instantiation. Deleting `RCT_EXPORT_MODULE` therefore produced an
unrecognized-selector crash, not graceful degradation.
This diff makes absence safe, without touching any macro:
- `RCTBridgeModule.h` — move `+ (NSString *)moduleName` below `optional`. It was
 already effectively optional; `RCTLogBox`, `RCTRedBox` and `RCTDevLoadingView`
 return `nil` today.
- `RCTBridge.mm` and `RCTComponentData.mm` — guard the call with
 `respondsToSelector:`, letting the pre-existing
 `name.length == 0 -> NSStringFromClass` fallback handle absence.
No behavior change for any module that declares `+moduleName`. This only unblocks
removal; it does not remove the macro anywhere.
**Why this is tagged Breaking.** `RCTBridgeModule` is public API, and the
`required` -> `optional` move changes its published contract. Existing
*implementors* are unaffected — an implementation that satisfies a required member
also satisfies an optional one — but any code that *consumed* the old guarantee,
i.e. called `[cls moduleName]` on an arbitrary `id<RCTBridgeModule>` without a
`respondsToSelector:` check, is no longer guaranteed a responder once modules
start dropping the macro. Third-party callers doing that need the same guard this
diff adds to the two in-tree call sites.
**Cxx / ObjC API snapshot: verified unchanged, no regeneration needed.** The
`scripts/cxx-api` snapshot format does not encode ObjC `required` / `optional`,
and members are sorted alphabetically rather than by declaration order, so neither
half of this edit is observable in the `.api` output. Verified empirically rather
than assumed — see the test plan.
Two caveats worth recording for whoever does the removal:
- It is **not** safe to drop the macro everywhere yet. In apps without
 `react-remove-legacy-module-interop[enabled]` (per-app, rollout incomplete) the
 `RCTGetModuleClasses()` scan at `RCTTurboModuleManager.mm:761` is live and is the
 only resolver for a custom-JS-named module absent from the plugin socket;
 removal there silently yields `nil` for `NativeModules.Foo`. Same for OSS
 third-party libs where ObjC class name != JS name — both removal flags are
 opt-in and off by default. Do not drop it from view managers.
- Pre-existing latent break, unrelated to this diff:
 `RCT_EXPORT_MODULE_NO_LOAD` sits at `RCTBridgeModule.h:112-121`, inside the
 `#else` branch, so `RCT_EXTERN_MODULE` (Swift) is undefined under
 interop-removal.
facebook
https://www.internalfb.com/agent-home?session_id=dmh-b3fdfe1a-fc40-4b19-9f59-aff69a033625
Differential Revision: D116325661
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 9, 2026

meta-codesync Bot commented Sep 9, 2026

Copy link
Copy Markdown

@christophpurrer has exported this pull request. If you are a Meta employee, you can view the originating Diff in D116325661.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /