Line data Source code
1 : /* Copyright (C) 2022 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_SCRIPTINTERFACE
19 : #define INCLUDED_SCRIPTINTERFACE
20 :
21 : #include "ps/Errors.h"
22 : #include "scriptinterface/ScriptConversions.h"
23 : #include "scriptinterface/ScriptExceptions.h"
24 : #include "scriptinterface/ScriptRequest.h"
25 : #include "scriptinterface/ScriptTypes.h"
26 :
27 : #include <map>
28 :
29 0 : ERROR_GROUP(Scripting);
30 : ERROR_TYPE(Scripting, SetupFailed);
31 :
32 : ERROR_SUBGROUP(Scripting, LoadFile);
33 : ERROR_TYPE(Scripting_LoadFile, OpenFailed);
34 : ERROR_TYPE(Scripting_LoadFile, EvalErrors);
35 :
36 : ERROR_TYPE(Scripting, CallFunctionFailed);
37 : ERROR_TYPE(Scripting, DefineConstantFailed);
38 : ERROR_TYPE(Scripting, CreateObjectFailed);
39 0 : ERROR_TYPE(Scripting, TypeDoesNotExist);
40 :
41 0 : ERROR_SUBGROUP(Scripting, DefineType);
42 0 : ERROR_TYPE(Scripting_DefineType, AlreadyExists);
43 0 : ERROR_TYPE(Scripting_DefineType, CreationFailed);
44 :
45 : // Set the maximum number of function arguments that can be handled
46 : // (This should be as small as possible (for compiler efficiency),
47 : // but as large as necessary for all wrapped functions)
48 : #define SCRIPT_INTERFACE_MAX_ARGS 8
49 :
50 : class ScriptInterface;
51 : struct ScriptInterface_impl;
52 :
53 : class ScriptContext;
54 : // Using a global object for the context is a workaround until Simulation, AI, etc,
55 : // use their own threads and also their own contexts.
56 : extern thread_local std::shared_ptr<ScriptContext> g_ScriptContext;
57 :
58 : namespace boost { namespace random { class rand48; } }
59 :
60 : class Path;
61 : using VfsPath = Path;
62 :
63 : /**
64 : * Abstraction around a SpiderMonkey JS::Realm.
65 : *
66 : * Thread-safety:
67 : * - May be used in non-main threads.
68 : * - Each ScriptInterface must be created, used, and destroyed, all in a single thread
69 : * (it must never be shared between threads).
70 : */
71 : class ScriptInterface
72 : {
73 : NONCOPYABLE(ScriptInterface);
74 :
75 : friend class ScriptRequest;
76 :
77 : public:
78 :
79 : /**
80 : * Constructor.
81 : * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will
82 : * be placed into, as a scoping mechanism; typically "Engine"
83 : * @param debugName Name of this interface for CScriptStats purposes.
84 : * @param context ScriptContext to use when initializing this interface.
85 : */
86 : ScriptInterface(const char* nativeScopeName, const char* debugName, const std::shared_ptr<ScriptContext>& context);
87 :
88 : /**
89 : * Alternate constructor. This creates the new Realm in the same Compartment as the neighbor scriptInterface.
90 : * This means that data can be freely exchanged between these two script interfaces without cloning.
91 : * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will
92 : * be placed into, as a scoping mechanism; typically "Engine"
93 : * @param debugName Name of this interface for CScriptStats purposes.
94 : * @param scriptInterface 'Neighbor' scriptInterface to share a compartment with.
95 : */
96 : ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor);
97 :
98 : ~ScriptInterface();
99 :
100 : struct CmptPrivate
101 : {
102 : friend class ScriptInterface;
103 : public:
104 : static const ScriptInterface& GetScriptInterface(JSContext* cx);
105 : static void* GetCBData(JSContext* cx);
106 : protected:
107 : ScriptInterface* pScriptInterface; // the ScriptInterface object the compartment belongs to
108 : void* pCBData; // meant to be used as the "this" object for callback functions
109 : };
110 :
111 : void SetCallbackData(void* pCBData);
112 :
113 : /**
114 : * Convert the CmptPrivate callback data to T*
115 : */
116 : template <typename T>
117 587 : static T* ObjectFromCBData(const ScriptRequest& rq)
118 : {
119 : static_assert(!std::is_same_v<void, T>);
120 587 : ScriptInterface::CmptPrivate::GetCBData(rq.cx);
121 587 : return static_cast<T*>(ObjectFromCBData<void>(rq));
122 : }
123 :
124 : /**
125 : * Variant for the function wrapper.
126 : */
127 : template <typename T>
128 124 : static T* ObjectFromCBData(const ScriptRequest& rq, JS::CallArgs&)
129 : {
130 124 : return ObjectFromCBData<T>(rq);
131 : }
132 :
133 : /**
134 : * GetGeneralJSContext returns the context without starting a GC request and without
135 : * entering the ScriptInterface compartment. It should only be used in specific situations,
136 : * for instance when initializing a persistent rooted.
137 : * If you need the compartmented context of the ScriptInterface, you should create a
138 : * ScriptInterface::Request and use the context from that.
139 : */
140 : JSContext* GetGeneralJSContext() const;
141 : std::shared_ptr<ScriptContext> GetContext() const;
142 :
143 : /**
144 : * Load global scripts that most script interfaces need,
145 : * located in the /globalscripts directory. VFS must be initialized.
146 : */
147 : bool LoadGlobalScripts();
148 :
149 : /**
150 : * Replace the default JS random number generator with a seeded, network-synced one.
151 : */
152 : bool ReplaceNondeterministicRNG(boost::random::rand48& rng);
153 :
154 : /**
155 : * Call a constructor function, equivalent to JS "new ctor(arg)".
156 : * @param ctor An object that can be used as constructor
157 : * @param argv Constructor arguments
158 : * @param out The new object; On error an error message gets logged and out is Null (out.isNull() == true).
159 : */
160 : void CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const;
161 :
162 : JSObject* CreateCustomObject(const std::string & typeName) const;
163 : void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs);
164 :
165 : /**
166 : * Set the named property on the global object.
167 : * Optionally makes it {ReadOnly, DontEnum}. We do not allow to make it DontDelete, so that it can be hotloaded
168 : * by deleting it and re-creating it, which is done by setting @p replace to true.
169 : */
170 : template<typename T>
171 : bool SetGlobal(const char* name, const T& value, bool replace = false, bool constant = true, bool enumerate = true);
172 :
173 : /**
174 : * Get an object from the global scope or any lexical scope.
175 : * This can return globally accessible objects even if they are not properties
176 : * of the global object (e.g. ES6 class definitions).
177 : * @param name - Name of the property.
178 : * @param out The object or null.
179 : */
180 : static bool GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out);
181 :
182 : bool SetPrototype(JS::HandleValue obj, JS::HandleValue proto);
183 :
184 : /**
185 : * Load and execute the given script in a new function scope.
186 : * @param filename Name for debugging purposes (not used to load the file)
187 : * @param code JS code to execute
188 : * @return true on successful compilation and execution; false otherwise
189 : */
190 : bool LoadScript(const VfsPath& filename, const std::string& code) const;
191 :
192 : /**
193 : * Load and execute the given script in the global scope.
194 : * @param filename Name for debugging purposes (not used to load the file)
195 : * @param code JS code to execute
196 : * @return true on successful compilation and execution; false otherwise
197 : */
198 : bool LoadGlobalScript(const VfsPath& filename, const std::string& code) const;
199 :
200 : /**
201 : * Load and execute the given script in the global scope.
202 : * @return true on successful compilation and execution; false otherwise
203 : */
204 : bool LoadGlobalScriptFile(const VfsPath& path) const;
205 :
206 : /**
207 : * Evaluate some JS code in the global scope.
208 : * @return true on successful compilation and execution; false otherwise
209 : */
210 : bool Eval(const char* code) const;
211 : bool Eval(const char* code, JS::MutableHandleValue out) const;
212 : template<typename T> bool Eval(const char* code, T& out) const;
213 :
214 : /**
215 : * Calls the random number generator assigned to this ScriptInterface instance and returns the generated number.
216 : */
217 : bool MathRandom(double& nbr) const;
218 :
219 : /**
220 : * JSNative wrapper of the above.
221 : */
222 : static bool Math_random(JSContext* cx, uint argc, JS::Value* vp);
223 :
224 : /**
225 : * Retrieve the private data field of a JSObject that is an instance of the given JSClass.
226 : */
227 : template <typename T>
228 : static T* GetPrivate(const ScriptRequest& rq, JS::HandleObject thisobj, JSClass* jsClass)
229 : {
230 : T* value = static_cast<T*>(JS_GetInstancePrivate(rq.cx, thisobj, jsClass, nullptr));
231 :
232 : if (value == nullptr)
233 : ScriptException::Raise(rq, "Private data of the given object is null!");
234 :
235 : return value;
236 : }
237 :
238 : /**
239 : * Retrieve the private data field of a JS Object that is an instance of the given JSClass.
240 : * If an error occurs, GetPrivate will report it with the according stack.
241 : */
242 : template <typename T>
243 1 : static T* GetPrivate(const ScriptRequest& rq, JS::CallArgs& callArgs, JSClass* jsClass)
244 : {
245 1 : if (!callArgs.thisv().isObject())
246 : {
247 0 : ScriptException::Raise(rq, "Cannot retrieve private JS class data because from a non-object value!");
248 0 : return nullptr;
249 : }
250 :
251 2 : JS::RootedObject thisObj(rq.cx, &callArgs.thisv().toObject());
252 1 : T* value = static_cast<T*>(JS_GetInstancePrivate(rq.cx, thisObj, jsClass, &callArgs));
253 :
254 1 : if (value == nullptr)
255 0 : ScriptException::Raise(rq, "Private data of the given object is null!");
256 :
257 1 : return value;
258 : }
259 :
260 : private:
261 : bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate);
262 :
263 11392 : struct CustomType
264 : {
265 : JS::PersistentRootedObject m_Prototype;
266 : JSClass* m_Class;
267 : JSNative m_Constructor;
268 : };
269 :
270 : CmptPrivate m_CmptPrivate;
271 :
272 : // Take care to keep this declaration before heap rooted members. Destructors of heap rooted
273 : // members have to be called before the custom destructor of ScriptInterface_impl.
274 : std::unique_ptr<ScriptInterface_impl> m;
275 :
276 : std::map<std::string, CustomType> m_CustomObjectTypes;
277 : };
278 :
279 : // Explicitly instantiate void* as that is used for the generic template,
280 : // and we want to define it in the .cpp file.
281 : template <> void* ScriptInterface::ObjectFromCBData(const ScriptRequest& rq);
282 :
283 : template<typename T>
284 10927 : bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate)
285 : {
286 21854 : ScriptRequest rq(this);
287 21854 : JS::RootedValue val(rq.cx);
288 10927 : Script::ToJSVal(rq, &val, value);
289 21854 : return SetGlobal_(name, val, replace, constant, enumerate);
290 : }
291 :
292 : template<typename T>
293 8 : bool ScriptInterface::Eval(const char* code, T& ret) const
294 : {
295 16 : ScriptRequest rq(this);
296 16 : JS::RootedValue rval(rq.cx);
297 8 : if (!Eval(code, &rval))
298 0 : return false;
299 8 : return Script::FromJSVal(rq, rval, ret);
300 : }
301 :
302 : #endif // INCLUDED_SCRIPTINTERFACE
|