Skip to content

Commit

Permalink
WebWorkers: Add ExecutorToken to route native module calls to/from wo…
Browse files Browse the repository at this point in the history
…rkers

Summary:To support native modules in web workers, native modules need to have an notion of the JS executor/thread that called into them in order to respond via Callback or JS module call to the right executor on the right thread.

ExecutorToken is an object that only serves as a token that can be used to identify an executor/message queue thread in the bridge. It doesn't expose any methods. Native modules Callback objects automatically have this ExecutionContext attached -- JSModule calls for modules that support workers will need to supply an appropriate ExecutorToken when retrieving the JSModule implementation from the ReactContext.

Reviewed By: mhorowitz

Differential Revision: D2965458

fb-gh-sync-id: 6e354d4df8536d40b12d02bd055f6d06b4ca595d
shipit-source-id: 6e354d4df8536d40b12d02bd055f6d06b4ca595d
  • Loading branch information
astreet authored and Facebook Github Bot 4 committed Mar 1, 2016
1 parent f67fa82 commit bd95b22
Show file tree
Hide file tree
Showing 26 changed files with 614 additions and 116 deletions.
Expand Up @@ -57,14 +57,14 @@ public int getJSArgumentsNeeded() {
}

public abstract @Nullable T extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex);
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex);
}

static final private ArgumentExtractor<Boolean> ARGUMENT_EXTRACTOR_BOOLEAN =
new ArgumentExtractor<Boolean>() {
@Override
public Boolean extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) {
return jsArguments.getBoolean(atIndex);
}
};
Expand All @@ -73,7 +73,7 @@ public Boolean extractArgument(
new ArgumentExtractor<Double>() {
@Override
public Double extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) {
return jsArguments.getDouble(atIndex);
}
};
Expand All @@ -82,7 +82,7 @@ public Double extractArgument(
new ArgumentExtractor<Float>() {
@Override
public Float extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) {
return (float) jsArguments.getDouble(atIndex);
}
};
Expand All @@ -91,7 +91,7 @@ public Float extractArgument(
new ArgumentExtractor<Integer>() {
@Override
public Integer extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) {
return (int) jsArguments.getDouble(atIndex);
}
};
Expand All @@ -100,7 +100,7 @@ public Integer extractArgument(
new ArgumentExtractor<String>() {
@Override
public String extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) {
return jsArguments.getString(atIndex);
}
};
Expand All @@ -109,7 +109,7 @@ public String extractArgument(
new ArgumentExtractor<ReadableNativeArray>() {
@Override
public ReadableNativeArray extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) {
return jsArguments.getArray(atIndex);
}
};
Expand All @@ -118,7 +118,7 @@ public ReadableNativeArray extractArgument(
new ArgumentExtractor<ReadableMap>() {
@Override
public ReadableMap extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) {
return jsArguments.getMap(atIndex);
}
};
Expand All @@ -127,12 +127,12 @@ public ReadableMap extractArgument(
new ArgumentExtractor<Callback>() {
@Override
public @Nullable Callback extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) {
if (jsArguments.isNull(atIndex)) {
return null;
} else {
int id = (int) jsArguments.getDouble(atIndex);
return new CallbackImpl(catalystInstance, id);
return new CallbackImpl(catalystInstance, executorToken, id);
}
}
};
Expand All @@ -146,11 +146,11 @@ public int getJSArgumentsNeeded() {

@Override
public Promise extractArgument(
CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) {
CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) {
Callback resolve = ARGUMENT_EXTRACTOR_CALLBACK
.extractArgument(catalystInstance, jsArguments, atIndex);
.extractArgument(catalystInstance, executorToken, jsArguments, atIndex);
Callback reject = ARGUMENT_EXTRACTOR_CALLBACK
.extractArgument(catalystInstance, jsArguments, atIndex + 1);
.extractArgument(catalystInstance, executorToken, jsArguments, atIndex + 1);
return new PromiseImpl(resolve, reject);
}
};
Expand All @@ -174,9 +174,21 @@ public JavaMethod(Method method) {
}

