From a8acf8a5ce74228c94a502b4e875dd6b755316bb Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Tue, 24 May 2016 19:24:57 -0700 Subject: [PATCH] Move cxx module support into oss Reviewed By: mhorowitz Differential Revision: D3319751 fbshipit-source-id: 87f91a541cfbbe45bd8561d94f269ba976a9f702 --- ReactAndroid/DEFS | 4 + ReactAndroid/src/main/jni/xreact/jni/BUCK | 2 +- .../main/jni/xreact/jni/CxxModuleWrapper.cpp | 2 +- .../main/jni/xreact/jni/CxxModuleWrapper.h | 2 +- .../jni/xreact/jni/ModuleRegistryHolder.cpp | 5 +- .../src/main/jni/xreact/perftests/BUCK | 4 +- .../src/main/jni/xreact/perftests/OnLoad.cpp | 4 +- ReactCommon/bridge/BUCK | 91 ++++++++++-- ReactCommon/bridge/CxxModule.h | 132 ++++++++++++++++++ ReactCommon/bridge/JsArgumentHelpers-inl.h | 90 ++++++++++++ ReactCommon/bridge/JsArgumentHelpers.h | 105 ++++++++++++++ ReactCommon/bridge/SampleCxxModule.cpp | 130 +++++++++++++++++ ReactCommon/bridge/SampleCxxModule.h | 51 +++++++ 13 files changed, 602 insertions(+), 20 deletions(-) create mode 100644 ReactCommon/bridge/CxxModule.h create mode 100644 ReactCommon/bridge/JsArgumentHelpers-inl.h create mode 100644 ReactCommon/bridge/JsArgumentHelpers.h create mode 100644 ReactCommon/bridge/SampleCxxModule.cpp create mode 100644 ReactCommon/bridge/SampleCxxModule.h diff --git a/ReactAndroid/DEFS b/ReactAndroid/DEFS index a11e2436f..028e49c07 100644 --- a/ReactAndroid/DEFS +++ b/ReactAndroid/DEFS @@ -10,6 +10,10 @@ import os def react_native_target(path): return '//ReactAndroid/src/main/' + path +# Example: react_native_target('bridge:bridge') +def react_native_xplat_target(path): + return '//ReactCommon/' + path + # Example: react_native_tests_target('java/com/facebook/react/modules:modules') def react_native_tests_target(path): return '//ReactAndroid/src/test/' + path diff --git a/ReactAndroid/src/main/jni/xreact/jni/BUCK b/ReactAndroid/src/main/jni/xreact/jni/BUCK index a68771a8e..57b172b4a 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/BUCK +++ b/ReactAndroid/src/main/jni/xreact/jni/BUCK @@ -17,9 +17,9 @@ cxx_library( '//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'), + react_native_xplat_target('bridge:module'), ], srcs = glob(['*.cpp']), exported_headers = EXPORTED_HEADERS, diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp index 182f03646..f0c3e4c0e 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h index 206eddc56..53a4d5be1 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h @@ -2,7 +2,7 @@ #pragma once -#include +#include #include #include #include diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp index c71c95936..29349f741 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp @@ -6,10 +6,9 @@ #include -#include -#include - +#include #include +#include #include #include diff --git a/ReactAndroid/src/main/jni/xreact/perftests/BUCK b/ReactAndroid/src/main/jni/xreact/perftests/BUCK index ee130d851..d111fa394 100644 --- a/ReactAndroid/src/main/jni/xreact/perftests/BUCK +++ b/ReactAndroid/src/main/jni/xreact/perftests/BUCK @@ -1,3 +1,5 @@ +include_defs('//ReactAndroid/DEFS') + cxx_library( name = 'perftests', srcs = [ 'OnLoad.cpp' ], @@ -9,7 +11,7 @@ cxx_library( '//native:base', '//native/fb:fb', '//xplat/folly:molly', - '//xplat/react/module:module', + react_native_xplat_target('bridge:module'), ], visibility = [ '//instrumentation_tests/com/facebook/react/...', diff --git a/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp index ba262b672..4c79034aa 100644 --- a/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp @@ -2,8 +2,8 @@ #include #include -#include -#include +#include +#include #include #include diff --git a/ReactCommon/bridge/BUCK b/ReactCommon/bridge/BUCK index 9d5a04cb0..cff2c6c69 100644 --- a/ReactCommon/bridge/BUCK +++ b/ReactCommon/bridge/BUCK @@ -38,7 +38,6 @@ if THIS_IS_FBANDROID: ) elif THIS_IS_FBOBJC: - def react_library(**kwargs): ios_library( name = 'bridge', @@ -55,20 +54,91 @@ elif THIS_IS_FBOBJC: ) ) -LOCAL_HEADERS = [ - 'JSCTracing.h', - 'JSCLegacyProfiler.h', - 'JSCLegacyTracing.h', - 'JSCMemory.h', - 'JSCPerfStats.h', -] +cxx_library( + name = 'module', + header_namespace = 'cxxreact', + force_static = True, + exported_headers = [ + 'CxxModule.h', + 'JsArgumentHelpers.h', + 'JsArgumentHelpers-inl.h', + ], + deps = [ + '//xplat/folly:molly', + ], + visibility = [ + 'PUBLIC', + ], +) + +cxx_library( + name = 'samplemodule', + soname = 'libxplat_react_module_samplemodule.so', + srcs = ['SampleCxxModule.cpp'], + exported_headers = ['SampleCxxModule.h'], + header_namespace = '', + compiler_flags = [ + '-fno-omit-frame-pointer', + '-Wall', + '-Werror', + '-std=c++11', + '-fexceptions', + ], + deps = [ + ':module', + '//xplat/folly:molly', + ], + visibility = [ + 'PUBLIC', + ], +) react_library( soname = 'libreactnativefb.so', header_namespace = 'cxxreact', force_static = True, - srcs = glob(['*.cpp']), - headers = LOCAL_HEADERS, + srcs = [ + 'Instance.cpp', + 'JSCExecutor.cpp', + 'JSCHelpers.cpp', + 'JSCLegacyProfiler.cpp', + 'JSCLegacyTracing.cpp', + 'JSCMemory.cpp', + 'JSCPerfStats.cpp', + 'JSCTracing.cpp', + 'JSCWebWorker.cpp', + 'MethodCall.cpp', + 'ModuleRegistry.cpp', + 'NativeToJsBridge.cpp', + 'Platform.cpp', + 'Value.cpp', + ], + headers = [ + 'JSCLegacyProfiler.h', + 'JSCLegacyTracing.h', + 'JSCMemory.h', + 'JSCPerfStats.h', + 'JSCTracing.h', + ], + exported_headers = [ + 'Executor.h', + 'ExecutorToken.h', + 'ExecutorTokenFactory.h', + 'Instance.h', + 'JSCExecutor.h', + 'JSCHelpers.h', + 'JSCWebWorker.h', + 'JSModulesUnbundle.h', + 'MessageQueueThread.h', + 'MethodCall.h', + 'ModuleRegistry.h', + 'NativeModule.h', + 'NativeToJsBridge.h', + 'noncopyable.h', + 'Platform.h', + 'SystraceSection.h', + 'Value.h', + ], preprocessor_flags = [ '-DLOG_TAG="ReactNative"', '-DWITH_FBSYSTRACE=1', @@ -79,7 +149,6 @@ react_library( '-fvisibility=hidden', '-frtti', ], - exported_headers = glob(['*.h'], excludes=LOCAL_HEADERS), deps = [ '//xplat/fbsystrace:fbsystrace', ], diff --git a/ReactCommon/bridge/CxxModule.h b/ReactCommon/bridge/CxxModule.h new file mode 100644 index 000000000..7c4b72ec1 --- /dev/null +++ b/ReactCommon/bridge/CxxModule.h @@ -0,0 +1,132 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#ifndef FBXPLATMODULE +#define FBXPLATMODULE + +#include + +#include + +#include +#include +#include + +using namespace std::placeholders; + +namespace facebook { namespace xplat { namespace module { + +/** + * Base class for Catalyst native modules whose implementations are + * written in C++. Native methods are represented by instances of the + * Method struct. Generally, a derived class will manage an instance + * which represents the data for the module, and non-Catalyst-specific + * methods can be wrapped in lambdas which convert between + * folly::dynamic and native C++ objects. The Callback arguments will + * pass through to js functions passed to the analogous javascript + * methods. At most two callbacks will be converted. Results should + * be passed to the first callback, and errors to the second callback. + * Exceptions thrown by a method will be converted to platform + * exceptions, and handled however they are handled on that platform. + * (TODO mhorowitz #7128529: this exception behavior is not yet + * implemented.) + * + * There are two sets of constructors here. The first set initializes + * a Method using a name and anything convertible to a std::function. + * This is most useful for registering a lambda as a RN method. There + * are overloads to support functions which take no arguments, + * arguments only, and zero, one, or two callbacks. + * + * The second set of methods is similar, but instead of taking a + * function, takes the method name, an object, and a pointer to a + * method on that object. + */ + +class CxxModule { +public: + typedef std::function)> Callback; + + struct Method { + std::string name; + size_t callbacks; + std::function func; + + // std::function/lambda ctors + + Method(std::string aname, + std::function&& afunc) + : name(std::move(aname)) + , callbacks(0) + , func(std::bind(std::move(afunc))) {} + + Method(std::string aname, + std::function&& afunc) + : name(std::move(aname)) + , callbacks(0) + , func(std::bind(std::move(afunc), _1)) {} + + Method(std::string aname, + std::function&& afunc) + : name(std::move(aname)) + , callbacks(1) + , func(std::bind(std::move(afunc), _1, _2)) {} + + Method(std::string aname, + std::function&& afunc) + : name(std::move(aname)) + , callbacks(2) + , func(std::move(afunc)) {} + + // method pointer ctors + + template + Method(std::string aname, T* t, void (T::*method)()) + : name(std::move(aname)) + , callbacks(0) + , func(std::bind(method, t)) {} + + template + Method(std::string aname, T* t, void (T::*method)(folly::dynamic)) + : name(std::move(aname)) + , callbacks(0) + , func(std::bind(method, t, _1)) {} + + template + Method(std::string aname, T* t, void (T::*method)(folly::dynamic, Callback)) + : name(std::move(aname)) + , callbacks(1) + , func(std::bind(method, t, _1, _2)) {} + + template + Method(std::string aname, T* t, void (T::*method)(folly::dynamic, Callback, Callback)) + : name(std::move(aname)) + , callbacks(2) + , func(std::bind(method, t, _1, _2, _3)) {} + }; + + /** + * This may block, if necessary to complete cleanup before the + * object is destroyed. + */ + virtual ~CxxModule() {} + + /** + * @return the name of this module. This will be the name used to {@code require()} this module + * from javascript. + */ + virtual std::string getName() = 0; + + /** + * Each entry in the map will be exported as a property to JS. The + * key is the property name, and the value can be anything. + */ + virtual auto getConstants() -> std::map = 0; + + /** + * @return a list of methods this module exports to JS. + */ + virtual auto getMethods() -> std::vector = 0; +}; + +}}} + +#endif diff --git a/ReactCommon/bridge/JsArgumentHelpers-inl.h b/ReactCommon/bridge/JsArgumentHelpers-inl.h new file mode 100644 index 000000000..ce8de8936 --- /dev/null +++ b/ReactCommon/bridge/JsArgumentHelpers-inl.h @@ -0,0 +1,90 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +namespace facebook { +namespace xplat { + +namespace detail { + +template +R jsArg1(const folly::dynamic& arg, M asFoo, const T&... desc) { + try { + return (arg.*asFoo)(); + } catch (const folly::TypeError& ex) { + throw JsArgumentException( + folly::to( + "Error converting javascript arg ", desc..., " to C++: ", ex.what())); + } catch (const std::range_error& ex) { + throw JsArgumentException( + folly::to( + "Could not convert argument ", desc..., " to required type: ", ex.what())); + } +} + +} + +template +R jsArg(const folly::dynamic& arg, R (folly::dynamic::*asFoo)() const, const T&... desc) { + return detail::jsArg1(arg, asFoo, desc...); +} + +template +R jsArg(const folly::dynamic& arg, R (folly::dynamic::*asFoo)() const&, const T&... desc) { + return detail::jsArg1(arg, asFoo, desc...); +} + +template +typename detail::is_dynamic::type& jsArgAsDynamic(T&& args, size_t n) { + try { + return args[n]; + } catch (const std::out_of_range& ex) { + // Use 1-base counting for argument description. + throw JsArgumentException( + folly::to( + "JavaScript provided ", args.size(), + " arguments for C++ method which references at least ", n + 1, + " arguments: ", ex.what())); + } +} + +template +R jsArgN(const folly::dynamic& args, size_t n, R (folly::dynamic::*asFoo)() const) { + return jsArg(jsArgAsDynamic(args, n), asFoo, n); +} +template +R jsArgN(const folly::dynamic& args, size_t n, R (folly::dynamic::*asFoo)() const&) { + return jsArg(jsArgAsDynamic(args, n), asFoo, n); +} + +namespace detail { + +// This is a helper for jsArgAsArray and jsArgAsObject. + +template +typename detail::is_dynamic::type& jsArgAsType(T&& args, size_t n, const char* required, + bool (folly::dynamic::*isFoo)() const) { + T& ret = jsArgAsDynamic(args, n); + if ((ret.*isFoo)()) { + return ret; + } + + // Use 1-base counting for argument description. + throw JsArgumentException( + folly::to( + "Argument ", n + 1, " of type ", ret.typeName(), " is not required type ", required)); +} + +} // end namespace detail + +template +typename detail::is_dynamic::type& jsArgAsArray(T&& args, size_t n) { + return detail::jsArgAsType(args, n, "Array", &folly::dynamic::isArray); +} + +template +typename detail::is_dynamic::type& jsArgAsObject(T&& args, size_t n) { + return detail::jsArgAsType(args, n, "Object", &folly::dynamic::isObject); +} + +}} diff --git a/ReactCommon/bridge/JsArgumentHelpers.h b/ReactCommon/bridge/JsArgumentHelpers.h new file mode 100644 index 000000000..80e3e48bd --- /dev/null +++ b/ReactCommon/bridge/JsArgumentHelpers.h @@ -0,0 +1,105 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include + +#include + +// When building a cross-platform module for React Native, arguments passed +// from JS are represented as a folly::dynamic. This class provides helpers to +// extract arguments from the folly::dynamic to concrete types usable by +// cross-platform code, and converting exceptions to a JsArgumentException so +// they can be caught and reported to RN consistently. The goal is to make the +// jsArgAs... methods at the end simple to use should be most common, but any +// non-detail method can be used when needed. + +namespace facebook { +namespace xplat { + +class JsArgumentException : public std::logic_error { +public: + JsArgumentException(const std::string& msg) : std::logic_error(msg) {} +}; + +// This extracts a single argument by calling the given method pointer on it. +// If an exception is thrown, the additional arguments are passed to +// folly::to<> to be included in the exception string. This will be most +// commonly used when extracting values from non-scalar argument. The second +// overload accepts ref-qualified member functions. + +template +R jsArg(const folly::dynamic& arg, R (folly::dynamic::*asFoo)() const, const T&... desc); +template +R jsArg(const folly::dynamic& arg, R (folly::dynamic::*asFoo)() const&, const T&... desc); + +// This is like jsArg, but a operates on a dynamic representing an array of +// arguments. The argument n is used both to index the array and build the +// exception message, if any. It can be used directly, but will more often be +// used by the type-specific methods following. + +template +R jsArgN(const folly::dynamic& args, size_t n, R (folly::dynamic::*asFoo)() const); +template +R jsArgN(const folly::dynamic& args, size_t n, R (folly::dynamic::*asFoo)() const&); + +namespace detail { + +// This is a type helper to implement functions which should work on both const +// and non-const folly::dynamic arguments, and return a type with the same +// constness. Basically, it causes the templates which use it to be defined +// only for types compatible with folly::dynamic. +template +struct is_dynamic { + typedef typename std::enable_if::value, T>::type type; +}; + +} // end namespace detail + +// Easy to use conversion helpers are here: + +// Extract the n'th arg from the given dynamic, as a dynamic. Throws a +// JsArgumentException if there is no n'th arg in the input. +template +typename detail::is_dynamic::type& jsArgAsDynamic(T&& args, size_t n); + +// Extract the n'th arg from the given dynamic, as a dynamic Array. Throws a +// JsArgumentException if there is no n'th arg in the input, or it is not an +// Array. +template +typename detail::is_dynamic::type& jsArgAsArray(T&& args, size_t n); + +// Extract the n'th arg from the given dynamic, as a dynamic Object. Throws a +// JsArgumentException if there is no n'th arg in the input, or it is not an +// Object. +template +typename detail::is_dynamic::type& jsArgAsObject(T&& args, size_t n); + +// Extract the n'th arg from the given dynamic, as a bool. Throws a +// JsArgumentException if this fails for some reason. +inline bool jsArgAsBool(const folly::dynamic& args, size_t n) { + return jsArgN(args, n, &folly::dynamic::asBool); +} + +// Extract the n'th arg from the given dynamic, as an integer. Throws a +// JsArgumentException if this fails for some reason. +inline int64_t jsArgAsInt(const folly::dynamic& args, size_t n) { + return jsArgN(args, n, &folly::dynamic::asInt); +} + +// Extract the n'th arg from the given dynamic, as a double. Throws a +// JsArgumentException if this fails for some reason. +inline double jsArgAsDouble(const folly::dynamic& args, size_t n) { + return jsArgN(args, n, &folly::dynamic::asDouble); +} + +// Extract the n'th arg from the given dynamic, as a string. Throws a +// JsArgumentException if this fails for some reason. +inline std::string jsArgAsString(const folly::dynamic& args, size_t n) { + return jsArgN(args, n, &folly::dynamic::asString); +} + +}} + +#include "JsArgumentHelpers-inl.h" diff --git a/ReactCommon/bridge/SampleCxxModule.cpp b/ReactCommon/bridge/SampleCxxModule.cpp new file mode 100644 index 000000000..08ba2ce21 --- /dev/null +++ b/ReactCommon/bridge/SampleCxxModule.cpp @@ -0,0 +1,130 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "SampleCxxModule.h" +#include + +#include +#include + +#include + +using namespace folly; + +namespace facebook { namespace xplat { namespace samples { + +std::string Sample::hello() { + LOG(WARNING) << "glog: hello, world"; + return "hello"; +} + +double Sample::add(double a, double b) { + return a + b; +} + +std::string Sample::concat(const std::string& a, const std::string& b) { + return a + b; +} + +std::string Sample::repeat(int count, const std::string& str) { + std::string ret; + for (int i = 0; i < count; i++) { + ret += str; + } + + return ret; +} + +void Sample::save(std::map dict) +{ + state_ = std::move(dict); +} + +std::map Sample::load() { + return state_; +} + +void Sample::except() { +// TODO mhorowitz #7128529: There's no way to automatically test this +// right now. + // throw std::runtime_error("oops"); +} + +void Sample::call_later(int msec, std::function f) { + std::thread t([=] { + std::this_thread::sleep_for(std::chrono::milliseconds(msec)); + f(); + }); + t.detach(); +} + +SampleCxxModule::SampleCxxModule(std::unique_ptr sample) + : sample_(std::move(sample)) {} + +std::string SampleCxxModule::getName() { + return "Sample"; +} + +auto SampleCxxModule::getConstants() -> std::map { + return { + { "one", 1 }, + { "two", 2 }, + { "animal", "fox" }, + }; +} + +auto SampleCxxModule::getMethods() -> std::vector { + return { + Method("hello", [this] { + sample_->hello(); + }), + Method("add", [this](dynamic args, Callback cb) { + LOG(WARNING) << "Sample: add => " + << sample_->add(jsArgAsDouble(args, 0), jsArgAsDouble(args, 1)); + cb({sample_->add(jsArgAsDouble(args, 0), jsArgAsDouble(args, 1))}); + }), + Method("concat", [this](dynamic args, Callback cb) { + cb({sample_->concat(jsArgAsString(args, 0), + jsArgAsString(args, 1))}); + }), + Method("repeat", [this](dynamic args, Callback cb) { + cb({sample_->repeat(jsArgAsInt(args, 0), + jsArgAsString(args, 1))}); + }), + Method("save", this, &SampleCxxModule::save), + Method("load", this, &SampleCxxModule::load), + Method("call_later", [this](dynamic args, Callback cb) { + sample_->call_later(jsArgAsInt(args, 0), [cb] { + cb({}); + }); + }), + Method("except", [this] { + sample_->except(); + }), + }; +} + +void SampleCxxModule::save(folly::dynamic args) { + std::map m; + for (const auto& p : jsArgN(args, 0, &dynamic::items)) { + m.emplace(jsArg(p.first, &dynamic::asString, "map key"), + jsArg(p.second, &dynamic::asString, "map value")); + } + sample_->save(std::move(m)); +} + +void SampleCxxModule::load(folly::dynamic args, Callback cb) { + dynamic d = dynamic::object; + for (const auto& p : sample_->load()) { + d.insert(p.first, p.second); + } + cb({d}); +} + +}}} + +// By convention, the function name should be the same as the class +// name. +extern "C" facebook::xplat::module::CxxModule *SampleCxxModule() { + return new facebook::xplat::samples::SampleCxxModule( + folly::make_unique()); +} diff --git a/ReactCommon/bridge/SampleCxxModule.h b/ReactCommon/bridge/SampleCxxModule.h new file mode 100644 index 000000000..5867208b6 --- /dev/null +++ b/ReactCommon/bridge/SampleCxxModule.h @@ -0,0 +1,51 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#ifndef FBSAMPLEXPLATMODULE +#define FBSAMPLEXPLATMODULE + +#include + +#include +#include + +namespace facebook { namespace xplat { namespace samples { + +// In a less contrived example, Sample would be part of a traditional +// C++ library. + +class Sample { +public: + std::string hello(); + double add(double a, double b); + std::string concat(const std::string& a, const std::string& b); + std::string repeat(int count, const std::string& str); + void save(std::map dict); + std::map load(); + void call_later(int msec, std::function f); + void except(); + +private: + std::map state_; +}; + +class SampleCxxModule : public module::CxxModule { +public: + SampleCxxModule(std::unique_ptr sample); + + std::string getName(); + + virtual auto getConstants() -> std::map; + + virtual auto getMethods() -> std::vector; + +private: + void save(folly::dynamic args); + void load(folly::dynamic args, Callback cb); + + std::unique_ptr sample_; +}; + +}}} + +#endif +