diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java index 41a0d65ee..f43f57f03 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java @@ -11,18 +11,8 @@ package com.facebook.react.bridge; import javax.annotation.Nullable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.HashMap; import java.util.Map; -import com.facebook.infer.annotation.Assertions; -import com.facebook.systrace.Systrace; -import com.facebook.systrace.SystraceMessage; - -import static com.facebook.infer.annotation.Assertions.assertNotNull; -import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; - /** * Base class for Catalyst native modules whose implementations are written in Java. Default * implementations for {@link #initialize} and {@link #onCatalystInstanceDestroy} are provided for @@ -53,385 +43,6 @@ public abstract class BaseJavaModule implements NativeModule { static final public String METHOD_TYPE_PROMISE= "promise"; static final public String METHOD_TYPE_SYNC = "sync"; - private static abstract class ArgumentExtractor { - public int getJSArgumentsNeeded() { - return 1; - } - - public abstract @Nullable T extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex); - } - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_BOOLEAN = - new ArgumentExtractor() { - @Override - public Boolean extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - return jsArguments.getBoolean(atIndex); - } - }; - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_DOUBLE = - new ArgumentExtractor() { - @Override - public Double extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - return jsArguments.getDouble(atIndex); - } - }; - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_FLOAT = - new ArgumentExtractor() { - @Override - public Float extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - return (float) jsArguments.getDouble(atIndex); - } - }; - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_INTEGER = - new ArgumentExtractor() { - @Override - public Integer extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - return (int) jsArguments.getDouble(atIndex); - } - }; - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_STRING = - new ArgumentExtractor() { - @Override - public String extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - return jsArguments.getString(atIndex); - } - }; - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_ARRAY = - new ArgumentExtractor() { - @Override - public ReadableNativeArray extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - return jsArguments.getArray(atIndex); - } - }; - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_DYNAMIC = - new ArgumentExtractor() { - @Override - public Dynamic extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - return DynamicFromArray.create(jsArguments, atIndex); - } - }; - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_MAP = - new ArgumentExtractor() { - @Override - public ReadableMap extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - return jsArguments.getMap(atIndex); - } - }; - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_CALLBACK = - new ArgumentExtractor() { - @Override - public @Nullable Callback extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - if (jsArguments.isNull(atIndex)) { - return null; - } else { - int id = (int) jsArguments.getDouble(atIndex); - return new CallbackImpl(jsInstance, executorToken, id); - } - } - }; - - static final private ArgumentExtractor ARGUMENT_EXTRACTOR_PROMISE = - new ArgumentExtractor() { - @Override - public int getJSArgumentsNeeded() { - return 2; - } - - @Override - public Promise extractArgument( - JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { - Callback resolve = ARGUMENT_EXTRACTOR_CALLBACK - .extractArgument(jsInstance, executorToken, jsArguments, atIndex); - Callback reject = ARGUMENT_EXTRACTOR_CALLBACK - .extractArgument(jsInstance, executorToken, jsArguments, atIndex + 1); - return new PromiseImpl(resolve, reject); - } - }; - - public class JavaMethod implements NativeMethod { - - private final Method mMethod; - private final Class[] mParameterTypes; - private final int mParamLength; - private boolean mArgumentsProcessed = false; - private @Nullable ArgumentExtractor[] mArgumentExtractors; - private @Nullable String mSignature; - private @Nullable Object[] mArguments; - private String mType = METHOD_TYPE_ASYNC; - private @Nullable int mJSArgumentsNeeded; - private String mTraceName; - - public JavaMethod(Method method, boolean isSync) { - mMethod = method; - mMethod.setAccessible(true); - Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "callGetParameterTypes"); - mParameterTypes = mMethod.getParameterTypes(); - Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); - mParamLength = mParameterTypes.length; - - if (isSync) { - mType = METHOD_TYPE_SYNC; - } else if (mParamLength > 0 && (mParameterTypes[mParamLength - 1] == Promise.class)) { - mType = METHOD_TYPE_PROMISE; - } - mTraceName = BaseJavaModule.this.getName() + "." + mMethod.getName(); - - } - - private void processArguments() { - if (mArgumentsProcessed) { - return; - } - mArgumentsProcessed = true; - mArgumentExtractors = buildArgumentExtractors(mParameterTypes); - mSignature = buildSignature(mMethod, mParameterTypes, (mType.equals(METHOD_TYPE_SYNC))); - // Since native methods are invoked from a message queue executed on a single thread, it is - // safe to allocate only one arguments object per method that can be reused across calls - mArguments = new Object[mParameterTypes.length]; - mJSArgumentsNeeded = calculateJSArgumentsNeeded(); - } - - public Method getMethod() { - return mMethod; - } - - public String getSignature() { - if (!mArgumentsProcessed) { - processArguments(); - } - return assertNotNull(mSignature); - } - - private String buildSignature(Method method, Class[] paramTypes, boolean isSync) { - StringBuilder builder = new StringBuilder(paramTypes.length + 2); - - if (isSync) { - builder.append(returnTypeToChar(method.getReturnType())); - builder.append('.'); - } else { - builder.append("v."); - } - - for (int i = 0; i < paramTypes.length; i++) { - Class paramClass = paramTypes[i]; - if (paramClass == ExecutorToken.class) { - if (!BaseJavaModule.this.supportsWebWorkers()) { - throw new RuntimeException( - "Module " + BaseJavaModule.this + " doesn't support web workers, but " + - mMethod.getName() + - " takes an ExecutorToken."); - } - } else if (paramClass == Promise.class) { - Assertions.assertCondition( - i == paramTypes.length - 1, "Promise must be used as last parameter only"); - if (!isSync) { - mType = METHOD_TYPE_PROMISE; - } - } - builder.append(paramTypeToChar(paramClass)); - } - - // Modules that support web workers are expected to take an ExecutorToken as the first - // parameter to all their @ReactMethod-annotated methods. - if (BaseJavaModule.this.supportsWebWorkers()) { - if (builder.charAt(2) != 'T') { - throw new RuntimeException( - "Module " + BaseJavaModule.this + " supports web workers, but " + mMethod.getName() + - "does not take an ExecutorToken as its first parameter."); - } - } - - return builder.toString(); - } - - private ArgumentExtractor[] buildArgumentExtractors(Class[] paramTypes) { - // Modules that support web workers are expected to take an ExecutorToken as the first - // parameter to all their @ReactMethod-annotated methods. We compensate for that here. - int executorTokenOffset = 0; - if (BaseJavaModule.this.supportsWebWorkers()) { - if (paramTypes[0] != ExecutorToken.class) { - throw new RuntimeException( - "Module " + BaseJavaModule.this + " supports web workers, but " + mMethod.getName() + - "does not take an ExecutorToken as its first parameter."); - } - executorTokenOffset = 1; - } - - ArgumentExtractor[] argumentExtractors = new ArgumentExtractor[paramTypes.length - executorTokenOffset]; - for (int i = 0; i < paramTypes.length - executorTokenOffset; i += argumentExtractors[i].getJSArgumentsNeeded()) { - int paramIndex = i + executorTokenOffset; - Class argumentClass = paramTypes[paramIndex]; - if (argumentClass == Boolean.class || argumentClass == boolean.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_BOOLEAN; - } else if (argumentClass == Integer.class || argumentClass == int.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_INTEGER; - } else if (argumentClass == Double.class || argumentClass == double.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_DOUBLE; - } else if (argumentClass == Float.class || argumentClass == float.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_FLOAT; - } else if (argumentClass == String.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_STRING; - } else if (argumentClass == Callback.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_CALLBACK; - } else if (argumentClass == Promise.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_PROMISE; - Assertions.assertCondition( - paramIndex == paramTypes.length - 1, "Promise must be used as last parameter only"); - mType = METHOD_TYPE_PROMISE; - } else if (argumentClass == ReadableMap.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_MAP; - } else if (argumentClass == ReadableArray.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_ARRAY; - } else if (argumentClass == Dynamic.class) { - argumentExtractors[i] = ARGUMENT_EXTRACTOR_DYNAMIC; - } else { - throw new RuntimeException( - "Got unknown argument class: " + argumentClass.getSimpleName()); - } - } - return argumentExtractors; - } - - private int calculateJSArgumentsNeeded() { - int n = 0; - for (ArgumentExtractor extractor : mArgumentExtractors) { - n += extractor.getJSArgumentsNeeded(); - } - return n; - } - - private String getAffectedRange(int startIndex, int jsArgumentsNeeded) { - return jsArgumentsNeeded > 1 ? - "" + startIndex + "-" + (startIndex + jsArgumentsNeeded - 1) : "" + startIndex; - } - - @Override - public void invoke(JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray parameters) { - SystraceMessage.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "callJavaModuleMethod") - .arg("method", mTraceName) - .flush(); - try { - if (!mArgumentsProcessed) { - processArguments(); - } - if (mArguments == null || mArgumentExtractors == null) { - throw new Error("processArguments failed"); - } - if (mJSArgumentsNeeded != parameters.size()) { - throw new NativeArgumentsParseException( - BaseJavaModule.this.getName() + "." + mMethod.getName() + " got " + - parameters.size() + " arguments, expected " + mJSArgumentsNeeded); - } - - // Modules that support web workers are expected to take an ExecutorToken as the first - // parameter to all their @ReactMethod-annotated methods. We compensate for that here. - int i = 0, jsArgumentsConsumed = 0; - int executorTokenOffset = 0; - if (BaseJavaModule.this.supportsWebWorkers()) { - mArguments[0] = executorToken; - executorTokenOffset = 1; - } - try { - for (; i < mArgumentExtractors.length; i++) { - mArguments[i + executorTokenOffset] = mArgumentExtractors[i].extractArgument( - jsInstance, executorToken, parameters, jsArgumentsConsumed); - jsArgumentsConsumed += mArgumentExtractors[i].getJSArgumentsNeeded(); - } - } catch (UnexpectedNativeTypeException e) { - throw new NativeArgumentsParseException( - e.getMessage() + " (constructing arguments for " + BaseJavaModule.this.getName() + - "." + mMethod.getName() + " at argument index " + - getAffectedRange(jsArgumentsConsumed, mArgumentExtractors[i].getJSArgumentsNeeded()) + - ")", - e); - } - - try { - mMethod.invoke(BaseJavaModule.this, mArguments); - } catch (IllegalArgumentException ie) { - throw new RuntimeException( - "Could not invoke " + BaseJavaModule.this.getName() + "." + mMethod.getName(), ie); - } catch (IllegalAccessException iae) { - throw new RuntimeException( - "Could not invoke " + BaseJavaModule.this.getName() + "." + mMethod.getName(), iae); - } catch (InvocationTargetException ite) { - // Exceptions thrown from native module calls end up wrapped in InvocationTargetException - // which just make traces harder to read and bump out useful information - if (ite.getCause() instanceof RuntimeException) { - throw (RuntimeException) ite.getCause(); - } - throw new RuntimeException( - "Could not invoke " + BaseJavaModule.this.getName() + "." + mMethod.getName(), ite); - } - } finally { - Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); - } - } - - /** - * Determines how the method is exported in JavaScript: - * METHOD_TYPE_ASYNC for regular methods - * METHOD_TYPE_PROMISE for methods that return a promise object to the caller. - * METHOD_TYPE_SYNC for sync methods - */ - @Override - public String getType() { - return mType; - } - } - - private @Nullable Map mMethods; - - private void findMethods() { - if (mMethods == null) { - Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "findMethods"); - mMethods = new HashMap<>(); - - Method[] targetMethods = getClass().getDeclaredMethods(); - for (Method targetMethod : targetMethods) { - ReactMethod annotation = targetMethod.getAnnotation(ReactMethod.class); - if (annotation != null) { - String methodName = targetMethod.getName(); - if (mMethods.containsKey(methodName)) { - // We do not support method overloading since js sees a function as an object regardless - // of number of params. - throw new IllegalArgumentException( - "Java Module " + getName() + " method name already registered: " + methodName); - } - mMethods.put( - methodName, - new JavaMethod(targetMethod, - annotation.isBlockingSynchronousMethod())); - } - } - Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); - } - } - - @Override - public final Map getMethods() { - findMethods(); - return assertNotNull(mMethods); - } - /** * @return a map of constants this module exports to JS. Supports JSON types. */ @@ -459,69 +70,4 @@ public abstract class BaseJavaModule implements NativeModule { public boolean supportsWebWorkers() { return false; } - - private static char paramTypeToChar(Class paramClass) { - char tryCommon = commonTypeToChar(paramClass); - if (tryCommon != '\0') { - return tryCommon; - } - if (paramClass == ExecutorToken.class) { - return 'T'; - } else if (paramClass == Callback.class) { - return 'X'; - } else if (paramClass == Promise.class) { - return 'P'; - } else if (paramClass == ReadableMap.class) { - return 'M'; - } else if (paramClass == ReadableArray.class) { - return 'A'; - } else if (paramClass == Dynamic.class) { - return 'Y'; - } else { - throw new RuntimeException( - "Got unknown param class: " + paramClass.getSimpleName()); - } - } - - private static char returnTypeToChar(Class returnClass) { - // Keep this in sync with MethodInvoker - char tryCommon = commonTypeToChar(returnClass); - if (tryCommon != '\0') { - return tryCommon; - } - if (returnClass == void.class) { - return 'v'; - } else if (returnClass == WritableMap.class) { - return 'M'; - } else if (returnClass == WritableArray.class) { - return 'A'; - } else { - throw new RuntimeException( - "Got unknown return class: " + returnClass.getSimpleName()); - } - } - - private static char commonTypeToChar(Class typeClass) { - if (typeClass == boolean.class) { - return 'z'; - } else if (typeClass == Boolean.class) { - return 'Z'; - } else if (typeClass == int.class) { - return 'i'; - } else if (typeClass == Integer.class) { - return 'I'; - } else if (typeClass == double.class) { - return 'd'; - } else if (typeClass == Double.class) { - return 'D'; - } else if (typeClass == float.class) { - return 'f'; - } else if (typeClass == Float.class) { - return 'F'; - } else if (typeClass == String.class) { - return 'S'; - } else { - return '\0'; - } - } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java index 830f56301..b4189f089 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java @@ -9,6 +9,9 @@ package com.facebook.react.bridge; +import javax.annotation.Nullable; + +import java.lang.reflect.Method; import java.util.Map; /** @@ -30,11 +33,6 @@ public interface NativeModule { */ String getName(); - /** - * @return methods callable from JS on this module - */ - Map getMethods(); - /** * This is called at the end of {@link CatalystApplicationFragment#createCatalystInstance()} * after the CatalystInstance has been created, in order to initialize NativeModules that require diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapperBase.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapperBase.java index acb06c0e0..f0b8af022 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapperBase.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapperBase.java @@ -28,11 +28,6 @@ public class CxxModuleWrapperBase implements NativeModule @Override public native String getName(); - @Override - public Map getMethods() { - throw new UnsupportedOperationException(); - } - @Override public void initialize() { // do nothing diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaMethodWrapper.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaMethodWrapper.java new file mode 100644 index 000000000..afaed7c18 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaMethodWrapper.java @@ -0,0 +1,424 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import javax.annotation.Nullable; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import com.facebook.react.bridge.*; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.cxxbridge.JavaModuleWrapper; +import com.facebook.systrace.Systrace; +import com.facebook.systrace.SystraceMessage; + +import static com.facebook.infer.annotation.Assertions.assertNotNull; +import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; + +public class JavaMethodWrapper implements NativeModule.NativeMethod { + + private static abstract class ArgumentExtractor { + public int getJSArgumentsNeeded() { + return 1; + } + + public abstract @Nullable T extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex); + } + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_BOOLEAN = + new ArgumentExtractor() { + @Override + public Boolean extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getBoolean(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_DOUBLE = + new ArgumentExtractor() { + @Override + public Double extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getDouble(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_FLOAT = + new ArgumentExtractor() { + @Override + public Float extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + return (float) jsArguments.getDouble(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_INTEGER = + new ArgumentExtractor() { + @Override + public Integer extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + return (int) jsArguments.getDouble(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_STRING = + new ArgumentExtractor() { + @Override + public String extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getString(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_ARRAY = + new ArgumentExtractor() { + @Override + public ReadableNativeArray extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getArray(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_DYNAMIC = + new ArgumentExtractor() { + @Override + public Dynamic extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + return DynamicFromArray.create(jsArguments, atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_MAP = + new ArgumentExtractor() { + @Override + public ReadableMap extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getMap(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_CALLBACK = + new ArgumentExtractor() { + @Override + public @Nullable Callback extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + if (jsArguments.isNull(atIndex)) { + return null; + } else { + int id = (int) jsArguments.getDouble(atIndex); + return new com.facebook.react.bridge.CallbackImpl(jsInstance, executorToken, id); + } + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_PROMISE = + new ArgumentExtractor() { + @Override + public int getJSArgumentsNeeded() { + return 2; + } + + @Override + public Promise extractArgument( + JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { + Callback resolve = ARGUMENT_EXTRACTOR_CALLBACK + .extractArgument(jsInstance, executorToken, jsArguments, atIndex); + Callback reject = ARGUMENT_EXTRACTOR_CALLBACK + .extractArgument(jsInstance, executorToken, jsArguments, atIndex + 1); + return new PromiseImpl(resolve, reject); + } + }; + + private static char paramTypeToChar(Class paramClass) { + char tryCommon = commonTypeToChar(paramClass); + if (tryCommon != '\0') { + return tryCommon; + } + if (paramClass == ExecutorToken.class) { + return 'T'; + } else if (paramClass == Callback.class) { + return 'X'; + } else if (paramClass == Promise.class) { + return 'P'; + } else if (paramClass == ReadableMap.class) { + return 'M'; + } else if (paramClass == ReadableArray.class) { + return 'A'; + } else if (paramClass == Dynamic.class) { + return 'Y'; + } else { + throw new RuntimeException( + "Got unknown param class: " + paramClass.getSimpleName()); + } + } + + private static char returnTypeToChar(Class returnClass) { + // Keep this in sync with MethodInvoker + char tryCommon = commonTypeToChar(returnClass); + if (tryCommon != '\0') { + return tryCommon; + } + if (returnClass == void.class) { + return 'v'; + } else if (returnClass == WritableMap.class) { + return 'M'; + } else if (returnClass == WritableArray.class) { + return 'A'; + } else { + throw new RuntimeException( + "Got unknown return class: " + returnClass.getSimpleName()); + } + } + + private static char commonTypeToChar(Class typeClass) { + if (typeClass == boolean.class) { + return 'z'; + } else if (typeClass == Boolean.class) { + return 'Z'; + } else if (typeClass == int.class) { + return 'i'; + } else if (typeClass == Integer.class) { + return 'I'; + } else if (typeClass == double.class) { + return 'd'; + } else if (typeClass == Double.class) { + return 'D'; + } else if (typeClass == float.class) { + return 'f'; + } else if (typeClass == Float.class) { + return 'F'; + } else if (typeClass == String.class) { + return 'S'; + } else { + return '\0'; + } + } + + private final Method mMethod; + private final Class[] mParameterTypes; + private final int mParamLength; + private final JavaModuleWrapper mModuleWrapper; + private String mType = BaseJavaModule.METHOD_TYPE_ASYNC; + private boolean mArgumentsProcessed = false; + private @Nullable ArgumentExtractor[] mArgumentExtractors; + private @Nullable String mSignature; + private @Nullable Object[] mArguments; + private @Nullable int mJSArgumentsNeeded; + + public JavaMethodWrapper(JavaModuleWrapper module, Method method, boolean isSync) { + mModuleWrapper = module; + mMethod = method; + mMethod.setAccessible(true); + mParameterTypes = mMethod.getParameterTypes(); + mParamLength = mParameterTypes.length; + + if (isSync) { + mType = BaseJavaModule.METHOD_TYPE_SYNC; + } else if (mParamLength > 0 && (mParameterTypes[mParamLength - 1] == Promise.class)) { + mType = BaseJavaModule.METHOD_TYPE_PROMISE; + } + } + + private void processArguments() { + if (mArgumentsProcessed) { + return; + } + mArgumentsProcessed = true; + mArgumentExtractors = buildArgumentExtractors(mParameterTypes); + mSignature = buildSignature(mMethod, mParameterTypes, (mType.equals(BaseJavaModule.METHOD_TYPE_SYNC))); + // Since native methods are invoked from a message queue executed on a single thread, it is + // safe to allocate only one arguments object per method that can be reused across calls + mArguments = new Object[mParameterTypes.length]; + mJSArgumentsNeeded = calculateJSArgumentsNeeded(); + } + + public Method getMethod() { + return mMethod; + } + + public String getSignature() { + if (!mArgumentsProcessed) { + processArguments(); + } + return assertNotNull(mSignature); + } + + private String buildSignature(Method method, Class[] paramTypes, boolean isSync) { + StringBuilder builder = new StringBuilder(paramTypes.length + 2); + + if (isSync) { + builder.append(returnTypeToChar(method.getReturnType())); + builder.append('.'); + } else { + builder.append("v."); + } + + for (int i = 0; i < paramTypes.length; i++) { + Class paramClass = paramTypes[i]; + if (paramClass == ExecutorToken.class) { + if (!mModuleWrapper.supportsWebWorkers()) { + throw new RuntimeException( + "Module " + mModuleWrapper.getName() + " doesn't support web workers, but " + + mMethod.getName() + + " takes an ExecutorToken."); + } + } else if (paramClass == Promise.class) { + Assertions.assertCondition( + i == paramTypes.length - 1, "Promise must be used as last parameter only"); + } + builder.append(paramTypeToChar(paramClass)); + } + + // Modules that support web workers are expected to take an ExecutorToken as the first + // parameter to all their @ReactMethod-annotated methods. + if (mModuleWrapper.supportsWebWorkers()) { + if (builder.charAt(2) != 'T') { + throw new RuntimeException( + "Module " + mModuleWrapper.getName() + " supports web workers, but " + mMethod.getName() + + "does not take an ExecutorToken as its first parameter."); + } + } + + return builder.toString(); + } + + private ArgumentExtractor[] buildArgumentExtractors(Class[] paramTypes) { + // Modules that support web workers are expected to take an ExecutorToken as the first + // parameter to all their @ReactMethod-annotated methods. We compensate for that here. + int executorTokenOffset = 0; + if (mModuleWrapper.supportsWebWorkers()) { + if (paramTypes[0] != ExecutorToken.class) { + throw new RuntimeException( + "Module " + mModuleWrapper.getName() + " supports web workers, but " + mMethod.getName() + + "does not take an ExecutorToken as its first parameter."); + } + executorTokenOffset = 1; + } + + ArgumentExtractor[] argumentExtractors = new ArgumentExtractor[paramTypes.length - executorTokenOffset]; + for (int i = 0; i < paramTypes.length - executorTokenOffset; i += argumentExtractors[i].getJSArgumentsNeeded()) { + int paramIndex = i + executorTokenOffset; + Class argumentClass = paramTypes[paramIndex]; + if (argumentClass == Boolean.class || argumentClass == boolean.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_BOOLEAN; + } else if (argumentClass == Integer.class || argumentClass == int.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_INTEGER; + } else if (argumentClass == Double.class || argumentClass == double.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_DOUBLE; + } else if (argumentClass == Float.class || argumentClass == float.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_FLOAT; + } else if (argumentClass == String.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_STRING; + } else if (argumentClass == Callback.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_CALLBACK; + } else if (argumentClass == Promise.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_PROMISE; + Assertions.assertCondition( + paramIndex == paramTypes.length - 1, "Promise must be used as last parameter only"); + } else if (argumentClass == ReadableMap.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_MAP; + } else if (argumentClass == ReadableArray.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_ARRAY; + } else if (argumentClass == Dynamic.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_DYNAMIC; + } else { + throw new RuntimeException( + "Got unknown argument class: " + argumentClass.getSimpleName()); + } + } + return argumentExtractors; + } + + private int calculateJSArgumentsNeeded() { + int n = 0; + for (ArgumentExtractor extractor : assertNotNull(mArgumentExtractors)) { + n += extractor.getJSArgumentsNeeded(); + } + return n; + } + + private String getAffectedRange(int startIndex, int jsArgumentsNeeded) { + return jsArgumentsNeeded > 1 ? + "" + startIndex + "-" + (startIndex + jsArgumentsNeeded - 1) : "" + startIndex; + } + + @Override + public void invoke(JSInstance jsInstance, ExecutorToken executorToken, ReadableNativeArray parameters) { + String traceName = mModuleWrapper.getName() + "." + mMethod.getName(); + SystraceMessage.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "callJavaModuleMethod") + .arg("method", traceName) + .flush(); + try { + if (!mArgumentsProcessed) { + processArguments(); + } + if (mArguments == null || mArgumentExtractors == null) { + throw new Error("processArguments failed"); + } + if (mJSArgumentsNeeded != parameters.size()) { + throw new NativeArgumentsParseException( + traceName + " got " + parameters.size() + " arguments, expected " + mJSArgumentsNeeded); + } + + // Modules that support web workers are expected to take an ExecutorToken as the first + // parameter to all their @ReactMethod-annotated methods. We compensate for that here. + int i = 0, jsArgumentsConsumed = 0; + int executorTokenOffset = 0; + if (mModuleWrapper.supportsWebWorkers()) { + mArguments[0] = executorToken; + executorTokenOffset = 1; + } + try { + for (; i < mArgumentExtractors.length; i++) { + mArguments[i + executorTokenOffset] = mArgumentExtractors[i].extractArgument( + jsInstance, executorToken, parameters, jsArgumentsConsumed); + jsArgumentsConsumed += mArgumentExtractors[i].getJSArgumentsNeeded(); + } + } catch (UnexpectedNativeTypeException e) { + throw new NativeArgumentsParseException( + e.getMessage() + " (constructing arguments for " + traceName + " at argument index " + + getAffectedRange(jsArgumentsConsumed, mArgumentExtractors[i].getJSArgumentsNeeded()) + + ")", + e); + } + + try { + mMethod.invoke(mModuleWrapper.getModule(), mArguments); + } catch (IllegalArgumentException ie) { + throw new RuntimeException("Could not invoke " + traceName, ie); + } catch (IllegalAccessException iae) { + throw new RuntimeException("Could not invoke " + traceName, iae); + } catch (InvocationTargetException ite) { + // Exceptions thrown from native module calls end up wrapped in InvocationTargetException + // which just make traces harder to read and bump out useful information + if (ite.getCause() instanceof RuntimeException) { + throw (RuntimeException) ite.getCause(); + } + throw new RuntimeException("Could not invoke " + traceName, ite); + } + } finally { + com.facebook.systrace.Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + /** + * Determines how the method is exported in JavaScript: + * METHOD_TYPE_ASYNC for regular methods + * METHOD_TYPE_PROMISE for methods that return a promise object to the caller. + * METHOD_TYPE_SYNC for sync methods + */ + @Override + public String getType() { + return mType; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java index fb3190911..6db0bdc08 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java @@ -11,7 +11,9 @@ package com.facebook.react.cxxbridge; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.Map; import com.facebook.proguard.annotations.DoNotStrip; @@ -21,6 +23,7 @@ import com.facebook.react.bridge.JSInstance; import com.facebook.react.bridge.NativeArray; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactMarker; +import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableNativeArray; import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; @@ -56,12 +59,16 @@ public class JavaModuleWrapper { private final JSInstance mJSInstance; private final ModuleHolder mModuleHolder; + private final Class mModuleClass; private final ArrayList mMethods; + private final ArrayList mDescs; - public JavaModuleWrapper(JSInstance jsInstance, ModuleHolder moduleHolder) { + public JavaModuleWrapper(JSInstance jsInstance, Class moduleClass, ModuleHolder moduleHolder) { mJSInstance = jsInstance; mModuleHolder = moduleHolder; + mModuleClass = moduleClass; mMethods = new ArrayList<>(); + mDescs = new ArrayList(); } @DoNotStrip @@ -75,24 +82,42 @@ public class JavaModuleWrapper { } @DoNotStrip - public List getMethodDescriptors() { - ArrayList descs = new ArrayList<>(); - for (Map.Entry entry : - getModule().getMethods().entrySet()) { - MethodDescriptor md = new MethodDescriptor(); - md.name = entry.getKey(); - md.type = entry.getValue().getType(); + private void findMethods() { + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "findMethods"); + Set methodNames = new HashSet<>(); - BaseJavaModule.JavaMethod method = (BaseJavaModule.JavaMethod) entry.getValue(); - if (md.type == BaseJavaModule.METHOD_TYPE_SYNC) { - md.signature = method.getSignature(); - md.method = method.getMethod(); + Method[] targetMethods = mModuleClass.getDeclaredMethods(); + for (Method targetMethod : targetMethods) { + ReactMethod annotation = targetMethod.getAnnotation(ReactMethod.class); + if (annotation != null) { + String methodName = targetMethod.getName(); + if (methodNames.contains(methodName)) { + // We do not support method overloading since js sees a function as an object regardless + // of number of params. + throw new IllegalArgumentException( + "Java Module " + getName() + " method name already registered: " + methodName); + } + MethodDescriptor md = new MethodDescriptor(); + JavaMethodWrapper method = new JavaMethodWrapper(this, targetMethod, annotation.isBlockingSynchronousMethod()); + md.name = methodName; + md.type = method.getType(); + if (md.type == BaseJavaModule.METHOD_TYPE_SYNC) { + md.signature = method.getSignature(); + md.method = targetMethod; + } + mMethods.add(method); + mDescs.add(md); } - mMethods.add(method); - - descs.add(md); } - return descs; + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); + } + + @DoNotStrip + public List getMethodDescriptors() { + if (mDescs.isEmpty()) { + findMethods(); + } + return mDescs; } // TODO mhorowitz: make this return NativeMap, which requires moving @@ -126,7 +151,7 @@ public class JavaModuleWrapper { @DoNotStrip public boolean supportsWebWorkers() { - return getModule().supportsWebWorkers(); + return mModuleHolder.getSupportsWebWorkers(); } @DoNotStrip diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java index 112f45cb0..63597eb85 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java @@ -45,9 +45,9 @@ public class NativeModuleRegistry { JSInstance jsInstance) { ArrayList javaModules = new ArrayList<>(); for (Map.Entry, ModuleHolder> entry : mModules.entrySet()) { - Class type = entry.getKey(); + Class type = entry.getKey(); if (!CxxModuleWrapperBase.class.isAssignableFrom(type)) { - javaModules.add(new JavaModuleWrapper(jsInstance, entry.getValue())); + javaModules.add(new JavaModuleWrapper(jsInstance, type, entry.getValue())); } } return javaModules;