OXYGENHUB
◆ main openmsx / src / commands / Interpreter.cc
15.5 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "Interpreter.hh"
2
3
#include "Command.hh"
4
#include "CommandException.hh"
5
#include "InterpreterOutput.hh"
6
#include "MSXCommandController.hh"
7
#include "TclObject.hh"
8
9
#include "FileOperations.hh"
10
#include "MSXCPUInterface.hh"
11
#include "MSXMotherBoard.hh"
12
#include "Setting.hh"
13
14
#include "narrow.hh"
15
#include "ranges.hh"
16
#include "stl.hh"
17
#include "unreachable.hh"
18
19
#include <bit>
20
#include <cstdint>
21
#include <iostream>
22
#include <span>
23
#include <utility>
24
#include <vector>
25
//#include <tk.h>
26
27
namespace openmsx {
28
29
// See comments in traceProc()
30
namespace {
31
	struct Trace {
32
		uintptr_t id;
33
		BaseSetting* setting;
34
	};
35
}
36
static std::vector<Trace> traces; // sorted on id
37
static uintptr_t traceCount = 0;
38
39
40
static int dummyClose(ClientData /*instanceData*/, Tcl_Interp* /*interp*/, int /*flags*/)
41
{
42
	return 0;
43
}
44
static int dummyInput(ClientData /*instanceData*/, char* /*buf*/,
45
                      int /*bufSize*/, int* /*errorCodePtr*/)
46
{
47
	return 0;
48
}
49
static void dummyWatch(ClientData /*instanceData*/, int /*mask*/)
50
{
51
}
52
static int dummyGetHandle(ClientData /*instanceData*/, int /*direction*/,
53
                          ClientData* /*handlePtr*/)
54
{
55
	return TCL_ERROR;
56
}
57
Tcl_ChannelType Interpreter::channelType = {
58
	"openMSX console",	 // Type name
59
	TCL_CHANNEL_VERSION_5,
60
	TCL_CLOSE2PROC,		 // Close proc (compatible with Tcl 8.6.13; 8.6.16 can have nullptr here)
61
	dummyInput,		 // Input proc
62
	Interpreter::outputProc, // Output proc
63
	nullptr,		 // Seek proc
64
	nullptr,		 // Set option proc
65
	nullptr,		 // Get option proc
66
	dummyWatch,		 // Watch for events on console
67
	dummyGetHandle,		 // Get a handle from the device
68
	dummyClose,		 // Tcl_DriverClose2Proc
69
	nullptr,		 // Tcl_DriverBlockModeProc
70
	nullptr,		 // Tcl_DriverFlushProc
71
	nullptr,		 // Tcl_DriverHandlerProc
72
	nullptr,		 // Tcl_DriverWideSeekProc
73
	nullptr,		 // Tcl_DriverThreadActionProc
74
	nullptr,		 // Tcl_DriverTruncateProc
75
};
76
77
void Interpreter::init(const char* programName) const
78
{
79
	Tcl_FindExecutable(programName);
80
	Tcl_SetSystemEncoding(interp, "utf-8"); // already the default on Linux, but not on Windows?
81
}
82
83
Interpreter::Interpreter()
84
	: interp(Tcl_CreateInterp())
85
{
86
	Tcl_Preserve(interp);
87
88
	// TODO need to investigate this: doesn't work on windows
89
	/*
90
	if (Tcl_Init(interp) != TCL_OK) {
91
		std::cout << "Tcl_Init: " << interp->result << '\n';
92
	}
93
	if (Tk_Init(interp) != TCL_OK) {
94
		std::cout << "Tk_Init error: " << interp->result << '\n';
95
	}
96
	if (Tcl_Eval(interp, "wm withdraw .") != TCL_OK) {
97
		std::cout << "wm withdraw error: " << interp->result << '\n';
98
	}
99
	*/
100
101
	Tcl_Channel channel = Tcl_CreateChannel(&channelType,
102
		"openMSX console", this, TCL_WRITABLE);
103
	if (channel) {
104
		Tcl_SetChannelOption(interp, channel, "-translation", "binary");
105
		Tcl_SetChannelOption(interp, channel, "-buffering", "line");
106
		Tcl_SetChannelOption(interp, channel, "-encoding", "utf-8");
107
	}
108
	Tcl_SetStdChannel(channel, TCL_STDOUT);
109
110
	setVariable(TclObject("env(OPENMSX_USER_DATA)"),
111
	            TclObject(FileOperations::getUserDataDir()));
112
	setVariable(TclObject("env(OPENMSX_SYSTEM_DATA)"),
113
	            TclObject(FileOperations::getSystemDataDir()));
114
}
115
116
Interpreter::~Interpreter()
117
{
118
	// see comment in MSXCPUInterface::cleanup()
119
	MSXCPUInterface::cleanup();
120
121
	if (!Tcl_InterpDeleted(interp)) {
122
		Tcl_DeleteInterp(interp);
123
	}
124
	Tcl_Release(interp);
125
126
	// Tcl_Finalize() should only be called once for the whole application
127
	// tcl8.6 checks for this (tcl8.5 did not).
128
	// Normally we only create/destroy exactly one Interpreter object for
129
	// openMSX, and then simply calling Tcl_Finalize() here is fine. Though
130
	// when running unittest we do create/destroy multiple Interpreter's.
131
	// Another option is to not call Tcl_Finalize(), but that leaves some
132
	// memory allocated, and makes memory-leak checkers report more errors.
133
	static bool scheduled = false;
134
	if (!scheduled) {
135
		scheduled = true;
136
		atexit(Tcl_Finalize);
137
	}
138
}
139
140
int Interpreter::outputProc(ClientData clientData, const char* buf,
141
                 int toWrite, int* /*errorCodePtr*/)
142
{
143
	try {
144
		auto* output = static_cast<Interpreter*>(clientData)->output;
145
		std::string_view text(buf, toWrite);
146
		if (!text.empty() && output) {
147
			output->output(text);
148
		}
149
	} catch (...) {
150
		UNREACHABLE; // we cannot let exceptions pass through Tcl
151
	}
152
	return toWrite;
153
}
154
155
bool Interpreter::hasCommand(zstring_view name) const
156
{
157
	// Note: these are not only the commands registered via
158
	// registerCommand(), but all commands know to this Tcl-interpreter.
159
	return Tcl_FindCommand(interp, name.c_str(), nullptr, 0);
160
}
161
162
void Interpreter::registerCommand(zstring_view name, Command& command)
163
{
164
	auto token = Tcl_CreateObjCommand(
165
		interp, name.c_str(), commandProc,
166
		static_cast<ClientData>(&command), nullptr);
167
	command.setToken(token);
168
}
169
170
void Interpreter::unregisterCommand(Command& command)
171
{
172
	Tcl_DeleteCommandFromToken(interp, static_cast<Tcl_Command>(command.getToken()));
173
}
174
175
int Interpreter::commandProc(ClientData clientData, Tcl_Interp* interp,
176
                             int objc, Tcl_Obj* const* objv)
177
{
178
	try {
179
		auto& command = *static_cast<Command*>(clientData);
180
		std::span<const TclObject> tokens(
181
			std::bit_cast<TclObject*>(const_cast<Tcl_Obj**>(objv)),
182
			objc);
183
		int res = TCL_OK;
184
		TclObject result;
185
		try {
186
			if (!command.isAllowedInEmptyMachine()) {
187
				if (const auto* controller =
188
					dynamic_cast<const MSXCommandController*>(
189
						&command.getCommandController())) {
190
					if (!controller->getMSXMotherBoard().getMachineConfig()) {
191
						throw CommandException(
192
							"Can't execute command in empty machine");
193
					}
194
				}
195
			}
196
			command.execute(tokens, result);
197
		} catch (MSXException& e) {
198
			result = e.getMessage();
199
			res = TCL_ERROR;
200
		}
201
		Tcl_SetObjResult(interp, result.getTclObject());
202
		return res;
203
	} catch (...) {
204
		UNREACHABLE; // we cannot let exceptions pass through Tcl
205
	}
206
}
207
208
// Returns
209
//   - build-in Tcl commands
210
//   - openmsx commands
211
//   - user-defined procs
212
TclObject Interpreter::getCommandNames()
213
{
214
	return execute("openmsx::all_command_names");
215
}
216
217
bool Interpreter::isComplete(zstring_view command) const
218
{
219
	return Tcl_CommandComplete(command.c_str()) != 0;
220
}
221
222
TclObject Interpreter::execute(zstring_view command)
223
{
224
	if (Tcl_Eval(interp, command.c_str()) != TCL_OK) {
225
		throw CommandException(Tcl_GetStringResult(interp));
226
	}
227
	return TclObject(Tcl_GetObjResult(interp));
228
}
229
230
TclObject Interpreter::executeFile(zstring_view filename)
231
{
232
	if (Tcl_EvalFile(interp, filename.c_str()) != TCL_OK) {
233
		throw CommandException(Tcl_GetStringResult(interp));
234
	}
235
	return TclObject(Tcl_GetObjResult(interp));
236
}
237
238
static void setVar(Tcl_Interp* interp, const TclObject& name, const TclObject& value)
239
{
240
	if (!Tcl_ObjSetVar2(interp, name.getTclObjectNonConst(), nullptr,
241
		            value.getTclObjectNonConst(),
242
		            TCL_GLOBAL_ONLY | TCL_LEAVE_ERR_MSG)) {
243
		// might contain error message of a trace proc
244
		std::cerr << Tcl_GetStringResult(interp) << '\n';
245
	}
246
}
247
static Tcl_Obj* getVar(Tcl_Interp* interp, const TclObject& name)
248
{
249
	return Tcl_ObjGetVar2(interp, name.getTclObjectNonConst(), nullptr,
250
	                      TCL_GLOBAL_ONLY);
251
}
252
253
void Interpreter::setVariable(const TclObject& name, const TclObject& value)
254
{
255
	if (!Tcl_ObjSetVar2(interp, name.getTclObjectNonConst(), nullptr,
256
		            value.getTclObjectNonConst(),
257
		            TCL_GLOBAL_ONLY | TCL_LEAVE_ERR_MSG)) {
258
		throw CommandException(Tcl_GetStringResult(interp));
259
	}
260
}
261
262
void Interpreter::setVariable(const TclObject& arrayName, const TclObject& arrayIndex, const TclObject& value)
263
{
264
	if (!Tcl_ObjSetVar2(interp, arrayName.getTclObjectNonConst(), arrayIndex.getTclObjectNonConst(),
265
		            value.getTclObjectNonConst(),
266
		            TCL_GLOBAL_ONLY | TCL_LEAVE_ERR_MSG)) {
267
		throw CommandException(Tcl_GetStringResult(interp));
268
	}
269
}
270
271
void Interpreter::unsetVariable(const char* name)
272
{
273
	Tcl_UnsetVar(interp, name, TCL_GLOBAL_ONLY);
274
}
275
276
static TclObject getSafeValue(const BaseSetting& setting)
277
{
278
	// TODO use c++23 std::optional<T>::or_else()
279
	if (auto val = setting.getOptionalValue()) {
280
		return *val;
281
	}
282
	return TclObject(0); // 'safe' value, see comment in registerSetting()
283
}
284
void Interpreter::registerSetting(BaseSetting& variable)
285
{
286
	const auto& name = variable.getFullNameObj();
287
	if (Tcl_Obj* tclVarValue = getVar(interp, name)) {
288
		// Tcl var already existed, use this value
289
		try {
290
			variable.setValueDirect(TclObject(tclVarValue));
291
		} catch (MSXException&) {
292
			// Ignore: can happen in case of proxy settings when
293
			// the current machine doesn't have this setting.
294
			// E.g.
295
			//   (start with cbios machine)
296
			//   set renshaturbo 0
297
			//   create_machine
298
			//   machine2::load_machine Panasonic_FS-A1GT
299
		}
300
	} else {
301
		// define Tcl var
302
		setVariable(name, getSafeValue(variable));
303
	}
304
305
	// The call setVariable() above can already trigger traces on this
306
	// variable (in Tcl it's possible to already set traces on a variable
307
	// before that variable is defined). We didn't yet set a trace on it
308
	// ourselves. So for example on proxy-settings we don't yet delegate
309
	// read/writes to the actual setting. This means that inside the trace
310
	// callback we see the value set above instead of the 'actual' value.
311
	//
312
	// This scenario can be triggered in the load_icons script by
313
	// executing the following commands (interactively):
314
	//   delete_machine machine1
315
	//   create_machine
316
	//   machine2::load_machine msx2
317
	//
318
	// Before changing the 'safe-value' (see getSafeValue()) to '0',
319
	// this gave errors because the load_icons script didn't expect to see
320
	// 'proxy' (the old 'safe-value') as value.
321
	//
322
	// The current solution (choosing '0' as safe value) is not ideal, but
323
	// good enough for now.
324
	//
325
	// A possible better solution is to move Tcl_TraceVar() before
326
	// setVariable(), I did an initial attempt but there were some
327
	// problems. TODO investigate this further.
328
329
	uintptr_t traceID = traceCount++;
330
	traces.emplace_back(Trace{.id = traceID, .setting = &variable}); // still in sorted order
331
	Tcl_TraceVar(interp, name.getString().data(), // 0-terminated
332
	             TCL_TRACE_READS | TCL_TRACE_WRITES | TCL_TRACE_UNSETS,
333
	             traceProc, std::bit_cast<ClientData>(traceID));
334
}
335
336
void Interpreter::unregisterSetting(BaseSetting& variable)
337
{
338
	auto it = rfind_unguarded(traces, &variable, &Trace::setting);
339
	uintptr_t traceID = it->id;
340
	traces.erase(it);
341
342
	const char* name = variable.getFullName().data(); // 0-terminated
343
	Tcl_UntraceVar(interp, name,
344
	               TCL_TRACE_READS | TCL_TRACE_WRITES | TCL_TRACE_UNSETS,
345
	               traceProc, std::bit_cast<ClientData>(traceID));
346
	unsetVariable(name);
347
}
348
349
static BaseSetting* getTraceSetting(uintptr_t traceID)
350
{
351
	auto t = binary_find(traces, traceID, {}, &Trace::id);
352
	return t ? t->setting : nullptr;
353
}
354
355
#ifndef NDEBUG
356
static std::string_view removeColonColon(std::string_view s)
357
{
358
	if (s.starts_with("::")) s.remove_prefix(2);
359
	return s;
360
}
361
#endif
362
363
char* Interpreter::traceProc(ClientData clientData, Tcl_Interp* interp,
364
                             const char* part1, const char* /*part2*/, int flags)
365
{
366
	try {
367
		// Lookup Setting object that belongs to this Tcl variable.
368
		//
369
		// In a previous implementation we passed this object directly
370
		// as the clientData. However this went wrong in the following
371
		// scenario:
372
		//
373
		//    proc foo {} { carta eject ; carta spmanbow.rom }
374
		//    bind Q foo
375
		//    [press Q twice]
376
		//
377
		// The problem is that when a SCC cartridge is removed, we
378
		// delete several settings (e.g. SCC_ch1_mute). While deleting
379
		// a setting we unset the corresponding Tcl variable (see
380
		// unregisterSetting() above), this in turn triggers a
381
		// TCL_TRACE_UNSET callback (this function). To prevent this
382
		// callback from triggering we first remove the trace before
383
		// unsetting the variable. However it seems when a setting is
384
		// deleted from within an active Tcl proc (like in the example
385
		// above), the callback is anyway triggered, but only at the
386
		// end of the proc (so in the foo proc above, the settings
387
		// are deleted after the first statement, but the callbacks
388
		// only happen after the second statement). By that time the
389
		// Setting object is already deleted and the callback function
390
		// works on a deleted object.
391
		//
392
		// To prevent this we don't anymore pass a pointer to the
393
		// Setting object as clientData, but we lookup the Setting in
394
		// a map. If the Setting was deleted, we won't find it anymore
395
		// in the map and return.
396
397
		auto traceID = std::bit_cast<uintptr_t>(clientData);
398
		auto* variable = getTraceSetting(traceID);
399
		if (!variable) return nullptr;
400
401
		const TclObject& part1Obj = variable->getFullNameObj();
402
		assert(removeColonColon(part1) == removeColonColon(part1Obj.getString()));
403
404
		static std::string static_string;
405
		if (flags & TCL_TRACE_READS) {
406
			try {
407
				setVar(interp, part1Obj, variable->getValue());
408
			} catch (MSXException& e) {
409
				static_string = std::move(e).getMessage();
410
				return static_string.data();
411
			}
412
		}
413
		if (flags & TCL_TRACE_WRITES) {
414
			try {
415
				Tcl_Obj* v = getVar(interp, part1Obj);
416
				TclObject newValue(v ? v : Tcl_NewObj());
417
				variable->setValueDirect(newValue);
418
				const TclObject& newValue2 = variable->getValue();
419
				if (newValue != newValue2) {
420
					setVar(interp, part1Obj, newValue2);
421
				}
422
			} catch (MSXException& e) {
423
				setVar(interp, part1Obj, getSafeValue(*variable));
424
				static_string = std::move(e).getMessage();
425
				return static_string.data();
426
			}
427
		}
428
		if (flags & TCL_TRACE_UNSETS) {
429
			try {
430
				// note we cannot use restoreDefault(), because
431
				// that goes via Tcl and the Tcl variable
432
				// doesn't exist at this point
433
				variable->setValueDirect(TclObject(
434
					variable->getDefaultValue()));
435
			} catch (MSXException&) {
436
				// for some reason default value is not valid ATM,
437
				// keep current value (happened for videosource
438
				// setting before turning on (set power on) the
439
				// MSX machine)
440
			}
441
			setVar(interp, part1Obj, getSafeValue(*variable));
442
			Tcl_TraceVar(interp, part1, TCL_TRACE_READS |
443
			                TCL_TRACE_WRITES | TCL_TRACE_UNSETS,
444
			             traceProc,
445
			             std::bit_cast<ClientData>(traceID));
446
		}
447
	} catch (...) {
448
		UNREACHABLE; // we cannot let exceptions pass through Tcl
449
	}
450
	return nullptr;
451
}
452
453
void Interpreter::createNamespace(const std::string& name)
454
{
455
	execute(tmpStrCat("namespace eval ", name, " {}"));
456
}
457
458
void Interpreter::deleteNamespace(const std::string& name)
459
{
460
	execute(tmpStrCat("namespace delete ", name));
461
}
462
463
void Interpreter::poll() const
464
{
465
	//Tcl_ServiceAll();
466
	Tcl_DoOneEvent(TCL_DONT_WAIT);
467
}
468
469
TclParser Interpreter::parse(std::string_view command)
470
{
471
	return {interp, command};
472
}
473
474
bool Interpreter::validExpression(std::string_view expression)
475
{
476
	Tcl_Parse parseInfo;
477
	int result = Tcl_ParseExpr(interp, expression.data(), narrow<int>(expression.size()), &parseInfo);
478
	Tcl_FreeParse(&parseInfo);
479
	return result == TCL_OK;
480
}
481
482
std::string Interpreter::parseCommandError(std::string_view command)
483
{
484
	Tcl_Parse parseInfo;
485
	int result = Tcl_ParseCommand(interp, command.data(), narrow<int>(command.size()), 0, &parseInfo);
486
	Tcl_FreeParse(&parseInfo);
487
	return (result == TCL_OK) ? std::string{} : std::string(Tcl_GetStringResult(interp));
488
}
489
490
std::string Interpreter::parseExpressionError(std::string_view expression)
491
{
492
	Tcl_Parse parseInfo;
493
	int result = Tcl_ParseExpr(interp, expression.data(), narrow<int>(expression.size()), &parseInfo);
494
	Tcl_FreeParse(&parseInfo);
495
	return (result == TCL_OK) ? std::string{} : std::string(Tcl_GetStringResult(interp));
496
}
497
498
void Interpreter::wrongNumArgs(unsigned argc, std::span<const TclObject> tokens, const char* message)
499
{
500
	assert(argc <= tokens.size());
501
	Tcl_WrongNumArgs(interp, narrow<int>(argc), std::bit_cast<Tcl_Obj* const*>(tokens.data()), message);
502
	// not efficient, but anyway on an error path
503
	throw CommandException(Tcl_GetStringResult(interp));
504
}
505
506
} // namespace openmsx