From 30cb78e709bccb4f7bf7aab3f6b0f1ba4261f577 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Fri, 11 Mar 2022 12:47:51 -0800 Subject: [PATCH] New bridging API for JSI <-> C++ Summary: This adds `bridging::toJs` and `bridging::fromJs` functions that will safely cast to and from JSI values and C++ types. This is extensible by specializing `Bridging` with `toJs` and/or `fromJs` static methods. There are specializations for most common C++ and JSI types along with tests for those. C++ functions and lambdas will effortlessly bridge into JS, and bridging JS functions back into C++ require you to choose `SyncCallback` or `AsyncCallback` types. The sync version allows for having a return value and is strictly not movable to prevent accidentally moving onto another thread. The async version will move its args onto the JS thread and safely call the callback there, but hence always has a `void` return value. For promises, you can construct a `AsyncPromise` that has `resolve` and `reject` methods that can be called from any thread, and will bridge into JS as a regular `Promise`. Changelog: [General][Added] - New bridging API for JSI <-> C++ Reviewed By: christophpurrer Differential Revision: D34607143 fbshipit-source-id: d832ac24cf84b4c1672a7b544d82e324d5fca3ef --- ReactCommon/react/bridging/Array.h | 108 +++++++ ReactCommon/react/bridging/BUCK | 36 ++- ReactCommon/react/bridging/Base.h | 124 +++++++ ReactCommon/react/bridging/Bool.h | 25 ++ ReactCommon/react/bridging/Bridging.h | 18 ++ ReactCommon/react/bridging/CallbackWrapper.h | 4 + ReactCommon/react/bridging/Convert.h | 118 +++++++ ReactCommon/react/bridging/Error.h | 51 +++ ReactCommon/react/bridging/Function.h | 214 +++++++++++++ .../react/bridging/LongLivedObject.cpp | 5 + ReactCommon/react/bridging/LongLivedObject.h | 1 + ReactCommon/react/bridging/Number.h | 47 +++ ReactCommon/react/bridging/Object.h | 100 ++++++ ReactCommon/react/bridging/Promise.h | 102 ++++++ ReactCommon/react/bridging/String.h | 42 +++ ReactCommon/react/bridging/Value.h | 96 ++++++ .../react/bridging/tests/BridgingTest.cpp | 302 ++++++++++++++++++ .../react/bridging/tests/BridgingTest.h | 77 +++++ 18 files changed, 1469 insertions(+), 1 deletion(-) create mode 100644 ReactCommon/react/bridging/Array.h create mode 100644 ReactCommon/react/bridging/Base.h create mode 100644 ReactCommon/react/bridging/Bool.h create mode 100644 ReactCommon/react/bridging/Bridging.h create mode 100644 ReactCommon/react/bridging/Convert.h create mode 100644 ReactCommon/react/bridging/Error.h create mode 100644 ReactCommon/react/bridging/Function.h create mode 100644 ReactCommon/react/bridging/Number.h create mode 100644 ReactCommon/react/bridging/Object.h create mode 100644 ReactCommon/react/bridging/Promise.h create mode 100644 ReactCommon/react/bridging/String.h create mode 100644 ReactCommon/react/bridging/Value.h create mode 100644 ReactCommon/react/bridging/tests/BridgingTest.cpp create mode 100644 ReactCommon/react/bridging/tests/BridgingTest.h diff --git a/ReactCommon/react/bridging/Array.h b/ReactCommon/react/bridging/Array.h new file mode 100644 index 0000000000..330e2e1a4f --- /dev/null +++ b/ReactCommon/react/bridging/Array.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace array_detail { + +template +struct BridgingStatic { + static jsi::Array toJs( + jsi::Runtime &rt, + const T &array, + const std::shared_ptr &jsInvoker) { + return toJs(rt, array, jsInvoker, std::make_index_sequence{}); + } + + private: + template + static jsi::Array toJs( + facebook::jsi::Runtime &rt, + const T &array, + const std::shared_ptr &jsInvoker, + std::index_sequence) { + return jsi::Array::createWithElements( + rt, bridging::toJs(rt, std::get(array), jsInvoker)...); + } +}; + +template +struct BridgingDynamic { + static jsi::Array toJs( + jsi::Runtime &rt, + const T &list, + const std::shared_ptr &jsInvoker) { + jsi::Array result(rt, list.size()); + size_t index = 0; + + for (const auto &item : list) { + result.setValueAtIndex(rt, index++, bridging::toJs(rt, item, jsInvoker)); + } + + return result; + } +}; + +} // namespace array_detail + +template +struct Bridging> + : array_detail::BridgingStatic, N> {}; + +template +struct Bridging> + : array_detail::BridgingStatic, 2> {}; + +template +struct Bridging> + : array_detail::BridgingStatic, sizeof...(Types)> {}; + +template +struct Bridging> : array_detail::BridgingDynamic> { +}; + +template +struct Bridging> + : array_detail::BridgingDynamic> {}; + +template +struct Bridging> : array_detail::BridgingDynamic> {}; + +template +struct Bridging> + : array_detail::BridgingDynamic> { + static std::vector fromJs( + facebook::jsi::Runtime &rt, + const jsi::Array &array, + const std::shared_ptr &jsInvoker) { + size_t length = array.length(rt); + + std::vector vector; + vector.reserve(length); + + for (size_t i = 0; i < length; i++) { + vector.push_back( + bridging::fromJs(rt, array.getValueAtIndex(rt, i), jsInvoker)); + } + + return vector; + } +}; + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/BUCK b/ReactCommon/react/bridging/BUCK index eb5b3ae0ae..d7de59311a 100644 --- a/ReactCommon/react/bridging/BUCK +++ b/ReactCommon/react/bridging/BUCK @@ -1,12 +1,15 @@ -load("//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "APPLE", "CXX", "react_native_xplat_shared_library_target", "react_native_xplat_target", "rn_xplat_cxx_library") +load("//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "APPLE", "CXX", "IOS", "MACOSX", "fb_xplat_cxx_test", "react_native_xplat_shared_library_target", "react_native_xplat_target", "rn_xplat_cxx_library") rn_xplat_cxx_library( name = "bridging", srcs = glob(["*.cpp"]), header_namespace = "react/bridging", exported_headers = glob(["*.h"]), + compiler_flags_enable_exceptions = True, + compiler_flags_enable_rtti = True, labels = ["supermodule:xplat/default/public.react_native.infra"], platforms = (ANDROID, APPLE, CXX), + tests = [":tests"], visibility = ["PUBLIC"], deps = [ "//xplat/folly:headers_only", @@ -17,3 +20,34 @@ rn_xplat_cxx_library( react_native_xplat_shared_library_target("jsi:jsi"), ], ) + +rn_xplat_cxx_library( + name = "testlib", + header_namespace = "react/bridging", + exported_headers = glob(["tests/*.h"]), + platforms = (ANDROID, APPLE, CXX), + visibility = ["PUBLIC"], + exported_deps = [ + "//xplat/third-party/gmock:gtest", + ], +) + +fb_xplat_cxx_test( + name = "tests", + srcs = glob(["tests/*.cpp"]), + headers = glob(["tests/*.h"]), + apple_sdks = (IOS, MACOSX), + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++17", + "-Wall", + ], + contacts = ["oncall+react_native@xmail.facebook.com"], + platforms = (ANDROID, APPLE, CXX), + deps = [ + ":bridging", + "//xplat/hermes/API:HermesAPI", + "//xplat/third-party/gmock:gtest", + ], +) diff --git a/ReactCommon/react/bridging/Base.h b/ReactCommon/react/bridging/Base.h new file mode 100644 index 0000000000..5ba0e5b5b0 --- /dev/null +++ b/ReactCommon/react/bridging/Base.h @@ -0,0 +1,124 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include + +#include +#include +#include + +namespace facebook::react { + +template +struct Bridging; + +template <> +struct Bridging { + // Highly generic code may result in "casting" to void. + static void fromJs(jsi::Runtime &, const jsi::Value &) {} +}; + +namespace bridging { +namespace detail { + +template +struct function_wrapper; + +template +struct function_wrapper { + using type = folly::Function; +}; + +template +struct function_wrapper { + using type = folly::Function; +}; + +template +struct bridging_wrapper { + using type = remove_cvref_t; +}; + +// Convert lambda types to move-only function types since we can't specialize +// Bridging templates for arbitrary lambdas. +template +struct bridging_wrapper< + T, + std::void_t::operator())>> + : function_wrapper::operator())> {}; + +} // namespace detail + +template +using bridging_t = typename detail::bridging_wrapper::type; + +template , int> = 0> +auto fromJs(jsi::Runtime &rt, T &&value, const std::shared_ptr &) + -> decltype(static_cast(convert(rt, std::forward(value)))) { + return convert(rt, std::forward(value)); +} + +template +auto fromJs(jsi::Runtime &rt, T &&value, const std::shared_ptr &) + -> decltype(Bridging>::fromJs( + rt, + convert(rt, std::forward(value)))) { + return Bridging>::fromJs( + rt, convert(rt, std::forward(value))); +} + +template +auto fromJs( + jsi::Runtime &rt, + T &&value, + const std::shared_ptr &jsInvoker) + -> decltype(Bridging>::fromJs( + rt, + convert(rt, std::forward(value)), + jsInvoker)) { + return Bridging>::fromJs( + rt, convert(rt, std::forward(value)), jsInvoker); +} + +template , int> = 0> +auto toJs( + jsi::Runtime &rt, + T &&value, + const std::shared_ptr & = nullptr) + -> decltype(convert(rt, std::forward(value))) { + return convert(rt, std::forward(value)); +} + +template +auto toJs( + jsi::Runtime &rt, + T &&value, + const std::shared_ptr & = nullptr) + -> decltype(Bridging>::toJs(rt, std::forward(value))) { + return Bridging>::toJs(rt, std::forward(value)); +} + +template +auto toJs( + jsi::Runtime &rt, + T &&value, + const std::shared_ptr &jsInvoker) + -> decltype(Bridging>::toJs( + rt, + std::forward(value), + jsInvoker)) { + return Bridging>::toJs(rt, std::forward(value), jsInvoker); +} + +} // namespace bridging +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/Bool.h b/ReactCommon/react/bridging/Bool.h new file mode 100644 index 0000000000..b720e120ae --- /dev/null +++ b/ReactCommon/react/bridging/Bool.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +template <> +struct Bridging { + static bool fromJs(jsi::Runtime &rt, const jsi::Value &value) { + return value.asBool(); + } + + static jsi::Value toJs(jsi::Runtime &, bool value) { + return value; + } +}; + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/Bridging.h b/ReactCommon/react/bridging/Bridging.h new file mode 100644 index 0000000000..340ad0dee5 --- /dev/null +++ b/ReactCommon/react/bridging/Bridging.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/ReactCommon/react/bridging/CallbackWrapper.h b/ReactCommon/react/bridging/CallbackWrapper.h index 92bba36ccb..fc2980cb92 100644 --- a/ReactCommon/react/bridging/CallbackWrapper.h +++ b/ReactCommon/react/bridging/CallbackWrapper.h @@ -84,6 +84,10 @@ class CallbackWrapper : public LongLivedObject { return *(jsInvoker_); } + std::shared_ptr jsInvokerPtr() { + return jsInvoker_; + } + void allowRelease() override { if (auto longLivedObjectCollection = longLivedObjectCollection_.lock()) { if (longLivedObjectCollection != nullptr) { diff --git a/ReactCommon/react/bridging/Convert.h b/ReactCommon/react/bridging/Convert.h new file mode 100644 index 0000000000..f594fd5c81 --- /dev/null +++ b/ReactCommon/react/bridging/Convert.h @@ -0,0 +1,118 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include + +namespace facebook::react::bridging { + +// std::remove_cvref_t is not available until C++20. +template +using remove_cvref_t = std::remove_cv_t>; + +template +inline constexpr bool is_jsi_v = + std::is_same_v> || + std::is_same_v> || + std::is_base_of_v>; + +template +struct ConverterBase { + ConverterBase(jsi::Runtime &rt, T &&value) + : rt_(rt), value_(std::forward(value)) {} + + operator T() && { + return std::forward(this->value_); + } + + protected: + jsi::Runtime &rt_; + T value_; +}; + +template +struct Converter : public ConverterBase { + using ConverterBase::ConverterBase; +}; + +template <> +struct Converter : public ConverterBase { + using ConverterBase::ConverterBase; + + operator jsi::String() && { + return std::move(value_).asString(rt_); + } + + operator jsi::Object() && { + return std::move(value_).asObject(rt_); + } + + operator jsi::Array() && { + return std::move(value_).asObject(rt_).asArray(rt_); + } + + operator jsi::Function() && { + return std::move(value_).asObject(rt_).asFunction(rt_); + } +}; + +template <> +struct Converter : public ConverterBase { + using ConverterBase::ConverterBase; + + operator jsi::Array() && { + return std::move(value_).asArray(rt_); + } + + operator jsi::Function() && { + return std::move(value_).asFunction(rt_); + } +}; + +template +struct Converter { + Converter(jsi::Runtime &rt, T &value) : rt_(rt), value_(value) {} + + operator T() && { + // Copy the reference into a Value that then can be moved from. + return Converter(rt_, jsi::Value(rt_, value_)); + } + + template < + typename U, + // Ensure the non-reference type can be converted to the desired type. + std::enable_if_t< + std::is_convertible_v>, U>, + int> = 0> + operator U() && { + return Converter(rt_, jsi::Value(rt_, value_)); + } + + private: + jsi::Runtime &rt_; + const T &value_; +}; + +template , int> = 0> +auto convert(jsi::Runtime &rt, T &&value) { + return Converter(rt, std::forward(value)); +} + +template , int> = 0> +auto convert(jsi::Runtime &rt, T &&value) { + return value; +} + +template +auto convert(jsi::Runtime &rt, Converter &&converter) { + return std::move(converter); +} + +} // namespace facebook::react::bridging diff --git a/ReactCommon/react/bridging/Error.h b/ReactCommon/react/bridging/Error.h new file mode 100644 index 0000000000..eed77305d7 --- /dev/null +++ b/ReactCommon/react/bridging/Error.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +class Error { + public: + // TODO (T114055466): Retain stack trace (at least caller location) + Error(std::string message) : message_(std::move(message)) {} + + Error(const char *message) : Error(std::string(message)) {} + + const std::string &message() const { + return message_; + } + + private: + std::string message_; +}; + +template <> +struct Bridging { + static jsi::JSError fromJs(jsi::Runtime &rt, const jsi::Value &value) { + return jsi::JSError(rt, jsi::Value(rt, value)); + } + + static jsi::JSError fromJs(jsi::Runtime &rt, jsi::Value &&value) { + return jsi::JSError(rt, std::move(value)); + } + + static jsi::Value toJs(jsi::Runtime &rt, std::string message) { + return jsi::Value(rt, jsi::JSError(rt, std::move(message)).value()); + } +}; + +template <> +struct Bridging { + static jsi::Value toJs(jsi::Runtime &rt, const Error &error) { + return jsi::Value(rt, jsi::JSError(rt, error.message()).value()); + } +}; + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/Function.h b/ReactCommon/react/bridging/Function.h new file mode 100644 index 0000000000..fdac94bb6f --- /dev/null +++ b/ReactCommon/react/bridging/Function.h @@ -0,0 +1,214 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include + +namespace facebook::react { + +template +class SyncCallback; + +template +class AsyncCallback { + public: + AsyncCallback( + jsi::Runtime &runtime, + jsi::Function function, + std::shared_ptr jsInvoker) + : callback_(std::make_shared>( + runtime, + std::move(function), + std::move(jsInvoker))) {} + + AsyncCallback(const AsyncCallback &) = default; + AsyncCallback &operator=(const AsyncCallback &) = default; + + void operator()(Args... args) const { + call(std::forward(args)...); + } + + void call(Args... args) const { + auto wrapper = callback_->wrapper_.lock(); + if (!wrapper) { + throw std::runtime_error("Failed to call invalidated async callback"); + } + + auto argsTuple = std::make_tuple(std::forward(args)...); + + wrapper->jsInvoker().invokeAsync( + [callback = callback_, + argsPtr = std::make_shared( + std::move(argsTuple))] { callback->apply(std::move(*argsPtr)); }); + } + + private: + friend Bridging; + + std::shared_ptr> callback_; +}; + +template +class SyncCallback { + public: + SyncCallback( + jsi::Runtime &rt, + jsi::Function function, + std::shared_ptr jsInvoker) + : wrapper_(CallbackWrapper::createWeak( + std::move(function), + rt, + std::move(jsInvoker))) {} + + // Disallow moving to prevent function from get called on another thread. + SyncCallback(SyncCallback &&) = delete; + SyncCallback &operator=(SyncCallback &&) = delete; + + ~SyncCallback() { + if (auto wrapper = wrapper_.lock()) { + wrapper->destroy(); + } + } + + R operator()(Args... args) const { + return call(std::forward(args)...); + } + + R call(Args... args) const { + auto wrapper = wrapper_.lock(); + if (!wrapper) { + throw std::runtime_error("Failed to call invalidated sync callback"); + } + + auto &callback = wrapper->callback(); + auto &rt = wrapper->runtime(); + auto jsInvoker = wrapper->jsInvokerPtr(); + + if constexpr (std::is_void_v) { + callback.call( + rt, bridging::toJs(rt, std::forward(args), jsInvoker)...); + } else { + return bridging::fromJs( + rt, + callback.call( + rt, bridging::toJs(rt, std::forward(args), jsInvoker)...), + jsInvoker); + } + } + + private: + friend AsyncCallback; + friend Bridging; + + R apply(std::tuple &&args) const { + return apply(std::move(args), std::index_sequence_for{}); + } + + template + R apply(std::tuple &&args, std::index_sequence) const { + return call(std::move(std::get(args))...); + } + + // Held weakly so lifetime is managed by LongLivedObjectCollection. + std::weak_ptr wrapper_; +}; + +template +struct Bridging> { + static AsyncCallback fromJs( + jsi::Runtime &rt, + jsi::Function &&value, + const std::shared_ptr &jsInvoker) { + return AsyncCallback(rt, std::move(value), jsInvoker); + } + + static jsi::Function toJs( + jsi::Runtime &rt, + const AsyncCallback &value) { + return value.callback_->function_.getFunction(rt); + } +}; + +template +struct Bridging> { + static SyncCallback fromJs( + jsi::Runtime &rt, + jsi::Function &&value, + const std::shared_ptr &jsInvoker) { + return SyncCallback(rt, std::move(value), jsInvoker); + } + + static jsi::Function toJs( + jsi::Runtime &rt, + const SyncCallback &value) { + return value.function_.getFunction(rt); + } +}; + +template +struct Bridging> { + using Func = folly::Function; + using IndexSequence = std::index_sequence_for; + + static constexpr size_t kArgumentCount = sizeof...(Args); + + static jsi::Function toJs( + jsi::Runtime &rt, + Func fn, + const std::shared_ptr &jsInvoker) { + return jsi::Function::createFromHostFunction( + rt, + jsi::PropNameID::forAscii(rt, "BridgedFunction"), + kArgumentCount, + [fn = std::make_shared(std::move(fn)), jsInvoker]( + jsi::Runtime &rt, + const jsi::Value &, + const jsi::Value *args, + size_t count) -> jsi::Value { + if (count < kArgumentCount) { + throw jsi::JSError(rt, "Incorrect number of arguments"); + } + + if constexpr (std::is_void_v) { + callFromJs(*fn, rt, args, jsInvoker, IndexSequence{}); + return jsi::Value(); + } else { + return bridging::toJs( + rt, + callFromJs(*fn, rt, args, jsInvoker, IndexSequence{}), + jsInvoker); + } + }); + } + + private: + template + static R callFromJs( + Func &fn, + jsi::Runtime &rt, + const jsi::Value *args, + const std::shared_ptr &jsInvoker, + std::index_sequence) { + return fn(bridging::fromJs(rt, args[Index], jsInvoker)...); + } +}; + +template +struct Bridging> + : Bridging> {}; + +template +struct Bridging : Bridging> {}; + +template +struct Bridging : Bridging> {}; + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/LongLivedObject.cpp b/ReactCommon/react/bridging/LongLivedObject.cpp index c449e7d180..dc6d44dbe6 100644 --- a/ReactCommon/react/bridging/LongLivedObject.cpp +++ b/ReactCommon/react/bridging/LongLivedObject.cpp @@ -41,6 +41,11 @@ void LongLivedObjectCollection::clear() const { collection_.clear(); } +size_t LongLivedObjectCollection::size() const { + std::lock_guard lock(collectionMutex_); + return collection_.size(); +} + // LongLivedObject LongLivedObject::LongLivedObject() {} LongLivedObject::~LongLivedObject() {} diff --git a/ReactCommon/react/bridging/LongLivedObject.h b/ReactCommon/react/bridging/LongLivedObject.h index 332dccb165..cfb10086d0 100644 --- a/ReactCommon/react/bridging/LongLivedObject.h +++ b/ReactCommon/react/bridging/LongLivedObject.h @@ -47,6 +47,7 @@ class LongLivedObjectCollection { void add(std::shared_ptr o) const; void remove(const LongLivedObject *o) const; void clear() const; + size_t size() const; private: mutable std::unordered_set> collection_; diff --git a/ReactCommon/react/bridging/Number.h b/ReactCommon/react/bridging/Number.h new file mode 100644 index 0000000000..4857b9eec2 --- /dev/null +++ b/ReactCommon/react/bridging/Number.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +template <> +struct Bridging { + static double fromJs(jsi::Runtime &, const jsi::Value &value) { + return value.asNumber(); + } + + static jsi::Value toJs(jsi::Runtime &, double value) { + return value; + } +}; + +template <> +struct Bridging { + static float fromJs(jsi::Runtime &, const jsi::Value &value) { + return (float)value.asNumber(); + } + + static jsi::Value toJs(jsi::Runtime &, float value) { + return (double)value; + } +}; + +template <> +struct Bridging { + static int32_t fromJs(jsi::Runtime &, const jsi::Value &value) { + return (int32_t)value.asNumber(); + } + + static jsi::Value toJs(jsi::Runtime &, int32_t value) { + return value; + } +}; + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/Object.h b/ReactCommon/react/bridging/Object.h new file mode 100644 index 0000000000..48074f3617 --- /dev/null +++ b/ReactCommon/react/bridging/Object.h @@ -0,0 +1,100 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static jsi::WeakObject fromJs(jsi::Runtime &rt, const jsi::Object &value) { + return jsi::WeakObject(rt, value); + } + + static jsi::Value toJs(jsi::Runtime &rt, jsi::WeakObject &value) { + return value.lock(rt); + } +}; + +template +struct Bridging< + std::shared_ptr, + std::enable_if_t>> { + static std::shared_ptr fromJs(jsi::Runtime &rt, const jsi::Object &value) { + return value.asHostObject(rt); + } + + static jsi::Object toJs(jsi::Runtime &rt, std::shared_ptr value) { + return jsi::Object::createFromHostObject(rt, std::move(value)); + } +}; + +namespace map_detail { + +template +struct Bridging { + static T fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + T result; + auto propertyNames = value.getPropertyNames(rt); + auto length = propertyNames.length(rt); + + for (size_t i = 0; i < length; i++) { + auto propertyName = propertyNames.getValueAtIndex(rt, i); + + result.emplace( + bridging::fromJs(rt, propertyName, jsInvoker), + bridging::fromJs( + rt, value.getProperty(rt, propertyName.asString(rt)), jsInvoker)); + } + + return result; + } + + static jsi::Object toJs( + jsi::Runtime &rt, + const T &map, + const std::shared_ptr &jsInvoker) { + auto resultObject = jsi::Object(rt); + + for (const auto &[key, value] : map) { + resultObject.setProperty( + rt, + jsi::PropNameID::forUtf8(rt, key), + bridging::toJs(rt, value, jsInvoker)); + } + + return resultObject; + } +}; + +} // namespace map_detail + +#ifdef BUTTER_USE_FOLLY_CONTAINERS +template +struct Bridging> + : map_detail::Bridging> {}; +#endif + +template +struct Bridging> + : map_detail::Bridging> {}; + +template +struct Bridging> + : map_detail::Bridging> {}; + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/Promise.h b/ReactCommon/react/bridging/Promise.h new file mode 100644 index 0000000000..00fb015161 --- /dev/null +++ b/ReactCommon/react/bridging/Promise.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace facebook::react { + +template +class AsyncPromise { + public: + AsyncPromise(jsi::Runtime &rt, const std::shared_ptr &jsInvoker) + : state_(std::make_shared()) { + auto constructor = rt.global().getPropertyAsFunction(rt, "Promise"); + + auto promise = constructor.callAsConstructor( + rt, + bridging::toJs( + rt, + // Safe to capture this since this is called synchronously. + [this](AsyncCallback resolve, AsyncCallback reject) { + state_->resolve = std::move(resolve); + state_->reject = std::move(reject); + }, + jsInvoker)); + + auto promiseHolder = std::make_shared(promise.asObject(rt)); + LongLivedObjectCollection::get().add(promiseHolder); + + // The shared state can retain the promise holder weakly now. + state_->promiseHolder = promiseHolder; + } + + void resolve(T value) { + std::lock_guard lock(state_->mutex); + + if (state_->resolve) { + state_->resolve->call(std::move(value)); + state_->resolve.reset(); + state_->reject.reset(); + } + } + + void reject(Error error) { + std::lock_guard lock(state_->mutex); + + if (state_->reject) { + state_->reject->call(std::move(error)); + state_->reject.reset(); + state_->resolve.reset(); + } + } + + jsi::Object get(jsi::Runtime &rt) const { + if (auto holder = state_->promiseHolder.lock()) { + return jsi::Value(rt, holder->promise).asObject(rt); + } else { + throw jsi::JSError(rt, "Failed to get invalidated promise"); + } + } + + private: + struct PromiseHolder : LongLivedObject { + PromiseHolder(jsi::Object p) : promise(std::move(p)) {} + + jsi::Object promise; + }; + + struct SharedState { + ~SharedState() { + if (auto holder = promiseHolder.lock()) { + holder->allowRelease(); + } + } + + std::mutex mutex; + std::weak_ptr promiseHolder; + std::optional> resolve; + std::optional> reject; + }; + + std::shared_ptr state_; +}; + +template +struct Bridging> { + static jsi::Object toJs(jsi::Runtime &rt, const AsyncPromise &promise) { + return promise.get(rt); + } +}; + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/String.h b/ReactCommon/react/bridging/String.h new file mode 100644 index 0000000000..cad91246ae --- /dev/null +++ b/ReactCommon/react/bridging/String.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static std::string fromJs(jsi::Runtime &rt, const jsi::String &value) { + return value.utf8(rt); + } + + static jsi::String toJs(jsi::Runtime &rt, const std::string &value) { + return jsi::String::createFromUtf8(rt, value); + } +}; + +template <> +struct Bridging { + static jsi::String toJs(jsi::Runtime &rt, std::string_view value) { + return jsi::String::createFromUtf8( + rt, reinterpret_cast(value.data()), value.length()); + } +}; + +template <> +struct Bridging : Bridging {}; + +template +struct Bridging : Bridging {}; + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/Value.h b/ReactCommon/react/bridging/Value.h new file mode 100644 index 0000000000..295565c1dd --- /dev/null +++ b/ReactCommon/react/bridging/Value.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static std::nullptr_t fromJs(jsi::Runtime &rt, const jsi::Value &value) { + if (value.isNull() || value.isUndefined()) { + return nullptr; + } else { + throw jsi::JSError(rt, "Cannot convert value to nullptr"); + } + } + + static std::nullptr_t toJs(jsi::Runtime &, std::nullptr_t) { + return nullptr; + } +}; + +template +struct Bridging> { + static std::optional fromJs( + jsi::Runtime &rt, + const jsi::Value &value, + const std::shared_ptr &jsInvoker) { + if (value.isNull() || value.isUndefined()) { + return {}; + } + return bridging::fromJs(rt, value, jsInvoker); + } + + static jsi::Value toJs( + jsi::Runtime &rt, + const std::optional &value, + const std::shared_ptr &jsInvoker) { + if (value) { + return bridging::toJs(rt, *value, jsInvoker); + } + return jsi::Value::null(); + } +}; + +template +struct Bridging< + std::shared_ptr, + std::enable_if_t>> { + static jsi::Value toJs( + jsi::Runtime &rt, + const std::shared_ptr &ptr, + const std::shared_ptr &jsInvoker) { + if (ptr) { + return bridging::toJs(rt, *ptr, jsInvoker); + } + return jsi::Value::null(); + } +}; + +template +struct Bridging> { + static jsi::Value toJs( + jsi::Runtime &rt, + const std::unique_ptr &ptr, + const std::shared_ptr &jsInvoker) { + if (ptr) { + return bridging::toJs(rt, *ptr, jsInvoker); + } + return jsi::Value::null(); + } +}; + +template +struct Bridging> { + static jsi::Value toJs( + jsi::Runtime &rt, + const std::weak_ptr &weakPtr, + const std::shared_ptr &jsInvoker) { + if (auto ptr = weakPtr.lock()) { + return bridging::toJs(rt, *ptr, jsInvoker); + } + return jsi::Value::null(); + } +}; + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/tests/BridgingTest.cpp b/ReactCommon/react/bridging/tests/BridgingTest.cpp new file mode 100644 index 0000000000..1fb001e6a1 --- /dev/null +++ b/ReactCommon/react/bridging/tests/BridgingTest.cpp @@ -0,0 +1,302 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "BridgingTest.h" + +namespace facebook::react { + +using namespace std::literals; + +TEST_F(BridgingTest, jsiTest) { + jsi::Value value = true; + jsi::Value string = jsi::String::createFromAscii(rt, "hello"); + jsi::Value object = jsi::Object(rt); + jsi::Value array = jsi::Array::createWithElements(rt, value, object); + jsi::Value func = function("() => {}"); + + // The bridging mechanism needs to know how to copy and downcast values. + EXPECT_NO_THROW(bridging::fromJs(rt, value, invoker)); + EXPECT_NO_THROW(bridging::fromJs(rt, string, invoker)); + EXPECT_NO_THROW(bridging::fromJs(rt, object, invoker)); + EXPECT_NO_THROW(bridging::fromJs(rt, array, invoker)); + EXPECT_NO_THROW(bridging::fromJs(rt, func, invoker)); + + // Should throw when attempting an invalid cast. + EXPECT_JSI_THROW(bridging::fromJs(rt, value, invoker)); + EXPECT_JSI_THROW(bridging::fromJs(rt, array, invoker)); + EXPECT_JSI_THROW(bridging::fromJs(rt, object, invoker)); + EXPECT_JSI_THROW(bridging::fromJs(rt, string, invoker)); + EXPECT_JSI_THROW(bridging::fromJs(rt, func, invoker)); + + // Should be able to generically no-op convert JSI. + EXPECT_NO_THROW(bridging::toJs(rt, value, invoker)); + EXPECT_NO_THROW(bridging::toJs(rt, string.asString(rt), invoker)); + EXPECT_NO_THROW(bridging::toJs(rt, object.asObject(rt), invoker)); + EXPECT_NO_THROW(bridging::toJs(rt, array.asObject(rt).asArray(rt), invoker)); + EXPECT_NO_THROW( + bridging::toJs(rt, func.asObject(rt).asFunction(rt), invoker)); +} + +TEST_F(BridgingTest, boolTest) { + EXPECT_TRUE(bridging::fromJs(rt, jsi::Value(true), invoker)); + EXPECT_FALSE(bridging::fromJs(rt, jsi::Value(false), invoker)); + EXPECT_JSI_THROW(bridging::fromJs(rt, jsi::Value(1), invoker)); + + EXPECT_TRUE(bridging::toJs(rt, true).asBool()); + EXPECT_FALSE(bridging::toJs(rt, false).asBool()); +} + +TEST_F(BridgingTest, numberTest) { + EXPECT_EQ(1, bridging::fromJs(rt, jsi::Value(1), invoker)); + EXPECT_FLOAT_EQ(1.2f, bridging::fromJs(rt, jsi::Value(1.2), invoker)); + EXPECT_DOUBLE_EQ(1.2, bridging::fromJs(rt, jsi::Value(1.2), invoker)); + EXPECT_JSI_THROW(bridging::fromJs(rt, jsi::Value(true), invoker)); + + EXPECT_EQ(1, static_cast(bridging::toJs(rt, 1).asNumber())); + EXPECT_FLOAT_EQ( + 1.2f, static_cast(bridging::toJs(rt, 1.2f).asNumber())); + EXPECT_DOUBLE_EQ(1.2, bridging::toJs(rt, 1.2).asNumber()); +} + +TEST_F(BridgingTest, stringTest) { + auto string = jsi::String::createFromAscii(rt, "hello"); + + EXPECT_EQ("hello"s, bridging::fromJs(rt, string, invoker)); + EXPECT_JSI_THROW(bridging::fromJs(rt, jsi::Value(1), invoker)); + + EXPECT_TRUE( + jsi::String::strictEquals(rt, string, bridging::toJs(rt, "hello"))); + EXPECT_TRUE( + jsi::String::strictEquals(rt, string, bridging::toJs(rt, "hello"s))); + EXPECT_TRUE( + jsi::String::strictEquals(rt, string, bridging::toJs(rt, "hello"sv))); +} + +TEST_F(BridgingTest, objectTest) { + auto object = jsi::Object(rt); + object.setProperty(rt, "foo", "bar"); + + auto omap = + bridging::fromJs>(rt, object, invoker); + auto umap = bridging::fromJs>( + rt, object, invoker); + auto bmap = bridging::fromJs>( + rt, object, invoker); + + EXPECT_EQ(1, omap.size()); + EXPECT_EQ(1, umap.size()); + EXPECT_EQ(1, bmap.size()); + EXPECT_EQ("bar"s, omap["foo"]); + EXPECT_EQ("bar"s, umap["foo"]); + EXPECT_EQ("bar"s, bmap["foo"]); + + EXPECT_EQ( + "bar"s, + bridging::toJs(rt, omap, invoker) + .getProperty(rt, "foo") + .asString(rt) + .utf8(rt)); + EXPECT_EQ( + "bar"s, + bridging::toJs(rt, umap, invoker) + .getProperty(rt, "foo") + .asString(rt) + .utf8(rt)); + EXPECT_EQ( + "bar"s, + bridging::toJs(rt, bmap, invoker) + .getProperty(rt, "foo") + .asString(rt) + .utf8(rt)); +} + +TEST_F(BridgingTest, hostObjectTest) { + struct TestHostObject : public jsi::HostObject { + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override { + if (name.utf8(rt) == "test") { + return jsi::Value(1); + } + return jsi::Value::undefined(); + } + }; + + auto hostobject = std::make_shared(); + auto object = bridging::toJs(rt, hostobject); + + EXPECT_EQ(1, object.getProperty(rt, "test").asNumber()); + EXPECT_EQ( + hostobject, bridging::fromJs(rt, object, invoker)); +} + +TEST_F(BridgingTest, weakbjectTest) { + auto object = jsi::Object(rt); + auto weakobject = jsi::WeakObject(rt, object); + + EXPECT_TRUE(jsi::Object::strictEquals( + rt, + object, + bridging::fromJs(rt, object, invoker) + .lock(rt) + .asObject(rt))); + + EXPECT_TRUE(jsi::Object::strictEquals( + rt, object, bridging::toJs(rt, weakobject).asObject(rt))); +} + +TEST_F(BridgingTest, arrayTest) { + auto vec = std::vector({"foo"s, "bar"s}); + auto array = jsi::Array::createWithElements(rt, "foo", "bar"); + + EXPECT_EQ( + vec, bridging::fromJs>(rt, array, invoker)); + + EXPECT_EQ(vec.size(), bridging::toJs(rt, vec, invoker).size(rt)); + for (size_t i = 0; i < vec.size(); i++) { + EXPECT_EQ( + vec[i], + bridging::toJs(rt, vec, invoker) + .getValueAtIndex(rt, i) + .asString(rt) + .utf8(rt)); + } + + EXPECT_EQ(2, bridging::toJs(rt, std::make_pair(1, "2"), invoker).size(rt)); + EXPECT_EQ(2, bridging::toJs(rt, std::make_tuple(1, "2"), invoker).size(rt)); + EXPECT_EQ(2, bridging::toJs(rt, std::array{1, 2}, invoker).size(rt)); + EXPECT_EQ(2, bridging::toJs(rt, std::deque{1, 2}, invoker).size(rt)); + EXPECT_EQ(2, bridging::toJs(rt, std::list{1, 2}, invoker).size(rt)); + EXPECT_EQ( + 2, + bridging::toJs(rt, std::initializer_list{1, 2}, invoker).size(rt)); +} + +TEST_F(BridgingTest, functionTest) { + auto object = jsi::Object(rt); + object.setProperty(rt, "foo", "bar"); + + auto lambda = [](std::map map, std::string key) { + return map[key]; + }; + + auto func = bridging::toJs(rt, lambda, invoker); + + EXPECT_EQ( + "bar"s, + func.call(rt, object, jsi::String::createFromAscii(rt, "foo")) + .asString(rt) + .utf8(rt)); + + // Should throw if not enough arguments are passed or are the wrong types. + EXPECT_JSI_THROW(func.call(rt, object)); + EXPECT_JSI_THROW(func.call(rt, object, jsi::Value(1))); + + // Test with non-capturing lambda converted to function pointer. + func = bridging::toJs(rt, +lambda, invoker); + + EXPECT_EQ( + "bar"s, + func.call(rt, object, jsi::String::createFromAscii(rt, "foo")) + .asString(rt) + .utf8(rt)); +} + +TEST_F(BridgingTest, syncCallbackTest) { + auto fn = function("(a, b) => a + b"); + auto cb = bridging::fromJs>( + rt, fn, invoker); + auto foo = "foo"s; + + EXPECT_EQ("foo1"s, cb(foo, 1)); // Tests lvalue string + EXPECT_EQ("bar2", cb("bar", 2)); // Tests rvalue C string + EXPECT_TRUE(fn.isFunction(rt)); // Ensure the function wasn't invalidated. +} + +TEST_F(BridgingTest, asyncCallbackTest) { + std::string output; + + auto func = std::function([&](auto str) { output = str; }); + + auto cb = bridging::fromJs>( + rt, function("(func, str) => func(str)"), invoker); + + cb(func, "hello"); + + flushQueue(); // Run scheduled async work + + EXPECT_EQ("hello"s, output); +} + +TEST_F(BridgingTest, promiseTest) { + auto func = function( + "(promise, obj) => {" + " promise.then(" + " (res) => { obj.res = res; }," + " (err) => { obj.err = err; }" + " )" + "}"); + + auto promise = AsyncPromise>(rt, invoker); + auto output = jsi::Object(rt); + + func.call(rt, bridging::toJs(rt, promise, invoker), output); + promise.resolve({"foo"s, "bar"s}); + flushQueue(); + + EXPECT_EQ(1, output.getPropertyNames(rt).size(rt)); + EXPECT_EQ(2, output.getProperty(rt, "res").asObject(rt).asArray(rt).size(rt)); + EXPECT_NO_THROW(promise.resolve({"ignored"})); + EXPECT_NO_THROW(promise.reject("ignored")); + + promise = AsyncPromise>(rt, invoker); + output = jsi::Object(rt); + + func.call(rt, bridging::toJs(rt, promise, invoker), output); + promise.reject("fail"); + flushQueue(); + + EXPECT_EQ(1, output.getPropertyNames(rt).size(rt)); + EXPECT_EQ( + "fail"s, + output.getProperty(rt, "err") + .asObject(rt) + .getProperty(rt, "message") + .asString(rt) + .utf8(rt)); + EXPECT_NO_THROW(promise.resolve({"ignored"})); + EXPECT_NO_THROW(promise.reject("ignored")); +} + +TEST_F(BridgingTest, optionalTest) { + EXPECT_EQ( + 1, bridging::fromJs>(rt, jsi::Value(1), invoker)); + EXPECT_FALSE( + bridging::fromJs>(rt, jsi::Value::undefined(), invoker) + .has_value()); + EXPECT_FALSE( + bridging::fromJs>(rt, jsi::Value::null(), invoker) + .has_value()); + + EXPECT_TRUE(bridging::toJs(rt, std::optional(), invoker).isNull()); + EXPECT_EQ(1, bridging::toJs(rt, std::optional(1), invoker).asNumber()); +} + +TEST_F(BridgingTest, pointerTest) { + auto str = "hi"s; + auto unique = std::make_unique(str); + auto shared = std::make_shared(str); + auto weak = std::weak_ptr(shared); + + EXPECT_EQ(str, bridging::toJs(rt, unique, invoker).asString(rt).utf8(rt)); + EXPECT_EQ(str, bridging::toJs(rt, shared, invoker).asString(rt).utf8(rt)); + EXPECT_EQ(str, bridging::toJs(rt, weak, invoker).asString(rt).utf8(rt)); + + shared.reset(); + + EXPECT_TRUE(bridging::toJs(rt, weak, invoker).isNull()); +} + +} // namespace facebook::react diff --git a/ReactCommon/react/bridging/tests/BridgingTest.h b/ReactCommon/react/bridging/tests/BridgingTest.h new file mode 100644 index 0000000000..38fbef8354 --- /dev/null +++ b/ReactCommon/react/bridging/tests/BridgingTest.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +#define EXPECT_JSI_THROW(expr) EXPECT_THROW((expr), facebook::jsi::JSIException) + +namespace facebook::react { + +class TestCallInvoker : public CallInvoker { + public: + void invokeAsync(std::function &&fn) override { + queue_.push_back(std::move(fn)); + } + + void invokeSync(std::function &&) override { + FAIL() << "JSCallInvoker does not support invokeSync()"; + } + + private: + friend class BridgingTest; + + std::list> queue_; +}; + +class BridgingTest : public ::testing::Test { + protected: + BridgingTest() + : invoker(std::make_shared()), + runtime(hermes::makeHermesRuntime( + ::hermes::vm::RuntimeConfig::Builder() + // Make promises work with Hermes microtasks. + .withVMExperimentFlags(1 << 14 /* JobQueue */) + .build())), + rt(*runtime) {} + + ~BridgingTest() { + LongLivedObjectCollection::get().clear(); + } + + void TearDown() override { + flushQueue(); + + // After flushing the invoker queue, we shouldn't leak memory. + EXPECT_EQ(0, LongLivedObjectCollection::get().size()); + } + + jsi::Value eval(const std::string &js) { + return rt.global().getPropertyAsFunction(rt, "eval").call(rt, js); + } + + jsi::Function function(const std::string &js) { + return eval(("(" + js + ")").c_str()).getObject(rt).getFunction(rt); + } + + void flushQueue() { + while (!invoker->queue_.empty()) { + invoker->queue_.front()(); + invoker->queue_.pop_front(); + rt.drainMicrotasks(); // Run microtasks every cycle. + } + } + + std::shared_ptr invoker; + std::unique_ptr runtime; + jsi::Runtime &rt; +}; + +} // namespace facebook::react