Pyrogenesis  trunk
FunctionWrapper.h
Go to the documentation of this file.
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  */
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  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  if (idx >= args.length())
100  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  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  if (idx >= args.length())
113  return std::forward_as_tuple(T{});
114  else
115  {
116  T ret;
117  went_ok &= Script::FromJSVal<T>(rq, args[idx], ret);
118  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  static std::tuple<T, V, Types...> DoConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok)
128  {
129  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  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  return {};
149  }
150  else
151  return DoConvertFromJS<0, Types...>(rq, args, went_ok);
152  }
153 
154  // Overloads for ScriptRequest& first argument.
155  template<typename ...Types>
156  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  return std::forward_as_tuple(rq);
163  }
164  else
165  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  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  return std::forward_as_tuple(rq.GetScriptInterface());
177  }
178  else
179  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  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  return std::apply(callable, args);
196  }
197  else
198  return std::apply(callable, std::tuple_cat(std::forward_as_tuple(*object), args));
199  }
200 
201  ///////////////////////////////////////////////////////////////////////////
202  ///////////////////////////////////////////////////////////////////////////
203 
204  struct IgnoreResult_t {};
206 
207  /**
208  * Recursive helper to call AssignOrToJSVal
209  */
210  template<int i, typename T, typename... Ts>
211  static void AssignOrToJSValHelper(const ScriptRequest& rq, JS::MutableHandleValueVector argv, const T& a, const Ts&... params)
212  {
213  Script::ToJSVal(rq, argv[i], a);
214  AssignOrToJSValHelper<i+1>(rq, argv, params...);
215  }
216 
217  template<int i, typename... Ts>
218  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  }
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  static bool Call_(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args)
232  {
233  JS::RootedObject obj(rq.cx);
234  if (!JS_ValueToObject(rq.cx, val, &obj) || !obj)
235  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  if (!JS_HasProperty(rq.cx, obj, name, &found) || !found)
241  return false;
242 
243  JS::RootedValueVector argv(rq.cx);
244  ignore_result(argv.resize(sizeof...(Args)));
245  AssignOrToJSValHelper<0>(rq, &argv, args...);
246 
247  bool success;
248  if constexpr (std::is_same_v<R, JS::MutableHandleValue>)
249  success = JS_CallFunctionName(rq.cx, obj, name, argv, ret);
250  else
251  {
252  JS::RootedValue jsRet(rq.cx);
253  success = JS_CallFunctionName(rq.cx, obj, name, argv, &jsRet);
254  if constexpr (!std::is_same_v<R, IgnoreResult_t>)
255  {
256  if (success)
257  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  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>
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  static bool ToJSNative(JSContext* cx, unsigned argc, JS::Value* vp)
292  {
293  using ObjType = typename args_info<decltype(callable)>::object_type;
294 
295  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
296  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  ObjType* obj = nullptr;
308  if constexpr (thisGetter != nullptr)
309  {
310  obj = thisGetter(rq, args);
311  if (!obj)
312  return false;
313  }
314 #ifdef __GNUC__
315 #pragma GCC diagnostic pop
316 #endif
317 
318  bool went_ok = true;
319  typename args_info<decltype(callable)>::arg_types outs = ConvertFromJS(rq, args, went_ok, static_cast<typename args_info<decltype(callable)>::arg_types*>(nullptr));
320  if (!went_ok)
321  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  call<callable>(obj, outs);
331  else if constexpr (std::is_same_v<JS::Value, typename args_info<decltype(callable)>::return_type>)
332  args.rval().set(call<callable>(obj, outs));
333  else
334  Script::ToJSVal(rq, args.rval(), call<callable>(obj, outs));
335 
336  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  static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args)
346  {
347  return Call_(rq, val, name, ret, std::forward<const Args>(args)...);
348  }
349 
350  // Specialisation for MutableHandleValue return.
351  template<typename ...Args>
352  static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, JS::MutableHandleValue ret, const Args&... args)
353  {
354  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  static bool CallVoid(const ScriptRequest& rq, JS::HandleValue val, const char* name, const Args&... args)
363  {
364  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  static JSFunctionSpec Wrap(const char* name)
372  {
373  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  static JSFunction* Create(const ScriptRequest& rq, const char* name)
381  {
382  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  static void Register(const ScriptRequest& rq, const char* name)
390  {
391  JS_DefineFunction(rq.cx, rq.nativeScope, name, &ToJSNative<callable, thisGetter>, args_info<decltype(callable)>::nb_args, flags);
392  }
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  static void Register(JSContext* cx, JS::HandleObject scope, const char* name)
401  {
402  JS_DefineFunction(cx, scope, name, &ToJSNative<callable, thisGetter>, args_info<decltype(callable)>::nb_args, flags);
403  }
404 };
405 
406 #endif // INCLUDED_FUNCTIONWRAPPER
JSContext * cx
Definition: ScriptRequest.h:92
#define UNUSED(param)
mark a function parameter as unused and avoid the corresponding compiler warning. ...
Definition: code_annotation.h:38
static bool Call(const ScriptRequest &rq, JS::HandleValue val, const char *name, R &ret, const Args &... args)
Call a JS function name, property of object val, with the arguments args.
Definition: FunctionWrapper.h:345
static std::tuple< T, V, Types... > DoConvertFromJS(const ScriptRequest &rq, JS::CallArgs &args, bool &went_ok)
Recursive wrapper: calls DoConvertFromJS for type T and recurses.
Definition: FunctionWrapper.h:127
static void AssignOrToJSValHelper(const ScriptRequest &rq, JS::MutableHandleValueVector argv, const T &a, const Ts &... params)
Recursive helper to call AssignOrToJSVal.
Definition: FunctionWrapper.h:211
static void Register(const ScriptRequest &rq, const char *name)
Register a function on the native scope (usually &#39;Engine&#39;).
Definition: FunctionWrapper.h:389
R return_type
Definition: FunctionWrapper.h:72
static std::tuple< const ScriptInterface &, Types... > ConvertFromJS(const ScriptRequest &rq, JS::CallArgs &args, bool &went_ok, std::tuple< const ScriptInterface &, Types... > *)
Definition: FunctionWrapper.h:170
static IgnoreResult_t IgnoreResult
Definition: FunctionWrapper.h:205
static void AssignOrToJSValHelper(const ScriptRequest &rq, JS::MutableHandleValueVector argv)
Definition: FunctionWrapper.h:218
static args_info< decltype(callable)>::return_type call(T *object, tuple &args)
Wrap std::apply for the case where we have an object method or a regular function.
Definition: FunctionWrapper.h:189
static std::tuple< Types... > ConvertFromJS(const ScriptRequest &rq, JS::CallArgs &args, bool &went_ok, std::tuple< Types... > *)
ConvertFromJS is a wrapper around DoConvertFromJS, and serves to:
Definition: FunctionWrapper.h:142
void ignore_result(const T &)
Silence the &#39;unused result&#39; warning.
Definition: code_annotation.h:65
#define UNUSED2(param)
mark a function local variable or parameter as unused and avoid the corresponding compiler warning...
Definition: code_annotation.h:56
Convenient struct to get info on a [class] [const] function pointer.
Definition: FunctionWrapper.h:66
Config::Value_type Value
Definition: json_spirit_value.h:182
static bool ToJSNative(JSContext *cx, unsigned argc, JS::Value *vp)
The meat of this file.
Definition: FunctionWrapper.h:291
void ToJSVal(const ScriptRequest &rq, JS::MutableHandleValue ret, T const &val)
Convert a C++ type to a JS::Value.
std::tuple< type_transform< Types >... > arg_types
Definition: FunctionWrapper.h:74
#define T(string_literal)
Definition: secure_crt.cpp:77
void object_type
Definition: FunctionWrapper.h:73
static std::tuple< T > DoConvertFromJS(const ScriptRequest &rq, JS::CallArgs &args, bool &went_ok)
DoConvertFromJS takes a type, a JS argument, and converts.
Definition: FunctionWrapper.h:92
static JSFunction * Create(const ScriptRequest &rq, const char *name)
Return a JSFunction from a C++ function.
Definition: FunctionWrapper.h:380
bool IsPending(const ScriptRequest &rq)
Definition: ScriptExceptions.cpp:28
static JSFunctionSpec Wrap(const char *name)
Return a function spec from a C++ function.
Definition: FunctionWrapper.h:371
const ScriptInterface & GetScriptInterface() const
Return the scriptInterface active when creating this ScriptRequest.
Definition: ScriptInterface.cpp:97
JS::HandleObject nativeScope
Definition: ScriptRequest.h:94
Definition: FunctionWrapper.h:204
static bool CallVoid(const ScriptRequest &rq, JS::HandleValue val, const char *name, const Args &... args)
Call a JS function name, property of object val, with the arguments args.
Definition: FunctionWrapper.h:362
Abstraction around a SpiderMonkey JS::Realm.
Definition: ScriptInterface.h:71
This class introduces templates to conveniently wrap C++ functions in JSNative functions.
Definition: FunctionWrapper.h:39
T *(*)(const ScriptRequest &, JS::CallArgs &) ObjectGetter
Definition: FunctionWrapper.h:270
static std::tuple< const ScriptRequest &, Types... > ConvertFromJS(const ScriptRequest &rq, JS::CallArgs &args, bool &went_ok, std::tuple< const ScriptRequest &, Types... > *)
Definition: FunctionWrapper.h:156
bool CatchPending(const ScriptRequest &rq)
Log and then clear the current pending exception.
Definition: ScriptExceptions.cpp:33
ObjectGetter< typename args_info< callableType >::object_type > GetterFor
Definition: FunctionWrapper.h:275
Spidermonkey maintains some &#39;local&#39; state via the JSContext* object.
Definition: ScriptRequest.h:59
ScriptFunction()=delete
std::conditional_t< std::is_same_v< const ScriptRequest &, T >||std::is_same_v< const ScriptInterface &, T >, T, std::remove_const_t< typename std::remove_reference_t< T > > > type_transform
In JS->C++ calls, types are converted using FromJSVal, and this requires them to be default-construct...
Definition: FunctionWrapper.h:59
static void Register(JSContext *cx, JS::HandleObject scope, const char *name)
Register a function on.
Definition: FunctionWrapper.h:400
static bool Call_(const ScriptRequest &rq, JS::HandleValue val, const char *name, R &ret, const Args &... args)
Wrapper around calling a JS function from C++.
Definition: FunctionWrapper.h:231
static bool Call(const ScriptRequest &rq, JS::HandleValue val, const char *name, JS::MutableHandleValue ret, const Args &... args)
Definition: FunctionWrapper.h:352
bool FromJSVal(const ScriptRequest &rq, const JS::HandleValue val, T &ret)
Convert a JS::Value to a C++ type.