File: ScriptInterface.h

package info (click to toggle)
0ad 0.27.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 171,928 kB
  • sloc: cpp: 194,011; javascript: 19,098; ansic: 15,066; python: 6,328; sh: 1,695; perl: 1,575; java: 533; xml: 415; php: 192; makefile: 99
file content (318 lines) | stat: -rw-r--r-- 10,972 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/* 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 <http://www.gnu.org/licenses/>.
 */

#ifndef INCLUDED_SCRIPTINTERFACE
#define INCLUDED_SCRIPTINTERFACE

#include "ps/Errors.h"
#include "scriptinterface/ScriptConversions.h"
#include "scriptinterface/ScriptExceptions.h"
#include "scriptinterface/ScriptRequest.h"
#include "scriptinterface/ScriptTypes.h"

#include <map>

ERROR_GROUP(Scripting);
ERROR_TYPE(Scripting, SetupFailed);

ERROR_SUBGROUP(Scripting, LoadFile);
ERROR_TYPE(Scripting_LoadFile, OpenFailed);
ERROR_TYPE(Scripting_LoadFile, EvalErrors);

ERROR_TYPE(Scripting, CallFunctionFailed);
ERROR_TYPE(Scripting, DefineConstantFailed);
ERROR_TYPE(Scripting, CreateObjectFailed);
ERROR_TYPE(Scripting, TypeDoesNotExist);

ERROR_SUBGROUP(Scripting, DefineType);
ERROR_TYPE(Scripting_DefineType, AlreadyExists);
ERROR_TYPE(Scripting_DefineType, CreationFailed);

// Set the maximum number of function arguments that can be handled
// (This should be as small as possible (for compiler efficiency),
// but as large as necessary for all wrapped functions)
#define SCRIPT_INTERFACE_MAX_ARGS 8

class ScriptInterface;
struct ScriptInterface_impl;

class ScriptContext;
// Using a global object for the context is a workaround until Simulation, AI, etc,
// use their own threads and also their own contexts.
extern thread_local std::shared_ptr<ScriptContext> g_ScriptContext;

namespace boost { namespace random { class rand48; } }

class Path;
using VfsPath = Path;

/**
 * Abstraction around a SpiderMonkey JS::Realm.
 *
 * Thread-safety:
 * - May be used in non-main threads.
 * - Each ScriptInterface must be created, used, and destroyed, all in a single thread
 *   (it must never be shared between threads).
 */
class ScriptInterface
{
	NONCOPYABLE(ScriptInterface);

	friend class ScriptRequest;

public:

	/**
	 * Constructor.
	 * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will
	 *   be placed into, as a scoping mechanism; typically "Engine"
	 * @param debugName Name of this interface for CScriptStats purposes.
	 * @param context ScriptContext to use when initializing this interface.
	 */
	ScriptInterface(const char* nativeScopeName, const char* debugName, ScriptContext& context);

	template<typename Context>
	ScriptInterface(const char* nativeScopeName, const char* debugName, Context&& context) :
		ScriptInterface(nativeScopeName, debugName, *context)
	{
		static_assert(std::is_lvalue_reference_v<Context>, "`ScriptInterface` doesn't take ownership "
			"of the context.");
	}

