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
|