openMSX
Interpreter.cc
Go to the documentation of this file.
1 #include "Interpreter.hh"
2 #include "EventDistributor.hh"
3 #include "Command.hh"
4 #include "TclObject.hh"
5 #include "CommandException.hh"
7 #include "MSXMotherBoard.hh"
8 #include "Setting.hh"
9 #include "InterpreterOutput.hh"
10 #include "MSXCPUInterface.hh"
11 #include "openmsx.hh"
12 #include "FileOperations.hh"
13 #include "StringOp.hh"
14 #include "unreachable.hh"
15 #include "xrange.hh"
16 #include <iostream>
17 //#include <tk.h>
18 #include "openmsx.hh"
19 
20 using std::string;
21 using std::vector;
22 
23 namespace openmsx {
24 
25 // See comments in traceProc()
26 static std::map<long, Setting*> traceMap;
27 static long traceCount = 0;
28 
29 
30 static int dummyClose(ClientData /*instanceData*/, Tcl_Interp* /*interp*/)
31 {
32  return 0;
33 }
34 static int dummyInput(ClientData /*instanceData*/, char* /*buf*/,
35  int /*bufSize*/, int* /*errorCodePtr*/)
36 {
37  return 0;
38 }
39 static void dummyWatch(ClientData /*instanceData*/, int /*mask*/)
40 {
41 }
42 static int dummyGetHandle(ClientData /*instanceData*/, int /*direction*/,
43  ClientData* /*handlePtr*/)
44 {
45  return TCL_ERROR;
46 }
47 Tcl_ChannelType Interpreter::channelType = {
48  const_cast<char*>("openMSX console"),// Type name
49  nullptr, // Always non-blocking
50  dummyClose, // Close proc
51  dummyInput, // Input proc
52  Interpreter::outputProc, // Output proc
53  nullptr, // Seek proc
54  nullptr, // Set option proc
55  nullptr, // Get option proc
56  dummyWatch, // Watch for events on console
57  dummyGetHandle, // Get a handle from the device
58  nullptr, // Tcl_DriverClose2Proc
59  nullptr, // Tcl_DriverBlockModeProc
60  nullptr, // Tcl_DriverFlushProc
61  nullptr, // Tcl_DriverHandlerProc
62  nullptr, // Tcl_DriverWideSeekProc
63  nullptr, // Tcl_DriverThreadActionProc
64  nullptr, // Tcl_DriverTruncateProc
65 };
66 
67 void Interpreter::init(const char* programName)
68 {
69  Tcl_FindExecutable(programName);
70 }
71 
73  : eventDistributor(eventDistributor_)
74 {
75  interp = Tcl_CreateInterp();
76  Tcl_Preserve(interp);
77 
78  // TODO need to investigate this: doesn't work on windows
79  /*
80  if (Tcl_Init(interp) != TCL_OK) {
81  std::cout << "Tcl_Init: " << interp->result << std::endl;
82  }
83  if (Tk_Init(interp) != TCL_OK) {
84  std::cout << "Tk_Init error: " << interp->result << std::endl;
85  }
86  if (Tcl_Eval(interp, "wm withdraw .") != TCL_OK) {
87  std::cout << "wm withdraw error: " << interp->result << std::endl;
88  }
89  */
90 
91  Tcl_Channel channel = Tcl_CreateChannel(&channelType,
92  "openMSX console", this, TCL_WRITABLE);
93  if (channel) {
94  Tcl_SetChannelOption(interp, channel, "-translation", "binary");
95  Tcl_SetChannelOption(interp, channel, "-buffering", "line");
96  Tcl_SetChannelOption(interp, channel, "-encoding", "utf-8");
97  }
98  Tcl_SetStdChannel(channel, TCL_STDOUT);
99 
100  setVariable("env(OPENMSX_USER_DATA)", FileOperations::getUserDataDir());
101  setVariable("env(OPENMSX_SYSTEM_DATA)", FileOperations::getSystemDataDir());
102 
103  eventDistributor.registerEventListener(OPENMSX_POLL_EVENT, *this);
104 
105 }
106 
108 {
109  // see comment in MSXCPUInterface::cleanup()
111 
112  eventDistributor.unregisterEventListener(OPENMSX_POLL_EVENT, *this);
113 
114  if (!Tcl_InterpDeleted(interp)) {
115  Tcl_DeleteInterp(interp);
116  }
117  Tcl_Release(interp);
118 
119  Tcl_Finalize();
120 }
121 
123 {
124  output = output_;
125 }
126 
127 int Interpreter::outputProc(ClientData clientData, const char* buf,
128  int toWrite, int* /*errorCodePtr*/)
129 {
130  try {
131  auto* output = static_cast<Interpreter*>(clientData)->output;
132  string_ref text(buf, toWrite);
133  if (!text.empty() && output) {
134  output->output(text);
135  }
136  } catch (...) {
137  UNREACHABLE; // we cannot let exceptions pass through Tcl
138  }
139  return toWrite;
140 }
141 
142 void Interpreter::registerCommand(const string& name, Command& command)
143 {
144  assert(commandTokenMap.find(name) == commandTokenMap.end());
145  commandTokenMap[name] = Tcl_CreateObjCommand(
146  interp, name.c_str(), commandProc,
147  static_cast<ClientData>(&command), nullptr);
148 }
149 
151 {
152  auto it = commandTokenMap.find(name);
153  assert(it != commandTokenMap.end());
154  Tcl_DeleteCommandFromToken(interp, it->second);
155  commandTokenMap.erase(it);
156 }
157 
158 int Interpreter::commandProc(ClientData clientData, Tcl_Interp* interp,
159  int objc, Tcl_Obj* const objv[])
160 {
161  try {
162  auto& command = *static_cast<Command*>(clientData);
163  vector<TclObject> tokens;
164  tokens.reserve(objc);
165  for (auto i : xrange(objc)) {
166  tokens.push_back(TclObject(interp, objv[i]));
167  }
168  int res = TCL_OK;
169  TclObject result(interp);
170  try {
171  if (!command.isAllowedInEmptyMachine()) {
172  if (auto controller =
173  dynamic_cast<MSXCommandController*>(
174  &command.getCommandController())) {
175  if (!controller->getMSXMotherBoard().getMachineConfig()) {
176  throw CommandException(
177  "Can't execute command in empty machine");
178  }
179  }
180  }
181  command.execute(tokens, result);
182  } catch (MSXException& e) {
183  PRT_DEBUG(
184  "Interpreter: Got an exception while executing a command: "
185  << e.getMessage());
186  result.setString(e.getMessage());
187  res = TCL_ERROR;
188  }
189  Tcl_SetObjResult(interp, result.getTclObject());
190  return res;
191  } catch (...) {
192  UNREACHABLE; // we cannot let exceptions pass through Tcl
193  return TCL_ERROR;
194  }
195 }
196 
197 // Returns
198 // - build-in Tcl commands
199 // - openmsx commands
200 // - user-defined procs
202 {
203  return splitList(execute("info commands"), interp);
204 }
205 
206 bool Interpreter::isComplete(const string& command) const
207 {
208  return Tcl_CommandComplete(command.c_str()) != 0;
209 }
210 
211 string Interpreter::execute(const string& command)
212 {
213  int success = Tcl_Eval(interp, command.c_str());
214  string result = Tcl_GetStringResult(interp);
215  if (success != TCL_OK) {
216  throw CommandException(result);
217  }
218  return result;
219 }
220 
221 string Interpreter::executeFile(const string& filename)
222 {
223  int success = Tcl_EvalFile(interp, filename.c_str());
224  string result = Tcl_GetStringResult(interp);
225  if (success != TCL_OK) {
226  throw CommandException(result);
227  }
228  return result;
229 }
230 
231 static void setVar(Tcl_Interp* interp, const char* name, const char* value)
232 {
233  if (!Tcl_SetVar(interp, name, value, TCL_GLOBAL_ONLY | TCL_LEAVE_ERR_MSG)) {
234  // might contain error message of a trace proc
235  std::cerr << Tcl_GetStringResult(interp) << std::endl;
236  }
237 }
238 static const char* getVar(Tcl_Interp* interp, const char* name)
239 {
240  return Tcl_GetVar(interp, name, TCL_GLOBAL_ONLY);
241 }
242 
243 void Interpreter::setVariable(const string& name, const string& value)
244 {
245  if (!Tcl_SetVar(interp, name.c_str(), value.c_str(),
246  TCL_GLOBAL_ONLY | TCL_LEAVE_ERR_MSG)) {
247  throw CommandException(Tcl_GetStringResult(interp));
248  }
249 }
250 
251 void Interpreter::unsetVariable(const string& name)
252 {
253  Tcl_UnsetVar(interp, name.c_str(), TCL_GLOBAL_ONLY);
254 }
255 
256 const char* Interpreter::getVariable(const string& name) const
257 {
258  return getVar(interp, name.c_str());
259 }
260 
261 static string getSafeValueString(Setting& setting)
262 {
263  try {
264  return setting.getValueString();
265  } catch (MSXException&) {
266  return "0"; // 'safe' value, see comment in registerSetting()
267  }
268 }
269 void Interpreter::registerSetting(Setting& variable, const string& name)
270 {
271  if (const char* tclVarValue = getVariable(name)) {
272  // Tcl var already existed, use this value
273  try {
274  variable.setValueStringDirect(tclVarValue);
275  } catch (MSXException&) {
276  // Ignore: can happen in case of proxy settings when
277  // the current machine doesn't have this setting.
278  // E.g.
279  // (start with cbios machine)
280  // set renshaturbo 0
281  // create_machine
282  // machine2::load_machine Panasonic_FS-A1GT
283  }
284  } else {
285  // define Tcl var
286  setVariable(name, getSafeValueString(variable));
287  }
288 
289  // The call setVariable() above can already trigger traces on this
290  // variable (in Tcl it's possible to already set traces on a variable
291  // before that variable is defined). We didn't yet set a trace on it
292  // ourselves. So for example on proxy-settings we don't yet delegate
293  // read/writes to the actual setting. This means that inside the trace
294  // callback we see the value set above instead of the 'actual' value.
295  //
296  // This scenario can be triggered in the load_icons script by
297  // executing the following commands (interactively):
298  // delete_machine machine1
299  // create_machine
300  // machine2::load_machine msx2
301  //
302  // Before changing the 'safe-value' (see getSafeValueString()) to '0',
303  // this gave errors because the load_icons script didn't expect to see
304  // 'proxy' (the old 'safe-value') as value.
305  //
306  // The current solution (choosing '0' as safe value) is not ideal, but
307  // good enough for now.
308  //
309  // A possible better solution is to move Tcl_TraceVar() before
310  // setVariable(), I did an initial attempt but there were some
311  // problems. TODO investigate this further.
312 
313  long traceID = traceCount++;
314  traceMap[traceID] = &variable;
315  Tcl_TraceVar(interp, name.c_str(),
316  TCL_TRACE_READS | TCL_TRACE_WRITES | TCL_TRACE_UNSETS,
317  traceProc, reinterpret_cast<ClientData>(traceID));
318 }
319 
320 void Interpreter::unregisterSetting(Setting& variable, const string& name)
321 {
322  auto it = traceMap.begin();
323  while (true) {
324  assert(it != traceMap.end());
325  if (it->second == &variable) break;
326  ++it;
327  }
328 
329  long traceID = it->first;
330  Tcl_UntraceVar(interp, name.c_str(),
331  TCL_TRACE_READS | TCL_TRACE_WRITES | TCL_TRACE_UNSETS,
332  traceProc, reinterpret_cast<ClientData>(traceID));
333  traceMap.erase(it);
334  unsetVariable(name);
335 }
336 
337 static Setting* getTraceSetting(unsigned traceID)
338 {
339  auto it = traceMap.find(traceID);
340  return (it != traceMap.end()) ? it->second : nullptr;
341 }
342 
343 char* Interpreter::traceProc(ClientData clientData, Tcl_Interp* interp,
344  const char* part1, const char* /*part2*/, int flags)
345 {
346  try {
347  // Lookup Setting object that belongs to this Tcl variable.
348  //
349  // In a previous implementation we passed this object directly
350  // as the clientData. However this went wrong in the following
351  // scenario:
352  //
353  // proc foo {} { carta eject ; carta spmanbow.rom }
354  // bind Q foo
355  // [press Q twice]
356  //
357  // The problem is that when a SCC cartridge is removed, we
358  // delete several settings (e.g. SCC_ch1_mute). While deleting
359  // a setting we unset the corresponsing Tcl variable (see
360  // unregisterSetting() above), this in turn triggers a
361  // TCL_TRACE_UNSET callback (this function). To prevent this
362  // callback from triggering we first remove the trace before
363  // unsetting the variable. However it seems when a setting is
364  // deleted from within an active Tcl proc (like in the example
365  // above), the callback is anyway triggered, but only at the
366  // end of the proc (so in the foo proc above, the settings
367  // are deleted after the first statement, but the callbacks
368  // only happen after the second statement). By that time the
369  // Setting object is already deleted and the callback function
370  // works on a deleted object.
371  //
372  // To prevent this we don't anymore pass a pointer to the
373  // Setting object as clientData, but we lookup the Setting in
374  // a map. If the Setting was deleted, we won't find it anymore
375  // in the map and return.
376 
377  auto traceID = reinterpret_cast<uintptr_t>(clientData);
378  auto* variable = getTraceSetting(traceID);
379  if (!variable) return nullptr;
380 
381  static string static_string;
382  if (flags & TCL_TRACE_READS) {
383  try {
384  setVar(interp, part1, variable->getValueString().c_str());
385  } catch (MSXException& e) {
386  static_string = e.getMessage();
387  return const_cast<char*>(static_string.c_str());
388  }
389  }
390  if (flags & TCL_TRACE_WRITES) {
391  try {
392  const char* v = getVar(interp, part1);
393  string newValue = v ? v : "";
394  variable->setValueStringDirect(newValue);
395  string newValue2 = variable->getValueString();
396  if (newValue != newValue2) {
397  setVar(interp, part1, newValue2.c_str());
398  }
399  } catch (MSXException& e) {
400  setVar(interp, part1, getSafeValueString(*variable).c_str());
401  static_string = e.getMessage();
402  return const_cast<char*>(static_string.c_str());
403  }
404  }
405  if (flags & TCL_TRACE_UNSETS) {
406  try {
407  // note we cannot use restoreDefault(), because
408  // that goes via Tcl and the Tcl variable
409  // doesn't exist at this point
410  variable->setValueStringDirect(
411  variable->getRestoreValueString());
412  } catch (MSXException&) {
413  // for some reason default value is not valid ATM,
414  // keep current value (happened for videosource
415  // setting before turning on (set power on) the
416  // MSX machine)
417  }
418  setVar(interp, part1, getSafeValueString(*variable).c_str());
419  Tcl_TraceVar(interp, part1, TCL_TRACE_READS |
420  TCL_TRACE_WRITES | TCL_TRACE_UNSETS,
421  traceProc,
422  reinterpret_cast<ClientData>(traceID));
423  }
424  } catch (...) {
425  UNREACHABLE; // we cannot let exceptions pass through Tcl
426  }
427  return nullptr;
428 }
429 
430 void Interpreter::createNamespace(const std::string& name)
431 {
432  execute("namespace eval ::" + name + " {}");
433 }
434 
435 void Interpreter::deleteNamespace(const std::string& name)
436 {
437  execute("namespace delete ::" + name);
438 }
439 
440 vector<string> Interpreter::splitList(const std::string& list)
441 {
442  return splitList(list, interp);
443 }
444 
445 vector<string> Interpreter::splitList(const string& list, Tcl_Interp* interp)
446 {
447  int argc;
448  const char** argv;
449  if (Tcl_SplitList(interp, list.c_str(), &argc, &argv) == TCL_ERROR) {
450  throw CommandException(
451  interp ? Tcl_GetStringResult(interp)
452  : "splitList error");
453  }
454  vector<string> result(argv, argv + argc);
455  Tcl_Free(reinterpret_cast<char*>(argv));
456  return result;
457 }
458 
459 int Interpreter::signalEvent(const std::shared_ptr<const Event>& event)
460 {
461  (void)event;
462  assert(event->getType() == OPENMSX_POLL_EVENT);
463  poll();
464  return 0;
465 }
466 
467 void Interpreter::poll()
468 {
469  //Tcl_ServiceAll();
470  Tcl_DoOneEvent(TCL_DONT_WAIT);
471 }
472 
474 {
475  return TclParser(interp, command);
476 }
477 
478 } // namespace openmsx