-
Notifications
You must be signed in to change notification settings - Fork 24.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Finish the JS-facing side of the TurboModule interop layer (#36630)
Summary: Pull Request resolved: #36630 ## Changes This diff hooks up global.nativeModuleProxy to the TurboModule interop layer. Now, when you call NativeModules.Foo, the TurboModule system will create the Foo interop module, and return it to JavaScript. |**Language**|**Abstraction**|**Description**| |Java/C++|MethodDescriptor| The information needed by JavaTurboModule::invokeJavaMethod() to execute a module method: [example](https://www.internalfb.com/code/fbsource/[78577e97310db97c489e976168ca6ddf4cb894c3]/xplat/js/react-native-github/ReactCommon/react/nativemodule/samples/platform/android/ReactCommon/SampleTurboModuleSpec.cpp?lines=34-36).| |Java|TurboModuleInteropUtils| Takes the interop module object, and parses out MethodDescriptors from the methods annotated with ReactMethod| |C++|JavaInteropTurboModule| Facilitates JavaScript -> Java method dispatch for interop modules. Extends [JavaTurboModule](https://www.internalfb.com/code/fbsource/[6f0698784af39dd0e881d9a69087ae6ac5e9cdc4]/xplat/js/react-native-github/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h?lines=28). Needs to be created with a list of MethodDescriptors.| Shape of MethodDescriptor: ``` class MethodDescriptor { String methodName; String jniSignature; String jsiReturnKind; int jsArgCount; } ``` ## Example global.nativeModuleProxy.Foo: 1. **Java:** Use TurboModuleManager to create the Java interop module for Foo 2. **Java:** Use TurboModuleInteropUtils to generate Foo's MethodDescriptors 3. **C++:** Use Foo's MethodDescriptors to create, cache, and return a JavaInteropTurboModule object to JavaScript. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D43918998 fbshipit-source-id: 562d3d7dc7f2ddb085dea6e94d72e1601012b741
- Loading branch information
1 parent
185bc24
commit 1f7daf9
Showing
10 changed files
with
861 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
288 changes: 288 additions & 0 deletions
288
...actAndroid/src/main/java/com/facebook/react/turbomodule/core/TurboModuleInteropUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
/* | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
package com.facebook.react.turbomodule.core; | ||
|
||
import androidx.annotation.Nullable; | ||
import com.facebook.proguard.annotations.DoNotStrip; | ||
import com.facebook.react.bridge.Callback; | ||
import com.facebook.react.bridge.Dynamic; | ||
import com.facebook.react.bridge.NativeModule; | ||
import com.facebook.react.bridge.Promise; | ||
import com.facebook.react.bridge.ReactMethod; | ||
import com.facebook.react.bridge.ReactModuleWithSpec; | ||
import com.facebook.react.bridge.ReadableArray; | ||
import com.facebook.react.bridge.ReadableMap; | ||
import com.facebook.react.bridge.WritableArray; | ||
import com.facebook.react.bridge.WritableMap; | ||
import java.lang.reflect.Method; | ||
import java.util.ArrayList; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
public class TurboModuleInteropUtils { | ||
public static class MethodDescriptor { | ||
@DoNotStrip public final String methodName; | ||
@DoNotStrip public final String jniSignature; | ||
@DoNotStrip public final String jsiReturnKind; | ||
@DoNotStrip public final int jsArgCount; | ||
|
||
MethodDescriptor(String methodName, String jniSignature, String jsiReturnKind, int jsArgCount) { | ||
this.methodName = methodName; | ||
this.jniSignature = jniSignature; | ||
this.jsiReturnKind = jsiReturnKind; | ||
this.jsArgCount = jsArgCount; | ||
} | ||
} | ||
|
||
public static class ParsingException extends RuntimeException { | ||
public ParsingException(String moduleName, String message) { | ||
super( | ||
"Unable to parse @ReactMethod annotations from native module: " | ||
+ moduleName | ||
+ ". Details: " | ||
+ message); | ||
} | ||
|
||
public ParsingException(String moduleName, String methodName, String message) { | ||
super( | ||
"Unable to parse @ReactMethod annotation from native module method: " | ||
+ moduleName | ||
+ "." | ||
+ methodName | ||
+ "()" | ||
+ ". Details: " | ||
+ message); | ||
} | ||
} | ||
|
||
public static List<MethodDescriptor> getMethodDescriptorsFromModule(NativeModule module) { | ||
final Method[] methods = getMethodsFromModule(module); | ||
|
||
List<MethodDescriptor> methodDescriptors = new ArrayList<>(); | ||
Set<String> methodNames = new HashSet<>(); | ||
|
||
for (Method method : methods) { | ||
@Nullable ReactMethod annotation = method.getAnnotation(ReactMethod.class); | ||
final String moduleName = module.getName(); | ||
final String methodName = method.getName(); | ||
if (annotation == null && !"getConstants".equals(methodName)) { | ||
continue; | ||
} | ||
|
||
if (methodNames.contains(methodName)) { | ||
throw new ParsingException( | ||
moduleName, | ||
"Module exports two methods to JavaScript with the same name: \"" + methodName); | ||
} | ||
|
||
methodNames.add(methodName); | ||
|
||
Class[] paramClasses = method.getParameterTypes(); | ||
Class returnType = method.getReturnType(); | ||
|
||
if ("getConstants".equals(methodName)) { | ||
if (returnType != Map.class) { | ||
// TODO(T145105887) Output error. getConstants must always have a return type of Map | ||
} | ||
} else if (annotation.isBlockingSynchronousMethod() && returnType == void.class | ||
|| !annotation.isBlockingSynchronousMethod() && returnType != void.class) { | ||
// TODO(T145105887): Output error. TurboModule system assumes returnType == void iff the | ||
// method is synchronous. | ||
} | ||
|
||
methodDescriptors.add( | ||
new MethodDescriptor( | ||
methodName, | ||
createJniSignature(moduleName, methodName, paramClasses, returnType), | ||
createJSIReturnKind(moduleName, methodName, paramClasses, returnType), | ||
getJsArgCount(moduleName, methodName, paramClasses))); | ||
} | ||
|
||
return methodDescriptors; | ||
} | ||
|
||
private static Method[] getMethodsFromModule(NativeModule module) { | ||
Class<? extends NativeModule> classForMethods = module.getClass(); | ||
Class<? extends NativeModule> superClass = | ||
(Class<? extends NativeModule>) classForMethods.getSuperclass(); | ||
if (ReactModuleWithSpec.class.isAssignableFrom(superClass)) { | ||
// For java module that is based on generated flow-type spec, inspect the | ||
// spec abstract class instead, which is the super class of the given java | ||
// module. | ||
classForMethods = superClass; | ||
} | ||
Method[] targetMethods = classForMethods.getDeclaredMethods(); | ||
return targetMethods; | ||
} | ||
|
||
private static String createJniSignature( | ||
String moduleName, String methodName, Class[] paramClasses, Class returnClass) { | ||
String jniSignature = "("; | ||
for (Class paramClass : paramClasses) { | ||
jniSignature += convertParamClassToJniType(moduleName, methodName, paramClass); | ||
} | ||
jniSignature += ")"; | ||
jniSignature += convertReturnClassToJniType(moduleName, methodName, returnClass); | ||
return jniSignature; | ||
} | ||
|
||
private static String convertParamClassToJniType( | ||
String moduleName, String methodName, Class paramClass) { | ||
if (paramClass == boolean.class) { | ||
return "Z"; | ||
} | ||
|
||
if (paramClass == int.class) { | ||
return "I"; | ||
} | ||
|
||
if (paramClass == double.class) { | ||
return "D"; | ||
} | ||
|
||
if (paramClass == float.class) { | ||
return "F"; | ||
} | ||
|
||
if (paramClass == Boolean.class | ||
|| paramClass == Integer.class | ||
|| paramClass == Double.class | ||
|| paramClass == Float.class | ||
|| paramClass == String.class | ||
|| paramClass == Callback.class | ||
|| paramClass == Promise.class | ||
|| paramClass == ReadableMap.class | ||
|| paramClass == ReadableArray.class) { | ||
return convertClassToJniType(paramClass); | ||
} | ||
|
||
if (paramClass == Dynamic.class) { | ||
// TODO(T145105887): Output warnings that TurboModules doesn't yet support Dynamic arguments | ||
} | ||
|
||
throw new ParsingException( | ||
moduleName, | ||
methodName, | ||
"Unable to parse JNI signature. Detected unsupported parameter class: " | ||
+ paramClass.getCanonicalName()); | ||
} | ||
|
||
private static String convertReturnClassToJniType( | ||
String moduleName, String methodName, Class returnClass) { | ||
if (returnClass == boolean.class) { | ||
return "Z"; | ||
} | ||
|
||
if (returnClass == int.class) { | ||
return "I"; | ||
} | ||
|
||
if (returnClass == double.class) { | ||
return "D"; | ||
} | ||
|
||
if (returnClass == float.class) { | ||
return "F"; | ||
} | ||
|
||
if (returnClass == void.class) { | ||
return "V"; | ||
} | ||
|
||
if (returnClass == Boolean.class | ||
|| returnClass == Integer.class | ||
|| returnClass == Double.class | ||
|| returnClass == Float.class | ||
|| returnClass == String.class | ||
|| returnClass == WritableMap.class | ||
|| returnClass == WritableArray.class | ||
|| returnClass == Map.class) { | ||
return convertClassToJniType(returnClass); | ||
} | ||
|
||
throw new ParsingException( | ||
moduleName, | ||
methodName, | ||
"Unable to parse JNI signature. Detected unsupported return class: " | ||
+ returnClass.getCanonicalName()); | ||
} | ||
|
||
private static String convertClassToJniType(Class cls) { | ||
return 'L' + cls.getCanonicalName().replace('.', '/') + ';'; | ||
} | ||
|
||
private static int getJsArgCount(String moduleName, String methodName, Class[] paramClasses) { | ||
for (int i = 0; i < paramClasses.length; i += 1) { | ||
if (paramClasses[i] == Promise.class) { | ||
if (i != (paramClasses.length - 1)) { | ||
throw new ParsingException( | ||
moduleName, | ||
methodName, | ||
"Unable to parse JavaScript arg count. Promises must be used as last parameter only."); | ||
} | ||
|
||
return paramClasses.length - 1; | ||
} | ||
} | ||
|
||
return paramClasses.length; | ||
} | ||
|
||
private static String createJSIReturnKind( | ||
String moduleName, String methodName, Class[] paramClasses, Class returnClass) { | ||
for (int i = 0; i < paramClasses.length; i += 1) { | ||
if (paramClasses[i] == Promise.class) { | ||
if (i != (paramClasses.length - 1)) { | ||
throw new ParsingException( | ||
moduleName, | ||
methodName, | ||
"Unable to parse JSI return kind. Promises must be used as last parameter only."); | ||
} | ||
|
||
return "PromiseKind"; | ||
} | ||
} | ||
|
||
if (returnClass == boolean.class || returnClass == Boolean.class) { | ||
return "BooleanKind"; | ||
} | ||
|
||
if (returnClass == double.class | ||
|| returnClass == Double.class | ||
|| returnClass == float.class | ||
|| returnClass == Float.class | ||
|| returnClass == int.class | ||
|| returnClass == Integer.class) { | ||
return "NumberKind"; | ||
} | ||
|
||
if (returnClass == String.class) { | ||
return "StringKind"; | ||
} | ||
|
||
if (returnClass == void.class) { | ||
return "VoidKind"; | ||
} | ||
|
||
if (returnClass == WritableMap.class || returnClass == Map.class) { | ||
return "ObjectKind"; | ||
} | ||
|
||
if (returnClass == WritableArray.class) { | ||
return "ArrayKind"; | ||
} | ||
|
||
throw new ParsingException( | ||
moduleName, | ||
methodName, | ||
"Unable to parse JSI return kind. Detected unsupported return class: " | ||
+ returnClass.getCanonicalName()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.