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<T>` 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<R(Args...)>` or `AsyncCallback<Args...>` 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<T>` 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
This commit is contained in:
Scott Kyle 2022-03-11 12:47:51 -08:00 коммит произвёл Facebook GitHub Bot
Родитель 6a9497dbbb
Коммит 30cb78e709
18 изменённых файлов: 1469 добавлений и 1 удалений

Просмотреть файл

@ -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 <react/bridging/Base.h>
#include <array>
#include <deque>
#include <initializer_list>
#include <list>
#include <tuple>
#include <utility>
#include <vector>
namespace facebook::react {
namespace array_detail {
template <typename T, size_t N>
struct BridgingStatic {
static jsi::Array toJs(
jsi::Runtime &rt,
const T &array,
const std::shared_ptr<CallInvoker> &jsInvoker) {
return toJs(rt, array, jsInvoker, std::make_index_sequence<N>{});
}
private:
template <size_t... Index>
static jsi::Array toJs(
facebook::jsi::Runtime &rt,
const T &array,
const std::shared_ptr<CallInvoker> &jsInvoker,
std::index_sequence<Index...>) {
return jsi::Array::createWithElements(
rt, bridging::toJs(rt, std::get<Index>(array), jsInvoker)...);
}
};
template <typename T>
struct BridgingDynamic {
static jsi::Array toJs(
jsi::Runtime &rt,
const T &list,
const std::shared_ptr<CallInvoker> &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 <typename T, size_t N>
struct Bridging<std::array<T, N>>
: array_detail::BridgingStatic<std::array<T, N>, N> {};
template <typename T1, typename T2>
struct Bridging<std::pair<T1, T2>>
: array_detail::BridgingStatic<std::pair<T1, T2>, 2> {};
template <typename... Types>
struct Bridging<std::tuple<Types...>>
: array_detail::BridgingStatic<std::tuple<Types...>, sizeof...(Types)> {};
template <typename T>
struct Bridging<std::deque<T>> : array_detail::BridgingDynamic<std::deque<T>> {
};
template <typename T>
struct Bridging<std::initializer_list<T>>
: array_detail::BridgingDynamic<std::initializer_list<T>> {};
template <typename T>
struct Bridging<std::list<T>> : array_detail::BridgingDynamic<std::list<T>> {};
template <typename T>
struct Bridging<std::vector<T>>
: array_detail::BridgingDynamic<std::vector<T>> {
static std::vector<T> fromJs(
facebook::jsi::Runtime &rt,
const jsi::Array &array,
const std::shared_ptr<CallInvoker> &jsInvoker) {
size_t length = array.length(rt);
std::vector<T> vector;
vector.reserve(length);
for (size_t i = 0; i < length; i++) {
vector.push_back(
bridging::fromJs<T>(rt, array.getValueAtIndex(rt, i), jsInvoker));
}
return vector;
}
};
} // namespace facebook::react

Просмотреть файл

@ -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",
],
)

Просмотреть файл

@ -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 <react/bridging/Convert.h>
#include <ReactCommon/CallInvoker.h>
#include <folly/Function.h>
#include <jsi/jsi.h>
#include <cstdint>
#include <memory>
#include <type_traits>
namespace facebook::react {
template <typename T, typename = void>
struct Bridging;
template <>
struct Bridging<void> {
// Highly generic code may result in "casting" to void.
static void fromJs(jsi::Runtime &, const jsi::Value &) {}
};
namespace bridging {
namespace detail {
template <typename F>
struct function_wrapper;
template <typename C, typename R, typename... Args>
struct function_wrapper<R (C::*)(Args...)> {
using type = folly::Function<R(Args...)>;
};
template <typename C, typename R, typename... Args>
struct function_wrapper<R (C::*)(Args...) const> {
using type = folly::Function<R(Args...)>;
};
template <typename T, typename = void>
struct bridging_wrapper {
using type = remove_cvref_t<T>;
};
// Convert lambda types to move-only function types since we can't specialize
// Bridging templates for arbitrary lambdas.
template <typename T>
struct bridging_wrapper<
T,
std::void_t<decltype(&remove_cvref_t<T>::operator())>>
: function_wrapper<decltype(&remove_cvref_t<T>::operator())> {};
} // namespace detail
template <typename T>
using bridging_t = typename detail::bridging_wrapper<T>::type;
template <typename R, typename T, std::enable_if_t<is_jsi_v<T>, int> = 0>
auto fromJs(jsi::Runtime &rt, T &&value, const std::shared_ptr<CallInvoker> &)
-> decltype(static_cast<R>(convert(rt, std::forward<T>(value)))) {
return convert(rt, std::forward<T>(value));
}
template <typename R, typename T>
auto fromJs(jsi::Runtime &rt, T &&value, const std::shared_ptr<CallInvoker> &)
-> decltype(Bridging<remove_cvref_t<R>>::fromJs(
rt,
convert(rt, std::forward<T>(value)))) {
return Bridging<remove_cvref_t<R>>::fromJs(
rt, convert(rt, std::forward<T>(value)));
}
template <typename R, typename T>
auto fromJs(
jsi::Runtime &rt,
T &&value,
const std::shared_ptr<CallInvoker> &jsInvoker)
-> decltype(Bridging<remove_cvref_t<R>>::fromJs(
rt,
convert(rt, std::forward<T>(value)),
jsInvoker)) {
return Bridging<remove_cvref_t<R>>::fromJs(
rt, convert(rt, std::forward<T>(value)), jsInvoker);
}
template <typename T, std::enable_if_t<is_jsi_v<T>, int> = 0>
auto toJs(
jsi::Runtime &rt,
T &&value,
const std::shared_ptr<CallInvoker> & = nullptr)
-> decltype(convert(rt, std::forward<T>(value))) {
return convert(rt, std::forward<T>(value));
}
template <typename T>
auto toJs(
jsi::Runtime &rt,
T &&value,
const std::shared_ptr<CallInvoker> & = nullptr)
-> decltype(Bridging<bridging_t<T>>::toJs(rt, std::forward<T>(value))) {
return Bridging<bridging_t<T>>::toJs(rt, std::forward<T>(value));
}
template <typename T>
auto toJs(
jsi::Runtime &rt,
T &&value,
const std::shared_ptr<CallInvoker> &jsInvoker)
-> decltype(Bridging<bridging_t<T>>::toJs(
rt,
std::forward<T>(value),
jsInvoker)) {
return Bridging<bridging_t<T>>::toJs(rt, std::forward<T>(value), jsInvoker);
}
} // namespace bridging
} // namespace facebook::react

Просмотреть файл

@ -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 <react/bridging/Base.h>
namespace facebook::react {
template <>
struct Bridging<bool> {
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

Просмотреть файл

@ -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 <react/bridging/Array.h>
#include <react/bridging/Bool.h>
#include <react/bridging/Error.h>
#include <react/bridging/Function.h>
#include <react/bridging/Number.h>
#include <react/bridging/Object.h>
#include <react/bridging/Promise.h>
#include <react/bridging/String.h>
#include <react/bridging/Value.h>

Просмотреть файл

@ -84,6 +84,10 @@ class CallbackWrapper : public LongLivedObject {
return *(jsInvoker_);
}
std::shared_ptr<CallInvoker> jsInvokerPtr() {
return jsInvoker_;
}
void allowRelease() override {
if (auto longLivedObjectCollection = longLivedObjectCollection_.lock()) {
if (longLivedObjectCollection != nullptr) {

Просмотреть файл

@ -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 <jsi/jsi.h>
#include <type_traits>
namespace facebook::react::bridging {
// std::remove_cvref_t is not available until C++20.
template <typename T>
using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
template <typename T>
inline constexpr bool is_jsi_v =
std::is_same_v<jsi::Value, remove_cvref_t<T>> ||
std::is_same_v<jsi::String, remove_cvref_t<T>> ||
std::is_base_of_v<jsi::Object, remove_cvref_t<T>>;
template <typename T>
struct ConverterBase {
ConverterBase(jsi::Runtime &rt, T &&value)
: rt_(rt), value_(std::forward<T>(value)) {}
operator T() && {
return std::forward<T>(this->value_);
}
protected:
jsi::Runtime &rt_;
T value_;
};
template <typename T>
struct Converter : public ConverterBase<T> {
using ConverterBase<T>::ConverterBase;
};
template <>
struct Converter<jsi::Value> : public ConverterBase<jsi::Value> {
using ConverterBase<jsi::Value>::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<jsi::Object> : public ConverterBase<jsi::Object> {
using ConverterBase<jsi::Object>::ConverterBase;
operator jsi::Array() && {
return std::move(value_).asArray(rt_);
}
operator jsi::Function() && {
return std::move(value_).asFunction(rt_);
}
};
template <typename T>
struct Converter<T &> {
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<jsi::Value>(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<Converter<std::remove_cv_t<T>>, U>,
int> = 0>
operator U() && {
return Converter<jsi::Value>(rt_, jsi::Value(rt_, value_));
}
private:
jsi::Runtime &rt_;
const T &value_;
};
template <typename T, std::enable_if_t<is_jsi_v<T>, int> = 0>
auto convert(jsi::Runtime &rt, T &&value) {
return Converter<T>(rt, std::forward<T>(value));
}
template <typename T, std::enable_if_t<std::is_scalar_v<T>, int> = 0>
auto convert(jsi::Runtime &rt, T &&value) {
return value;
}
template <typename T>
auto convert(jsi::Runtime &rt, Converter<T> &&converter) {
return std::move(converter);
}
} // namespace facebook::react::bridging

Просмотреть файл

@ -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 <react/bridging/Base.h>
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<jsi::JSError> {
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<Error> {
static jsi::Value toJs(jsi::Runtime &rt, const Error &error) {
return jsi::Value(rt, jsi::JSError(rt, error.message()).value());
}
};
} // namespace facebook::react

Просмотреть файл

@ -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 <react/bridging/Base.h>
#include <react/bridging/CallbackWrapper.h>
#include <folly/Function.h>
namespace facebook::react {
template <typename F>
class SyncCallback;
template <typename... Args>
class AsyncCallback {
public:
AsyncCallback(
jsi::Runtime &runtime,
jsi::Function function,
std::shared_ptr<CallInvoker> jsInvoker)
: callback_(std::make_shared<SyncCallback<void(Args...)>>(
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>(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>(args)...);
wrapper->jsInvoker().invokeAsync(
[callback = callback_,
argsPtr = std::make_shared<decltype(argsTuple)>(
std::move(argsTuple))] { callback->apply(std::move(*argsPtr)); });
}
private:
friend Bridging<AsyncCallback>;
std::shared_ptr<SyncCallback<void(Args...)>> callback_;
};
template <typename R, typename... Args>
class SyncCallback<R(Args...)> {
public:
SyncCallback(
jsi::Runtime &rt,
jsi::Function function,
std::shared_ptr<CallInvoker> 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>(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<R>) {
callback.call(
rt, bridging::toJs(rt, std::forward<Args>(args), jsInvoker)...);
} else {
return bridging::fromJs<R>(
rt,
callback.call(
rt, bridging::toJs(rt, std::forward<Args>(args), jsInvoker)...),
jsInvoker);
}
}
private:
friend AsyncCallback<Args...>;
friend Bridging<SyncCallback>;
R apply(std::tuple<Args...> &&args) const {
return apply(std::move(args), std::index_sequence_for<Args...>{});
}
template <size_t... Index>
R apply(std::tuple<Args...> &&args, std::index_sequence<Index...>) const {
return call(std::move(std::get<Index>(args))...);
}
// Held weakly so lifetime is managed by LongLivedObjectCollection.
std::weak_ptr<CallbackWrapper> wrapper_;
};
template <typename... Args>
struct Bridging<AsyncCallback<Args...>> {
static AsyncCallback<Args...> fromJs(
jsi::Runtime &rt,
jsi::Function &&value,
const std::shared_ptr<CallInvoker> &jsInvoker) {
return AsyncCallback<Args...>(rt, std::move(value), jsInvoker);
}
static jsi::Function toJs(
jsi::Runtime &rt,
const AsyncCallback<Args...> &value) {
return value.callback_->function_.getFunction(rt);
}
};
template <typename R, typename... Args>
struct Bridging<SyncCallback<R(Args...)>> {
static SyncCallback<R(Args...)> fromJs(
jsi::Runtime &rt,
jsi::Function &&value,
const std::shared_ptr<CallInvoker> &jsInvoker) {
return SyncCallback<R(Args...)>(rt, std::move(value), jsInvoker);
}
static jsi::Function toJs(
jsi::Runtime &rt,
const SyncCallback<R(Args...)> &value) {
return value.function_.getFunction(rt);
}
};
template <typename R, typename... Args>
struct Bridging<folly::Function<R(Args...)>> {
using Func = folly::Function<R(Args...)>;
using IndexSequence = std::index_sequence_for<Args...>;
static constexpr size_t kArgumentCount = sizeof...(Args);
static jsi::Function toJs(
jsi::Runtime &rt,
Func fn,
const std::shared_ptr<CallInvoker> &jsInvoker) {
return jsi::Function::createFromHostFunction(
rt,
jsi::PropNameID::forAscii(rt, "BridgedFunction"),
kArgumentCount,
[fn = std::make_shared<Func>(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<R>) {
callFromJs(*fn, rt, args, jsInvoker, IndexSequence{});
return jsi::Value();
} else {
return bridging::toJs(
rt,
callFromJs(*fn, rt, args, jsInvoker, IndexSequence{}),
jsInvoker);
}
});
}
private:
template <size_t... Index>
static R callFromJs(
Func &fn,
jsi::Runtime &rt,
const jsi::Value *args,
const std::shared_ptr<CallInvoker> &jsInvoker,
std::index_sequence<Index...>) {
return fn(bridging::fromJs<Args>(rt, args[Index], jsInvoker)...);
}
};
template <typename R, typename... Args>
struct Bridging<std::function<R(Args...)>>
: Bridging<folly::Function<R(Args...)>> {};
template <typename R, typename... Args>
struct Bridging<R(Args...)> : Bridging<folly::Function<R(Args...)>> {};
template <typename R, typename... Args>
struct Bridging<R (*)(Args...)> : Bridging<folly::Function<R(Args...)>> {};
} // namespace facebook::react

Просмотреть файл

@ -41,6 +41,11 @@ void LongLivedObjectCollection::clear() const {
collection_.clear();
}
size_t LongLivedObjectCollection::size() const {
std::lock_guard<std::mutex> lock(collectionMutex_);
return collection_.size();
}
// LongLivedObject
LongLivedObject::LongLivedObject() {}
LongLivedObject::~LongLivedObject() {}

Просмотреть файл

@ -47,6 +47,7 @@ class LongLivedObjectCollection {
void add(std::shared_ptr<LongLivedObject> o) const;
void remove(const LongLivedObject *o) const;
void clear() const;
size_t size() const;
private:
mutable std::unordered_set<std::shared_ptr<LongLivedObject>> collection_;

Просмотреть файл

@ -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 <react/bridging/Base.h>
namespace facebook::react {
template <>
struct Bridging<double> {
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<float> {
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<int32_t> {
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

Просмотреть файл

@ -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 <react/bridging/Base.h>
#include <react/bridging/String.h>
#include <butter/map.h>
#include <map>
#include <unordered_map>
namespace facebook::react {
template <>
struct Bridging<jsi::WeakObject> {
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 <typename T>
struct Bridging<
std::shared_ptr<T>,
std::enable_if_t<std::is_base_of_v<jsi::HostObject, T>>> {
static std::shared_ptr<T> fromJs(jsi::Runtime &rt, const jsi::Object &value) {
return value.asHostObject<T>(rt);
}
static jsi::Object toJs(jsi::Runtime &rt, std::shared_ptr<T> value) {
return jsi::Object::createFromHostObject(rt, std::move(value));
}
};
namespace map_detail {
template <typename T>
struct Bridging {
static T fromJs(
jsi::Runtime &rt,
const jsi::Object &value,
const std::shared_ptr<CallInvoker> &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<std::string>(rt, propertyName, jsInvoker),
bridging::fromJs<typename T::mapped_type>(
rt, value.getProperty(rt, propertyName.asString(rt)), jsInvoker));
}
return result;
}
static jsi::Object toJs(
jsi::Runtime &rt,
const T &map,
const std::shared_ptr<CallInvoker> &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 <typename... Args>
struct Bridging<butter::map<std::string, Args...>>
: map_detail::Bridging<butter::map<std::string, Args...>> {};
#endif
template <typename... Args>
struct Bridging<std::map<std::string, Args...>>
: map_detail::Bridging<std::map<std::string, Args...>> {};
template <typename... Args>
struct Bridging<std::unordered_map<std::string, Args...>>
: map_detail::Bridging<std::unordered_map<std::string, Args...>> {};
} // namespace facebook::react

Просмотреть файл

@ -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 <react/bridging/Error.h>
#include <react/bridging/Function.h>
#include <react/bridging/LongLivedObject.h>
#include <mutex>
#include <optional>
namespace facebook::react {
template <typename T>
class AsyncPromise {
public:
AsyncPromise(jsi::Runtime &rt, const std::shared_ptr<CallInvoker> &jsInvoker)
: state_(std::make_shared<SharedState>()) {
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<T> resolve, AsyncCallback<Error> reject) {
state_->resolve = std::move(resolve);
state_->reject = std::move(reject);
},
jsInvoker));
auto promiseHolder = std::make_shared<PromiseHolder>(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<std::mutex> 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<std::mutex> 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> promiseHolder;
std::optional<AsyncCallback<T>> resolve;
std::optional<AsyncCallback<Error>> reject;
};
std::shared_ptr<SharedState> state_;
};
template <typename T>
struct Bridging<AsyncPromise<T>> {
static jsi::Object toJs(jsi::Runtime &rt, const AsyncPromise<T> &promise) {
return promise.get(rt);
}
};
} // namespace facebook::react

Просмотреть файл

@ -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 <react/bridging/Base.h>
#include <string>
#include <string_view>
namespace facebook::react {
template <>
struct Bridging<std::string> {
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<std::string_view> {
static jsi::String toJs(jsi::Runtime &rt, std::string_view value) {
return jsi::String::createFromUtf8(
rt, reinterpret_cast<const uint8_t *>(value.data()), value.length());
}
};
template <>
struct Bridging<const char *> : Bridging<std::string_view> {};
template <size_t N>
struct Bridging<char[N]> : Bridging<std::string_view> {};
} // namespace facebook::react

Просмотреть файл

@ -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 <react/bridging/Base.h>
#include <memory>
#include <optional>
namespace facebook::react {
template <>
struct Bridging<std::nullptr_t> {
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 <typename T>
struct Bridging<std::optional<T>> {
static std::optional<T> fromJs(
jsi::Runtime &rt,
const jsi::Value &value,
const std::shared_ptr<CallInvoker> &jsInvoker) {
if (value.isNull() || value.isUndefined()) {
return {};
}
return bridging::fromJs<T>(rt, value, jsInvoker);
}
static jsi::Value toJs(
jsi::Runtime &rt,
const std::optional<T> &value,
const std::shared_ptr<CallInvoker> &jsInvoker) {
if (value) {
return bridging::toJs(rt, *value, jsInvoker);
}
return jsi::Value::null();
}
};
template <typename T>
struct Bridging<
std::shared_ptr<T>,
std::enable_if_t<!std::is_base_of_v<jsi::HostObject, T>>> {
static jsi::Value toJs(
jsi::Runtime &rt,
const std::shared_ptr<T> &ptr,
const std::shared_ptr<CallInvoker> &jsInvoker) {
if (ptr) {
return bridging::toJs(rt, *ptr, jsInvoker);
}
return jsi::Value::null();
}
};
template <typename T>
struct Bridging<std::unique_ptr<T>> {
static jsi::Value toJs(
jsi::Runtime &rt,
const std::unique_ptr<T> &ptr,
const std::shared_ptr<CallInvoker> &jsInvoker) {
if (ptr) {
return bridging::toJs(rt, *ptr, jsInvoker);
}
return jsi::Value::null();
}
};
template <typename T>
struct Bridging<std::weak_ptr<T>> {
static jsi::Value toJs(
jsi::Runtime &rt,
const std::weak_ptr<T> &weakPtr,
const std::shared_ptr<CallInvoker> &jsInvoker) {
if (auto ptr = weakPtr.lock()) {
return bridging::toJs(rt, *ptr, jsInvoker);
}
return jsi::Value::null();
}
};
} // namespace facebook::react

Просмотреть файл

@ -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<jsi::Value>(rt, value, invoker));
EXPECT_NO_THROW(bridging::fromJs<jsi::String>(rt, string, invoker));
EXPECT_NO_THROW(bridging::fromJs<jsi::Object>(rt, object, invoker));
EXPECT_NO_THROW(bridging::fromJs<jsi::Array>(rt, array, invoker));
EXPECT_NO_THROW(bridging::fromJs<jsi::Function>(rt, func, invoker));
// Should throw when attempting an invalid cast.
EXPECT_JSI_THROW(bridging::fromJs<jsi::Object>(rt, value, invoker));
EXPECT_JSI_THROW(bridging::fromJs<jsi::String>(rt, array, invoker));
EXPECT_JSI_THROW(bridging::fromJs<jsi::Array>(rt, object, invoker));
EXPECT_JSI_THROW(bridging::fromJs<jsi::Array>(rt, string, invoker));
EXPECT_JSI_THROW(bridging::fromJs<jsi::Array>(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<bool>(rt, jsi::Value(true), invoker));
EXPECT_FALSE(bridging::fromJs<bool>(rt, jsi::Value(false), invoker));
EXPECT_JSI_THROW(bridging::fromJs<bool>(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<int>(rt, jsi::Value(1), invoker));
EXPECT_FLOAT_EQ(1.2f, bridging::fromJs<float>(rt, jsi::Value(1.2), invoker));
EXPECT_DOUBLE_EQ(1.2, bridging::fromJs<double>(rt, jsi::Value(1.2), invoker));
EXPECT_JSI_THROW(bridging::fromJs<double>(rt, jsi::Value(true), invoker));
EXPECT_EQ(1, static_cast<int>(bridging::toJs(rt, 1).asNumber()));
EXPECT_FLOAT_EQ(
1.2f, static_cast<float>(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<std::string>(rt, string, invoker));
EXPECT_JSI_THROW(bridging::fromJs<std::string>(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<std::map<std::string, std::string>>(rt, object, invoker);
auto umap = bridging::fromJs<std::unordered_map<std::string, std::string>>(
rt, object, invoker);
auto bmap = bridging::fromJs<butter::map<std::string, std::string>>(
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<TestHostObject>();
auto object = bridging::toJs(rt, hostobject);
EXPECT_EQ(1, object.getProperty(rt, "test").asNumber());
EXPECT_EQ(
hostobject, bridging::fromJs<decltype(hostobject)>(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<jsi::WeakObject>(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<std::vector<std::string>>(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<int, 2>{1, 2}, invoker).size(rt));
EXPECT_EQ(2, bridging::toJs(rt, std::deque<int>{1, 2}, invoker).size(rt));
EXPECT_EQ(2, bridging::toJs(rt, std::list<int>{1, 2}, invoker).size(rt));
EXPECT_EQ(
2,
bridging::toJs(rt, std::initializer_list<int>{1, 2}, invoker).size(rt));
}
TEST_F(BridgingTest, functionTest) {
auto object = jsi::Object(rt);
object.setProperty(rt, "foo", "bar");
auto lambda = [](std::map<std::string, std::string> 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<SyncCallback<std::string(std::string, int)>>(
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<void(std::string)>([&](auto str) { output = str; });
auto cb = bridging::fromJs<AsyncCallback<decltype(func), std::string>>(
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<std::vector<std::string>>(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<std::vector<std::string>>(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<std::optional<int>>(rt, jsi::Value(1), invoker));
EXPECT_FALSE(
bridging::fromJs<std::optional<int>>(rt, jsi::Value::undefined(), invoker)
.has_value());
EXPECT_FALSE(
bridging::fromJs<std::optional<int>>(rt, jsi::Value::null(), invoker)
.has_value());
EXPECT_TRUE(bridging::toJs(rt, std::optional<int>(), invoker).isNull());
EXPECT_EQ(1, bridging::toJs(rt, std::optional<int>(1), invoker).asNumber());
}
TEST_F(BridgingTest, pointerTest) {
auto str = "hi"s;
auto unique = std::make_unique<std::string>(str);
auto shared = std::make_shared<std::string>(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

Просмотреть файл

@ -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 <gtest/gtest.h>
#include <hermes/hermes.h>
#include <react/bridging/Bridging.h>
#define EXPECT_JSI_THROW(expr) EXPECT_THROW((expr), facebook::jsi::JSIException)
namespace facebook::react {
class TestCallInvoker : public CallInvoker {
public:
void invokeAsync(std::function<void()> &&fn) override {
queue_.push_back(std::move(fn));
}
void invokeSync(std::function<void()> &&) override {
FAIL() << "JSCallInvoker does not support invokeSync()";
}
private:
friend class BridgingTest;
std::list<std::function<void()>> queue_;
};
class BridgingTest : public ::testing::Test {
protected:
BridgingTest()
: invoker(std::make_shared<TestCallInvoker>()),
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<TestCallInvoker> invoker;
std::unique_ptr<jsi::Runtime> runtime;
jsi::Runtime &rt;
};
} // namespace facebook::react