diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/BUCK b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/BUCK new file mode 100644 index 000000000..08483b1c1 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/BUCK @@ -0,0 +1,25 @@ +load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "react_native_target", "rn_android_library") + +rn_android_library( + name = "core", + srcs = glob( + [ + "*.java", + ], + ), + required_for_source_only_abi = True, + visibility = [ + "PUBLIC", + ], + deps = [ + react_native_target("java/com/facebook/react/turbomodule/core/interfaces:interfaces"), + react_native_dep("java/com/facebook/systrace:systrace"), + react_native_dep("libraries/soloader/java/com/facebook/soloader:soloader"), + react_native_dep("third-party/java/infer-annotations:infer-annotations"), + react_native_dep("third-party/java/jsr-305:jsr-305"), + react_native_target("java/com/facebook/react/turbomodule/core/jni:jni"), + react_native_target("java/com/facebook/debug/holder:holder"), + react_native_target("java/com/facebook/react/bridge:interfaces"), + react_native_target("java/com/facebook/react/bridge:bridge"), + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/TurboModuleManager.java b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/TurboModuleManager.java new file mode 100644 index 000000000..aa9029dd1 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/TurboModuleManager.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.JSIModule; +import com.facebook.react.bridge.JavaScriptContextHolder; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.queue.MessageQueueThread; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; +import com.facebook.soloader.SoLoader; + +/** +* This is the main class and entry point for TurboModules. +* Note that this is a hybrid class, and has a C++ counterpart +* This class installs the JSI bindings. It also implements the method to get a Java module, that the C++ counterpart calls. +*/ +public class TurboModuleManager implements JSIModule { + static { + SoLoader.loadLibrary("turbomodulejsijni"); + } + + private final ReactApplicationContext mReactApplicationContext; + + @DoNotStrip + @SuppressWarnings("unused") + private final HybridData mHybridData; + private final ModuleProvider mModuleProvider; + + public TurboModuleManager( + ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext, ModuleProvider moduleProvider) { + mReactApplicationContext = reactApplicationContext; + MessageQueueThread jsMessageQueueThread = + mReactApplicationContext + .getCatalystInstance() + .getReactQueueConfiguration() + .getJSQueueThread(); + mHybridData = initHybrid(jsContext.get(), jsMessageQueueThread); + mModuleProvider = moduleProvider; + } + + @DoNotStrip + @SuppressWarnings("unused") + protected TurboModule getJavaModule(String name) { + return mModuleProvider.getModule(name, mReactApplicationContext); + } + + protected native HybridData initHybrid(long jsContext, MessageQueueThread jsQueue); + + protected native void installJSIBindings(); + + public void installBindings() { + installJSIBindings(); + } + + protected ReactApplicationContext getReactApplicationContext() { + return mReactApplicationContext; + } + + @Override + public void initialize() {} + + @Override + public void onCatalystInstanceDestroy() {} + + /** All applications must implement this interface, and provide the Java TurboModule class */ + public interface ModuleProvider { + TurboModule getModule(String name, ReactApplicationContext reactApplicationContext); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/interfaces/BUCK b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/interfaces/BUCK new file mode 100644 index 000000000..26200c1fe --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/interfaces/BUCK @@ -0,0 +1,12 @@ +load("//tools/build_defs/oss:rn_defs.bzl", "rn_android_library") + +rn_android_library( + name = "interfaces", + srcs = [ + "TurboModule.java", + ], + required_for_source_only_abi = True, + visibility = [ + "PUBLIC", + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/interfaces/TurboModule.java b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/interfaces/TurboModule.java new file mode 100644 index 000000000..180c45d96 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/interfaces/TurboModule.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its 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.interfaces; + +/** + * All turbo modules should inherit from this interface + */ +public interface TurboModule { + /** When CatalystInstance is destroyed, this method will be called. All implementing TurboModules can perform cleanup here. */ + void invalidate(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/BUCK b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/BUCK new file mode 100644 index 000000000..10eeaa4cd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/BUCK @@ -0,0 +1,38 @@ +load("@fbsource//tools/build_defs:glob_defs.bzl", "subdir_glob") +load("@fbsource//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "react_native_target", "react_native_xplat_target", "rn_xplat_cxx_library") + +rn_xplat_cxx_library( + name = "jni", + srcs = glob(["**/*.cpp"]), + header_namespace = "", + exported_headers = subdir_glob( + [ + ("", "**/*.h"), + ], + prefix = "jsireact", + ), + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + force_static = True, + platforms = ANDROID, + preprocessor_flags = [ + "-DLOG_TAG=\"ReactNative\"", + "-DWITH_FBSYSTRACE=1", + ], + visibility = [ + "PUBLIC", + ], + deps = [ + react_native_target("jni/react/jni:jni"), + "xplat//jsi:JSIDynamic", + "xplat//jsi:jsi", + ], + exported_deps = [ + "xplat//jsi:jsi", + react_native_xplat_target("turbomodule/core:core"), + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/TurboModuleManager.cpp b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/TurboModuleManager.cpp new file mode 100644 index 000000000..d5cd4feae --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/TurboModuleManager.cpp @@ -0,0 +1,74 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +#include +#include + +#include + +#include + +#include "TurboModuleManager.h" + +namespace facebook { +namespace react { + +static JTurboModuleProviderFunctionType moduleProvider_ = nullptr; + +TurboModuleManager::TurboModuleManager( + jni::alias_ref jThis, + jsi::Runtime* rt, + std::shared_ptr jsMessageQueueThread +): + javaPart_(make_global(jThis)), + runtime_(rt), + jsMessageQueueThread_(jsMessageQueueThread) + {} + +jni::local_ref TurboModuleManager::initHybrid( + jni::alias_ref jThis, + jlong jsContext, + jni::alias_ref jsQueue +) { + auto sharedJSMessageQueueThread = std::make_shared (jsQueue); + return makeCxxInstance(jThis, (jsi::Runtime *) jsContext, sharedJSMessageQueueThread); +} + +void TurboModuleManager::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", TurboModuleManager::initHybrid), + makeNativeMethod("installJSIBindings", TurboModuleManager::installJSIBindings), + }); +} + +void TurboModuleManager::installJSIBindings() { + if (!runtime_) { + return; // Runtime doesn't exist when attached to Chrome debugger. + } + TurboModuleBinding::install(*runtime_, std::make_shared( + [this](const std::string &name) { + const auto moduleInstance = getJavaModule(name); + const auto jsInvoker = std::make_shared(jsMessageQueueThread_); + return moduleProvider_(name, moduleInstance, jsInvoker); + }) + ); +} + +jni::global_ref TurboModuleManager::getJavaModule(std::string name) { + static auto method = javaClassStatic()->getMethod(const std::string&)>("getJavaModule"); + return make_global(method(javaPart_.get(), name)); +} + +void TurboModuleManager::setModuleProvider(JTurboModuleProviderFunctionType fn) { + moduleProvider_ = fn; +} + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/TurboModuleManager.h b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/TurboModuleManager.h new file mode 100644 index 000000000..24273ac3a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/TurboModuleManager.h @@ -0,0 +1,49 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +using JTurboModuleProviderFunctionType = std::function( + const std::string &name, jni::global_ref moduleInstance, std::shared_ptr jsInvoker)>; + +class TurboModuleManager : public jni::HybridClass { +public: + static auto constexpr kJavaDescriptor = "Lcom/facebook/react/turbomodule/core/TurboModuleManager;"; + static jni::local_ref initHybrid( + jni::alias_ref jThis, + jlong jsContext, + jni::alias_ref jsQueue + ); + static void registerNatives(); + static void setModuleProvider(JTurboModuleProviderFunctionType moduleProvider); +private: + friend HybridBase; + jni::global_ref javaPart_; + jsi::Runtime* runtime_; + std::shared_ptr jsMessageQueueThread_; + + jni::global_ref getJavaModule(std::string name); + void installJSIBindings(); + explicit TurboModuleManager( + jni::alias_ref jThis, + jsi::Runtime* rt, + std::shared_ptr jsMessageQueueThread + ); +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/turbomodule/core/BUCK b/ReactCommon/turbomodule/core/BUCK index 9d3326bf9..fb0df5a03 100644 --- a/ReactCommon/turbomodule/core/BUCK +++ b/ReactCommon/turbomodule/core/BUCK @@ -1,5 +1,5 @@ load("@fbsource//tools/build_defs/apple:flag_defs.bzl", "OBJC_ARC_PREPROCESSOR_FLAGS", "get_debug_preprocessor_flags", "get_static_library_ios_flags") -load("//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "APPLE", "react_native_xplat_target", "rn_xplat_cxx_library", "subdir_glob") +load("//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "APPLE", "react_native_target", "react_native_xplat_target", "rn_xplat_cxx_library", "subdir_glob") rn_xplat_cxx_library( name = "core", @@ -17,6 +17,20 @@ rn_xplat_cxx_library( "-std=c++14", "-Wall", ], + fbandroid_deps = [ + react_native_target("jni/react/jni:jni"), + ], + fbandroid_exported_headers = subdir_glob( + [ + ("platform/android", "*.h"), + ], + prefix = "jsireact", + ), + fbandroid_srcs = glob( + [ + "platform/android/**/*.cpp", + ], + ), fbobjc_compiler_flags = [ "-Wall", "-fobjc-arc-exceptions", diff --git a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp b/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp new file mode 100644 index 000000000..35767f047 --- /dev/null +++ b/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp @@ -0,0 +1,199 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +#include +#include + +#include +#import + +#include +#include +#include +#include +#include + +#include "JavaTurboModule.h" + +namespace facebook { +namespace react { + +JavaTurboModule::JavaTurboModule(const std::string &name, jni::global_ref instance, std::shared_ptr jsInvoker) + : TurboModule(name, jsInvoker), instance_(instance) {} + +// fnjni already does this conversion, but since we are using plain JNI, this needs to be done again +// TODO (axe) Reuse existing implementation as needed - the exist in MethodInvoker.cpp +// TODO (axe) If at runtime, JS sends incorrect arguments and this is not typechecked, conversion here will fail. Check for that case (OSS) +std::unique_ptr convertFromJValueArgsToJNIArgs( + JNIEnv *env, + jsi::Runtime &rt, + const jsi::Value *args, + size_t count, + std::shared_ptr jsInvoker + ) { + auto jargs = std::make_unique(count); + for (size_t i = 0; i < count; i++) { + const jsi::Value *arg = &args[i]; + if (arg->isBool()) { + jargs[i].z = arg->getBool(); + } else if (arg->isNumber()) { + jargs[i].d = arg->getNumber(); + } else if (arg->isNull() || arg->isUndefined()) { + // What happens if Java is expecting a bool or a number, and JS sends a null or undefined? + jargs[i].l = nullptr; + } else if (arg->isString()) { + // We are basically creating a whole new string here + // TODO (axe) Is there a way to copy this instead of creating a whole new string ? + jargs[i].l = env->NewStringUTF(arg->getString(rt).utf8(rt).c_str()); + } else if (arg->isObject()) { + auto objectArg = arg->getObject(rt); + // We are currently using folly:dynamic to convert JSON to Writable Map + // TODO (axe) Don't use folly:dynamic, instead construct Java map directly + if (objectArg.isArray(rt)) { + auto dynamicFromValue = jsi::dynamicFromValue(rt, args[i]); + auto jParams = ReadableNativeArray::newObjectCxxArgs(std::move(dynamicFromValue)); + jargs[i].l = jParams.release(); + } else if (objectArg.isFunction(rt)) { + auto wrapper = std::make_shared(objectArg.getFunction(rt), rt, jsInvoker); + std::function fn = [wrapper](folly::dynamic responses){ + if (wrapper == nullptr) { + throw std::runtime_error("callback arg cannot be called more than once"); + } + std::shared_ptr rw = wrapper; + wrapper->jsInvoker->invokeAsync([rw, responses]() { + // TODO (axe) valueFromDynamic already returns a Value array. Don't iterate again + jsi::Value args = jsi::valueFromDynamic(rw->runtime, responses); + auto argsArray = args.getObject(rw->runtime).asArray(rw->runtime); + std::vector result; + for (size_t i = 0; i < argsArray.size(rw->runtime); i++) { + result.emplace_back(rw->runtime, argsArray.getValueAtIndex(rw->runtime, i)); + } + rw->callback.call(rw->runtime, (const jsi::Value *)result.data(), result.size()); + }); + }; + wrapper = nullptr; + // TODO Use our own implementation of callback instead of relying on JCxxCallbackImpl + auto callback = JCxxCallbackImpl::newObjectCxxArgs(fn); + jargs[i].l = callback.release(); + } else { + auto dynamicFromValue = jsi::dynamicFromValue(rt, args[i]); + auto jParams = ReadableNativeMap::createWithContents(std::move(dynamicFromValue)); + jargs[i].l = jParams.release(); + } + } + } + return jargs; +} + +jsi::Value convertFromJMapToValue(JNIEnv *env, jsi::Runtime &rt, jobject arg) { + // We currently use Java Argument.makeNativeMap() method to do this conversion + // This could also be done purely in C++, but iterative over map methods + // but those may end up calling reflection methods anyway + // TODO (axe) Investigate the best way to convert Java Map to Value + static jclass jArguments = env->FindClass("com/facebook/react/bridge/Arguments"); + static jmethodID jMakeNativeMap = env->GetStaticMethodID(jArguments, "makeNativeMap", "(Ljava/util/Map;)Lcom/facebook/react/bridge/WritableNativeMap;"); + auto constants = (jobject) env->CallStaticObjectMethod(jArguments, jMakeNativeMap, arg); + auto jResult = jni::adopt_local(constants); + auto result = jni::static_ref_cast(jResult); + return jsi::valueFromDynamic(rt, result->cthis()->consume()); +} + +jsi::Value JavaTurboModule::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) { + std::string propNameUtf8 = propName.utf8(runtime); + if (propNameUtf8 == "getConstants") { + // This is the special method to get the constants from the module. + // Since `getConstants` in Java only returns a Map, this function takes the map + // and converts it to a WritableMap. + return jsi::Function::createFromHostFunction( + runtime, + propName, + 0, + [this](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { + JNIEnv *env = jni::Environment::current(); + jclass cls = env->FindClass(jClassName_.c_str()); + static jmethodID methodID = env->GetMethodID(cls, "getConstants", "()Ljava/util/Map;"); + auto constantsMap = (jobject) env->CallObjectMethod(instance_.get(), methodID); + if (constantsMap == nullptr) { + return jsi::Value::undefined(); + } + return convertFromJMapToValue(env, rt, constantsMap); + } + ); + } else { + return TurboModule::get(runtime, propName); + } +} + +jsi::Value JavaTurboModule::invokeJavaMethod( + jsi::Runtime &rt, + TurboModuleMethodValueKind valueKind, + const std::string &methodName, + const std::string &methodSignature, + const jsi::Value *args, + size_t count) { + + // We are using JNI directly instead of fbjni since we don't want template functiosn + // when finding methods. + JNIEnv *env = jni::Environment::current(); + // TODO (axe) Memoize this class, so that we don't have to find it for every calls + jclass cls = env->FindClass(jClassName_.c_str()); + + // TODO (axe) Memoize method call, so we don't look it up each time the method is called + jmethodID methodID = env->GetMethodID(cls, methodName.c_str(), methodSignature.c_str()); + + std::unique_ptrjargs = convertFromJValueArgsToJNIArgs(env, rt, args, count, jsInvoker_); + auto instance = instance_.get(); + + switch (valueKind) { + case VoidKind: { + env->CallVoidMethodA(instance, methodID, jargs.get()); + return jsi::Value::undefined(); + } + case BooleanKind: { + return jsi::Value((bool)env->CallBooleanMethodA(instance, methodID, jargs.get())); + } + case NumberKind: { + return jsi::Value((double)env->CallDoubleMethodA(instance, methodID, jargs.get())); + } + case StringKind: { + auto returnString = (jstring) env->CallObjectMethodA(instance, methodID, jargs.get()); + if (returnString == nullptr) { + return jsi::Value::null(); + } + const char *js = env->GetStringUTFChars(returnString, nullptr); + std::string result = js; + env->ReleaseStringUTFChars(returnString, js); + return jsi::Value(rt, jsi::String::createFromUtf8(rt, result)); + } + case ObjectKind: { + auto returnObject = (jobject) env->CallObjectMethodA(instance, methodID, jargs.get()); + if (returnObject == nullptr) { + return jsi::Value::null(); + } + auto jResult = jni::adopt_local(returnObject); + auto result = jni::static_ref_cast(jResult); + return jsi::valueFromDynamic(rt, result->cthis()->consume()); + } + case ArrayKind: { + auto returnObject = (jobject) env->CallObjectMethodA(instance, methodID, jargs.get()); + if (returnObject == nullptr) { + return jsi::Value::null(); + } + auto jResult = jni::adopt_local(returnObject); + auto result = jni::static_ref_cast(jResult); + return jsi::valueFromDynamic(rt, result->cthis()->consume()); + } + default: + throw std::runtime_error("Unable to find method module: " + methodName + "(" + methodSignature + ")" + "in module " + jClassName_); + } +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h b/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h new file mode 100644 index 000000000..38837618e --- /dev/null +++ b/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include + +namespace facebook { +namespace react { + +struct JTurboModule : jni::JavaClass { + static auto constexpr kJavaDescriptor = "Lcom/facebook/react/turbomodule/core/interfaces/TurboModule;"; +}; + +class JSI_EXPORT JavaTurboModule : public TurboModule { +public: + JavaTurboModule(const std::string &name, jni::global_ref instance, std::shared_ptr jsInvoker); + jsi::Value invokeJavaMethod( + jsi::Runtime &runtime, + TurboModuleMethodValueKind valueKind, + const std::string &methodName, + const std::string &methodSignature, + const jsi::Value *args, + size_t count); + + virtual facebook::jsi::Value get(facebook::jsi::Runtime& runtime, const facebook::jsi::PropNameID& propName) override; + +protected: + // TODO (axe) Specify class name as kJavaDescriptor instead of a class variable + std::string jClassName_; +private: + jni::global_ref instance_; + jclass findClass(JNIEnv *env) const; +}; + +} // namespace react +} // namespace facebook