From efd2bc867877d266bd2613bfa5db6451d05b75d4 Mon Sep 17 00:00:00 2001 From: Giovanni Cappellotto Date: Wed, 30 Sep 2026 12:13:44 -0700 Subject: [PATCH] Expose the JS CallInvoker on RCTHost Summary: Android exposes the React instance's `CallInvoker` through `ReactContext.getJSCallInvokerHolder()`. iOS has no equivalent: `RCTInstance` creates the same invoker but only hands it to its TurboModules. This adds `-[RCTHost jsCallInvoker]`, backed by a new `RCTInstance.jsCallInvoker` that keeps that invoker. Unlike `RuntimeExecutor`, a `CallInvoker` lets native code schedule work on the JS thread with a priority. - Set once during instance init, so it can be read from any thread. - Holding the invoker past a reload, or past the host's lifetime, is safe: it only references the instance's scheduler weakly, so work sent after the instance is gone is dropped. - Null before `start`. Every reload creates a new instance with a new invoker, so callers read it again after `hostDidStart:`. - The C++/Objective-C API snapshots are updated for the two new declarations. Changelog: [iOS][Added] - Add `-[RCTHost jsCallInvoker]` to access the React instance's `CallInvoker` Reviewed By: zeyap Differential Revision: D122169845 --- .../react/runtime/platform/ios/ReactCommon/RCTHost.h | 8 ++++++++ .../react/runtime/platform/ios/ReactCommon/RCTHost.mm | 6 ++++++ .../react/runtime/platform/ios/ReactCommon/RCTInstance.h | 7 +++++++ .../react/runtime/platform/ios/ReactCommon/RCTInstance.mm | 7 +++++++ scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api | 2 ++ scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api | 2 ++ scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 2 ++ 7 files changed, 34 insertions(+) diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.h b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.h index cd2f5d546c4f..a7471de8af52 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.h +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.h @@ -110,6 +110,14 @@ typedef std::shared_ptr (^RCTHostJSEngineProv - (void)callFunctionOnJSModule:(NSString *)moduleName method:(NSString *)method args:(NSArray *)args; +/** + * The CallInvoker of the current React instance, the same one its TurboModules receive. It schedules work on the JS + * thread with a priority. The returned invoker can be used from any thread. Null before `start` and while a reload + * replaces the instance; every reload creates a new invoker, so read it again after `hostDidStart:`. Holding it past a + * reload or past the host's lifetime is safe: work sent to an invoker whose instance is gone is dropped. + */ +- (std::shared_ptr)jsCallInvoker; + // Renderer API - (RCTFabricSurface *)createSurfaceWithModuleName:(NSString *)moduleName diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm index 815e469ec0c0..7fe97d087f1f 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm @@ -466,6 +466,12 @@ - (void)callFunctionOnJSModule:(NSString *)moduleName method:(NSString *)method [_instance callFunctionOnJSModule:moduleName method:method args:args]; } +- (std::shared_ptr)jsCallInvoker +{ + RCTInstance *instance = _instance; + return instance ? instance.jsCallInvoker : nullptr; +} + #pragma mark - RCTReloadListener - (void)didReceiveReloadCommand diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.h b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.h index c4903a458cf6..8ac61fc1de60 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.h +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.h @@ -11,6 +11,7 @@ #import #import +#import #import #import @@ -85,6 +86,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, strong) RCTSurfacePresenter *surfacePresenter; +/** + * The CallInvoker this instance hands to TurboModules. Schedules work on the JS thread with a priority, and can be + * called from any thread. Set during init; null once the instance is invalidated. + */ +@property (nonatomic, readonly) std::shared_ptr jsCallInvoker; + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm index 01e337931de1..d79f9029bb75 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm @@ -74,6 +74,7 @@ @implementation RCTInstance { RCTDisplayLink *_displayLink; RCTTurboModuleManager *_turboModuleManager; RCTBridgeProxy *_bridgeProxy; + std::shared_ptr _jsCallInvoker; std::mutex _invalidationMutex; std::atomic _valid; RCTJSThreadManager *_jsThreadManager; @@ -207,6 +208,11 @@ - (void)invalidate }]; } +- (std::shared_ptr)jsCallInvoker +{ + return _valid ? _jsCallInvoker : nullptr; +} + - (void)registerSegmentWithId:(NSNumber *)segmentId path:(NSString *)path { if (_valid) { @@ -290,6 +296,7 @@ - (void)_start timerManager->setRuntimeExecutor(bufferedRuntimeExecutor); auto jsCallInvoker = _reactInstance->createJSCallInvoker(); + _jsCallInvoker = jsCallInvoker; RCTBridgeProxy *bridgeProxy = [[RCTBridgeProxy alloc] initWithViewRegistry:_bridgeModuleDecorator.viewRegistry_DEPRECATED moduleRegistry:_bridgeModuleDecorator.moduleRegistry diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index a8f040724fa6..d0d014ec91cb 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -1159,6 +1159,7 @@ interface RCTHost : public NSObject { public virtual instancetype initWithBundleURL:hostDelegate:turboModuleManagerDelegate:jsEngineProvider:launchOptions:(NSURL* bundleURL, id hostDelegate, id turboModuleManagerDelegate, RCTHostJSEngineProvider jsEngineProvider, _Nullable NSDictionary* launchOptions); public virtual instancetype initWithBundleURLProvider:hostDelegate:turboModuleManagerDelegate:jsEngineProvider:launchOptions:(RCTHostBundleURLProvider provider, id hostDelegate, id turboModuleManagerDelegate, RCTHostJSEngineProvider jsEngineProvider, _Nullable NSDictionary* launchOptions); public virtual instancetype initWithBundleURLProvider:hostDelegate:turboModuleManagerDelegate:jsEngineProvider:launchOptions:bundleConfiguration:devMenuConfiguration:(RCTHostBundleURLProvider provider, id hostDelegate, id turboModuleManagerDelegate, RCTHostJSEngineProvider jsEngineProvider, _Nullable NSDictionary* launchOptions, RCTBundleConfiguration* bundleConfiguration, RCTDevMenuConfiguration* devMenuConfiguration); + public virtual std::shared_ptr jsCallInvoker(); public virtual void callFunctionOnJSModule:method:args:(NSString* moduleName, NSString* method, NSArray* args); public virtual void start(); } @@ -1258,6 +1259,7 @@ interface RCTInspectorWebSocketReporter : public NSObject { } interface RCTInstance : public NSObject { + public @property (assign, readonly) std::shared_ptr jsCallInvoker; public @property (strong, readonly) RCTSurfacePresenter* surfacePresenter; public virtual instancetype initWithDelegate:jsRuntimeFactory:bundleManager:turboModuleManagerDelegate:moduleRegistry:parentInspectorTarget:launchOptions:(id delegate, std::shared_ptr jsRuntimeFactory, RCTBundleManager* bundleManager, id turboModuleManagerDelegate, RCTModuleRegistry* moduleRegistry, facebook::react::jsinspector_modern::HostTarget* parentInspectorTarget, _Nullable NSDictionary* launchOptions); public virtual instancetype initWithDelegate:jsRuntimeFactory:bundleManager:turboModuleManagerDelegate:moduleRegistry:parentInspectorTarget:launchOptions:devMenuConfiguration:(id delegate, std::shared_ptr jsRuntimeFactory, RCTBundleManager* bundleManager, id turboModuleManagerDelegate, RCTModuleRegistry* moduleRegistry, facebook::react::jsinspector_modern::HostTarget* parentInspectorTarget, _Nullable NSDictionary* launchOptions, RCTDevMenuConfiguration* _Nullable devMenuConfiguration); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 3c0a535fb10e..4290dcb0e425 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -1159,6 +1159,7 @@ interface RCTHost : public NSObject { public virtual instancetype initWithBundleURL:hostDelegate:turboModuleManagerDelegate:jsEngineProvider:launchOptions:(NSURL* bundleURL, id hostDelegate, id turboModuleManagerDelegate, RCTHostJSEngineProvider jsEngineProvider, _Nullable NSDictionary* launchOptions); public virtual instancetype initWithBundleURLProvider:hostDelegate:turboModuleManagerDelegate:jsEngineProvider:launchOptions:(RCTHostBundleURLProvider provider, id hostDelegate, id turboModuleManagerDelegate, RCTHostJSEngineProvider jsEngineProvider, _Nullable NSDictionary* launchOptions); public virtual instancetype initWithBundleURLProvider:hostDelegate:turboModuleManagerDelegate:jsEngineProvider:launchOptions:bundleConfiguration:devMenuConfiguration:(RCTHostBundleURLProvider provider, id hostDelegate, id turboModuleManagerDelegate, RCTHostJSEngineProvider jsEngineProvider, _Nullable NSDictionary* launchOptions, RCTBundleConfiguration* bundleConfiguration, RCTDevMenuConfiguration* devMenuConfiguration); + public virtual std::shared_ptr jsCallInvoker(); public virtual void callFunctionOnJSModule:method:args:(NSString* moduleName, NSString* method, NSArray* args); public virtual void start(); } @@ -1258,6 +1259,7 @@ interface RCTInspectorWebSocketReporter : public NSObject { } interface RCTInstance : public NSObject { + public @property (assign, readonly) std::shared_ptr jsCallInvoker; public @property (strong, readonly) RCTSurfacePresenter* surfacePresenter; public virtual instancetype initWithDelegate:jsRuntimeFactory:bundleManager:turboModuleManagerDelegate:moduleRegistry:parentInspectorTarget:launchOptions:(id delegate, std::shared_ptr jsRuntimeFactory, RCTBundleManager* bundleManager, id turboModuleManagerDelegate, RCTModuleRegistry* moduleRegistry, facebook::react::jsinspector_modern::HostTarget* parentInspectorTarget, _Nullable NSDictionary* launchOptions); public virtual instancetype initWithDelegate:jsRuntimeFactory:bundleManager:turboModuleManagerDelegate:moduleRegistry:parentInspectorTarget:launchOptions:devMenuConfiguration:(id delegate, std::shared_ptr jsRuntimeFactory, RCTBundleManager* bundleManager, id turboModuleManagerDelegate, RCTModuleRegistry* moduleRegistry, facebook::react::jsinspector_modern::HostTarget* parentInspectorTarget, _Nullable NSDictionary* launchOptions, RCTDevMenuConfiguration* _Nullable devMenuConfiguration); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 958586fc805e..72a9805b05fd 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -1159,6 +1159,7 @@ interface RCTHost : public NSObject { public virtual instancetype initWithBundleURL:hostDelegate:turboModuleManagerDelegate:jsEngineProvider:launchOptions:(NSURL* bundleURL, id hostDelegate, id turboModuleManagerDelegate, RCTHostJSEngineProvider jsEngineProvider, _Nullable NSDictionary* launchOptions); public virtual instancetype initWithBundleURLProvider:hostDelegate:turboModuleManagerDelegate:jsEngineProvider:launchOptions:(RCTHostBundleURLProvider provider, id hostDelegate, id turboModuleManagerDelegate, RCTHostJSEngineProvider jsEngineProvider, _Nullable NSDictionary* launchOptions); public virtual instancetype initWithBundleURLProvider:hostDelegate:turboModuleManagerDelegate:jsEngineProvider:launchOptions:bundleConfiguration:devMenuConfiguration:(RCTHostBundleURLProvider provider, id hostDelegate, id turboModuleManagerDelegate, RCTHostJSEngineProvider jsEngineProvider, _Nullable NSDictionary* launchOptions, RCTBundleConfiguration* bundleConfiguration, RCTDevMenuConfiguration* devMenuConfiguration); + public virtual std::shared_ptr jsCallInvoker(); public virtual void callFunctionOnJSModule:method:args:(NSString* moduleName, NSString* method, NSArray* args); public virtual void start(); } @@ -1258,6 +1259,7 @@ interface RCTInspectorWebSocketReporter : public NSObject { } interface RCTInstance : public NSObject { + public @property (assign, readonly) std::shared_ptr jsCallInvoker; public @property (strong, readonly) RCTSurfacePresenter* surfacePresenter; public virtual instancetype initWithDelegate:jsRuntimeFactory:bundleManager:turboModuleManagerDelegate:moduleRegistry:parentInspectorTarget:launchOptions:(id delegate, std::shared_ptr jsRuntimeFactory, RCTBundleManager* bundleManager, id turboModuleManagerDelegate, RCTModuleRegistry* moduleRegistry, facebook::react::jsinspector_modern::HostTarget* parentInspectorTarget, _Nullable NSDictionary* launchOptions); public virtual instancetype initWithDelegate:jsRuntimeFactory:bundleManager:turboModuleManagerDelegate:moduleRegistry:parentInspectorTarget:launchOptions:devMenuConfiguration:(id delegate, std::shared_ptr jsRuntimeFactory, RCTBundleManager* bundleManager, id turboModuleManagerDelegate, RCTModuleRegistry* moduleRegistry, facebook::react::jsinspector_modern::HostTarget* parentInspectorTarget, _Nullable NSDictionary* launchOptions, RCTDevMenuConfiguration* _Nullable devMenuConfiguration);