private ArgumentExtractor[] buildArgumentExtractors(Class[] paramTypes) {
ArgumentExtractor[] argumentExtractors = new ArgumentExtractor[paramTypes.length];
for (int i = 0; i < paramTypes.length; i += argumentExtractors[i].getJSArgumentsNeeded()) {
Class argumentClass = paramTypes[i];
// 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()) {
Class argumentClass = paramTypes[i + executorTokenOffset];
if (argumentClass == Boolean.class || argumentClass == boolean.class) {
argumentExtractors[i] = ARGUMENT_EXTRACTOR_BOOLEAN;
} else if (argumentClass == Integer.class || argumentClass == int.class) {
Expand Down Expand Up @@ -220,7 +232,7 @@ private String getAffectedRange(int startIndex, int jsArgumentsNeeded) {
}

@Override
public void invoke(CatalystInstance catalystInstance, ReadableNativeArray parameters) {
public void invoke(CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray parameters) {
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "callJavaModuleMethod");
try {
if (mJSArgumentsNeeded != parameters.size()) {
Expand All @@ -229,11 +241,18 @@ public void invoke(CatalystInstance catalystInstance, ReadableNativeArray parame
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] = mArgumentExtractors[i].extractArgument(
catalystInstance, parameters, jsArgumentsConsumed);
mArguments[i + executorTokenOffset] = mArgumentExtractors[i].extractArgument(
catalystInstance, executorToken, parameters, jsArgumentsConsumed);
jsArgumentsConsumed += mArgumentExtractors[i].getJSArgumentsNeeded();
}
} catch (UnexpectedNativeTypeException e) {
Expand Down Expand Up @@ -341,7 +360,7 @@ public void onReactBridgeInitialized(ReactBridge bridge) {
public void onCatalystInstanceDestroy() {
// do nothing
}

@Override
public boolean supportsWebWorkers() {
return false;
Expand Down
Expand Up @@ -15,15 +15,17 @@
public final class CallbackImpl implements Callback {

private final CatalystInstance mCatalystInstance;
private final ExecutorToken mExecutorToken;
private final int mCallbackId;

public CallbackImpl(CatalystInstance bridge, int callbackId) {
public CallbackImpl(CatalystInstance bridge, ExecutorToken executorToken, int callbackId) {
mCatalystInstance = bridge;
mExecutorToken = executorToken;
mCallbackId = callbackId;
}

@Override
public void invoke(Object... args) {
mCatalystInstance.invokeCallback(mCallbackId, Arguments.fromJavaArgs(args));
mCatalystInstance.invokeCallback(mExecutorToken, mCallbackId, Arguments.fromJavaArgs(args));
}
}
Expand Up @@ -14,7 +14,6 @@
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.common.annotations.VisibleForTesting;

/**
* A higher level API on top of the asynchronous JSC bridge. This provides an
Expand All @@ -27,7 +26,7 @@ public interface CatalystInstance {
// This is called from java code, so it won't be stripped anyway, but proguard will rename it,
// which this prevents.
@DoNotStrip
void invokeCallback(final int callbackID, final NativeArray arguments);
void invokeCallback(ExecutorToken executorToken, final int callbackID, final NativeArray arguments);
/**
* Destroys this catalyst instance, waiting for any other threads in ReactQueueConfiguration
* (besides the UI thread) to finish running. Must be called from the UI thread so that we can
Expand All @@ -45,6 +44,7 @@ public interface CatalystInstance {
ReactQueueConfiguration getReactQueueConfiguration();

<T extends JavaScriptModule> T getJSModule(Class<T> jsInterface);
<T extends JavaScriptModule> T getJSModule(ExecutorToken executorToken, Class<T> jsInterface);
<T extends NativeModule> T getNativeModule(Class<T> nativeModuleInterface);
Collection<NativeModule> getNativeModules();

Expand Down
Expand Up @@ -54,6 +54,7 @@ public class CatalystInstanceImpl implements CatalystInstance {
private final JavaScriptModuleRegistry mJSModuleRegistry;
private final JSBundleLoader mJSBundleLoader;
private final Object mTeardownLock = new Object();
private @Nullable ExecutorToken mMainExecutorToken;

// Access from native modules thread
private final NativeModuleRegistry mJavaRegistry;
Expand Down Expand Up @@ -113,6 +114,7 @@ private ReactBridge initializeBridge(
jsExecutor,
new NativeModulesReactCallback(),
mReactQueueConfiguration.getNativeModulesQueueThread());
mMainExecutorToken = bridge.getMainExecutorToken();
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
}
Expand Down Expand Up @@ -165,10 +167,11 @@ public Boolean call() throws Exception {
}

/* package */ void callFunction(
final int moduleId,
final int methodId,
final NativeArray arguments,
final String tracingName) {
ExecutorToken executorToken,
int moduleId,
int methodId,
NativeArray arguments,
String tracingName) {
synchronized (mTeardownLock) {
if (mDestroyed) {
FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed.");
Expand All @@ -177,15 +180,15 @@ public Boolean call() throws Exception {

incrementPendingJSCalls();

Assertions.assertNotNull(mBridge).callFunction(moduleId, methodId, arguments, tracingName);
Assertions.assertNotNull(mBridge).callFunction(executorToken, moduleId, methodId, arguments, tracingName);
}
}

// This is called from java code, so it won't be stripped anyway, but proguard will rename it,
// which this prevents.
@DoNotStrip
@Override
public void invokeCallback(final int callbackID, final NativeArray arguments) {
public void invokeCallback(ExecutorToken executorToken, int callbackID, NativeArray arguments) {
synchronized (mTeardownLock) {
if (mDestroyed) {
FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed.");
Expand All @@ -194,7 +197,7 @@ public void invokeCallback(final int callbackID, final NativeArray arguments) {

incrementPendingJSCalls();

Assertions.assertNotNull(mBridge).invokeCallback(callbackID, arguments);
Assertions.assertNotNull(mBridge).invokeCallback(executorToken, callbackID, arguments);
}
}

Expand Down Expand Up @@ -269,7 +272,12 @@ public ReactQueueConfiguration getReactQueueConfiguration() {

@Override
public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
return Assertions.assertNotNull(mJSModuleRegistry).getJavaScriptModule(jsInterface);
return getJSModule(Assertions.assertNotNull(mMainExecutorToken), jsInterface);
}

@Override
public <T extends JavaScriptModule> T getJSModule(ExecutorToken executorToken, Class<T> jsInterface) {
return Assertions.assertNotNull(mJSModuleRegistry).getJavaScriptModule(executorToken, jsInterface);
}

@Override
Expand Down Expand Up @@ -388,15 +396,15 @@ private void decrementPendingJSCalls() {
private class NativeModulesReactCallback implements ReactCallback {

@Override
public void call(int moduleId, int methodId, ReadableNativeArray parameters) {
public void call(ExecutorToken executorToken, int moduleId, int methodId, ReadableNativeArray parameters) {
mReactQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread();

// Suppress any callbacks if destroyed - will only lead to sadness.
if (mDestroyed) {
return;
}

mJavaRegistry.call(CatalystInstanceImpl.this, moduleId, methodId, parameters);
mJavaRegistry.call(CatalystInstanceImpl.this, executorToken, moduleId, methodId, parameters);
}

@Override
Expand Down Expand Up @@ -441,12 +449,13 @@ public void run() {
private class JSProfilerTraceListener implements TraceListener {
@Override
public void onTraceStarted() {
getJSModule(com.facebook.react.bridge.Systrace.class).setEnabled(true);
getJSModule(Assertions.assertNotNull(mMainExecutorToken), com.facebook.react.bridge.Systrace.class).setEnabled(
true);
}

@Override
public void onTraceStopped() {
getJSModule(com.facebook.react.bridge.Systrace.class).setEnabled(false);
getJSModule(Assertions.assertNotNull(mMainExecutorToken), com.facebook.react.bridge.Systrace.class).setEnabled(false);
}
}

Expand Down
@@ -0,0 +1,24 @@
package com.facebook.react.bridge;

import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;

/**
* Class corresponding to a JS VM that can call into native modules. In Java, this should
* just be treated as a black box to be used to help the framework route native->JS calls back to
* the proper JS VM. See {@link ReactContext#getJSModule(ExecutorToken, Class)} and
* {@link BaseJavaModule#supportsWebWorkers()}.
*
* Note: If your application doesn't use web workers, it will only have a single ExecutorToken
* per instance of React Native.
*/
@DoNotStrip
public class ExecutorToken {

private final HybridData mHybridData;

@DoNotStrip
private ExecutorToken(HybridData hybridData) {
mHybridData = hybridData;
}
}

0 comments on commit bd95b22

Please sign in to comment.