Skip to content

Commit

Permalink
Finish the JS-facing side of the TurboModule interop layer (#36630)
Browse files Browse the repository at this point in the history
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
RSNara authored and facebook-github-bot committed Mar 28, 2023
1 parent 185bc24 commit 1f7daf9
Show file tree
Hide file tree
Showing 10 changed files with 861 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ const NativeModules = require('../BatchedBridge/NativeModules');

const turboModuleProxy = global.__turboModuleProxy;

// TODO(148943970): Consider reversing the lookup here:
// Lookup on __turboModuleProxy, then lookup on nativeModuleProxy
function requireModule<T: TurboModule>(name: string): ?T {
// Bridgeless mode requires TurboModules
if (global.RN$Bridgeless !== true) {
const isBridgeless = global.RN$Bridgeless === true;
const isTurboModuleInteropEnabled = global.RN$TurboInterop === true;
if (!isBridgeless || isTurboModuleInteropEnabled) {
// Backward compatibility layer during migration.
const legacyModule = NativeModules[name];
if (legacyModule != null) {
Expand Down
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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import com.facebook.react.turbomodule.core.interfaces.TurboModuleRegistry;
import com.facebook.soloader.SoLoader;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* This is the main class and entry point for TurboModules. Note that this is a hybrid class, and
Expand Down Expand Up @@ -61,7 +65,7 @@ public TurboModuleManager(
(CallInvokerHolderImpl) jsCallInvokerHolder,
(CallInvokerHolderImpl) nativeCallInvokerHolder,
delegate);
installJSIBindings();
installJSIBindings(shouldCreateLegacyModules());

mEagerInitModuleNames =
delegate == null ? new ArrayList<String>() : delegate.getEagerInitModuleNames();
Expand Down Expand Up @@ -120,16 +124,42 @@ public List<String> getEagerInitModuleNames() {
return mEagerInitModuleNames;
}

@DoNotStrip
private static List<TurboModuleInteropUtils.MethodDescriptor> getMethodDescriptorsFromModule(
NativeModule module) {
return TurboModuleInteropUtils.getMethodDescriptorsFromModule(module);
}

@DoNotStrip
@Nullable
private NativeModule getLegacyJavaModule(String moduleName) {
final NativeModule module = getNativeModule(moduleName);
return !(module instanceof CxxModuleWrapper) && !(module instanceof TurboModule)
? module
: null;
}

@DoNotStrip
@Nullable
private CxxModuleWrapper getLegacyCxxModule(String moduleName) {
final NativeModule module = getNativeModule(moduleName);
return module instanceof CxxModuleWrapper ? (CxxModuleWrapper) module : null;
return module instanceof CxxModuleWrapper && !(module instanceof TurboModule)
? (CxxModuleWrapper) module
: null;
}

@DoNotStrip
@Nullable
private CxxModuleWrapper getTurboLegacyCxxModule(String moduleName) {
final NativeModule module = getNativeModule(moduleName);
return module instanceof CxxModuleWrapper && module instanceof TurboModule
? (CxxModuleWrapper) module
: null;
}

@DoNotStrip
@Nullable
private TurboModule getJavaModule(String moduleName) {
private TurboModule getTurboJavaModule(String moduleName) {
final NativeModule module = getNativeModule(moduleName);
return !(module instanceof CxxModuleWrapper) && module instanceof TurboModule
? (TurboModule) module
Expand Down Expand Up @@ -341,7 +371,7 @@ private native HybridData initHybrid(
CallInvokerHolderImpl nativeCallInvokerHolder,
TurboModuleManagerDelegate tmmDelegate);

private native void installJSIBindings();
private native void installJSIBindings(boolean shouldCreateLegacyModules);

@Override
public void initialize() {}
Expand Down
Loading

0 comments on commit 1f7daf9

Please sign in to comment.