Skip to content

Commit b86a6e3

Browse files
davidaureliofacebook-github-bot-4
authored andcommitted
Add support for async bridged methods to android
Differential Revision: D2595414 fb-gh-sync-id: 3b44ce1737bdd1e0861a285a45976631a57ab3b5
1 parent b73bf16 commit b86a6e3

File tree

5 files changed

+167
-36
lines changed

5 files changed

+167
-36
lines changed

ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java

Lines changed: 98 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import com.fasterxml.jackson.core.JsonGenerator;
1313

14+
import com.facebook.infer.annotation.Assertions;
1415
import com.facebook.systrace.Systrace;
1516

1617
import javax.annotation.Nullable;
@@ -46,8 +47,16 @@
4647
* with the same name.
4748
*/
4849
public abstract class BaseJavaModule implements NativeModule {
49-
private interface ArgumentExtractor {
50-
@Nullable Object extractArgument(
50+
// taken from Libraries/Utilities/MessageQueue.js
51+
static final public String METHOD_TYPE_REMOTE = "remote";
52+
static final public String METHOD_TYPE_REMOTE_ASYNC = "remoteAsync";
53+
54+
private static abstract class ArgumentExtractor {
55+
public int getJSArgumentsNeeded() {
56+
return 1;
57+
}
58+
59+
public abstract @Nullable Object extractArgument(
5160
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex);
5261
}
5362

@@ -129,67 +138,110 @@ public Object extractArgument(
129138
}
130139
};
131140

132-
private static ArgumentExtractor[] buildArgumentExtractors(Class[] parameterTypes) {
133-
ArgumentExtractor[] argumentExtractors = new ArgumentExtractor[parameterTypes.length];
134-
for (int i = 0; i < parameterTypes.length; i++) {
135-
Class argumentClass = parameterTypes[i];
136-
if (argumentClass == Boolean.class || argumentClass == boolean.class) {
137-
argumentExtractors[i] = ARGUMENT_EXTRACTOR_BOOLEAN;
138-
} else if (argumentClass == Integer.class || argumentClass == int.class) {
139-
argumentExtractors[i] = ARGUMENT_EXTRACTOR_INTEGER;
140-
} else if (argumentClass == Double.class || argumentClass == double.class) {
141-
argumentExtractors[i] = ARGUMENT_EXTRACTOR_DOUBLE;
142-
} else if (argumentClass == Float.class || argumentClass == float.class) {
143-
argumentExtractors[i] = ARGUMENT_EXTRACTOR_FLOAT;
144-
} else if (argumentClass == String.class) {
145-
argumentExtractors[i] = ARGUMENT_EXTRACTOR_STRING;
146-
} else if (argumentClass == Callback.class) {
147-
argumentExtractors[i] = ARGUMENT_EXTRACTOR_CALLBACK;
148-
} else if (argumentClass == ReadableMap.class) {
149-
argumentExtractors[i] = ARGUMENT_EXTRACTOR_MAP;
150-
} else if (argumentClass == ReadableArray.class) {
151-
argumentExtractors[i] = ARGUMENT_EXTRACTOR_ARRAY;
152-
} else {
153-
throw new RuntimeException(
154-
"Got unknown argument class: " + argumentClass.getSimpleName());
155-
}
141+
static final private ArgumentExtractor ARGUMENT_EXTRACTOR_PROMISE = new ArgumentExtractor() {
142+
@Override
143+
public int getJSArgumentsNeeded() {
144+
return 2;
156145
}
157-
return argumentExtractors;
158-
}
146+
147+
@Override
148+
public Promise extractArgument(
149+
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
150+
Callback resolve = (Callback) ARGUMENT_EXTRACTOR_CALLBACK
151+
.extractArgument(catalystInstance, jsArguments, atIndex);
152+
Callback reject = (Callback) ARGUMENT_EXTRACTOR_CALLBACK
153+
.extractArgument(catalystInstance, jsArguments, atIndex + 1);
154+
return new PromiseImpl(resolve, reject);
155+
}
156+
};
159157

160158
private class JavaMethod implements NativeMethod {
159+
161160
private Method mMethod;
162-
private ArgumentExtractor[] mArgumentExtractors;
163-
private Object[] mArguments;
161+
private final ArgumentExtractor[] mArgumentExtractors;
162+
private final Object[] mArguments;
163+
private String mType = METHOD_TYPE_REMOTE;
164+
private final int mJSArgumentsNeeded;
164165

165166
public JavaMethod(Method method) {
166167
mMethod = method;
167168
Class[] parameterTypes = method.getParameterTypes();
168169
mArgumentExtractors = buildArgumentExtractors(parameterTypes);
169170
// Since native methods are invoked from a message queue executed on a single thread, it is
170171
// save to allocate only one arguments object per method that can be reused across calls
171-
mArguments = new Object[mArgumentExtractors.length];
172+
mArguments = new Object[parameterTypes.length];
173+
mJSArgumentsNeeded = calculateJSArgumentsNeeded();
174+
}
175+
176+
private ArgumentExtractor[] buildArgumentExtractors(Class[] paramTypes) {
177+
ArgumentExtractor[] argumentExtractors = new ArgumentExtractor[paramTypes.length];
178+
for (int i = 0; i < paramTypes.length; i += argumentExtractors[i].getJSArgumentsNeeded()) {
179+
Class argumentClass = paramTypes[i];
180+
if (argumentClass == Boolean.class || argumentClass == boolean.class) {
181+
argumentExtractors[i] = ARGUMENT_EXTRACTOR_BOOLEAN;
182+
} else if (argumentClass == Integer.class || argumentClass == int.class) {
183+
argumentExtractors[i] = ARGUMENT_EXTRACTOR_INTEGER;
184+
} else if (argumentClass == Double.class || argumentClass == double.class) {
185+
argumentExtractors[i] = ARGUMENT_EXTRACTOR_DOUBLE;
186+
} else if (argumentClass == Float.class || argumentClass == float.class) {
187+
argumentExtractors[i] = ARGUMENT_EXTRACTOR_FLOAT;
188+
} else if (argumentClass == String.class) {
189+
argumentExtractors[i] = ARGUMENT_EXTRACTOR_STRING;
190+
} else if (argumentClass == Callback.class) {
191+
argumentExtractors[i] = ARGUMENT_EXTRACTOR_CALLBACK;
192+
} else if (argumentClass == Promise.class) {
193+
argumentExtractors[i] = ARGUMENT_EXTRACTOR_PROMISE;
194+
Assertions.assertCondition(
195+
i == paramTypes.length - 1, "Promise must be used as last parameter only");
196+
mType = METHOD_TYPE_REMOTE_ASYNC;
197+
} else if (argumentClass == ReadableMap.class) {
198+
argumentExtractors[i] = ARGUMENT_EXTRACTOR_MAP;
199+
} else if (argumentClass == ReadableArray.class) {
200+
argumentExtractors[i] = ARGUMENT_EXTRACTOR_ARRAY;
201+
} else {
202+
throw new RuntimeException(
203+
"Got unknown argument class: " + argumentClass.getSimpleName());
204+
}
205+
}
206+
return argumentExtractors;
207+
}
208+
209+
private int calculateJSArgumentsNeeded() {
210+
int n = 0;
211+
for (ArgumentExtractor extractor : mArgumentExtractors) {
212+
n += extractor.getJSArgumentsNeeded();
213+
}
214+
return n;
215+
}
216+
217+
private String getAffectedRange(int startIndex, int jsArgumentsNeeded) {
218+
return jsArgumentsNeeded > 1 ?
219+
"" + startIndex + "-" + (startIndex + jsArgumentsNeeded - 1) : "" + startIndex;
172220
}
173221

174222
@Override
175223
public void invoke(CatalystInstance catalystInstance, ReadableNativeArray parameters) {
176224
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "callJavaModuleMethod");
177225
try {
178-
if (mArgumentExtractors.length != parameters.size()) {
226+
if (mJSArgumentsNeeded != parameters.size()) {
179227
throw new NativeArgumentsParseException(
180228
BaseJavaModule.this.getName() + "." + mMethod.getName() + " got " +
181-
parameters.size() + " arguments, expected " + mArgumentExtractors.length);
229+
parameters.size() + " arguments, expected " + mJSArgumentsNeeded);
182230
}
183231

184-
int i = 0;
232+
int i = 0, jsArgumentsConsumed = 0;
185233
try {
186234
for (; i < mArgumentExtractors.length; i++) {
187-
mArguments[i] = mArgumentExtractors[i].extractArgument(catalystInstance, parameters, i);
235+
mArguments[i] = mArgumentExtractors[i].extractArgument(
236+
catalystInstance, parameters, jsArgumentsConsumed);
237+
jsArgumentsConsumed += mArgumentExtractors[i].getJSArgumentsNeeded();
188238
}
189239
} catch (UnexpectedNativeTypeException e) {
190240
throw new NativeArgumentsParseException(
191241
e.getMessage() + " (constructing arguments for " + BaseJavaModule.this.getName() +
192-
"." + mMethod.getName() + " at argument index " + i + ")",
242+
"." + mMethod.getName() + " at argument index " +
243+
getAffectedRange(jsArgumentsConsumed, mArgumentExtractors[i].getJSArgumentsNeeded()) +
244+
")",
193245
e);
194246
}
195247

@@ -214,6 +266,16 @@ public void invoke(CatalystInstance catalystInstance, ReadableNativeArray parame
214266
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
215267
}
216268
}
269+
270+
/**
271+
* Determines how the method is exported in JavaScript:
272+
* METHOD_TYPE_REMOTE for regular methods
273+
* METHOD_TYPE_REMOTE_ASYNC for methods that return a promise object to the caller.
274+
*/
275+
@Override
276+
public String getType() {
277+
return mType;
278+
}
217279
}
218280

219281
@Override

ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
public interface NativeModule {
2525
public static interface NativeMethod {
2626
void invoke(CatalystInstance catalystInstance, ReadableNativeArray parameters);
27+
String getType();
2728
}
2829

2930
/**

ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ public NativeModuleRegistry build() {
199199
MethodRegistration method = module.methods.get(i);
200200
jg.writeObjectFieldStart(method.name);
201201
jg.writeNumberField("methodID", i);
202+
jg.writeStringField("type", method.method.getType());
202203
jg.writeEndObject();
203204
}
204205
jg.writeEndObject();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
package com.facebook.react.bridge;
11+
12+
/**
13+
* Interface that represents a JavaScript Promise which can be passed to the native module as a
14+
* method parameter.
15+
*
16+
* Methods annotated with {@link ReactMethod} that use {@link Promise} as type of the last parameter
17+
* will be marked as "remoteAsync" and will return a promise when invoked from JavaScript.
18+
*/
19+
public interface Promise {
20+
void resolve(Object value);
21+
void reject(String reason);
22+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
/**
11+
* Implementation of two javascript functions that can be used to resolve or reject a js promise.
12+
*/
13+
package com.facebook.react.bridge;
14+
15+
import javax.annotation.Nullable;
16+
17+
public class PromiseImpl implements Promise {
18+
private @Nullable Callback mResolve;
19+
private @Nullable Callback mReject;
20+
21+
public PromiseImpl(@Nullable Callback resolve, @Nullable Callback reject) {
22+
mResolve = resolve;
23+
mReject = reject;
24+
}
25+
26+
@Override
27+
public void resolve(Object value) {
28+
if (mResolve != null) {
29+
mResolve.invoke(value);
30+
}
31+
}
32+
33+
@Override
34+
public void reject(String reason) {
35+
if (mReject != null) {
36+
// The JavaScript side expects a map with at least the error message.
37+
// It is possible to expose all kind of information. It will be available on the JS
38+
// error instance.
39+
// TODO(8850038): add more useful information, e.g. the native stack trace.
40+
WritableNativeMap errorInfo = new WritableNativeMap();
41+
errorInfo.putString("message", reason);
42+
mReject.invoke(errorInfo);
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)