LCOV - code coverage report
Current view: top level - source/scriptinterface - FunctionWrapper.h (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 70 75 93.3 %
Date: 2023-01-19 00:18:29 Functions: 611 1957 31.2 %

          Line data    Source code
       1             : /* Copyright (C) 2021 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #ifndef INCLUDED_FUNCTIONWRAPPER
      19             : #define INCLUDED_FUNCTIONWRAPPER
      20             : 
      21             : #include "Object.h"
      22             : #include "ScriptConversions.h"
      23             : #include "ScriptExceptions.h"
      24             : #include "ScriptRequest.h"
      25             : 
      26             : #include <tuple>
      27             : #include <type_traits>
      28             : 
      29             : class ScriptInterface;
      30             : 
      31             : /**
      32             :  * This class introduces templates to conveniently wrap C++ functions in JSNative functions.
      33             :  * This _is_ rather template heavy, so compilation times beware.
      34             :  * The C++ code can have arbitrary arguments and arbitrary return types, so long
      35             :  * as they can be converted to/from JS using Script::ToJSVal (FromJSVal respectively),
      36             :  * and they are default-constructible (TODO: that can probably changed).
      37             :  * (This could be a namespace, but I like being able to specify public/private).
      38             :  */
      39             : class ScriptFunction {
      40             : private:
      41             :     ScriptFunction() = delete;
      42             :     ScriptFunction(const ScriptFunction&) = delete;
      43             :     ScriptFunction(ScriptFunction&&) = delete;
      44             : 
      45             :     /**
      46             :      * In JS->C++ calls, types are converted using FromJSVal,
      47             :      * and this requires them to be default-constructible (as that function takes an out parameter)
      48             :      * thus constref needs to be removed when defining the tuple.
      49             :      * Exceptions are:
      50             :      *  - const ScriptRequest& (as the first argument only, for implementation simplicity).
      51             :      *  - const ScriptInterface& (as the first argument only, for implementation simplicity).
      52             :      *  - JS::HandleValue
      53             :      */
      54             :     template<typename T>
      55             :     using type_transform = std::conditional_t<
      56             :         std::is_same_v<const ScriptRequest&, T> || std::is_same_v<const ScriptInterface&, T>,
      57             :         T,
      58             :         std::remove_const_t<typename std::remove_reference_t<T>>
      59             :     >;
      60             : 
      61             :     /**
      62             :      * Convenient struct to get info on a [class] [const] function pointer.
      63             :      * TODO VS19: I ran into a really weird bug with an auto specialisation on this taking function pointers.
      64             :      * It'd be good to add it back once we upgrade.
      65             :      */
      66             :     template <class T> struct args_info;
      67             : 
      68             :     template<typename R, typename ...Types>
      69             :     struct args_info<R(*)(Types ...)>
      70             :     {
      71             :         static constexpr const size_t nb_args = sizeof...(Types);
      72             :         using return_type = R;
      73             :         using object_type = void;
      74             :         using arg_types = std::tuple<type_transform<Types>...>;
      75             :     };
      76             : 
      77             :     template<typename C, typename R, typename ...Types>
      78             :     struct args_info<R(C::*)(Types ...)> : public args_info<R(*)(Types ...)> { using object_type = C; };
      79             :     template<typename C, typename R, typename ...Types>
      80             :     struct args_info<R(C::*)(Types ...) const> : public args_info<R(C::*)(Types ...)> {};
      81             : 
      82             :     ///////////////////////////////////////////////////////////////////////////
      83             :     ///////////////////////////////////////////////////////////////////////////
      84             : 
      85             :     /**
      86             :      * DoConvertFromJS takes a type, a JS argument, and converts.
      87             :      * The type T must be default constructible (except for HandleValue, which is handled specially).
      88             :      * (possible) TODO: this could probably be changed if FromJSVal had a different signature.
      89             :      * @param went_ok - true if the conversion succeeded and went_ok was true before, false otherwise.
      90             :      */
      91             :     template<size_t idx, typename T>
      92        1460 :     static std::tuple<T> DoConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok)
      93             :     {
      94             :         // No need to convert JS values.
      95             :         if constexpr (std::is_same_v<T, JS::HandleValue>)
      96             :         {
      97             :             // Default-construct values that aren't passed by JS.
      98             :             // TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky.
      99         676 :             if (idx >= args.length())
     100           1 :                 return std::forward_as_tuple(JS::UndefinedHandleValue);
     101             :             else
     102             :             {
     103             :                 // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch.
     104             :                 UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok);
     105         675 :                 return std::forward_as_tuple(args[idx]); // This passes the null handle value if idx is beyond the length of args.
     106             :             }
     107             :         }
     108             :         else
     109             :         {
     110             :             // Default-construct values that aren't passed by JS.
     111             :             // TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky.
     112         784 :             if (idx >= args.length())
     113           1 :                 return std::forward_as_tuple(T{});
     114             :             else
     115             :             {
     116        1376 :                 T ret;
     117         783 :                 went_ok &= Script::FromJSVal<T>(rq, args[idx], ret);
     118         783 :                 return std::forward_as_tuple(ret);
     119             :             }
     120             :         }
     121             :     }
     122             : 
     123             :     /**
     124             :      * Recursive wrapper: calls DoConvertFromJS for type T and recurses.
     125             :      */
     126             :     template<size_t idx, typename T, typename V, typename ...Types>
     127         174 :     static std::tuple<T, V, Types...> DoConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok)
     128             :     {
     129         174 :         return std::tuple_cat(DoConvertFromJS<idx, T>(rq, args, went_ok), DoConvertFromJS<idx + 1, V, Types...>(rq, args, went_ok));
     130             :     }
     131             : 
     132             :     /**
     133             :      * ConvertFromJS is a wrapper around DoConvertFromJS, and serves to:
     134             :      *  - unwrap the tuple types as a parameter pack
     135             :      *  - handle specific cases for the first argument (ScriptRequest, ...).
     136             :      *
     137             :      * Trick: to unpack the types of the tuple as a parameter pack, we deduce them from the function signature.
     138             :      * To do that, we want the tuple in the arguments, but we don't want to actually have to default-instantiate,
     139             :      * so we'll pass a nullptr that's static_cast to what we want.
     140             :      */
     141             :     template<typename ...Types>
     142         143 :     static std::tuple<Types...> ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple<Types...>*)
     143             :     {
     144             :         if constexpr (sizeof...(Types) == 0)
     145             :         {
     146             :             // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch.
     147             :             UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok);
     148           6 :             return {};
     149             :         }
     150             :         else
     151         137 :             return DoConvertFromJS<0, Types...>(rq, args, went_ok);
     152             :     }
     153             : 
     154             :     // Overloads for ScriptRequest& first argument.
     155             :     template<typename ...Types>
     156         332 :     static std::tuple<const ScriptRequest&, Types...> ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple<const ScriptRequest&, Types...>*)
     157             :     {
     158             :         if constexpr (sizeof...(Types) == 0)
     159             :         {
     160             :             // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch.
     161             :             UNUSED2(args); UNUSED2(went_ok);
     162           0 :             return std::forward_as_tuple(rq);
     163             :         }
     164             :         else
     165         332 :             return std::tuple_cat(std::forward_as_tuple(rq), DoConvertFromJS<0, Types...>(rq, args, went_ok));
     166             :     }
     167             : 
     168             :     // Overloads for ScriptInterface& first argument.
     169             :     template<typename ...Types>
     170         817 :     static std::tuple<const ScriptInterface&, Types...> ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple<const ScriptInterface&, Types...>*)
     171             :     {
     172             :         if constexpr (sizeof...(Types) == 0)
     173             :         {
     174             :             // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch.
     175             :             UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok);
     176           0 :             return std::forward_as_tuple(rq.GetScriptInterface());
     177             :         }
     178             :         else
     179         817 :             return std::tuple_cat(std::forward_as_tuple(rq.GetScriptInterface()), DoConvertFromJS<0, Types...>(rq, args, went_ok));
     180             :     }
     181             : 
     182             :     ///////////////////////////////////////////////////////////////////////////
     183             :     ///////////////////////////////////////////////////////////////////////////
     184             : 
     185             :     /**
     186             :      * Wrap std::apply for the case where we have an object method or a regular function.
     187             :      */
     188             :     template <auto callable, typename T, typename tuple>
     189        1292 :     static typename args_info<decltype(callable)>::return_type call(T* object, tuple& args)
     190             :     {
     191             :         if constexpr(std::is_same_v<T, void>)
     192             :         {
     193             :             // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch.
     194             :             UNUSED2(object);
     195        1278 :             return std::apply(callable, args);
     196             :         }
     197             :         else
     198         125 :             return std::apply(callable, std::tuple_cat(std::forward_as_tuple(*object), args));
     199             :     }
     200             : 
     201             :     ///////////////////////////////////////////////////////////////////////////
     202             :     ///////////////////////////////////////////////////////////////////////////
     203             : 
     204             :     struct IgnoreResult_t {};
     205             :     static inline IgnoreResult_t IgnoreResult;
     206             : 
     207             :     /**
     208             :      * Recursive helper to call AssignOrToJSVal
     209             :      */
     210             :     template<int i, typename T, typename... Ts>
     211          98 :     static void AssignOrToJSValHelper(const ScriptRequest& rq, JS::MutableHandleValueVector argv, const T& a, const Ts&... params)
     212             :     {
     213          98 :         Script::ToJSVal(rq, argv[i], a);
     214          98 :         AssignOrToJSValHelper<i+1>(rq, argv, params...);
     215          98 :     }
     216             : 
     217             :     template<int i, typename... Ts>
     218         244 :     static void AssignOrToJSValHelper(const ScriptRequest& UNUSED(rq), JS::MutableHandleValueVector UNUSED(argv))
     219             :     {
     220             :         static_assert(sizeof...(Ts) == 0);
     221             :         // Nop, for terminating the template recursion.
     222         244 :     }
     223             : 
     224             :     /**
     225             :      * Wrapper around calling a JS function from C++.
     226             :      * Arguments are const& to avoid lvalue/rvalue issues, and so can't be used as out-parameters.
     227             :      * In particular, the problem is that Rooted are deduced as Rooted, not Handle, and so can't be copied.
     228             :      * This could be worked around with more templates, but it doesn't seem particularly worth doing.
     229             :      */
     230             :     template<typename R, typename ...Args>
     231         306 :     static bool Call_(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args)
     232             :     {
     233         612 :         JS::RootedObject obj(rq.cx);
     234         306 :         if (!JS_ValueToObject(rq.cx, val, &obj) || !obj)
     235           0 :             return false;
     236             : 
     237             :         // Check that the named function actually exists, to avoid ugly JS error reports
     238             :         // when calling an undefined value
     239             :         bool found;
     240         306 :         if (!JS_HasProperty(rq.cx, obj, name, &found) || !found)
     241          62 :             return false;
     242             : 
     243         488 :         JS::RootedValueVector argv(rq.cx);
     244         244 :         ignore_result(argv.resize(sizeof...(Args)));
     245         244 :         AssignOrToJSValHelper<0>(rq, &argv, args...);
     246             : 
     247             :         bool success;
     248             :         if constexpr (std::is_same_v<R, JS::MutableHandleValue>)
     249           8 :             success = JS_CallFunctionName(rq.cx, obj, name, argv, ret);
     250             :         else
     251             :         {
     252         472 :             JS::RootedValue jsRet(rq.cx);
     253         236 :             success = JS_CallFunctionName(rq.cx, obj, name, argv, &jsRet);
     254             :             if constexpr (!std::is_same_v<R, IgnoreResult_t>)
     255             :             {
     256         182 :                 if (success)
     257         182 :                     Script::FromJSVal(rq, jsRet, ret);
     258             :             }
     259             :             else
     260             :                 UNUSED2(ret); // VS2017 complains.
     261             :         }
     262             :         // Even if everything succeeded, there could be pending exceptions
     263         244 :         return !ScriptException::CatchPending(rq) && success;
     264             :     }
     265             : 
     266             :     ///////////////////////////////////////////////////////////////////////////
     267             :     ///////////////////////////////////////////////////////////////////////////
     268             : public:
     269             :     template <typename T>
     270             :     using ObjectGetter = T*(*)(const ScriptRequest&, JS::CallArgs&);
     271             : 
     272             :     // TODO: the fact that this takes class and not auto is to work around an odd VS17 bug.
     273             :     // It can be removed with VS19.
     274             :     template <class callableType>
     275             :     using GetterFor = ObjectGetter<typename args_info<callableType>::object_type>;
     276             : 
     277             :     /**
     278             :      * The meat of this file. This wraps a C++ function into a JSNative,
     279             :      * so that it can be called from JS and manipulated in Spidermonkey.
     280             :      * Most C++ functions can be directly wrapped, so long as their arguments are
     281             :      * convertible from JS::Value and their return value is convertible to JS::Value (or void)
     282             :      * The C++ function may optionally take const ScriptRequest& or ScriptInterface& as its first argument.
     283             :      * The function may be an object method, in which case you need to pass an appropriate getter
     284             :      *
     285             :      * Optimisation note: the ScriptRequest object is created even without arguments,
     286             :      * as it's necessary for IsExceptionPending.
     287             :      *
     288             :      * @param thisGetter to get the object, if necessary.
     289             :      */
     290             :     template <auto callable, GetterFor<decltype(callable)> thisGetter = nullptr>
     291        1292 :     static bool ToJSNative(JSContext* cx, unsigned argc, JS::Value* vp)
     292             :     {
     293             :         using ObjType = typename args_info<decltype(callable)>::object_type;
     294             : 
     295        1292 :         JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
     296        2584 :         ScriptRequest rq(cx);
     297             : 
     298             :         // If the callable is an object method, we must specify how to fetch the object.
     299             :         static_assert(std::is_same_v<typename args_info<decltype(callable)>::object_type, void> || thisGetter != nullptr,
     300             :                       "ScriptFunction::Register - No getter specified for object method");
     301             : 
     302             : // GCC 7 triggers spurious warnings
     303             : #ifdef __GNUC__
     304             : #pragma GCC diagnostic push
     305             : #pragma GCC diagnostic ignored "-Waddress"
     306             : #endif
     307        1292 :         ObjType* obj = nullptr;
     308             :         if constexpr (thisGetter != nullptr)
     309             :         {
     310         125 :             obj = thisGetter(rq, args);
     311         125 :             if (!obj)
     312           0 :                 return false;
     313             :         }
     314             : #ifdef __GNUC__
     315             : #pragma GCC diagnostic pop
     316             : #endif
     317             : 
     318        1292 :         bool went_ok = true;
     319        1974 :         typename args_info<decltype(callable)>::arg_types outs = ConvertFromJS(rq, args, went_ok, static_cast<typename args_info<decltype(callable)>::arg_types*>(nullptr));
     320        1292 :         if (!went_ok)
     321           0 :             return false;
     322             : 
     323             :         /**
     324             :          * TODO: error handling isn't standard, and since this can call any C++ function,
     325             :          * there's no simple obvious way to deal with it.
     326             :          * For now we check for pending JS exceptions, but it would probably be nicer
     327             :          * to standardise on something, or perhaps provide an "errorHandler" here.
     328             :          */
     329             :         if constexpr (std::is_same_v<void, typename args_info<decltype(callable)>::return_type>)
     330         563 :             call<callable>(obj, outs);
     331             :         else if constexpr (std::is_same_v<JS::Value, typename args_info<decltype(callable)>::return_type>)
     332         683 :             args.rval().set(call<callable>(obj, outs));
     333             :         else
     334          46 :             Script::ToJSVal(rq, args.rval(), call<callable>(obj, outs));
     335             : 
     336        1292 :         return !ScriptException::IsPending(rq);
     337             :     }
     338             : 
     339             :     /**
     340             :      * Call a JS function @a name, property of object @a val, with the arguments @a args.
     341             :      * @a ret will be updated with the return value, if any.
     342             :      * @return the success (or failure) thereof.
     343             :      */
     344             :     template<typename R, typename ...Args>
     345         298 :     static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args)
     346             :     {
     347         298 :         return Call_(rq, val, name, ret, std::forward<const Args>(args)...);
     348             :     }
     349             : 
     350             :     // Specialisation for MutableHandleValue return.
     351             :     template<typename ...Args>
     352           8 :     static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, JS::MutableHandleValue ret, const Args&... args)
     353             :     {
     354           8 :         return Call_(rq, val, name, ret, std::forward<const Args>(args)...);
     355             :     }
     356             : 
     357             :     /**
     358             :      * Call a JS function @a name, property of object @a val, with the arguments @a args.
     359             :      * @return the success (or failure) thereof.
     360             :      */
     361             :     template<typename ...Args>
     362         116 :     static bool CallVoid(const ScriptRequest& rq, JS::HandleValue val, const char* name, const Args&... args)
     363             :     {
     364         116 :         return Call(rq, val, name, IgnoreResult, std::forward<const Args>(args)...);
     365             :     }
     366             : 
     367             :     /**
     368             :      * Return a function spec from a C++ function.
     369             :      */
     370             :     template <auto callable, GetterFor<decltype(callable)> thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT>
     371         182 :     static JSFunctionSpec Wrap(const char* name)
     372             :     {
     373         182 :         return JS_FN(name, (&ToJSNative<callable, thisGetter>), args_info<decltype(callable)>::nb_args, flags);
     374             :     }
     375             : 
     376             :     /**
     377             :      * Return a JSFunction from a C++ function.
     378             :      */
     379             :     template <auto callable, GetterFor<decltype(callable)> thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT>
     380         174 :     static JSFunction* Create(const ScriptRequest& rq, const char* name)
     381             :     {
     382         174 :         return JS_NewFunction(rq.cx, &ToJSNative<callable, thisGetter>, args_info<decltype(callable)>::nb_args, flags, name);
     383             :     }
     384             : 
     385             :     /**
     386             :      * Register a function on the native scope (usually 'Engine').
     387             :      */
     388             :     template <auto callable, GetterFor<decltype(callable)> thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT>
     389        4294 :     static void Register(const ScriptRequest& rq, const char* name)
     390             :     {
     391        4294 :         JS_DefineFunction(rq.cx, rq.nativeScope, name, &ToJSNative<callable, thisGetter>, args_info<decltype(callable)>::nb_args, flags);
     392        4294 :     }
     393             : 
     394             :     /**
     395             :      * Register a function on @param scope.
     396             :      * Prefer the version taking ScriptRequest unless you have a good reason not to.
     397             :      * @see Register
     398             :      */
     399             :     template <auto callable, GetterFor<decltype(callable)> thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT>
     400        1410 :     static void Register(JSContext* cx, JS::HandleObject scope, const char* name)
     401             :     {
     402        1410 :         JS_DefineFunction(cx, scope, name, &ToJSNative<callable, thisGetter>, args_info<decltype(callable)>::nb_args, flags);
     403        1410 :     }
     404             : };
     405             : 
     406             : #endif // INCLUDED_FUNCTIONWRAPPER

Generated by: LCOV version 1.13