Skip to content

Commit

Permalink
Add C++20 std::span-based APIs for function arguments (facebook#43489)
Browse files Browse the repository at this point in the history
Summary:

Changelog: [General][Changed] - Add `std::span` methods to `jsi::Function` when compiled as C++20

Adds `std::span<const jsi::Value>` 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
  • Loading branch information
motiz88 authored and facebook-github-bot committed Mar 15, 2024
1 parent a9e6759 commit 61ffa58
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 2 deletions.
48 changes: 48 additions & 0 deletions packages/react-native/ReactCommon/jsi/jsi/jsi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,54 @@ inline Value Function::callAsConstructor(Runtime& runtime, Args&&... args)
runtime, {detail::toValue(runtime, std::forward<Args>(args))...});
}

#ifdef JSI_FUNCTION_HAS_SPAN_APIS

template <typename Fn>
inline Function Function::createFromHostFunction(
Runtime& runtime,
const jsi::PropNameID& name,
unsigned int paramCount,
Fn func) requires CallableAsHostFunction<Fn> ||
CallableAsHostFunctionWithSpan<Fn> {
HostFunctionType hostFunc;
if constexpr (CallableAsHostFunction<Fn>) {
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 <typename Span>
inline Value Function::call(Runtime& runtime, Span&& args)
const requires std::convertible_to<Span, std::span<const Value>> {
auto argsSpan = std::span<const Value>(std::forward<Span>(args));
return call(runtime, argsSpan.data(), argsSpan.size());
}

template <typename Span>
inline Value
Function::callWithThis(Runtime& runtime, const Object& jsThis, Span&& args)
const requires std::convertible_to<Span, std::span<const Value>> {
auto argsSpan = std::span<const Value>(std::forward<Span>(args));
return callWithThis(runtime, jsThis, argsSpan.data(), argsSpan.size());
}

template <typename Span>
inline Value Function::callAsConstructor(Runtime& runtime, Span&& args)
const requires std::convertible_to<Span, std::span<const Value>> {
auto argsSpan = std::span<const Value>(std::forward<Span>(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);
}
Expand Down
108 changes: 106 additions & 2 deletions packages/react-native/ReactCommon/jsi/jsi/jsi.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
#include <string>
#include <vector>

#ifdef JSI_FUNCTION_HAS_SPAN_APIS
#undef JSI_FUNCTION_HAS_SPAN_APIS
#endif

#if defined(__cpp_concepts) && __has_include(<concepts>) && \
__has_include(<span>)
#include <concepts>
#include <span>
#define JSI_FUNCTION_HAS_SPAN_APIS
#endif

#ifndef JSI_EXPORT
#ifdef _MSC_VER
#ifdef CREATE_SHARED_LIBRARY
Expand Down Expand Up @@ -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 <typename Fn>
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 <typename Fn>
concept CallableAsHostFunctionWithSpan = std::is_invocable_r_v<
Value /*return value*/,
Fn,
Runtime& /*rt*/,
const Value& /*thisVal*/,
std::span<const Value> /*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 {
Expand Down Expand Up @@ -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 <typename Fn>
static inline Function createFromHostFunction(
Runtime& runtime,
const jsi::PropNameID& name,
unsigned int paramCount,
Fn func) requires CallableAsHostFunction<Fn> ||
CallableAsHostFunctionWithSpan<Fn>;

// 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<const Value>, including:
// - span<Value>
// - span<const Value>
// - span<Value, N> where N is a static extent
// - span<const Value, N> where N is a static extent
// - array<Value, N>, vector<Value> 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 <typename Span>
inline Value call(Runtime& runtime, Span&& args)
const requires std::convertible_to<Span, std::span<const Value>>;

/// Calls the function with a \c std::span of Value arguments and \c jsThis
/// passed as the \c this value.
template <typename Span>
inline Value callWithThis(Runtime& runtime, const Object& jsThis, Span&& args)
const requires std::convertible_to<Span, std::span<const Value>>;

/// Same as above `callAsConstructor`, except use a std::span to
/// supply the arguments.
template <typename Span>
inline Value callAsConstructor(Runtime& runtime, Span&& args)
const requires std::convertible_to<Span, std::span<const Value>>;

#endif // JSI_FUNCTION_HAS_SPAN_APIS

private : friend class Object;
friend class Value;
friend class Runtime;

Expand Down Expand Up @@ -1529,3 +1628,8 @@ class JSI_EXPORT JSError : public JSIException {
} // namespace facebook

#include <jsi/jsi-inl.h>

#ifdef JSI_FUNCTION_HAS_SPAN_APIS
// Don't leak to includers
#undef JSI_FUNCTION_HAS_SPAN_APIS
#endif // JSI_FUNCTION_HAS_SPAN_APIS
102 changes: 102 additions & 0 deletions packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@
#include <jsi/jsi.h>

#include <stdlib.h>
#include <array>
#include <chrono>
#include <functional>
#include <thread>
#include <unordered_map>
#include <unordered_set>

#if __has_include(<span>)
#include <span>
#endif

using namespace facebook::jsi;

class JSITest : public JSITestBase {};
Expand Down Expand Up @@ -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<int>(1);
Function plusOne = Function::createFromHostFunction(
rt,
PropNameID::forAscii(rt, "plusOne"),
2,
[one, savedRt = &rt](
Runtime& rt, const Value& thisVal, std::span<const Value> 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<Value, 3> argsArray{1, 2, 3};
std::vector<Value> argsVec;
argsVec.emplace_back(1);
argsVec.emplace_back(2);
argsVec.emplace_back(3);
std::span<Value, 3> argsSpanStatic(argsArray);
std::span<const Value, 3> argsConstSpanStatic(argsArray);
std::span<Value> argsSpanDynamic(argsVec);
std::span<const Value> 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,
Expand Down

0 comments on commit 61ffa58

Please sign in to comment.