From 9e2de39a593a37db893c6e1458768117036ab608 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Tue, 12 May 2026 15:34:54 -0700 Subject: [PATCH] Implement callFunctionOnModule on BridgelessCatalystInstance Summary: Implement `BridgelessCatalystInstance.callFunction` using the existing `ReactHostImpl.callFunctionOnModule` so callers that go through `CatalystInstance` (rather than directly through `JavaScriptModule` proxies) work in bridgeless mode. While here, propagate nullability of `args` through the call chain. `InvocationHandler.invoke` is documented to pass `null` when the proxied method has no parameters, and the existing `Arguments.fromJavaArgs(args)` would NPE in that case. The JNI layer substitutes an empty `folly::dynamic::array()` when `args` is null, so the JS call still has a well-formed argument list. Changelog: [Android][Fixed] JSModule method without args are correctly dispatched Differential Revision: D104901132 --- .../com/facebook/react/runtime/BridgelessCatalystInstance.kt | 2 +- .../com/facebook/react/runtime/BridgelessReactContext.kt | 5 ++--- .../main/java/com/facebook/react/runtime/ReactHostImpl.kt | 2 +- .../main/java/com/facebook/react/runtime/ReactInstance.kt | 2 +- .../src/main/jni/react/runtime/jni/JReactInstance.cpp | 5 ++++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessCatalystInstance.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessCatalystInstance.kt index 32baa61aa1e1..50f58fbbe7fb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessCatalystInstance.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessCatalystInstance.kt @@ -77,7 +77,7 @@ internal class BridgelessCatalystInstance(private val reactHost: ReactHostImpl) } override fun callFunction(module: String, method: String, arguments: NativeArray?) { - throw UnsupportedOperationException("Unimplemented method 'callFunction'") + reactHost.callFunctionOnModule(module, method, arguments) } override fun destroy() { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt index 91a376498681..936dc6c99537 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt @@ -17,7 +17,6 @@ import com.facebook.react.bridge.CatalystInstance import com.facebook.react.bridge.JavaScriptContextHolder import com.facebook.react.bridge.JavaScriptModule import com.facebook.react.bridge.JavaScriptModuleRegistry -import com.facebook.react.bridge.NativeArray import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactSoftExceptionLogger.logSoftException @@ -117,8 +116,8 @@ internal class BridgelessReactContext(context: Context, private val reactHost: R private val reactHost: ReactHostImpl, private val jsModuleInterface: Class, ) : InvocationHandler { - override fun invoke(proxy: Any, method: Method, args: Array): Any? { - val jsArgs: NativeArray = Arguments.fromJavaArgs(args) + override fun invoke(proxy: Any, method: Method, args: Array?): Any? { + val jsArgs = if (args != null) Arguments.fromJavaArgs(args) else null reactHost.callFunctionOnModule( JavaScriptModuleRegistry.getJSModuleName(jsModuleInterface), method.name, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 42c78a912082..987e55ac6bc4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -806,7 +806,7 @@ public class ReactHostImpl( internal fun callFunctionOnModule( moduleName: String, methodName: String, - args: NativeArray, + args: NativeArray?, ): Task { val method = "callFunctionOnModule(\"$moduleName\", \"$methodName\")" return callWithExistingReactInstance(method) { reactInstance: ReactInstance -> diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt index a5fdda0edd31..02942aca5b41 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt @@ -462,7 +462,7 @@ internal class ReactInstance( private external fun getJavaScriptContext(): Long - external fun callFunctionOnModule(moduleName: String, methodName: String, args: NativeArray) + external fun callFunctionOnModule(moduleName: String, methodName: String, args: NativeArray?) private external fun registerSegmentNative(segmentId: Int, segmentPath: String) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactInstance.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactInstance.cpp index b2911f092b5f..b6d7e64a82f4 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactInstance.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactInstance.cpp @@ -173,7 +173,10 @@ void JReactInstance::callFunctionOnModule( const std::string& moduleName, const std::string& methodName, NativeArray* args) { - instance_->callFunctionOnModule(moduleName, methodName, args->consume()); + instance_->callFunctionOnModule( + moduleName, + methodName, + args != nullptr ? args->consume() : folly::dynamic::array()); } jni::alias_ref