diff --git a/ReactCommon/react/bridging/Array.h b/ReactCommon/react/bridging/Array.h new file mode 100644 index 00000000000..330e2e1a4f8 --- /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 eb5b3ae0ae4..d7de59311a0 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 00000000000..5ba0e5b5b01 --- /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 00000000000..b720e120aee --- /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 00000000000..340ad0dee56 --- /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 92bba36ccb3..fc2980cb925 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 00000000000..f594fd5c815 --- /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 00000000000..eed77305d70 --- /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 00000000000..fdac94bb6fd --- /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 c449e7d180c..dc6d44dbe61 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 332dccb1659..cfb10086d09 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 00000000000..4857b9eec24 --- /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 00000000000..48074f3617c --- /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 00000000000..00fb0151610 --- /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 00000000000..cad91246ae4 --- /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 00000000000..295565c1dd2 --- /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 00000000000..1fb001e6a1e --- /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 00000000000..38fbef8354c --- /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