diff --git a/ReactAndroid/src/main/jni/xreact/jni/BUCK b/ReactAndroid/src/main/jni/xreact/jni/BUCK new file mode 100644 index 000000000..a68771a8e --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/BUCK @@ -0,0 +1,48 @@ +include_defs('//ReactAndroid/DEFS') + +# We depend on JSC, support the same platforms +SUPPORTED_PLATFORMS = '^android-(armv7|x86)$' + +EXPORTED_HEADERS = [ + 'CxxModuleWrapper.h', +] + +cxx_library( + name='jni', + soname = 'libreactnativejnifb.so', + header_namespace = 'react/jni', + supported_platforms_regex = SUPPORTED_PLATFORMS, + deps = JSC_DEPS + [ + '//native/fb:fb', + '//native/third-party/android-ndk:android', + '//xplat/folly:molly', + '//xplat/fbsystrace:fbsystrace', + '//xplat/react/module:module', + react_native_target('jni/react/jni:jni'), + react_native_xplat_target('bridge:bridge'), + ], + srcs = glob(['*.cpp']), + exported_headers = EXPORTED_HEADERS, + headers = glob(['*.h'], excludes=EXPORTED_HEADERS), + preprocessor_flags = [ + '-DLOG_TAG="ReactNativeJNI"', + '-DWITH_FBSYSTRACE=1', + ], + compiler_flags = [ + '-Wall', + '-Werror', + '-fexceptions', + '-std=c++11', + '-fvisibility=hidden', + '-frtti', + '-Wno-pessimizing-move', + '-Wno-inconsistent-missing-override', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':jni', +) diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp new file mode 100644 index 000000000..6bdeae504 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp @@ -0,0 +1,228 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "CatalystInstanceImpl.h" + +#include +#include + +#include +#include + +#include + +#include +#include + +#include + +#include +#include +#include + +#include "JSLoader.h" +#include "JavaScriptExecutorHolder.h" +#include "JniJSModulesUnbundle.h" +#include "ModuleRegistryHolder.h" +#include "JNativeRunnable.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +namespace { + + +class Exception : public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Ljava/lang/Exception;"; +}; + +class JInstanceCallback : public InstanceCallback { + public: + explicit JInstanceCallback(alias_ref jobj) + : jobj_(make_global(jobj)) {} + + void onBatchComplete() override { + static auto method = + ReactCallback::javaClassStatic()->getMethod("onBatchComplete"); + method(jobj_); + } + + void incrementPendingJSCalls() override { + static auto method = + ReactCallback::javaClassStatic()->getMethod("incrementPendingJSCalls"); + method(jobj_); + } + + void decrementPendingJSCalls() override { + static auto method = + ReactCallback::javaClassStatic()->getMethod("decrementPendingJSCalls"); + method(jobj_); + } + + void onNativeException(const std::string& what) override { + static auto exCtor = + Exception::javaClassStatic()->getConstructor(); + static auto method = + ReactCallback::javaClassStatic()->getMethod("onNativeException"); + + method(jobj_, Exception::javaClassStatic()->newObject( + exCtor, jni::make_jstring(what).get()).get()); + } + + ExecutorToken createExecutorToken() override { + auto jobj = JExecutorToken::newObjectCxxArgs(); + return jobj->cthis()->getExecutorToken(jobj); + } + + void onExecutorStopped(ExecutorToken) override { + // TODO(cjhopman): implement this. + } + + private: + global_ref jobj_; +}; + +} + +jni::local_ref CatalystInstanceImpl::initHybrid( + jni::alias_ref) { + return makeCxxInstance(); +} + +CatalystInstanceImpl::CatalystInstanceImpl() + : instance_(folly::make_unique()) {} + +void CatalystInstanceImpl::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", CatalystInstanceImpl::initHybrid), + makeNativeMethod("initializeBridge", CatalystInstanceImpl::initializeBridge), + makeNativeMethod("loadScriptFromAssets", + "(Landroid/content/res/AssetManager;Ljava/lang/String;)V", + CatalystInstanceImpl::loadScriptFromAssets), + makeNativeMethod("loadScriptFromFile", CatalystInstanceImpl::loadScriptFromFile), + makeNativeMethod("callJSFunction", CatalystInstanceImpl::callJSFunction), + makeNativeMethod("callJSCallback", CatalystInstanceImpl::callJSCallback), + makeNativeMethod("getMainExecutorToken", CatalystInstanceImpl::getMainExecutorToken), + makeNativeMethod("setGlobalVariable", CatalystInstanceImpl::setGlobalVariable), + makeNativeMethod("supportsProfiling", CatalystInstanceImpl::supportsProfiling), + makeNativeMethod("startProfiler", CatalystInstanceImpl::startProfiler), + makeNativeMethod("stopProfiler", CatalystInstanceImpl::stopProfiler), + }); + + JNativeRunnable::registerNatives(); +} + +void CatalystInstanceImpl::initializeBridge( + jni::alias_ref callback, + // This executor is actually a factory holder. + JavaScriptExecutorHolder* jseh, + jni::alias_ref jsQueue, + jni::alias_ref moduleQueue, + ModuleRegistryHolder* mrh) { + // TODO mhorowitz: how to assert here? + // Assertions.assertCondition(mBridge == null, "initializeBridge should be called once"); + + // This used to be: + // + // Java CatalystInstanceImpl -> C++ CatalystInstanceImpl -> Bridge -> Bridge::Callback + // --weak--> ReactCallback -> Java CatalystInstanceImpl + // + // Now the weak ref is a global ref. So breaking the loop depends on + // CatalystInstanceImpl#destroy() calling mHybridData.resetNative(), which + // should cause all the C++ pointers to be cleaned up (except C++ + // CatalystInstanceImpl might be kept alive for a short time by running + // callbacks). This also means that all native calls need to be pre-checked + // to avoid NPE. + + // See the comment in callJSFunction. Once js calls switch to strings, we + // don't need jsModuleDescriptions any more, all the way up and down the + // stack. + + instance_->initializeBridge(folly::make_unique(callback), + jseh->getExecutorFactory(), + folly::make_unique(jsQueue), + folly::make_unique(moduleQueue), + mrh->getModuleRegistry()); +} + +void CatalystInstanceImpl::loadScriptFromAssets(jobject assetManager, + const std::string& assetURL) { + const int kAssetsLength = 9; // strlen("assets://"); + auto sourceURL = assetURL.substr(kAssetsLength); + + auto manager = react::extractAssetManager(assetManager); + auto script = react::loadScriptFromAssets(manager, sourceURL); + if (JniJSModulesUnbundle::isUnbundle(manager, sourceURL)) { + instance_->loadUnbundle( + folly::make_unique(manager, sourceURL), + std::move(script), + sourceURL); + } else { + instance_->loadScriptFromString(std::move(script), std::move(sourceURL)); + } +} + +void CatalystInstanceImpl::loadScriptFromFile(jni::alias_ref fileName, + const std::string& sourceURL) { + return instance_->loadScriptFromFile(fileName ? fileName->toStdString() : "", + sourceURL); +} + +void CatalystInstanceImpl::callJSFunction( + JExecutorToken* token, std::string module, std::string method, NativeArray* arguments, + const std::string& tracingName) { + // We want to share the C++ code, and on iOS, modules pass module/method + // names as strings all the way through to JS, and there's no way to do + // string -> id mapping on the objc side. So on Android, we convert the + // number to a string, here which gets passed as-is to JS. There, they they + // used as ids if isFinite(), which handles this case, and looked up as + // strings otherwise. Eventually, we'll probably want to modify the stack + // from the JS proxy through here to use strings, too. + instance_->callJSFunction(token->getExecutorToken(nullptr), + module, + method, + std::move(arguments->array), + tracingName); +} + +void CatalystInstanceImpl::callJSCallback(JExecutorToken* token, jint callbackId, NativeArray* arguments) { + instance_->callJSCallback(token->getExecutorToken(nullptr), callbackId, std::move(arguments->array)); +} + +local_ref CatalystInstanceImpl::getMainExecutorToken() { + return JExecutorToken::extractJavaPartFromToken(instance_->getMainExecutorToken()); +} + +void CatalystInstanceImpl::setGlobalVariable(std::string propName, + std::string&& jsonValue) { + // This is only ever called from Java with short strings, and only + // for testing, so no need to try hard for zero-copy here. + + instance_->setGlobalVariable(std::move(propName), + folly::make_unique(std::move(jsonValue))); +} + +jboolean CatalystInstanceImpl::supportsProfiling() { + if (!instance_) { + return false; + } + return instance_->supportsProfiling(); +} + +void CatalystInstanceImpl::startProfiler(const std::string& title) { + if (!instance_) { + return; + } + return instance_->startProfiler(title); +} + +void CatalystInstanceImpl::stopProfiler(const std::string& title, const std::string& filename) { + if (!instance_) { + return; + } + return instance_->stopProfiler(title, filename); +} + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h new file mode 100644 index 000000000..0bb6302fc --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h @@ -0,0 +1,67 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include + +#include + +#include + +#include "JMessageQueueThread.h" +#include "JExecutorToken.h" + +namespace facebook { +namespace react { + +class Instance; +class JavaScriptExecutorHolder; +class ModuleRegistryHolder; +class NativeArray; + +struct ReactCallback : public jni::JavaClass { + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/ReactCallback;"; +}; + +class CatalystInstanceImpl : public jni::HybridClass { + public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/CatalystInstanceImpl;"; + + static jni::local_ref initHybrid(jni::alias_ref); + + static void registerNatives(); + + std::shared_ptr getInstance() { + return instance_; + } + + private: + friend HybridBase; + + CatalystInstanceImpl(); + + void initializeBridge( + jni::alias_ref callback, + // This executor is actually a factory holder. + JavaScriptExecutorHolder* jseh, + jni::alias_ref jsQueue, + jni::alias_ref moduleQueue, + ModuleRegistryHolder* mrh); + void loadScriptFromAssets(jobject assetManager, const std::string& assetURL); + void loadScriptFromFile(jni::alias_ref fileName, const std::string& sourceURL); + void callJSFunction(JExecutorToken* token, std::string module, std::string method, NativeArray* arguments, + const std::string& tracingName); + void callJSCallback(JExecutorToken* token, jint callbackId, NativeArray* arguments); + local_ref getMainExecutorToken(); + void setGlobalVariable(std::string propName, + std::string&& jsonValue); + jboolean supportsProfiling(); + void startProfiler(const std::string& title); + void stopProfiler(const std::string& title, const std::string& filename); + + // This should be the only long-lived strong reference, but every C++ class + // will have a weak reference. + std::shared_ptr instance_; +}; + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp new file mode 100644 index 000000000..182f03646 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp @@ -0,0 +1,219 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "CxxModuleWrapper.h" + +#include + +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include +#include + +using namespace facebook::jni; +using namespace facebook::xplat::module; +using namespace facebook::react; + +namespace { + +class ExecutorToken : public HybridClass { +public: + constexpr static const char *const kJavaDescriptor = "Lcom/facebook/react/bridge/ExecutorToken;"; +}; + +class CxxMethodWrapper : public HybridClass { +public: + constexpr static const char *const kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/CxxModuleWrapper$MethodWrapper;"; + + static local_ref initHybrid(alias_ref) { + return makeCxxInstance(); + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", CxxMethodWrapper::initHybrid), + makeNativeMethod("invoke", + "(Lcom/facebook/react/bridge/CatalystInstance;Lcom/facebook/react/bridge/ExecutorToken;Lcom/facebook/react/bridge/ReadableNativeArray;)V", + CxxMethodWrapper::invoke), + }); + } + + void invoke(jobject catalystinstance, ExecutorToken::jhybridobject executorToken, NativeArray* args); + + const CxxModule::Method* method_; +}; + +void CxxMethodWrapper::invoke(jobject jCatalystInstance, ExecutorToken::jhybridobject jExecutorToken, NativeArray* arguments) { + CxxModule::Callback first; + CxxModule::Callback second; + + if (method_->callbacks >= 1) { + auto catalystInstance = make_global(adopt_local(jCatalystInstance)); + global_ref executorToken = make_global(jExecutorToken); + // TODO(10184774): Support ExecutorToken in CxxModules + static auto sCatalystInstanceInvokeCallback = + catalystInstance->getClass()->getMethod( + "invokeCallback"); + + int id1; + + if (!arguments->array[arguments->array.size() - 1].isInt()) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "Expected callback as last argument"); + } + + if (method_->callbacks == 2) { + if (!arguments->array[arguments->array.size() - 2].isInt()) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "Expected callback as penultimate argument"); + return; + } + + id1 = arguments->array[arguments->array.size() - 2].getInt(); + int id2 = arguments->array[arguments->array.size() - 1].getInt(); + second = [catalystInstance, executorToken, id2](std::vector args) mutable { + ThreadScope guard; + sCatalystInstanceInvokeCallback( + catalystInstance.get(), executorToken.get(), id2, + ReadableNativeArray::newObjectCxxArgs(std::move(args)).get()); + catalystInstance.reset(); + executorToken.reset(); + }; + } else { + id1 = arguments->array[arguments->array.size() - 1].getInt(); + } + + first = [catalystInstance, executorToken, id1](std::vector args) mutable { + ThreadScope guard; + sCatalystInstanceInvokeCallback( + catalystInstance.get(), executorToken.get(), id1, + ReadableNativeArray::newObjectCxxArgs(std::move(args)).get()); + // This is necessary because by the time the lambda's dtor runs, + // the guard has been destroyed, and it may not be possible to + // get a JNIEnv* to clean up the captured global_ref. + catalystInstance.reset(); + executorToken.reset(); + }; + } + + // I've got a few flawed options here. I can catch C++ exceptions + // here, and log/convert them to java exceptions. This lets all the + // java handling work ok, but the only info I can capture about the + // C++ exception is the what() string, not the stack. I can let the + // C++ exception escape, crashing the app. This causes the full, + // accurate C++ stack trace to be added to logcat by debuggerd. The + // java state is lost, but in practice, the java stack is always the + // same in this case since the javascript stack is not visible. The + // what() value is also lost. Finally, I can catch, log the java + // stack, then rethrow the C++ exception. In this case I get java + // and C++ stack data, but the C++ stack is as of the rethrow, not + // the original throw, both the C++ and java stacks always look the + // same. + // + // I am going with option 2, since that seems like the most useful + // choice. It would be nice to be able to get what() and the C++ + // stack. I'm told that will be possible in the future. TODO + // mhorowitz #7128529: convert C++ exceptions to Java + + folly::dynamic dargs = arguments->array; + dargs.resize(arguments->array.size() - method_->callbacks); + + try { + method_->func(dargs, first, second); + } catch (const facebook::xplat::JsArgumentException& ex) { + throwNewJavaException(gJavaLangIllegalArgumentException, ex.what()); + } +} + +} + +// CxxModuleWrapper + +void CxxModuleWrapper::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", CxxModuleWrapper::initHybrid), + makeNativeMethod("getName", CxxModuleWrapper::getName), + makeNativeMethod("getConstantsJson", CxxModuleWrapper::getConstantsJson), + makeNativeMethod("getMethods", "()Ljava/util/Map;", CxxModuleWrapper::getMethods), + }); + + CxxMethodWrapper::registerNatives(); +} + +CxxModuleWrapper::CxxModuleWrapper(const std::string& soPath, const std::string& fname) { + // soPath is the path of a library which has already been loaded by + // java SoLoader.loadLibrary(). So this returns the same handle, + // and increments the reference counter. We can't just use + // dlsym(RTLD_DEFAULT, ...), because that crashes on 4.4.2 and + // earlier: https://code.google.com/p/android/issues/detail?id=61799 + void* handle = dlopen(soPath.c_str(), RTLD_NOW); + if (!handle) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "module shared library %s is not found", soPath.c_str()); + } + // Now, arrange to close the handle so the counter is decremented. + // The handle will remain valid until java closes it. There's no + // way to do this on Android, but that's no reason to be sloppy + // here. + auto guard = folly::makeGuard([&] { FBASSERT(dlclose(handle) == 0); }); + + void* sym = dlsym(handle, fname.c_str()); + if (!sym) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "module function %s in shared library %s is not found", + fname.c_str(), soPath.c_str()); + } + auto factory = reinterpret_cast(sym); + module_.reset((*factory)()); + methods_ = module_->getMethods(); +} + +std::string CxxModuleWrapper::getName() { + return module_->getName(); +} + +std::string CxxModuleWrapper::getConstantsJson() { + std::map constants = module_->getConstants(); + folly::dynamic constsobject = folly::dynamic::object; + + for (auto& c : constants) { + constsobject.insert(std::move(c.first), std::move(c.second)); + } + + return folly::toJson(constsobject); +} + +jobject CxxModuleWrapper::getMethods() { + static auto hashmap = findClassStatic("java/util/HashMap"); + static auto hashmap_put = hashmap->getMethod("put"); + + auto methods = hashmap->newObject(hashmap->getConstructor()); + + std::unordered_set names; + for (const auto& m : methods_) { + if (names.find(m.name) != names.end()) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "C++ Module %s method name already registered: %s", + module_->getName().c_str(), m.name.c_str()); + } + names.insert(m.name); + auto name = make_jstring(m.name); + static auto ctor = + CxxMethodWrapper::javaClassStatic()->getConstructor(); + auto method = CxxMethodWrapper::javaClassStatic()->newObject(ctor); + cthis(method)->method_ = &m; + hashmap_put(methods.get(), name.get(), method.get()); + } + + return methods.release(); +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h new file mode 100644 index 000000000..206eddc56 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h @@ -0,0 +1,53 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +class CxxModuleWrapper : public jni::HybridClass { +public: + constexpr static const char *const kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/CxxModuleWrapper;"; + + static void registerNatives(); + + CxxModuleWrapper(const std::string& soPath, const std::string& fname); + + static jni::local_ref initHybrid( + jni::alias_ref, const std::string& soPath, const std::string& fname) { + return makeCxxInstance(soPath, fname); + } + + // JNI methods + std::string getName(); + std::string getConstantsJson(); + jobject getMethods(); + + // This steals ownership of the underlying module for use by the C++ bridge + std::unique_ptr getModule() { + // TODO mhorowitz: remove this (and a lot of other code) once the java + // bridge is dead. + methods_.clear(); + return std::move(module_); + } + +protected: + friend HybridBase; + + explicit CxxModuleWrapper(std::unique_ptr module) + : module_(std::move(module)) + , methods_(module_->getMethods()) {} + + std::unique_ptr module_; + std::vector methods_; +}; + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h new file mode 100644 index 000000000..539273104 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h @@ -0,0 +1,44 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +#include + +namespace facebook { +namespace react { + +class Instance; + +struct JCallback : public jni::JavaClass { + constexpr static auto kJavaDescriptor = "Lcom/facebook/react/bridge/Callback;"; +}; + +class JCallbackImpl : public jni::HybridClass { +public: + constexpr static auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/CallbackImpl;"; + + static void registerNatives() { + javaClassStatic()->registerNatives({ + makeNativeMethod("nativeInvoke", JCallbackImpl::invoke), + }); + } +private: + friend HybridBase; + + using Callback = std::function; + JCallbackImpl(Callback callback) : callback_(std::move(callback)) {} + + void invoke(NativeArray* arguments) { + callback_(std::move(arguments->array)); + } + + Callback callback_; +}; + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.cpp b/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.cpp new file mode 100644 index 000000000..c55e8f861 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.cpp @@ -0,0 +1,25 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JExecutorToken.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +ExecutorToken JExecutorToken::getExecutorToken(alias_ref jobj) { + std::lock_guard guard(createTokenGuard_); + auto sharedOwner = owner_.lock(); + if (!sharedOwner) { + sharedOwner = std::shared_ptr(new JExecutorTokenHolder(jobj)); + owner_ = sharedOwner; + } + return ExecutorToken(sharedOwner); +} + +local_ref JExecutorToken::extractJavaPartFromToken(ExecutorToken token) { + return make_local(static_cast(token.getPlatformExecutorToken().get())->getJobj()); +} + + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.h b/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.h new file mode 100644 index 000000000..0505801ea --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.h @@ -0,0 +1,60 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include + +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class JExecutorTokenHolder; +class JExecutorToken : public HybridClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/ExecutorToken;"; + + ExecutorToken getExecutorToken(alias_ref jobj); + + static local_ref extractJavaPartFromToken(ExecutorToken token); +private: + friend HybridBase; + friend JExecutorTokenHolder; + + JExecutorToken() {} + + std::weak_ptr owner_; + std::mutex createTokenGuard_; +}; + +/** + * Wrapper class to hold references to both the c++ and Java parts of the + * ExecutorToken object. The goal is to allow a reference to a token from either + * c++ or Java to keep both the Java object and c++ hybrid part alive. For c++ + * references, we accomplish this by having JExecutorTokenHolder keep a reference + * to the Java object (which has a reference to the JExecutorToken hybrid part). + * For Java references, we allow the JExecutorTokenHolder to be deallocated if there + * are no references to it in c++ from a PlatformExecutorToken, but will dynamically + * create a new one in JExecutorToken.getExecutorToken if needed. + */ +class JExecutorTokenHolder : public PlatformExecutorToken, public noncopyable { +public: + explicit JExecutorTokenHolder(alias_ref jobj) : + jobj_(make_global(jobj)), + impl_(cthis(jobj)) { + } + + JExecutorToken::javaobject getJobj() { + return jobj_.get(); + } + +private: + global_ref jobj_; + JExecutorToken *impl_; +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.cpp b/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.cpp new file mode 100644 index 000000000..6cba3235c --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.cpp @@ -0,0 +1,66 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JMessageQueueThread.h" + +#include +#include + +#include +#include +#include + +#include "JNativeRunnable.h" + +namespace facebook { +namespace react { + +JMessageQueueThread::JMessageQueueThread(alias_ref jobj) : + m_jobj(make_global(jobj)) { +} + +void JMessageQueueThread::runOnQueue(std::function&& runnable) { + static auto method = JavaMessageQueueThread::javaClassStatic()-> + getMethod("runOnQueue"); + method(m_jobj, JNativeRunnable::newObjectCxxArgs(runnable).get()); +} + +void JMessageQueueThread::runOnQueueSync(std::function&& runnable) { + static auto jIsOnThread = JavaMessageQueueThread::javaClassStatic()-> + getMethod("isOnThread"); + + if (jIsOnThread(m_jobj)) { + runnable(); + } else { + std::mutex signalMutex; + std::condition_variable signalCv; + bool runnableComplete = false; + + runOnQueue([&] () mutable { + std::lock_guard lock(signalMutex); + + runnable(); + runnableComplete = true; + + signalCv.notify_one(); + }); + + std::unique_lock lock(signalMutex); + signalCv.wait(lock, [&runnableComplete] { return runnableComplete; }); + } +} + +void JMessageQueueThread::quitSynchronous() { + static auto method = JavaMessageQueueThread::javaClassStatic()-> + getMethod("quitSynchronous"); + method(m_jobj); +} + +/* static */ +std::unique_ptr JMessageQueueThread::currentMessageQueueThread() { + static auto method = MessageQueueThreadRegistry::javaClassStatic()-> + getStaticMethod("myMessageQueueThread"); + return folly::make_unique(method(MessageQueueThreadRegistry::javaClassStatic())); +} + +} } + diff --git a/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.h b/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.h new file mode 100644 index 000000000..8615f496c --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.h @@ -0,0 +1,60 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include + +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class JavaMessageQueueThread : public jni::JavaClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/queue/MessageQueueThread;"; +}; + +class JMessageQueueThread : public MessageQueueThread { +public: + JMessageQueueThread(alias_ref jobj); + + /** + * Enqueues the given function to run on this MessageQueueThread. + */ + void runOnQueue(std::function&& runnable) override; + + /** + * Synchronously executes the given function to run on this + * MessageQueueThread, waiting until it completes. Can be called from any + * thread, but will block if not called on this MessageQueueThread. + */ + void runOnQueueSync(std::function&& runnable) override; + + /** + * Synchronously quits the current MessageQueueThread. Can be called from any thread, but will + * block if not called on this MessageQueueThread. + */ + void quitSynchronous() override; + + JavaMessageQueueThread::javaobject jobj() { + return m_jobj.get(); + } + + /** + * Returns the current MessageQueueThread that owns this thread. + */ + static std::unique_ptr currentMessageQueueThread(); +private: + global_ref m_jobj; +}; + +class MessageQueueThreadRegistry : public jni::JavaClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/queue/MessageQueueThreadRegistry;"; +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JNativeRunnable.h b/ReactAndroid/src/main/jni/xreact/jni/JNativeRunnable.h new file mode 100644 index 000000000..5550af342 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JNativeRunnable.h @@ -0,0 +1,44 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class Runnable : public JavaClass { +public: + static constexpr auto kJavaDescriptor = "Ljava/lang/Runnable;"; +}; + +/** + * The c++ interface for the Java NativeRunnable class + */ +class JNativeRunnable : public HybridClass { +public: + static auto constexpr kJavaDescriptor = "Lcom/facebook/react/bridge/queue/NativeRunnable;"; + + void run() { + m_runnable(); + } + + static void registerNatives() { + javaClassStatic()->registerNatives({ + makeNativeMethod("run", JNativeRunnable::run), + }); + } +private: + friend HybridBase; + + JNativeRunnable(std::function runnable) + : m_runnable(std::move(runnable)) {} + + std::function m_runnable; +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp new file mode 100644 index 000000000..1b7b5e688 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp @@ -0,0 +1,244 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSCPerfLogging.h" + +#include +#include +#include + +using namespace facebook::jni; + +struct _jqplProvider : _jobject {}; +using jqplProvider = _jqplProvider*; + +struct _jqpl : _jobject {}; +using jqpl = _jqpl*; + +namespace facebook { namespace jni { + + +template<> +class JObjectWrapper : public JObjectWrapper { + + public: + static constexpr const char* kJavaDescriptor = "Lcom/facebook/quicklog/QuickPerformanceLogger;"; + + using JObjectWrapper::JObjectWrapper; + + void markerStart(int markerId, int instanceKey, long timestamp) { + static auto markerStartMethod = + qplClass()->getMethod("markerStart"); + markerStartMethod(this_, markerId, instanceKey, timestamp); + } + + void markerEnd(int markerId, int instanceKey, short actionId, long timestamp) { + static auto markerEndMethod = + qplClass()->getMethod("markerEnd"); + markerEndMethod(this_, markerId, instanceKey, actionId, timestamp); + } + + void markerNote(int markerId, int instanceKey, short actionId, long timestamp) { + static auto markerNoteMethod = + qplClass()->getMethod("markerNote"); + markerNoteMethod(this_, markerId, instanceKey, actionId, timestamp); + } + + void markerCancel(int markerId, int instanceKey) { + static auto markerCancelMethod = + qplClass()->getMethod("markerCancel"); + markerCancelMethod(this_, markerId, instanceKey); + } + + int64_t currentMonotonicTimestamp() { + static auto currentTimestampMethod = + qplClass()->getMethod("currentMonotonicTimestamp"); + return currentTimestampMethod(this_); + } + + private: + + static alias_ref qplClass() { + static auto cls = findClassStatic("com/facebook/quicklog/QuickPerformanceLogger"); + return cls; + } + +}; +using JQuickPerformanceLogger = JObjectWrapper; + + +template<> +class JObjectWrapper : public JObjectWrapper { + public: + static constexpr const char* kJavaDescriptor = + "Lcom/facebook/quicklog/QuickPerformanceLoggerProvider;"; + + using JObjectWrapper::JObjectWrapper; + + static global_ref get() { + static auto getQPLInstMethod = qplProviderClass()->getStaticMethod("getQPLInstance"); + static global_ref theQpl = make_global(getQPLInstMethod(qplProviderClass().get())); + return theQpl; + } + + static bool check() { + static auto getQPLInstMethod = qplProviderClass()->getStaticMethod("getQPLInstance"); + auto theQpl = getQPLInstMethod(qplProviderClass().get()); + return (theQpl.get() != nullptr); + } + + private: + + static alias_ref qplProviderClass() { + static auto cls = findClassStatic("com/facebook/quicklog/QuickPerformanceLoggerProvider"); + return cls; + } +}; +using JQuickPerformanceLoggerProvider = JObjectWrapper; + +}} + +static bool isReady() { + static bool ready = false; + if (!ready) { + try { + findClassStatic("com/facebook/quicklog/QuickPerformanceLoggerProvider"); + } catch(...) { + // Swallow this exception - we don't want to crash the app, an error is enough. + FBLOGE("Calling QPL from JS before class has been loaded in Java. Ignored."); + return false; + } + if (JQuickPerformanceLoggerProvider::check()) { + ready = true; + } else { + FBLOGE("Calling QPL from JS before it has been initialized in Java. Ignored."); + return false; + } + } + return ready; +} + +// After having read the implementation of PNaN that is returned from JSValueToNumber, and some +// more material on how NaNs are constructed, I think this is the most consistent way to verify +// NaN with how we generate it. +// Once the integration completes, I'll play around with it some more and potentially change this +// implementation to use std::isnan() if it is exactly commensurate with our usage. +static bool isNan(double value) { + return (value != value); +} + +// Safely translates JSValues to an array of doubles. +static bool grabDoubles( + size_t targetsCount, + double targets[], + JSContextRef ctx, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (argumentCount < targetsCount) { + return false; + } + for (size_t i = 0 ; i < targetsCount ; i++) { + targets[i] = JSValueToNumber(ctx, arguments[i], exception); + if (isNan(targets[i])) { + return false; + } + } + return true; +} + +static JSValueRef nativeQPLMarkerStart( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + double targets[3]; + if (isReady() && grabDoubles(3, targets, ctx, argumentCount, arguments, exception)) { + int32_t markerId = (int32_t) targets[0]; + int32_t instanceKey = (int32_t) targets[1]; + int64_t timestamp = (int64_t) targets[2]; + JQuickPerformanceLoggerProvider::get()->markerStart(markerId, instanceKey, timestamp); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeQPLMarkerEnd( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + double targets[4]; + if (isReady() && grabDoubles(4, targets, ctx, argumentCount, arguments, exception)) { + int32_t markerId = (int32_t) targets[0]; + int32_t instanceKey = (int32_t) targets[1]; + int16_t actionId = (int16_t) targets[2]; + int64_t timestamp = (int64_t) targets[3]; + JQuickPerformanceLoggerProvider::get()->markerEnd(markerId, instanceKey, actionId, timestamp); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeQPLMarkerNote( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + double targets[4]; + if (isReady() && grabDoubles(4, targets, ctx, argumentCount, arguments, exception)) { + int32_t markerId = (int32_t) targets[0]; + int32_t instanceKey = (int32_t) targets[1]; + int16_t actionId = (int16_t) targets[2]; + int64_t timestamp = (int64_t) targets[3]; + JQuickPerformanceLoggerProvider::get()->markerNote(markerId, instanceKey, actionId, timestamp); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeQPLMarkerCancel( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + double targets[2]; + if (isReady() && grabDoubles(2, targets, ctx, argumentCount, arguments, exception)) { + int32_t markerId = (int32_t) targets[0]; + int32_t instanceKey = (int32_t) targets[1]; + JQuickPerformanceLoggerProvider::get()->markerCancel(markerId, instanceKey); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeQPLTimestamp( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (!isReady()) { + return JSValueMakeNumber(ctx, 0); + } + int64_t timestamp = JQuickPerformanceLoggerProvider::get()->currentMonotonicTimestamp(); + // Since this is monotonic time, I assume the 52 bits of mantissa are enough in the double value. + return JSValueMakeNumber(ctx, timestamp); +} + +namespace facebook { +namespace react { + +void addNativePerfLoggingHooks(JSGlobalContextRef ctx) { + installGlobalFunction(ctx, "nativeQPLMarkerStart", nativeQPLMarkerStart); + installGlobalFunction(ctx, "nativeQPLMarkerEnd", nativeQPLMarkerEnd); + installGlobalFunction(ctx, "nativeQPLMarkerNote", nativeQPLMarkerNote); + installGlobalFunction(ctx, "nativeQPLMarkerCancel", nativeQPLMarkerCancel); + installGlobalFunction(ctx, "nativeQPLTimestamp", nativeQPLTimestamp); +} + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.h b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.h new file mode 100644 index 000000000..256755b1f --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.h @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +namespace facebook { +namespace react { + +void addNativePerfLoggingHooks(JSGlobalContextRef ctx); + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLoader.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSLoader.cpp new file mode 100644 index 000000000..62d8cb9e5 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLoader.cpp @@ -0,0 +1,99 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSLoader.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef WITH_FBSYSTRACE +#include +using fbsystrace::FbSystraceSection; +#endif + +namespace facebook { +namespace react { + +static jclass gApplicationHolderClass; +static jmethodID gGetApplicationMethod; +static jmethodID gGetAssetManagerMethod; + +std::unique_ptr loadScriptFromAssets(const std::string& assetName) { + JNIEnv *env = jni::Environment::current(); + jobject application = env->CallStaticObjectMethod( + gApplicationHolderClass, + gGetApplicationMethod); + jobject assetManager = env->CallObjectMethod(application, gGetAssetManagerMethod); + return loadScriptFromAssets(AAssetManager_fromJava(env, assetManager), assetName); +} + +AAssetManager *extractAssetManager(jobject jassetManager) { + auto env = jni::Environment::current(); + return AAssetManager_fromJava(env, jassetManager); +} + +std::unique_ptr loadScriptFromAssets( + AAssetManager *manager, + const std::string& assetName) { + #ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_loadScriptFromAssets", + "assetName", assetName); + #endif + if (manager) { + auto asset = AAssetManager_open( + manager, + assetName.c_str(), + AASSET_MODE_STREAMING); // Optimized for sequential read: see AssetManager.java for docs + if (asset) { + auto buf = folly::make_unique(AAsset_getLength(asset)); + size_t offset = 0; + int readbytes; + while ((readbytes = AAsset_read(asset, buf->data() + offset, buf->size() - offset)) > 0) { + offset += readbytes; + } + AAsset_close(asset); + if (offset == buf->size()) { + return std::move(buf); + } + } + } + FBLOGE("Unable to load script from assets: %s", assetName.c_str()); + return folly::make_unique(""); +} + +std::string loadScriptFromFile(const std::string& fileName) { + #ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_loadScriptFromFile", + "fileName", fileName); + #endif + std::ifstream jsfile(fileName); + if (jsfile) { + std::string output; + jsfile.seekg(0, std::ios::end); + output.reserve(jsfile.tellg()); + jsfile.seekg(0, std::ios::beg); + output.assign( + (std::istreambuf_iterator(jsfile)), + std::istreambuf_iterator()); + return output; + } + + FBLOGE("Unable to load script from file: %s", fileName.c_str()); + return ""; +} + +void registerJSLoaderNatives() { + JNIEnv *env = jni::Environment::current(); + jclass applicationHolderClass = env->FindClass("com/facebook/react/common/ApplicationHolder"); + gApplicationHolderClass = (jclass)env->NewGlobalRef(applicationHolderClass); + gGetApplicationMethod = env->GetStaticMethodID(applicationHolderClass, "getApplication", "()Landroid/app/Application;"); + + jclass appClass = env->FindClass("android/app/Application"); + gGetAssetManagerMethod = env->GetMethodID(appClass, "getAssets", "()Landroid/content/res/AssetManager;"); +} + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLoader.h b/ReactAndroid/src/main/jni/xreact/jni/JSLoader.h new file mode 100644 index 000000000..6dc1d90bd --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLoader.h @@ -0,0 +1,33 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * Helper method for loading a JS script from Android assets without + * a reference to an AssetManager. + */ +std::unique_ptr loadScriptFromAssets(const std::string& assetName); + +/** + * Helper method for loading JS script from android asset + */ +AAssetManager *extractAssetManager(jobject jassetManager); + +std::unique_ptr loadScriptFromAssets(AAssetManager *assetManager, const std::string& assetName); + +/** + * Helper method for loading JS script from a file + */ +std::string loadScriptFromFile(const std::string& fileName); + +void registerJSLoaderNatives(); + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp new file mode 100644 index 000000000..7690a5656 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp @@ -0,0 +1,36 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSLogging.h" + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +JSValueRef nativeLoggingHook( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + android_LogPriority logLevel = ANDROID_LOG_DEBUG; + if (argumentCount > 1) { + int level = (int) JSValueToNumber(ctx, arguments[1], NULL); + // The lowest log level we get from JS is 0. We shift and cap it to be + // in the range the Android logging method expects. + logLevel = std::min( + static_cast(level + ANDROID_LOG_DEBUG), + ANDROID_LOG_FATAL); + } + if (argumentCount > 0) { + JSStringRef jsString = JSValueToStringCopy(ctx, arguments[0], NULL); + String message = String::adopt(jsString); + FBLOG_PRI(logLevel, "ReactNativeJS", "%s", message.str().c_str()); + } + return JSValueMakeUndefined(ctx); +} + +}}; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.h b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.h new file mode 100644 index 000000000..27756b665 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.h @@ -0,0 +1,15 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +namespace facebook { +namespace react { +JSValueRef nativeLoggingHook( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception); +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JavaScriptExecutorHolder.h b/ReactAndroid/src/main/jni/xreact/jni/JavaScriptExecutorHolder.h new file mode 100644 index 000000000..f59d9ad55 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JavaScriptExecutorHolder.h @@ -0,0 +1,29 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include + +#include + +#include + +namespace facebook { +namespace react { + +class JavaScriptExecutorHolder : public jni::HybridClass { + public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/JavaScriptExecutor;"; + + std::shared_ptr getExecutorFactory() { + return mExecutorFactory; + } + + protected: + JavaScriptExecutorHolder(std::shared_ptr factory) + : mExecutorFactory(factory) {} + + private: + std::shared_ptr mExecutorFactory; +}; + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.cpp b/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.cpp new file mode 100644 index 000000000..20c71bc8c --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.cpp @@ -0,0 +1,83 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JniJSModulesUnbundle.h" + +#include +#include +#include +#include +#include +#include +#include + +using magic_number_t = uint32_t; +const magic_number_t MAGIC_FILE_HEADER = 0xFB0BD1E5; +const std::string MAGIC_FILE_NAME = "UNBUNDLE"; + +namespace facebook { +namespace react { + +using asset_ptr = + std::unique_ptr>; + +static std::string jsModulesDir(const std::string& entryFile) { + std::string dir = dirname(entryFile.c_str()); + + // android's asset manager does not work with paths that start with a dot + return dir == "." ? "js-modules/" : dir + "/js-modules/"; +} + +static asset_ptr openAsset( + AAssetManager *manager, + const std::string& fileName, + int mode = AASSET_MODE_STREAMING) { + return asset_ptr( + AAssetManager_open(manager, fileName.c_str(), mode), + AAsset_close); +} + +JniJSModulesUnbundle::JniJSModulesUnbundle(AAssetManager *assetManager, const std::string& entryFile) : + m_assetManager(assetManager), + m_moduleDirectory(jsModulesDir(entryFile)) {} + +bool JniJSModulesUnbundle::isUnbundle( + AAssetManager *assetManager, + const std::string& assetName) { + if (!assetManager) { + return false; + } + + auto magicFileName = jsModulesDir(assetName) + MAGIC_FILE_NAME; + auto asset = openAsset(assetManager, magicFileName.c_str()); + if (asset == nullptr) { + return false; + } + + magic_number_t fileHeader = 0; + AAsset_read(asset.get(), &fileHeader, sizeof(fileHeader)); + return fileHeader == htole32(MAGIC_FILE_HEADER); +} + +JSModulesUnbundle::Module JniJSModulesUnbundle::getModule(uint32_t moduleId) const { + // can be nullptr for default constructor. + FBASSERTMSGF(m_assetManager != nullptr, "Unbundle has not been initialized with an asset manager"); + + std::ostringstream sourceUrlBuilder; + sourceUrlBuilder << moduleId << ".js"; + auto sourceUrl = sourceUrlBuilder.str(); + + auto fileName = m_moduleDirectory + sourceUrl; + auto asset = openAsset(m_assetManager, fileName, AASSET_MODE_BUFFER); + + const char *buffer = nullptr; + if (asset != nullptr) { + buffer = static_cast(AAsset_getBuffer(asset.get())); + } + if (buffer == nullptr) { + throw ModuleNotFound("Module not found: " + sourceUrl); + } + return {sourceUrl, std::string(buffer, AAsset_getLength(asset.get()))}; +} + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.h b/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.h new file mode 100644 index 000000000..daf88df51 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.h @@ -0,0 +1,31 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +class JniJSModulesUnbundle : public JSModulesUnbundle { + /** + * This implementation reads modules as single file from the assets of an apk. + */ +public: + JniJSModulesUnbundle() = default; + JniJSModulesUnbundle(AAssetManager *assetManager, const std::string& entryFile); + JniJSModulesUnbundle(JniJSModulesUnbundle&& other) = delete; + JniJSModulesUnbundle& operator= (JSModulesUnbundle&& other) = delete; + + static bool isUnbundle( + AAssetManager *assetManager, + const std::string& assetName); + virtual Module getModule(uint32_t moduleId) const override; +private: + AAssetManager *m_assetManager = nullptr; + std::string m_moduleDirectory; +}; + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JniWebWorkers.h b/ReactAndroid/src/main/jni/xreact/jni/JniWebWorkers.h new file mode 100644 index 000000000..e560fd1e9 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JniWebWorkers.h @@ -0,0 +1,32 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +#include "JMessageQueueThread.h" +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class JniWebWorkers : public JavaClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/webworkers/WebWorkers;"; + + static std::unique_ptr createWebWorkerQueue(int id, MessageQueue* ownerMessageQueue) { + static auto method = JniWebWorkers::javaClassStatic()-> + getStaticMethod("createWebWorkerThread"); + + JMessageQueueThread* ownerMessageQueueThread = static_cast(ownerMessageQueue); + auto res = method(JniWebWorkers::javaClassStatic(), id, ownerMessageQueueThread->jobj()); + return folly::make_unique(res); + } +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp new file mode 100644 index 000000000..8c304378d --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp @@ -0,0 +1,230 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "MethodInvoker.h" + +#include +#ifdef WITH_FBSYSTRACE +#include +#endif + +#include "ModuleRegistryHolder.h" +#include "JCallback.h" +#include "JExecutorToken.h" + +namespace facebook { +namespace react { + +namespace { + +using dynamic_iterator = folly::dynamic::const_iterator; + +struct JPromiseImpl : public jni::JavaClass { + constexpr static auto kJavaDescriptor = "Lcom/facebook/react/bridge/PromiseImpl;"; + + static jni::local_ref create(jni::local_ref resolve, jni::local_ref reject) { + return newInstance(resolve, reject); + } +}; + +// HACK: Exposes constructor +struct ExposedReadableNativeArray : public ReadableNativeArray { + explicit ExposedReadableNativeArray(folly::dynamic array) + : ReadableNativeArray(std::move(array)) {} +}; + +jdouble extractDouble(const folly::dynamic& value) { + if (value.isInt()) { + return static_cast(value.getInt()); + } else { + return static_cast(value.getDouble()); + } +} + +jni::local_ref extractCallback(std::weak_ptr& instance, ExecutorToken token, const folly::dynamic& value) { + if (value.isNull()) { + return jni::local_ref(nullptr); + } else { + return JCallbackImpl::newObjectCxxArgs(makeCallback(instance, token, value)); + } +} + +jni::local_ref extractPromise(std::weak_ptr& instance, ExecutorToken token, dynamic_iterator& it, dynamic_iterator& end) { + auto resolve = extractCallback(instance, token, *it++); + CHECK(it != end); + auto reject = extractCallback(instance, token, *it++); + return JPromiseImpl::create(resolve, reject); +} + +jobject valueOf(jboolean value) { + static auto kClass = jni::findClassStatic("java/lang/Boolean"); + static auto kValueOf = kClass->getStaticMethod("valueOf"); + return kValueOf(kClass, value).release(); +} + +jobject valueOf(jint value) { + static auto kClass = jni::findClassStatic("java/lang/Integer"); + static auto kValueOf = kClass->getStaticMethod("valueOf"); + return kValueOf(kClass, value).release(); +} + +jobject valueOf(jdouble value) { + static auto kClass = jni::findClassStatic("java/lang/Double"); + static auto kValueOf = kClass->getStaticMethod("valueOf"); + return kValueOf(kClass, value).release(); +} + +jobject valueOf(jfloat value) { + static auto kClass = jni::findClassStatic("java/lang/Float"); + static auto kValueOf = kClass->getStaticMethod("valueOf"); + return kValueOf(kClass, value).release(); +} + +bool isNullable(char type) { + switch (type) { + case 'Z': + case 'I': + case 'F': + case 'S': + case 'A': + case 'M': + case 'X': + return true; + default: + return false;; + } +} + +jvalue extract(std::weak_ptr& instance, ExecutorToken token, char type, dynamic_iterator& it, dynamic_iterator& end) { + CHECK(it != end); + jvalue value; + if (type == 'P') { + value.l = extractPromise(instance, token, it, end).release(); + return value; + } else if (type == 'T') { + value.l = JExecutorToken::extractJavaPartFromToken(token).release(); + return value; + } + + const auto& arg = *it++; + if (isNullable(type) && arg.isNull()) { + value.l = nullptr; + return value; + } + + switch (type) { + case 'z': + value.z = static_cast(arg.getBool()); + break; + case 'Z': + value.l = valueOf(static_cast(arg.getBool())); + break; + case 'i': + value.i = static_cast(arg.getInt()); + break; + case 'I': + value.l = valueOf(static_cast(arg.getInt())); + break; + case 'f': + value.f = static_cast(extractDouble(arg)); + break; + case 'F': + value.l = valueOf(static_cast(extractDouble(arg))); + break; + case 'd': + value.d = extractDouble(arg); + break; + case 'D': + value.l = valueOf(extractDouble(arg)); + break; + case 'S': + value.l = jni::make_jstring(arg.getString()).release(); + break; + case 'A': + value.l = ReadableNativeArray::newObjectCxxArgs(arg).release(); + break; + case 'M': + // HACK: Workaround for constructing ReadableNativeMap + value.l = ExposedReadableNativeArray(folly::dynamic::array(arg)).getMap(0); + break; + case 'X': + value.l = extractCallback(instance, token, arg).release(); + break; + default: + LOG(FATAL) << "Unknown param type: " << type; + } + return value; +} + +std::size_t countJsArgs(const std::string& signature) { + std::size_t count = 0; + for (char c : signature) { + switch (c) { + case 'T': + break; + case 'P': + count += 2; + break; + default: + count += 1; + break; + } + } + return count; +} + +} + +MethodInvoker::MethodInvoker(jni::alias_ref method, std::string signature, std::string traceName, bool isSync) + : method_(method->getMethodID()), + jsArgCount_(countJsArgs(signature) - 2), + signature_(std::move(signature)), + traceName_(std::move(traceName)), + isSync_(isSync) { + CHECK(signature_.at(1) == '.') << "Improper module method signature"; + CHECK(!isSync || signature_.at(0) == 'v') << "Non-sync hooks cannot have a non-void return type"; + } + +MethodCallResult MethodInvoker::invoke(std::weak_ptr& instance, JBaseJavaModule::javaobject module, ExecutorToken token, const folly::dynamic& params) { + #ifdef WITH_FBSYSTRACE + fbsystrace::FbSystraceSection s( + TRACE_TAG_REACT_CXX_BRIDGE, + isSync_ ? "callJavaSyncHook" : "callJavaModuleMethod", + "method", + traceName_); + #endif + if (params.size() != jsArgCount_) { + throw std::invalid_argument(folly::to("expected ", jsArgCount_, " arguments, got ", params.size())); + } + auto argCount = signature_.size() - 2; + jni::JniLocalScope scope(jni::Environment::current(), argCount); + jvalue args[argCount]; + std::transform( + signature_.begin() + 2, + signature_.end(), + args, + [&instance, token, it = params.begin(), end = params.end()] (char type) mutable { + return extract(instance, token, type, it, end); + }); + + // TODO(t10768795): Use fbjni here + folly::dynamic ret = folly::dynamic::object(); + bool isReturnUndefined = false; + char returnType = signature_.at(0); + switch (returnType) { + case 'v': + jni::Environment::current()->CallVoidMethodA(module, method_, args); + ret = nullptr; + isReturnUndefined = true; + break; + default: + LOG(FATAL) << "Unknown return type: " << returnType; + // TODO: other cases + } + + jni::throwPendingJniExceptionAsCppException(); + + return MethodCallResult{ret, isReturnUndefined}; +} + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.h b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.h new file mode 100644 index 000000000..ef78213d6 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.h @@ -0,0 +1,37 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +#include + +#include "ModuleRegistryHolder.h" + +namespace facebook { +namespace react { + +class Instance; + +class MethodInvoker { +public: + MethodInvoker(jni::alias_ref method, std::string signature, std::string traceName, bool isSync); + + MethodCallResult invoke(std::weak_ptr& instance, JBaseJavaModule::javaobject module, ExecutorToken token, const folly::dynamic& params); + + bool isSyncHook() const { + return isSync_; + } +private: + jmethodID method_; + std::size_t jsArgCount_; + std::string signature_; + std::string traceName_; + bool isSync_; +}; + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp new file mode 100644 index 000000000..c71c95936 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp @@ -0,0 +1,344 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "ModuleRegistryHolder.h" + +#include + +#include + +#include +#include + +#include +#include +#include + +#include "MethodInvoker.h" + +#include "CatalystInstanceImpl.h" + +using facebook::xplat::module::CxxModule; + +namespace facebook { +namespace react { + +namespace { + +class JavaNativeModule : public NativeModule { + public: + JavaNativeModule(jni::alias_ref wrapper) + : wrapper_(make_global(wrapper)) {} + + std::string getName() override { + static auto getNameMethod = wrapper_->getClass()->getMethod("getName"); + return getNameMethod(wrapper_)->toStdString(); + } + + std::vector getMethods() override { + static auto getMDMethod = + wrapper_->getClass()->getMethod::javaobject()>( + "getMethodDescriptors"); + + std::vector ret; + auto descs = getMDMethod(wrapper_); + for (const auto& desc : *descs) { + static auto nameField = + JMethodDescriptor::javaClassStatic()->getField("name"); + static auto typeField = + JMethodDescriptor::javaClassStatic()->getField("type"); + + ret.emplace_back( + desc->getFieldValue(nameField)->toStdString(), + desc->getFieldValue(typeField)->toStdString() + ); + } + return ret; + } + + folly::dynamic getConstants() override { + static auto constantsMethod = + wrapper_->getClass()->getMethod("getConstants"); + auto constants = constantsMethod(wrapper_); + if (!constants) { + return nullptr; + } else { + // See JavaModuleWrapper#getConstants for the other side of this hack. + return cthis(constants)->array[0]; + } + } + + virtual bool supportsWebWorkers() override { + static auto supportsWebWorkersMethod = + wrapper_->getClass()->getMethod("supportsWebWorkers"); + return supportsWebWorkersMethod(wrapper_); + } + + void invoke(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + static auto invokeMethod = + wrapper_->getClass()->getMethod("invoke"); + invokeMethod(wrapper_, JExecutorToken::extractJavaPartFromToken(token).get(), static_cast(reactMethodId), + ReadableNativeArray::newObjectCxxArgs(std::move(params)).get()); + } + + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + throw std::runtime_error("Unsupported operation."); + } + + private: + jni::global_ref wrapper_; +}; + +class NewJavaNativeModule : public NativeModule { + public: + NewJavaNativeModule(std::weak_ptr instance, jni::alias_ref wrapper) + : instance_(std::move(instance)), + wrapper_(make_global(wrapper)), + module_(make_global(wrapper->getModule())) { + auto descs = wrapper_->getMethodDescriptors(); + std::string moduleName = getName(); + methods_.reserve(descs->size()); + + for (const auto& desc : *descs) { + auto type = desc->getType(); + auto name = desc->getName(); + methods_.emplace_back( + desc->getMethod(), + desc->getSignature(), + moduleName + "." + name, + type == "syncHook"); + + methodDescriptors_.emplace_back(name, type); + } + } + + std::string getName() override { + static auto getNameMethod = wrapper_->getClass()->getMethod("getName"); + return getNameMethod(wrapper_)->toStdString(); + } + + std::vector getMethods() override { + return methodDescriptors_; + } + + folly::dynamic getConstants() override { + static auto constantsMethod = + wrapper_->getClass()->getMethod("getConstants"); + auto constants = constantsMethod(wrapper_); + if (!constants) { + return nullptr; + } else { + // See JavaModuleWrapper#getConstants for the other side of this hack. + return cthis(constants)->array[0]; + } + } + + virtual bool supportsWebWorkers() override { + static auto supportsWebWorkersMethod = + wrapper_->getClass()->getMethod("supportsWebWorkers"); + return supportsWebWorkersMethod(wrapper_); + } + + void invoke(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + if (reactMethodId >= methods_.size()) { + throw std::invalid_argument( + folly::to("methodId ", reactMethodId, " out of range [0..", methods_.size(), "]")); + } + CHECK(!methods_[reactMethodId].isSyncHook()) << "Trying to invoke a synchronous hook asynchronously"; + invokeInner(token, reactMethodId, std::move(params)); + } + + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + if (reactMethodId >= methods_.size()) { + throw std::invalid_argument( + folly::to("methodId ", reactMethodId, " out of range [0..", methods_.size(), "]")); + } + CHECK(methods_[reactMethodId].isSyncHook()) << "Trying to invoke a asynchronous method as synchronous hook"; + return invokeInner(token, reactMethodId, std::move(params)); + } + + private: + std::weak_ptr instance_; + jni::global_ref wrapper_; + jni::global_ref module_; + std::vector methods_; + std::vector methodDescriptors_; + + MethodCallResult invokeInner(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) { + if (!params.isArray()) { + throw std::invalid_argument( + folly::to("method parameters should be array, but are ", params.typeName())); + } + return methods_[reactMethodId].invoke(instance_, module_.get(), token, params); + } +}; + +class CxxNativeModule : public NativeModule { + public: + CxxNativeModule(std::weak_ptr instance, + std::unique_ptr module) + : instance_(instance) + , module_(std::move(module)) + , methods_(module_->getMethods()) {} + + std::string getName() override { + return module_->getName(); + } + + virtual std::vector getMethods() override { + // Same as MessageQueue.MethodTypes.remote + static const auto kMethodTypeRemote = "remote"; + + std::vector descs; + for (auto& method : methods_) { + descs.emplace_back(method.name, kMethodTypeRemote); + } + return descs; + } + + virtual folly::dynamic getConstants() override { + folly::dynamic constants = folly::dynamic::object(); + for (auto& pair : module_->getConstants()) { + constants.insert(std::move(pair.first), std::move(pair.second)); + } + return constants; + } + + virtual bool supportsWebWorkers() override { + // TODO(andrews): web worker support in cxxmodules + return true; + } + + // TODO mhorowitz: do we need initialize()/onCatalystInstanceDestroy() in C++ + // or only Java? + virtual void invoke(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + if (reactMethodId >= methods_.size()) { + throw std::invalid_argument( + folly::to("methodId ", reactMethodId, " out of range [0..", methods_.size(), "]")); + } + if (!params.isArray()) { + throw std::invalid_argument( + folly::to("method parameters should be array, but are ", params.typeName())); + } + + CxxModule::Callback first; + CxxModule::Callback second; + + const auto& method = methods_[reactMethodId]; + + if (params.size() < method.callbacks) { + throw std::invalid_argument( + folly::to("Expected ", method.callbacks, " callbacks, but only ", + params.size(), " parameters provided")); + } + + if (method.callbacks == 1) { + first = makeCallback(instance_, token, params[params.size() - 1]); + } else if (method.callbacks == 2) { + first = makeCallback(instance_, token, params[params.size() - 2]); + second = makeCallback(instance_, token, params[params.size() - 1]); + } + + params.resize(params.size() - method.callbacks); + + // I've got a few flawed options here. I can let the C++ exception + // propogate, and the registry will log/convert them to java exceptions. + // This lets all the java and red box handling work ok, but the only info I + // can capture about the C++ exception is the what() string, not the stack. + // I can std::terminate() the app. This causes the full, accurate C++ + // stack trace to be added to logcat by debuggerd. The java state is lost, + // but in practice, the java stack is always the same in this case since + // the javascript stack is not visible, and the crash is unfriendly to js + // developers, but crucial to C++ developers. The what() value is also + // lost. Finally, I can catch, log the java stack, then rethrow the C++ + // exception. In this case I get java and C++ stack data, but the C++ + // stack is as of the rethrow, not the original throw, both the C++ and + // java stacks always look the same. + // + // I am going with option 2, since that seems like the most useful + // choice. It would be nice to be able to get what() and the C++ + // stack. I'm told that will be possible in the future. TODO + // mhorowitz #7128529: convert C++ exceptions to Java + + try { + method.func(std::move(params), first, second); + } catch (const facebook::xplat::JsArgumentException& ex) { + // This ends up passed to the onNativeException callback. + throw; + } catch (...) { + // This means some C++ code is buggy. As above, we fail hard so the C++ + // developer can debug and fix it. + std::terminate(); + } + } + + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int hookId, folly::dynamic&& args) override { + throw std::runtime_error("Not supported"); + } + + private: + std::weak_ptr instance_; + std::unique_ptr module_; + std::vector methods_; +}; + +} + +jni::local_ref JMethodDescriptor::getMethod() const { + static auto method = javaClassStatic()->getField("method"); + return getFieldValue(method); +} + +std::string JMethodDescriptor::getSignature() const { + static auto signature = javaClassStatic()->getField("signature"); + return getFieldValue(signature)->toStdString(); +} + +std::string JMethodDescriptor::getName() const { + static auto name = javaClassStatic()->getField("name"); + return getFieldValue(name)->toStdString(); +} + +std::string JMethodDescriptor::getType() const { + static auto type = javaClassStatic()->getField("type"); + return getFieldValue(type)->toStdString(); +} + +void ModuleRegistryHolder::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", ModuleRegistryHolder::initHybrid), + }); +} + +ModuleRegistryHolder::ModuleRegistryHolder( + CatalystInstanceImpl* catalystInstanceImpl, + jni::alias_ref::javaobject> javaModules, + jni::alias_ref::javaobject> cxxModules) { + std::vector> modules; + std::weak_ptr winstance(catalystInstanceImpl->getInstance()); + for (const auto& jm : *javaModules) { + modules.emplace_back(folly::make_unique(jm)); + } + for (const auto& cm : *cxxModules) { + modules.emplace_back( + folly::make_unique(winstance, std::move(cthis(cm)->getModule()))); + } + + registry_ = std::make_shared(std::move(modules)); +} + +Callback makeCallback(std::weak_ptr instance, ExecutorToken token, const folly::dynamic& callbackId) { + if (!callbackId.isInt()) { + throw std::invalid_argument("Expected callback(s) as final argument"); + } + + auto id = callbackId.getInt(); + return [winstance = std::move(instance), token, id](folly::dynamic args) { + if (auto instance = winstance.lock()) { + jni::ThreadScope guard; + instance->callJSCallback(token, id, std::move(args)); + } + }; +} + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h new file mode 100644 index 000000000..40a114363 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h @@ -0,0 +1,94 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +namespace facebook { +namespace react { + +class Instance; +class CatalystInstanceImpl; + +struct JReflectMethod : public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Ljava/lang/reflect/Method;"; + + jmethodID getMethodID() { + auto id = jni::Environment::current()->FromReflectedMethod(self()); + jni::throwPendingJniExceptionAsCppException(); + return id; + } +}; + +struct JMethodDescriptor : public jni::JavaClass { + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/JavaModuleWrapper$MethodDescriptor;"; + + jni::local_ref getMethod() const; + std::string getSignature() const; + std::string getName() const; + std::string getType() const; +}; + +struct JBaseJavaModule : public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/BaseJavaModule;"; +}; + +struct JavaModuleWrapper : jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/JavaModuleWrapper;"; + + jni::local_ref getModule() { + static auto getModule = javaClassStatic()->getMethod("getModule"); + return getModule(self()); + } + + jni::local_ref::javaobject> getMethodDescriptors() { + static auto getMethods = + getClass()->getMethod::javaobject()>("getMethodDescriptors"); + return getMethods(self()); + } + + jni::local_ref::javaobject> newGetMethodDescriptors() { + static auto getMethods = + getClass()->getMethod::javaobject()>("newGetMethodDescriptors"); + return getMethods(self()); + } +}; + +class ModuleRegistryHolder : public jni::HybridClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/ModuleRegistryHolder;"; + + std::shared_ptr getModuleRegistry() { + return registry_; + } + + static jni::local_ref initHybrid( + jni::alias_ref, + CatalystInstanceImpl* catalystInstanceImpl, + jni::alias_ref::javaobject> javaModules, + jni::alias_ref::javaobject> cxxModules) { + return makeCxxInstance(catalystInstanceImpl, javaModules, cxxModules); + } + + static void registerNatives(); + + private: + friend HybridBase; + ModuleRegistryHolder( + CatalystInstanceImpl* catalystInstanceImpl, + jni::alias_ref::javaobject> javaModules, + jni::alias_ref::javaobject> cxxModules); + + facebook::xplat::module::CxxModule::Callback makeCallback(const folly::dynamic& callbackId); + + std::shared_ptr registry_; +}; + +using Callback = std::function; +Callback makeCallback(std::weak_ptr instance, ExecutorToken token, const folly::dynamic& callbackId); + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp new file mode 100644 index 000000000..3242c59ca --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp @@ -0,0 +1,186 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include +#include +#include +#include +#include +#include +#include +#include +#include "CatalystInstanceImpl.h" +#include "JavaScriptExecutorHolder.h" +#include "JSCPerfLogging.h" +#include "JSLoader.h" +#include "ModuleRegistryHolder.h" +#include "ProxyExecutor.h" +#include "WebWorkers.h" +#include "JCallback.h" + +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +namespace { + +static std::string getApplicationDir(const char* methodName) { + // Get the Application Context object + auto getApplicationClass = findClassLocal( + "com/facebook/react/common/ApplicationHolder"); + auto getApplicationMethod = getApplicationClass->getStaticMethod( + "getApplication", + "()Landroid/app/Application;" + ); + auto application = getApplicationMethod(getApplicationClass); + + // Get getCacheDir() from the context + auto getDirMethod = findClassLocal("android/app/Application") + ->getMethod(methodName, + "()Ljava/io/File;" + ); + auto dirObj = getDirMethod(application); + + // Call getAbsolutePath() on the returned File object + auto getAbsolutePathMethod = findClassLocal("java/io/File") + ->getMethod("getAbsolutePath"); + return getAbsolutePathMethod(dirObj)->toStdString(); +} + +static std::string getApplicationCacheDir() { + return getApplicationDir("getCacheDir"); +} + +static std::string getApplicationPersistentDir() { + return getApplicationDir("getFilesDir"); +} + +static JSValueRef nativeLoggingHook( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + android_LogPriority logLevel = ANDROID_LOG_DEBUG; + if (argumentCount > 1) { + int level = (int) JSValueToNumber(ctx, arguments[1], NULL); + // The lowest log level we get from JS is 0. We shift and cap it to be + // in the range the Android logging method expects. + logLevel = std::min( + static_cast(level + ANDROID_LOG_DEBUG), + ANDROID_LOG_FATAL); + } + if (argumentCount > 0) { + JSStringRef jsString = JSValueToStringCopy(ctx, arguments[0], NULL); + String message = String::adopt(jsString); + FBLOG_PRI(logLevel, "ReactNativeJS", "%s", message.str().c_str()); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativePerformanceNow( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + static const int64_t NANOSECONDS_IN_SECOND = 1000000000LL; + static const int64_t NANOSECONDS_IN_MILLISECOND = 1000000LL; + + // This is equivalent to android.os.SystemClock.elapsedRealtime() in native + struct timespec now; + clock_gettime(CLOCK_MONOTONIC_RAW, &now); + int64_t nano = now.tv_sec * NANOSECONDS_IN_SECOND + now.tv_nsec; + return JSValueMakeNumber(ctx, (nano / (double)NANOSECONDS_IN_MILLISECOND)); +} + +class JSCJavaScriptExecutorHolder : public HybridClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/JSCJavaScriptExecutor;"; + + static local_ref initHybrid(alias_ref, ReadableNativeArray* jscConfigArray) { + // See JSCJavaScriptExecutor.Factory() for the other side of this hack. + folly::dynamic jscConfigMap = jscConfigArray->array[0]; + jscConfigMap["PersistentDirectory"] = getApplicationPersistentDir(); + return makeCxxInstance( + std::make_shared(getApplicationCacheDir(), std::move(jscConfigMap))); + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JSCJavaScriptExecutorHolder::initHybrid), + }); + } + + private: + friend HybridBase; + using HybridBase::HybridBase; +}; + +struct JavaJSExecutor : public JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/JavaJSExecutor;"; +}; + +class ProxyJavaScriptExecutorHolder : public HybridClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/ProxyJavaScriptExecutor;"; + + static local_ref initHybrid( + alias_ref, alias_ref executorInstance) { + return makeCxxInstance( + std::make_shared( + make_global(executorInstance))); + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", ProxyJavaScriptExecutorHolder::initHybrid), + }); + } + + private: + friend HybridBase; + using HybridBase::HybridBase; +}; + + +class JReactMarker : public JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/ReactMarker;"; + static void logMarker(const std::string& marker) { + static auto cls = javaClassStatic(); + static auto meth = cls->getStaticMethod("logMarker"); + meth(cls, marker); + } +}; + +} + +extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + return initialize(vm, [] { + // Inject some behavior into react/ + ReactMarker::logMarker = JReactMarker::logMarker; + WebWorkerUtil::createWebWorkerThread = WebWorkers::createWebWorkerThread; + WebWorkerUtil::loadScriptFromAssets = + [] (const std::string& assetName) { + return loadScriptFromAssets(assetName); + }; + WebWorkerUtil::loadScriptFromNetworkSync = WebWorkers::loadScriptFromNetworkSync; + PerfLogging::installNativeHooks = addNativePerfLoggingHooks; + JSNativeHooks::loggingHook = nativeLoggingHook; + JSNativeHooks::nowHook = nativePerformanceNow; + JSCJavaScriptExecutorHolder::registerNatives(); + ProxyJavaScriptExecutorHolder::registerNatives(); + CatalystInstanceImpl::registerNatives(); + ModuleRegistryHolder::registerNatives(); + CxxModuleWrapper::registerNatives(); + JCallbackImpl::registerNatives(); + registerJSLoaderNatives(); + }); +} + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h new file mode 100644 index 000000000..5cd3c1749 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h @@ -0,0 +1,14 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +jmethodID getLogMarkerMethod(); +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp new file mode 100644 index 000000000..659c9f841 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp @@ -0,0 +1,116 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "ProxyExecutor.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace facebook { +namespace react { + +const auto EXECUTOR_BASECLASS = "com/facebook/react/bridge/JavaJSExecutor"; + +static std::string executeJSCallWithProxy( + jobject executor, + const std::string& methodName, + const std::vector& arguments) { + static auto executeJSCall = + jni::findClassStatic(EXECUTOR_BASECLASS)->getMethod("executeJSCall"); + + auto result = executeJSCall( + executor, + jni::make_jstring(methodName).get(), + jni::make_jstring(folly::toJson(arguments).c_str()).get()); + return result->toString(); +} + +std::unique_ptr ProxyExecutorOneTimeFactory::createJSExecutor( + std::shared_ptr delegate, std::shared_ptr) { + return folly::make_unique(std::move(m_executor), delegate); +} + +ProxyExecutor::ProxyExecutor(jni::global_ref&& executorInstance, + std::shared_ptr delegate) + : m_executor(std::move(executorInstance)) + , m_delegate(delegate) { + + folly::dynamic nativeModuleConfig = folly::dynamic::array; + + { + SystraceSection s("collectNativeModuleDescriptions"); + for (const auto& name : delegate->moduleNames()) { + nativeModuleConfig.push_back(delegate->getModuleConfig(name)); + } + } + + folly::dynamic config = + folly::dynamic::object + ("remoteModuleConfig", std::move(nativeModuleConfig)); + + SystraceSection t("setGlobalVariable"); + setGlobalVariable( + "__fbBatchedBridgeConfig", + folly::make_unique(folly::toJson(config))); +} + +ProxyExecutor::~ProxyExecutor() { + m_executor.reset(); +} + +void ProxyExecutor::loadApplicationScript( + std::unique_ptr, + std::string sourceURL) { + static auto loadApplicationScript = + jni::findClassStatic(EXECUTOR_BASECLASS)->getMethod("loadApplicationScript"); + + // The proxy ignores the script data passed in. + + loadApplicationScript( + m_executor.get(), + jni::make_jstring(sourceURL).get()); + executeJSCallWithProxy(m_executor.get(), "flushedQueue", std::vector()); +} + +void ProxyExecutor::setJSModulesUnbundle(std::unique_ptr) { + jni::throwNewJavaException( + "java/lang/UnsupportedOperationException", + "Loading application unbundles is not supported for proxy executors"); +} + +void ProxyExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) { + std::vector call{ + moduleId, + methodId, + std::move(arguments), + }; + std::string result = executeJSCallWithProxy(m_executor.get(), "callFunctionReturnFlushedQueue", std::move(call)); + m_delegate->callNativeModules(*this, result, true); +} + +void ProxyExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) { + std::vector call{ + (double) callbackId, + std::move(arguments) + }; + std::string result = executeJSCallWithProxy(m_executor.get(), "invokeCallbackAndReturnFlushedQueue", std::move(call)); + m_delegate->callNativeModules(*this, result, true); +} + +void ProxyExecutor::setGlobalVariable(std::string propName, + std::unique_ptr jsonValue) { + static auto setGlobalVariable = + jni::findClassStatic(EXECUTOR_BASECLASS)->getMethod("setGlobalVariable"); + + setGlobalVariable( + m_executor.get(), + jni::make_jstring(propName).get(), + jni::make_jstring(jsonValue->c_str()).get()); +} + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.h b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.h new file mode 100644 index 000000000..02328dd7b --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.h @@ -0,0 +1,56 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include +#include "OnLoad.h" + +namespace facebook { +namespace react { + +/** + * This executor factory can only create a single executor instance because it moves + * executorInstance global reference to the executor instance it creates. + */ +class ProxyExecutorOneTimeFactory : public JSExecutorFactory { +public: + ProxyExecutorOneTimeFactory(jni::global_ref&& executorInstance) : + m_executor(std::move(executorInstance)) {} + virtual std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr queue) override; + +private: + jni::global_ref m_executor; +}; + +class ProxyExecutor : public JSExecutor { +public: + ProxyExecutor(jni::global_ref&& executorInstance, + std::shared_ptr delegate); + virtual ~ProxyExecutor() override; + virtual void loadApplicationScript( + std::unique_ptr script, + std::string sourceURL) override; + virtual void setJSModulesUnbundle( + std::unique_ptr bundle) override; + virtual void callFunction( + const std::string& moduleId, + const std::string& methodId, + const folly::dynamic& arguments) override; + virtual void invokeCallback( + const double callbackId, + const folly::dynamic& arguments) override; + virtual void setGlobalVariable( + std::string propName, + std::unique_ptr jsonValue) override; + +private: + jni::global_ref m_executor; + std::shared_ptr m_delegate; +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/WebWorkers.h b/ReactAndroid/src/main/jni/xreact/jni/WebWorkers.h new file mode 100644 index 000000000..a37df26c6 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/WebWorkers.h @@ -0,0 +1,50 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "JMessageQueueThread.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class WebWorkers : public JavaClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/webworkers/WebWorkers;"; + + static std::unique_ptr createWebWorkerThread(int id, MessageQueueThread *ownerMessageQueueThread) { + static auto method = WebWorkers::javaClassStatic()-> + getStaticMethod("createWebWorkerThread"); + auto res = method(WebWorkers::javaClassStatic(), id, static_cast(ownerMessageQueueThread)->jobj()); + return folly::make_unique(res); + } + + static std::string loadScriptFromNetworkSync(const std::string& url, const std::string& tempfileName) { + static auto method = WebWorkers::javaClassStatic()-> + getStaticMethod("downloadScriptToFileSync"); + method( + WebWorkers::javaClassStatic(), + jni::make_jstring(url).get(), + jni::make_jstring(tempfileName).get()); + + std::ifstream tempFile(tempfileName); + if (!tempFile.good()) { + throw std::runtime_error("Didn't find worker script file at " + tempfileName); + } + std::stringstream buffer; + buffer << tempFile.rdbuf(); + std::remove(tempfileName.c_str()); + return buffer.str(); + } +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/perftests/BUCK b/ReactAndroid/src/main/jni/xreact/perftests/BUCK new file mode 100644 index 000000000..ee130d851 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/perftests/BUCK @@ -0,0 +1,21 @@ +cxx_library( + name = 'perftests', + srcs = [ 'OnLoad.cpp' ], + soname = 'libnativereactperftests.so', + compiler_flags = [ + '-fexceptions', + ], + deps = [ + '//native:base', + '//native/fb:fb', + '//xplat/folly:molly', + '//xplat/react/module:module', + ], + visibility = [ + '//instrumentation_tests/com/facebook/react/...', + ], +) + +project_config( + src_target = ':perftests', +) diff --git a/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp new file mode 100644 index 000000000..ba262b672 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp @@ -0,0 +1,156 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include +#include +#include +#include + +#include +#include + +namespace facebook { +namespace react { + +using facebook::jni::alias_ref; + +namespace { + +// This is a wrapper around the Java proxy to the javascript module. This +// allows us to call functions on the js module from c++. +class JavaJSModule : public jni::JavaClass { +public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/CatalystBridgeBenchmarks$BridgeBenchmarkModule;"; + + static void bounceCxx(alias_ref obj, int iters) { + static auto method = javaClassLocal()->getMethod("bounceCxx"); + method(obj, iters); + } + + static void bounceArgsCxx( + alias_ref obj, + int iters, + int a, int b, + double x, double y, + const std::string& s, const std::string& t) { + static auto method = + javaClassLocal()->getMethod("bounceArgsCxx"); + method(obj, iters, a, b, x, y, jni::make_jstring(s).get(), jni::make_jstring(t).get()); + } +}; + +// This is just the test instance itself. Used only to countdown the latch. +class CatalystBridgeBenchmarks : public jni::JavaClass { +public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/CatalystBridgeBenchmarks;"; + + static void countDown(alias_ref obj) { + static auto method = javaClassLocal()->getMethod("countDown"); + method(obj); + } +}; + +// This is the shared data for two cxx bounce threads. +struct Data { + std::mutex m; + std::condition_variable cv; + bool leftActive; + Data() : leftActive(true) {} +}; +Data data; + +void runBounce(jni::alias_ref, bool isLeft, int iters) { + for (int i = 0; i < iters; i++) { + std::unique_lock lk(data.m); + data.cv.wait(lk, [&]{ return data.leftActive == isLeft; }); + data.leftActive = !isLeft; + data.cv.notify_one(); + } +} + +static jni::global_ref jsModule; +static jni::global_ref javaTestInstance; + +class CxxBenchmarkModule : public xplat::module::CxxModule { +public: + virtual std::string getName() override { + return "CxxBenchmarkModule"; + } + + virtual auto getConstants() -> std::map override { + return std::map(); + } + + virtual auto getMethods() -> std::vector override { + return std::vector{ + Method("bounce", [this] (folly::dynamic args) { + this->bounce(xplat::jsArgAsInt(args, 0)); + }), + Method("bounceArgs", [this] (folly::dynamic args) { + this->bounceArgs( + xplat::jsArgAsInt(args, 0), + xplat::jsArgAsInt(args, 1), + xplat::jsArgAsInt(args, 2), + xplat::jsArgAsDouble(args, 3), + xplat::jsArgAsDouble(args, 4), + xplat::jsArgAsString(args, 5), + xplat::jsArgAsString(args, 6)); + }), + }; + } + + void bounce(int iters) { + if (iters == 0) { + CatalystBridgeBenchmarks::countDown(javaTestInstance); + } else { + JavaJSModule::bounceCxx(jsModule, iters - 1); + } + } + + void bounceArgs( + int iters, + int a, int b, + double x, double y, + const std::string& s, const std::string& t) { + if (iters == 0) { + CatalystBridgeBenchmarks::countDown(javaTestInstance); + } else { + JavaJSModule::bounceArgsCxx(jsModule, iters - 1, a, b, x, y, s, t); + } + } +}; + + +void setUp( + alias_ref obj, + alias_ref mod) { + javaTestInstance = jni::make_global(obj); + jsModule = jni::make_global(mod); +} + +void tearDown( + alias_ref) { + javaTestInstance.reset(); + jsModule.reset(); +} + +} +} +} + +extern "C" facebook::xplat::module::CxxModule* CxxBenchmarkModule() { + return new facebook::react::CxxBenchmarkModule(); +} + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + return facebook::jni::initialize(vm, [] { + facebook::jni::registerNatives( + "com/facebook/react/CatalystBridgeBenchmarks", { + makeNativeMethod("runNativeBounce", facebook::react::runBounce), + makeNativeMethod("nativeSetUp", facebook::react::setUp), + makeNativeMethod("nativeTearDown", facebook::react::tearDown), + }); + }); +} +