/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_FUNCTIONWRAPPER #define INCLUDED_FUNCTIONWRAPPER #include "ScriptConversions.h" #include "ScriptExceptions.h" #include "ScriptRequest.h" #include #include #include #include #include class ScriptInterface; /** * This class introduces templates to conveniently wrap C++ functions in JSNative functions. * This _is_ rather template heavy, so compilation times beware. * The C++ code can have arbitrary arguments and arbitrary return types, so long * as they can be converted to/from JS using Script::ToJSVal (FromJSVal respectively), * and they are default-constructible (TODO: that can probably changed). * (This could be a namespace, but I like being able to specify public/private). */ class ScriptFunction { private: ScriptFunction() = delete; ScriptFunction(const ScriptFunction&) = delete; ScriptFunction(ScriptFunction&&) = delete; /** * In JS->C++ calls, types are converted using FromJSVal, * and this requires them to be default-constructible (as that function takes an out parameter) * thus constref needs to be removed when defining the tuple. * Exceptions are: * - const ScriptRequest& (as the first argument only, for implementation simplicity). * - const ScriptInterface& (as the first argument only, for implementation simplicity). * - JS::HandleValue */ template using type_transform = std::conditional_t< std::is_same_v || std::is_same_v, T, std::remove_const_t> >; /** * Convenient struct to get info on a [class] [const] function pointer. * TODO VS19: I ran into a really weird bug with an auto specialisation on this taking function pointers. * It'd be good to add it back once we upgrade. */ template struct args_info; template struct args_info { static constexpr const size_t nb_args = sizeof...(Types); using return_type = R; using object_type = void; using arg_types = std::tuple...>; }; template struct args_info : public args_info { using object_type = C; }; template struct args_info : public args_info {}; struct IteratorResultError : std::runtime_error { IteratorResultError(const std::string& property) : IteratorResultError{property.c_str()} {} IteratorResultError(const char* property) : std::runtime_error{fmt::format("Failed to get `{}` from an `IteratorResult`.", property)} {} using std::runtime_error::runtime_error; }; /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /** * DoConvertFromJS takes a type, a JS argument, and converts. * The type T must be default constructible (except for HandleValue, which is handled specially). * (possible) TODO: this could probably be changed if FromJSVal had a different signature. * @param wentOk - true if the conversion succeeded and wentOk was true before, false otherwise. */ template static T DoConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& wentOk) { // No need to convert JS values. if constexpr (std::is_same_v) { // Default-construct values that aren't passed by JS. // TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky. if (idx >= args.length()) return JS::UndefinedHandleValue; else { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(rq); UNUSED2(args); UNUSED2(wentOk); return args[idx]; // This passes the null handle value if idx is beyond the length of args. } } else { // Default-construct values that aren't passed by JS. // TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky. if (idx >= args.length()) return {}; else { T ret; wentOk &= Script::FromJSVal(rq, args[idx], ret); return ret; } } } /** * Wrapper: calls DoConvertFromJS for each element in T. */ template static std::tuple DoConvertFromJS(std::index_sequence, const ScriptRequest& rq, JS::CallArgs& args, bool& wentOk) { return {DoConvertFromJS(rq, args, wentOk)...}; } /** * ConvertFromJS is a wrapper around DoConvertFromJS, and handles specific cases for the * first argument (ScriptRequest, ...). * * Trick: to unpack the types of the tuple as a parameter pack, we deduce them from the function signature. * To do that, we want the tuple in the arguments, but we don't want to actually have to default-instantiate, * so we'll pass a nullptr that's static_cast to what we want. */ template static std::tuple ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& wentOk, std::tuple*) { return DoConvertFromJS(std::index_sequence_for(), rq, args, wentOk); } // Overloads for ScriptRequest& first argument. template static std::tuple ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& wentOk, std::tuple*) { return std::tuple_cat(std::tie(rq), DoConvertFromJS( std::index_sequence_for(), rq, args, wentOk)); } // Overloads for ScriptInterface& first argument. template static std::tuple ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& wentOk, std::tuple*) { return std::tuple_cat(std::tie(rq.GetScriptInterface()), DoConvertFromJS(std::index_sequence_for(), rq, args, wentOk)); } /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /** * Wrap std::apply for the case where we have an object method or a regular function. */ template static typename args_info::return_type call(T* object, tuple& args) { if constexpr(std::is_same_v) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(object); return std::apply(callable, args); } else return std::apply(callable, std::tuple_cat(std::forward_as_tuple(*object), args)); } /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// struct IgnoreResult_t {}; static inline IgnoreResult_t IgnoreResult; /** * Converts any number of arguments to a `JS::MutableHandleValueVector`. * If `idx` is empty this function does nothing. For that case there is a * `[[maybe_unused]]` on `argv`. GCC would issue a * "-Wunused-but-set-parameter" warning. * For references like `rq` this warning isn't issued. */ template static void ToJSValVector(std::index_sequence, const ScriptRequest& rq, [[maybe_unused]] JS::MutableHandleValueVector argv, const Types&... params) { (Script::ToJSVal(rq, argv[idx], params), ...); } /** * Wrapper around calling a JS function from C++. * Arguments are const& to avoid lvalue/rvalue issues, and so can't be used as out-parameters. * In particular, the problem is that Rooted are deduced as Rooted, not Handle, and so can't be copied. * This could be worked around with more templates, but it doesn't seem particularly worth doing. */ template static bool Call_(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args) { JS::RootedObject obj(rq.cx); if (!JS_ValueToObject(rq.cx, val, &obj) || !obj) return false; // Fetch the property explicitly - this avoids converting the arguments if it doesn't exist. JS::RootedValue func(rq.cx); if (!JS_GetProperty(rq.cx, obj, name, &func) || func.isUndefined()) return false; JS::RootedValueVector argv(rq.cx); ignore_result(argv.resize(sizeof...(Args))); ToJSValVector(std::index_sequence_for{}, rq, &argv, args...); bool success; if constexpr (std::is_same_v) success = JS_CallFunctionValue(rq.cx, obj, func, argv, ret); else { JS::RootedValue jsRet(rq.cx); success = JS_CallFunctionValue(rq.cx, obj, func, argv, &jsRet); if constexpr (!std::is_same_v) { if (success) Script::FromJSVal(rq, jsRet, ret); } else UNUSED2(ret); // VS2017 complains. } // Even if everything succeeded, there could be pending exceptions return !ScriptException::CatchPending(rq) && success; } /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// public: template using ObjectGetter = T*(*)(const ScriptRequest&, JS::CallArgs&); // TODO: the fact that this takes class and not auto is to work around an odd VS17 bug. // It can be removed with VS19. template using GetterFor = ObjectGetter::object_type>; /** * The meat of this file. This wraps a C++ function into a JSNative, * so that it can be called from JS and manipulated in Spidermonkey. * Most C++ functions can be directly wrapped, so long as their arguments are * convertible from JS::Value and their return value is convertible to JS::Value (or void) * The C++ function may optionally take const ScriptRequest& or ScriptInterface& as its first argument. * The function may be an object method, in which case you need to pass an appropriate getter * * Optimisation note: the ScriptRequest object is created even without arguments, * as it's necessary for IsExceptionPending. * * @param thisGetter to get the object, if necessary. */ template thisGetter = nullptr> static bool ToJSNative(JSContext* cx, unsigned argc, JS::Value* vp) { using ObjType = typename args_info::object_type; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(cx); // If the callable is an object method, we must specify how to fetch the object. static_assert(std::is_same_v::object_type, void> || thisGetter != nullptr, "ScriptFunction::Register - No getter specified for object method"); // GCC 7 triggers spurious warnings #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Waddress" #endif ObjType* obj = nullptr; if constexpr (thisGetter != nullptr) { obj = thisGetter(rq, args); if (!obj) return false; } #ifdef __GNUC__ #pragma GCC diagnostic pop #endif bool wentOk = true; typename args_info::arg_types outs = ConvertFromJS(rq, args, wentOk, static_cast::arg_types*>(nullptr)); if (!wentOk) return false; /** * TODO: error handling isn't standard, and since this can call any C++ function, * there's no simple obvious way to deal with it. * For now we check for pending JS exceptions, but it would probably be nicer * to standardise on something, or perhaps provide an "errorHandler" here. */ if constexpr (std::is_same_v::return_type>) call(obj, outs); else if constexpr (std::is_same_v::return_type>) args.rval().set(call(obj, outs)); else Script::ToJSVal(rq, args.rval(), call(obj, outs)); return !ScriptException::IsPending(rq); } /** * Call a JS function @a name, property of object @a val, with the arguments @a args. * @a ret will be updated with the return value, if any. * @return the success (or failure) thereof. */ template static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args) { return Call_(rq, val, name, ret, std::forward(args)...); } // Specialisation for MutableHandleValue return. template static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, JS::MutableHandleValue ret, const Args&... args) { return Call_(rq, val, name, ret, std::forward(args)...); } /** * Call a JS function @a name, property of object @a val, with the arguments @a args. * @return the success (or failure) thereof. */ template static bool CallVoid(const ScriptRequest& rq, JS::HandleValue val, const char* name, const Args&... args) { return Call(rq, val, name, IgnoreResult, std::forward(args)...); } /** * Call a JS function @a name, property of object @a val, with the argument @a args. Repeatetly * invokes @a yieldCallback with the yielded value. * @return the final value of the generator. */ template static JS::Value RunGenerator(const ScriptRequest& rq, JS::HandleValue val, const char* name, JS::HandleValue arg, Callback yieldCallback) { JS::RootedValue generator{rq.cx}; if (!ScriptFunction::Call(rq, val, name, &generator, arg)) throw std::runtime_error{fmt::format("Failed to call the generator `{}`.", name)}; const auto continueGenerator = [&](const char* property, auto... args) -> JS::Value { JS::RootedValue iteratorResult{rq.cx}; if (!ScriptFunction::Call(rq, generator, property, &iteratorResult, args...)) throw std::runtime_error{fmt::format("Failed to call `{}`.", name)}; return iteratorResult; }; JS::PersistentRootedValue error{rq.cx, JS::UndefinedValue()}; while (true) { JS::RootedValue iteratorResult{rq.cx, error.isUndefined() ? continueGenerator("next") : continueGenerator("throw", std::exchange(error, JS::UndefinedValue()))}; try { JS::RootedObject iteratorResultObject{rq.cx, &iteratorResult.toObject()}; bool done; if (!Script::FromJSProperty(rq, iteratorResult, "done", done, true)) throw IteratorResultError{"done"}; JS::RootedValue value{rq.cx}; if (!JS_GetProperty(rq.cx, iteratorResultObject, "value", &value)) throw IteratorResultError{"value"}; if (done) return value; yieldCallback(value); } catch (const std::exception& e) { JS::RootedValue global{rq.cx, rq.globalValue()}; if (!ScriptFunction::Call(rq, global, "Error", &error, e.what())) throw std::runtime_error{"Failed to construct `Error`."}; } } } /** * Return a function spec from a C++ function. */ template thisGetter = nullptr> static JSFunctionSpec Wrap(const char* name, const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT) { return JS_FN(name, (&ToJSNative), args_info::nb_args, flags); } /** * Return a JSFunction from a C++ function. */ template thisGetter = nullptr> static JSFunction* Create(const ScriptRequest& rq, const char* name, const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT) { return JS_NewFunction(rq.cx, &ToJSNative, args_info::nb_args, flags, name); } /** * Register a function on the native scope (usually 'Engine'). */ template thisGetter = nullptr> static void Register(const ScriptRequest& rq, const char* name, const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT) { JS_DefineFunction(rq.cx, rq.nativeScope, name, &ToJSNative, args_info::nb_args, flags); } /** * Register a function on @param scope. * Prefer the version taking ScriptRequest unless you have a good reason not to. * @see Register */ template thisGetter = nullptr> static void Register(JSContext* cx, JS::HandleObject scope, const char* name, const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT) { JS_DefineFunction(cx, scope, name, &ToJSNative, args_info::nb_args, flags); } }; #endif // INCLUDED_FUNCTIONWRAPPER