From 61ffa5810eaca46e281f7ac6f41c05b9613e84a9 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Fri, 15 Mar 2024 01:37:13 -0700 Subject: [PATCH] Add C++20 std::span-based APIs for function arguments (#43489) Summary: Changelog: [General][Changed] - Add `std::span` methods to `jsi::Function` when compiled as C++20 Adds `std::span` overloads to public JSI methods that currently accept (or are designed to emit) `const jsi::Value* args, size_t count` for a dynamically-sized span of function arguments. These are: * `Function::call` * `Function::callWithThis` * `Function::callAsConstructor` * `Function::createFromHostFunction` ## Backwards compatibility * The new APIs are compiled conditionally to support pre-C++20 compilers. * No overloads are removed. * The new overloads are implemented as `inline` wrappers over the pointer+size versions. * No change is made to the JSI contract for implementers (i.e. no new overloads, virtual or otherwise, are added to `Runtime`). Differential Revision: D54901847 --- .../ReactCommon/jsi/jsi/jsi-inl.h | 48 ++++++++ .../react-native/ReactCommon/jsi/jsi/jsi.h | 108 +++++++++++++++++- .../ReactCommon/jsi/jsi/test/testlib.cpp | 102 +++++++++++++++++ 3 files changed, 256 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactCommon/jsi/jsi/jsi-inl.h b/packages/react-native/ReactCommon/jsi/jsi/jsi-inl.h index f3955815e41f06..0cce7919669395 100644 --- a/packages/react-native/ReactCommon/jsi/jsi/jsi-inl.h +++ b/packages/react-native/ReactCommon/jsi/jsi/jsi-inl.h @@ -348,6 +348,54 @@ inline Value Function::callAsConstructor(Runtime& runtime, Args&&... args) runtime, {detail::toValue(runtime, std::forward(args))...}); } +#ifdef JSI_FUNCTION_HAS_SPAN_APIS + +template +inline Function Function::createFromHostFunction( + Runtime& runtime, + const jsi::PropNameID& name, + unsigned int paramCount, + Fn func) requires CallableAsHostFunction || + CallableAsHostFunctionWithSpan { + HostFunctionType hostFunc; + if constexpr (CallableAsHostFunction) { + hostFunc = std::move(func); + } else { + hostFunc = [func = std::move(func)]( + Runtime& rt, + const Value& thisVal, + const Value* args, + size_t count) mutable -> Value { + return func(rt, thisVal, {args, count}); + }; + } + return createFromHostFunction(runtime, name, paramCount, hostFunc); +} + +template +inline Value Function::call(Runtime& runtime, Span&& args) + const requires std::convertible_to> { + auto argsSpan = std::span(std::forward(args)); + return call(runtime, argsSpan.data(), argsSpan.size()); +} + +template +inline Value +Function::callWithThis(Runtime& runtime, const Object& jsThis, Span&& args) + const requires std::convertible_to> { + auto argsSpan = std::span(std::forward(args)); + return callWithThis(runtime, jsThis, argsSpan.data(), argsSpan.size()); +} + +template +inline Value Function::callAsConstructor(Runtime& runtime, Span&& args) + const requires std::convertible_to> { + auto argsSpan = std::span(std::forward(args)); + return callAsConstructor(runtime, argsSpan.data(), argsSpan.size()); +} + +#endif // JSI_FUNCTION_HAS_SPAN_APIS + String BigInt::toString(Runtime& runtime, int radix) const { return runtime.bigintToString(*this, radix); } diff --git a/packages/react-native/ReactCommon/jsi/jsi/jsi.h b/packages/react-native/ReactCommon/jsi/jsi/jsi.h index d77255f61a86c7..fde376483e9395 100644 --- a/packages/react-native/ReactCommon/jsi/jsi/jsi.h +++ b/packages/react-native/ReactCommon/jsi/jsi/jsi.h @@ -15,6 +15,17 @@ #include #include +#ifdef JSI_FUNCTION_HAS_SPAN_APIS +#undef JSI_FUNCTION_HAS_SPAN_APIS +#endif + +#if defined(__cpp_concepts) && __has_include() && \ + __has_include() +#include +#include +#define JSI_FUNCTION_HAS_SPAN_APIS +#endif + #ifndef JSI_EXPORT #ifdef _MSC_VER #ifdef CREATE_SHARED_LIBRARY @@ -111,6 +122,46 @@ class JSError; using HostFunctionType = std::function< Value(Runtime& rt, const Value& thisVal, const Value* args, size_t count)>; +#ifdef JSI_FUNCTION_HAS_SPAN_APIS +/// A function which has this type can be registered as a function +/// callable from JavaScript using Function::createFromHostFunction(). +/// When the function is called, args will point to the arguments, and +/// count will indicate how many arguments are passed. The function +/// can return a Value to the caller, or throw an exception. If a C++ +/// exception is thrown, a JS Error will be created and thrown into +/// JS; if the C++ exception extends std::exception, the Error's +/// message will be whatever what() returns. Note that it is undefined whether +/// HostFunctions may or may not be called in strict mode; that is `thisVal` +/// can be any value - it will not necessarily be coerced to an object or +/// or set to the global object. +template +concept CallableAsHostFunction = std::is_invocable_r_v< + Value /*return value*/, + Fn, + Runtime& /*rt*/, + const Value& /*thisVal*/, + const Value* /*args*/, + size_t /*count*/>; + +/// A function which satisfies this concept can be registered as a function +/// callable from JavaScript using Function::createFromHostFunction(). +/// When the function is called, args will point to the arguments. The function +/// can return a Value to the caller, or throw an exception. If a C++ +/// exception is thrown, a JS Error will be created and thrown into +/// JS; if the C++ exception extends std::exception, the Error's +/// message will be whatever what() returns. Note that it is undefined whether +/// HostFunctions may or may not be called in strict mode; that is `thisVal` +/// can be any value - it will not necessarily be coerced to an object or +/// or set to the global object. +template +concept CallableAsHostFunctionWithSpan = std::is_invocable_r_v< + Value /*return value*/, + Fn, + Runtime& /*rt*/, + const Value& /*thisVal*/, + std::span /*args*/>; +#endif // JSI_FUNCTION_HAS_SPAN_APIS + /// An object which implements this interface can be registered as an /// Object with the JS runtime. class JSI_EXPORT HostObject { @@ -1089,8 +1140,56 @@ class JSI_EXPORT Function : public Object { return runtime.getHostFunction(*this); } - private: - friend class Object; +#ifdef JSI_FUNCTION_HAS_SPAN_APIS + /// Create a function which, when invoked, calls C++ code. If the + /// function throws an exception, a JS Error will be created and + /// thrown. + /// \param name the name property for the function. + /// \param paramCount the length property for the function, which + /// may not be the number of arguments the function is passed. + template + static inline Function createFromHostFunction( + Runtime& runtime, + const jsi::PropNameID& name, + unsigned int paramCount, + Fn func) requires CallableAsHostFunction || + CallableAsHostFunctionWithSpan; + + // The std::span overloads of call[...] below are templated to prevent + // ambiguity with the variadic (Args...) overloads that attempt to convert + // each argument to a Value. They accept anything that can be implicitly + // converted to std::span, including: + // - span + // - span + // - span where N is a static extent + // - span where N is a static extent + // - array, vector etc + + /// Calls the function with a \c std::span of Value arguments. The \c this + /// value of the JS function will not be set by the C++ caller, similar to + /// calling Function.prototype.apply(undefined, args) in JS. + /// \b Note: as with Function.prototype.apply, \c this may not always be + /// \c undefined in the function itself. If the function is non-strict, + /// \c this will be set to the global object. + template + inline Value call(Runtime& runtime, Span&& args) + const requires std::convertible_to>; + + /// Calls the function with a \c std::span of Value arguments and \c jsThis + /// passed as the \c this value. + template + inline Value callWithThis(Runtime& runtime, const Object& jsThis, Span&& args) + const requires std::convertible_to>; + + /// Same as above `callAsConstructor`, except use a std::span to + /// supply the arguments. + template + inline Value callAsConstructor(Runtime& runtime, Span&& args) + const requires std::convertible_to>; + +#endif // JSI_FUNCTION_HAS_SPAN_APIS + + private : friend class Object; friend class Value; friend class Runtime; @@ -1529,3 +1628,8 @@ class JSI_EXPORT JSError : public JSIException { } // namespace facebook #include + +#ifdef JSI_FUNCTION_HAS_SPAN_APIS +// Don't leak to includers +#undef JSI_FUNCTION_HAS_SPAN_APIS +#endif // JSI_FUNCTION_HAS_SPAN_APIS diff --git a/packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp b/packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp index d9090bdb2aac65..84051ecb1009d2 100644 --- a/packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp +++ b/packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp @@ -12,12 +12,17 @@ #include #include +#include #include #include #include #include #include +#if __has_include() +#include +#endif + using namespace facebook::jsi; class JSITest : public JSITestBase {}; @@ -1576,6 +1581,103 @@ TEST_P(JSITest, UTF8ExceptionTest) { } } +#ifdef __cpp_lib_span +TEST_P(JSITest, SpanAPITest) { + // Test that we can create a host function that accepts args as a std::span. + auto one = std::make_shared(1); + Function plusOne = Function::createFromHostFunction( + rt, + PropNameID::forAscii(rt, "plusOne"), + 2, + [one, savedRt = &rt]( + Runtime& rt, const Value& thisVal, std::span args) { + EXPECT_EQ(savedRt, &rt); + // We don't know if we're in strict mode or not, so it's either global + // or undefined. + EXPECT_TRUE( + Value::strictEquals(rt, thisVal, rt.global()) || + thisVal.isUndefined()); + EXPECT_EQ(args.size(), 2); + return *one + args[0].getNumber() + args[1].getNumber(); + }); + + EXPECT_EQ(plusOne.call(rt, 1, 2).getNumber(), 4); + EXPECT_TRUE(checkValue(plusOne.call(rt, 3, 5), "9")); + + // Test that we can call functions with a std::span of args. + + // Spans come in different varieties: static/dynamic extents, + // const/non-const, and can also be implicitly constructed from other range + // types. To ensure that our slightly-involved templated API works as + // intended, we test all of these cases explicitly. + + std::array argsArray{1, 2, 3}; + std::vector argsVec; + argsVec.emplace_back(1); + argsVec.emplace_back(2); + argsVec.emplace_back(3); + std::span argsSpanStatic(argsArray); + std::span argsConstSpanStatic(argsArray); + std::span argsSpanDynamic(argsVec); + std::span argsConstSpanDynamic(argsVec); + + EXPECT_TRUE( + function("function(a, b, c) { return a + b + c; }") + .call(rt, argsSpanStatic) + .getNumber() == 6); + EXPECT_TRUE( + function("function(a, b, c) { return a + b + c; }") + .call(rt, argsConstSpanStatic) + .getNumber() == 6); + EXPECT_TRUE( + function("function(a, b, c) { return a + b + c; }") + .call(rt, argsSpanDynamic) + .getNumber() == 6); + EXPECT_TRUE( + function("function(a, b, c) { return a + b + c; }") + .call(rt, argsConstSpanDynamic) + .getNumber() == 6); + EXPECT_TRUE( + function("function(a, b, c) { return a + b + c; }") + .call(rt, argsArray) + .getNumber() == 6); + EXPECT_TRUE( + function("function(a, b, c) { return a + b + c; }") + .call(rt, argsVec) + .getNumber() == 6); + + Function ctor = function( + "function (a, b, c) {" + " this.value = a + b + c;" + "}"); + + auto checkObj = [&](const Object& obj) { + EXPECT_TRUE(obj.hasProperty(rt, "value")); + EXPECT_TRUE(obj.getProperty(rt, "value").getNumber() == 6); + }; + checkObj(ctor.callAsConstructor(rt, argsSpanStatic).getObject(rt)); + checkObj(ctor.callAsConstructor(rt, argsConstSpanStatic).getObject(rt)); + checkObj(ctor.callAsConstructor(rt, argsSpanDynamic).getObject(rt)); + checkObj(ctor.callAsConstructor(rt, argsConstSpanDynamic).getObject(rt)); + checkObj(ctor.callAsConstructor(rt, argsArray).getObject(rt)); + checkObj(ctor.callAsConstructor(rt, argsVec).getObject(rt)); + + auto obj = ctor.callAsConstructor(rt, argsArray).getObject(rt); + Function checkPropertyFunction = + function("function(a, b, c) { return this.value === a + b + c; }"); + EXPECT_TRUE( + checkPropertyFunction.callWithThis(rt, obj, argsSpanStatic).getBool()); + EXPECT_TRUE( + checkPropertyFunction.callWithThis(rt, obj, argsConstSpanStatic).getBool()); + EXPECT_TRUE( + checkPropertyFunction.callWithThis(rt, obj, argsSpanDynamic).getBool()); + EXPECT_TRUE(checkPropertyFunction.callWithThis(rt, obj, argsConstSpanDynamic) + .getBool()); + EXPECT_TRUE(checkPropertyFunction.callWithThis(rt, obj, argsArray).getBool()); + EXPECT_TRUE(checkPropertyFunction.callWithThis(rt, obj, argsVec).getBool()); +} +#endif + INSTANTIATE_TEST_CASE_P( Runtimes, JSITest,