	/**
	 * Alternate constructor. This creates the new Realm in the same Compartment as the neighbor scriptInterface.
	 * This means that data can be freely exchanged between these two script interfaces without cloning.
	 * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will
	 *   be placed into, as a scoping mechanism; typically "Engine"
	 * @param debugName Name of this interface for CScriptStats purposes.
	 * @param scriptInterface 'Neighbor' scriptInterface to share a compartment with.
	 */
	ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor);

	~ScriptInterface();

	struct CmptPrivate
	{
		friend class ScriptInterface;
	public:
		static const ScriptInterface& GetScriptInterface(JSContext* cx);
		static void* GetCBData(JSContext* cx);
	protected:
		ScriptInterface* pScriptInterface; // the ScriptInterface object the compartment belongs to
		void* pCBData; // meant to be used as the "this" object for callback functions
	};

	void SetCallbackData(void* pCBData);

	/**
	 * Convert the CmptPrivate callback data to T*
	 */
	template <typename T>
	static T* ObjectFromCBData(const ScriptRequest& rq)
	{
		static_assert(!std::is_same_v<void, T>);
		return static_cast<T*>(ObjectFromCBData<void>(rq));
	}

	/**
	 * Variant for the function wrapper.
	 */
	template <typename T>
	static T* ObjectFromCBData(const ScriptRequest& rq, JS::CallArgs&)
	{
		return ObjectFromCBData<T>(rq);
	}

	/**
	 * GetGeneralJSContext returns the context without starting a GC request and without
	 * entering the ScriptInterface compartment. It should only be used in specific situations,
	 * for instance when initializing a persistent rooted.
	 * If you need the compartmented context of the ScriptInterface, you should create a
	 * ScriptInterface::Request and use the context from that.
	 */
	JSContext* GetGeneralJSContext() const;
	ScriptContext& GetContext() const;

	/**
	 * Load global scripts that most script interfaces need,
	 * located in the /globalscripts directory. VFS must be initialized.
	 */
	bool LoadGlobalScripts();

	/**
	 * Replace the default JS random number generator with a seeded, network-synced one.
	 */
	bool ReplaceNondeterministicRNG(boost::random::rand48& rng);

	/**
	 * Call a constructor function, equivalent to JS "new ctor(arg)".
	 * @param ctor An object that can be used as constructor
	 * @param argv Constructor arguments
	 * @param out The new object; On error an error message gets logged and out is Null (out.isNull() == true).
	 */
	void CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const;

	JSObject* CreateCustomObject(const std::string & typeName) const;
	void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs);

	/**
	 * Set the named property on the global object.
	 * Optionally makes it {ReadOnly, DontEnum}. We do not allow to make it DontDelete, so that it can be hotloaded
	 * by deleting it and re-creating it, which is done by setting @p replace to true.
	 */
	template<typename T>
	bool SetGlobal(const char* name, const T& value, bool replace = false, bool constant = true, bool enumerate = true);

	/**
	 * Get an object from the global scope or any lexical scope.
	 * This can return globally accessible objects even if they are not properties
	 * of the global object (e.g. ES6 class definitions).
	 * @param name - Name of the property.
	 * @param out The object or null.
	 */
	static bool GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out);

	bool SetPrototype(JS::HandleValue obj, JS::HandleValue proto);

	/**
	 * Load and execute the given script in a new function scope.
	 * @param filename Name for debugging purposes (not used to load the file)
	 * @param code JS code to execute
	 * @return true on successful compilation and execution; false otherwise
	 */
	bool LoadScript(const VfsPath& filename, const std::string& code) const;

	/**
	 * Load and execute the given script in the global scope.
	 * @param filename Name for debugging purposes (not used to load the file)
	 * @param code JS code to execute
	 * @return true on successful compilation and execution; false otherwise
	 */
	bool LoadGlobalScript(const VfsPath& filename, const std::string& code) const;

	/**
	 * Load and execute the given script in the global scope.
	 * @return true on successful compilation and execution; false otherwise
	 */
	bool LoadGlobalScriptFile(const VfsPath& path) const;

	/**
	 * Evaluate some JS code in the global scope.
	 * @return true on successful compilation and execution; false otherwise
	 */
	bool Eval(const char* code) const;
	bool Eval(const char* code, JS::MutableHandleValue out) const;
	template<typename T> bool Eval(const char* code, T& out) const;

	/**
	 * Calls the random number generator assigned to this ScriptInterface instance and returns the generated number.
	 */
	bool MathRandom(double& nbr) const;

	/**
	 * JSNative wrapper of the above.
	 */
	static bool Math_random(JSContext* cx, uint argc, JS::Value* vp);

	/**
	 * Name the reserved slots we may need to use in custom JSObjects.
	 * When using JSCLASS_HAS_RESERVED_SLOTS in the definition of your JSClass, use the number
	 * of the highest slot you need plus 1.
	 */
	enum JSObjectReservedSlots {
		PRIVATE = 0
	};

	/**
	 * Retrieve the private data field of a JSObject.
	 */
	template <typename T>
	static T* GetPrivate(const ScriptRequest& rq, JS::HandleObject thisobj)
	{
		T* value = JS::GetMaybePtrFromReservedSlot<T>(thisobj, JSObjectReservedSlots::PRIVATE);

		if (value == nullptr)
			ScriptException::Raise(rq, "Private data of the given object is null!");

		return value;
	}

	/**
	 * Retrieve the private data field of a JS Object.
	 * If an error occurs, GetPrivate will report it with the according stack.
	 */
	template <typename T>
	static T* GetPrivate(const ScriptRequest& rq, JS::CallArgs& callArgs)
	{
		if (!callArgs.thisv().isObject())
		{
			ScriptException::Raise(rq, "Cannot retrieve private JS object data because from a non-object value!");
			return nullptr;
		}

		JS::RootedObject thisObj(rq.cx, &callArgs.thisv().toObject());
		T* value = JS::GetMaybePtrFromReservedSlot<T>(thisObj, JSObjectReservedSlots::PRIVATE);

		if (value == nullptr)
			ScriptException::Raise(rq, "Private data of the given object is null!");

		return value;
	}

private:
	bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate);

	struct CustomType
	{
		JS::PersistentRootedObject m_Prototype;
		JSClass* m_Class;
		JSNative m_Constructor;
	};

	CmptPrivate m_CmptPrivate;

	// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
	// members have to be called before the custom destructor of ScriptInterface_impl.
	std::unique_ptr<ScriptInterface_impl> m;

	std::map<std::string, CustomType> m_CustomObjectTypes;
};

// Explicitly instantiate void* as that is used for the generic template,
// and we want to define it in the .cpp file.
template <> void* ScriptInterface::ObjectFromCBData(const ScriptRequest& rq);

template<typename T>
bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate)
{
	ScriptRequest rq(this);
	JS::RootedValue val(rq.cx);
	Script::ToJSVal(rq, &val, value);
	return SetGlobal_(name, val, replace, constant, enumerate);
}

template<typename T>
bool ScriptInterface::Eval(const char* code, T& ret) const
{
	ScriptRequest rq(this);
	JS::RootedValue rval(rq.cx);
	if (!Eval(code, &rval))
		return false;
	return Script::FromJSVal(rq, rval, ret);
}

#endif // INCLUDED_SCRIPTINTERFACE