Skip to content

Commit

Permalink
Support invoking and returning arbitrary types from Java sync hooks
Browse files Browse the repository at this point in the history
Reviewed By: mhorowitz

Differential Revision: D4409900

fbshipit-source-id: 347e33c442b32f64355d343308c218c15cf5a70f
  • Loading branch information
javache authored and facebook-github-bot committed Jan 31, 2017
1 parent cfb9028 commit f8c72f5
Show file tree
Hide file tree
Showing 13 changed files with 318 additions and 52 deletions.
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ deps = [
react_native_target('java/com/facebook/react:react'), react_native_target('java/com/facebook/react:react'),
react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/common:common'),
react_native_target('java/com/facebook/react/module/annotations:annotations'),
react_native_target('java/com/facebook/react/modules/core:core'), react_native_target('java/com/facebook/react/modules/core:core'),
react_native_target('java/com/facebook/react/modules/datepicker:datepicker'), react_native_target('java/com/facebook/react/modules/datepicker:datepicker'),
react_native_target('java/com/facebook/react/modules/share:share'), react_native_target('java/com/facebook/react/modules/share:share'),
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@


/** /**
* Integration test to verify passing various types of parameters from JS to Java works * Integration test to verify passing various types of parameters from JS to Java works
*
* TODO: we should run these tests with isBlockingSynchronousMethod = true as well,
* since they currrently use a completely different codepath
*/ */
public class CatalystNativeJSToJavaParametersTestCase extends ReactIntegrationTestCase { public class CatalystNativeJSToJavaParametersTestCase extends ReactIntegrationTestCase {


private interface TestJSToJavaParametersModule extends JavaScriptModule { private interface TestJSToJavaParametersModule extends JavaScriptModule {
void returnBasicTypes(); void returnBasicTypes();
void returnBoxedTypes();
void returnDynamicTypes(); void returnDynamicTypes();


void returnArrayWithBasicTypes(); void returnArrayWithBasicTypes();
Expand Down Expand Up @@ -116,6 +120,19 @@ public void testBasicTypes() {
assertNull(args[3]); assertNull(args[3]);
} }


public void testBoxedTypes() {
mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnBoxedTypes();
waitForBridgeAndUIIdle();

List<Object[]> boxedTypesCalls = mRecordingTestModule.getBoxedTypesCalls();
assertEquals(1, boxedTypesCalls.size());

Object[] args = boxedTypesCalls.get(0);
assertEquals(Integer.valueOf(42), args[0]);
assertEquals(Double.valueOf(3.14), args[1]);
assertEquals(Boolean.valueOf(true), args[2]);
}

public void testDynamicType() { public void testDynamicType() {
mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnDynamicTypes(); mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnDynamicTypes();
waitForBridgeAndUIIdle(); waitForBridgeAndUIIdle();
Expand Down Expand Up @@ -682,9 +699,10 @@ private void mapGetByType(ReadableMap map, String key, String typeToAskFor) {
} }
} }


private class RecordingTestModule extends BaseJavaModule { private static class RecordingTestModule extends BaseJavaModule {


private final List<Object[]> mBasicTypesCalls = new ArrayList<Object[]>(); private final List<Object[]> mBasicTypesCalls = new ArrayList<Object[]>();
private final List<Object[]> mBoxedTypesCalls = new ArrayList<Object[]>();
private final List<ReadableArray> mArrayCalls = new ArrayList<ReadableArray>(); private final List<ReadableArray> mArrayCalls = new ArrayList<ReadableArray>();
private final List<ReadableMap> mMapCalls = new ArrayList<ReadableMap>(); private final List<ReadableMap> mMapCalls = new ArrayList<ReadableMap>();
private final List<Dynamic> mDynamicCalls = new ArrayList<Dynamic>(); private final List<Dynamic> mDynamicCalls = new ArrayList<Dynamic>();
Expand All @@ -699,6 +717,11 @@ public void receiveBasicTypes(String s, double d, boolean b, String nullableStri
mBasicTypesCalls.add(new Object[]{s, d, b, nullableString}); mBasicTypesCalls.add(new Object[]{s, d, b, nullableString});
} }


@ReactMethod
public void receiveBoxedTypes(Integer i, Double d, Boolean b) {
mBoxedTypesCalls.add(new Object[]{i, d, b});
}

@ReactMethod @ReactMethod
public void receiveArray(ReadableArray array) { public void receiveArray(ReadableArray array) {
mArrayCalls.add(array); mArrayCalls.add(array);
Expand All @@ -718,6 +741,10 @@ public List<Object[]> getBasicTypesCalls() {
return mBasicTypesCalls; return mBasicTypesCalls;
} }


public List<Object[]> getBoxedTypesCalls() {
return mBoxedTypesCalls;
}

public List<ReadableArray> getArrayCalls() { public List<ReadableArray> getArrayCalls() {
return mArrayCalls; return mArrayCalls;
} }
Expand Down
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Copyright (c) 2014-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.tests;

import java.util.ArrayList;

import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.CatalystInstance;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeArray;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.testing.AssertModule;
import com.facebook.react.testing.FakeWebSocketModule;
import com.facebook.react.testing.ReactIntegrationTestCase;
import com.facebook.react.testing.ReactTestHelper;
import com.facebook.react.uimanager.UIImplementationProvider;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;

/**
* Test marshalling return values from Java to JS
*/
public class CatalystNativeJavaToJSReturnValuesTestCase extends ReactIntegrationTestCase {

private interface TestJavaToJSReturnValuesModule extends JavaScriptModule {
void callMethod(String methodName, String expectedReturnType, String expectedJSON);
void triggerException();
}

@ReactModule(name = "TestModule")
private static class TestModule extends BaseJavaModule {
@Override
public String getName() {
return "TestModule";
}

@ReactMethod(isBlockingSynchronousMethod = true)
boolean getBoolean() {
return true;
}

@ReactMethod(isBlockingSynchronousMethod = true)
Boolean getBoxedBoolean() {
return Boolean.valueOf(true);
}

@ReactMethod(isBlockingSynchronousMethod = true)
int getInt() {
return 42;
}

@ReactMethod(isBlockingSynchronousMethod = true)
Integer getBoxedInt() {
return Integer.valueOf(42);
}

@ReactMethod(isBlockingSynchronousMethod = true)
double getDouble() {
return 3.14159;
}

@ReactMethod(isBlockingSynchronousMethod = true)
Double getBoxedDouble() {
return Double.valueOf(3.14159);
}

@ReactMethod(isBlockingSynchronousMethod = true)
String getString() {
return "Hello world!";
}

@ReactMethod(isBlockingSynchronousMethod = true)
WritableArray getArray() {
WritableArray arr = new WritableNativeArray();
arr.pushString("a");
arr.pushBoolean(true);
return arr;
}

@ReactMethod(isBlockingSynchronousMethod = true)
WritableMap getMap() {
WritableMap map = new WritableNativeMap();
map.putBoolean("a", true);
map.putBoolean("b", false);
return map;
}

@ReactMethod(isBlockingSynchronousMethod = true)
boolean triggerException() {
throw new RuntimeException("Exception triggered");
}
}

private AssertModule mAssertModule;
private CatalystInstance mInstance;

@Override
protected void setUp() throws Exception {
super.setUp();

final UIManagerModule mUIManager = new UIManagerModule(
getContext(),
new ArrayList<ViewManager>(),
new UIImplementationProvider(),
false);

mAssertModule = new AssertModule();

mInstance = ReactTestHelper.catalystInstanceBuilder(this)
.addNativeModule(mAssertModule)
.addNativeModule(new FakeWebSocketModule())
.addJSModule(TestJavaToJSReturnValuesModule.class)
.addNativeModule(mUIManager)
.addNativeModule(new TestModule())
.build();
}

public void testGetPrimitives() {
TestJavaToJSReturnValuesModule m = mInstance.getJSModule(TestJavaToJSReturnValuesModule.class);

// jboolean is actually an unsigned char, so we don't get JS booleans
m.callMethod("getBoolean", "number", "1");
m.callMethod("getBoxedBoolean", "number", "1");

m.callMethod("getInt", "number", "42");
m.callMethod("getBoxedInt", "number", "42");

m.callMethod("getDouble", "number", "3.14159");
m.callMethod("getBoxedDouble", "number", "3.14159");

waitForBridgeAndUIIdle();
mAssertModule.verifyAssertsAndReset();
}

public void testObjectTypes() {
TestJavaToJSReturnValuesModule m = mInstance.getJSModule(TestJavaToJSReturnValuesModule.class);

m.callMethod("getString", "string", "\"Hello world!\"");
m.callMethod("getArray", "object", "[\"a\",true]");
m.callMethod("getMap", "object", "{\"b\":false,\"a\":true}");

waitForBridgeAndUIIdle();
mAssertModule.verifyAssertsAndReset();
}

public void testThrowsException() {
TestJavaToJSReturnValuesModule m = mInstance.getJSModule(TestJavaToJSReturnValuesModule.class);
m.triggerException();

waitForBridgeAndUIIdle();
mAssertModule.verifyAssertsAndReset();
}
}
1 change: 1 addition & 0 deletions ReactAndroid/src/androidTest/js/TestBundle.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require('ViewRenderingTestModule');
require('TestJavaToJSArgumentsModule'); require('TestJavaToJSArgumentsModule');
require('TestJSLocaleModule'); require('TestJSLocaleModule');
require('TestJSToJavaParametersModule'); require('TestJSToJavaParametersModule');
require('TestJavaToJSReturnValuesModule');
require('UIManagerTestModule'); require('UIManagerTestModule');


require('CatalystRootViewTestModule'); require('CatalystRootViewTestModule');
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ var TestJSToJavaParametersModule = {
returnBasicTypes: function() { returnBasicTypes: function() {
Recording.receiveBasicTypes('foo', 3.14, true, null); Recording.receiveBasicTypes('foo', 3.14, true, null);
}, },
returnBoxedTypes: function() {
Recording.receiveBoxedTypes(42, 3.14, true);
},
returnDynamicTypes: function() { returnDynamicTypes: function() {
Recording.receiveDynamic('foo'); Recording.receiveDynamic('foo');
Recording.receiveDynamic(3.14); Recording.receiveDynamic(3.14);
Expand Down
40 changes: 40 additions & 0 deletions ReactAndroid/src/androidTest/js/TestJavaToJSReturnValuesModule.js
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2013-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.
*
* @providesModule TestJavaToJSReturnValuesModule
*/

'use strict';

const BatchedBridge = require('BatchedBridge');

const {assertEquals, assertTrue} = require('Asserts');
const {TestModule} = require('NativeModules');

var TestJavaToJSReturnValuesModule = {
callMethod: function(methodName, expectedType, expectedJSON) {
const result = TestModule[methodName]();
assertEquals(expectedType, typeof result);
assertEquals(expectedJSON, JSON.stringify(result));
},

triggerException: function() {
try {
TestModule.triggerException();
} catch (ex) {
assertTrue(ex.message.indexOf('Exception triggered') !== -1);
}
}
};

BatchedBridge.registerCallableModule(
'TestJavaToJSReturnValuesModule',
TestJavaToJSReturnValuesModule
);

module.exports = TestJavaToJSReturnValuesModule;
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ private static char paramTypeToChar(Class paramClass) {
} }


private static char returnTypeToChar(Class returnClass) { private static char returnTypeToChar(Class returnClass) {
// Keep this in sync with MethodInvoker
char tryCommon = commonTypeToChar(returnClass); char tryCommon = commonTypeToChar(returnClass);
if (tryCommon != '\0') { if (tryCommon != '\0') {
return tryCommon; return tryCommon;
Expand Down
37 changes: 32 additions & 5 deletions ReactAndroid/src/main/jni/xreact/jni/JavaModuleWrapper.cpp
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@


#include "JavaModuleWrapper.h" #include "JavaModuleWrapper.h"


#include <folly/json.h>

#include <fb/fbjni.h> #include <fb/fbjni.h>

#include <folly/json.h>
#include <cxxreact/CxxModule.h> #include <cxxreact/CxxModule.h>
#include <cxxreact/CxxNativeModule.h> #include <cxxreact/CxxNativeModule.h>
#include <cxxreact/Instance.h> #include <cxxreact/Instance.h>
Expand Down Expand Up @@ -42,9 +40,30 @@ std::string JavaNativeModule::getName() {


std::vector<MethodDescriptor> JavaNativeModule::getMethods() { std::vector<MethodDescriptor> JavaNativeModule::getMethods() {
std::vector<MethodDescriptor> ret; std::vector<MethodDescriptor> ret;
syncMethods_.clear();
auto descs = wrapper_->getMethodDescriptors(); auto descs = wrapper_->getMethodDescriptors();
for (const auto& desc : *descs) { for (const auto& desc : *descs) {
ret.emplace_back(desc->getName(), desc->getType()); auto methodName = desc->getName();
auto methodType = desc->getType();

if (methodType == "sync") {
// allow for the sync methods vector to have empty values, resize on demand
size_t methodIndex = ret.size();
if (methodIndex >= syncMethods_.size()) {
syncMethods_.resize(methodIndex + 1);
}
syncMethods_.insert(syncMethods_.begin() + methodIndex, MethodInvoker(
desc->getMethod(),
desc->getSignature(),
getName() + "." + methodName,
true
));
}

ret.emplace_back(
std::move(methodName),
std::move(methodType)
);
} }
return ret; return ret;
} }
Expand Down Expand Up @@ -75,7 +94,15 @@ void JavaNativeModule::invoke(ExecutorToken token, unsigned int reactMethodId, f
} }


MethodCallResult JavaNativeModule::callSerializableNativeHook(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) { MethodCallResult JavaNativeModule::callSerializableNativeHook(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) {
throw std::runtime_error("Unsupported operation."); // TODO: evaluate whether calling through invoke is potentially faster
if (reactMethodId >= syncMethods_.size()) {
throw std::invalid_argument(
folly::to<std::string>("methodId ", reactMethodId, " out of range [0..", syncMethods_.size(), "]"));
}

auto& method = syncMethods_[reactMethodId];
CHECK(method.hasValue() && method->isSyncHook()) << "Trying to invoke a asynchronous method as synchronous hook";
return method->invoke(instance_, wrapper_->getModule(), token, params);
} }


NewJavaNativeModule::NewJavaNativeModule( NewJavaNativeModule::NewJavaNativeModule(
Expand Down
2 changes: 2 additions & 0 deletions ReactAndroid/src/main/jni/xreact/jni/JavaModuleWrapper.h
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


#include <cxxreact/NativeModule.h> #include <cxxreact/NativeModule.h>
#include <fb/fbjni.h> #include <fb/fbjni.h>
#include <folly/Optional.h>


#include "MethodInvoker.h" #include "MethodInvoker.h"


Expand Down Expand Up @@ -54,6 +55,7 @@ class JavaNativeModule : public NativeModule {
private: private:
std::weak_ptr<Instance> instance_; std::weak_ptr<Instance> instance_;
jni::global_ref<JavaModuleWrapper::javaobject> wrapper_; jni::global_ref<JavaModuleWrapper::javaobject> wrapper_;
std::vector<folly::Optional<MethodInvoker>> syncMethods_;
}; };


// Experimental new implementation that uses direct method invocation // Experimental new implementation that uses direct method invocation
Expand Down
Loading

0 comments on commit f8c72f5

Please sign in to comment.