So, we were talking about the debugger.
We’ve seen how hooks are inserted into the execution unit, let’s see how you interact with the debugger.
We extend Common::Console, which already provides some very minimal functionality.
It mainly enables us to easily bind commands to functions, tokenizing arguments on our behalf and giving us C-style argc/argv.
It works like this:
Console::Console(WintermuteEngine *vm) : GUI::Debugger() { DCmd_Register("print_foo", WRAP_METHOD(Console, Cmd_PrintFoo)); DCmd_Register("do_bar", WRAP_METHOD(Console, Cmd_DoBar)); // } // ... bool Console::Cmd_AddBreakpoint(int argc, const char **argv) { DebugPrintf("Foo!\n"); return true; } //...
Then, we proceed to parse user input on our own.
Our methods must return true or false – true for keeping the console open, false for closing it.
Ideally, for simple tasks, we could work on the engine directly from there, e.g.:
DCmd_Register( .... Cmd_SkipInstruction)); Cmd_SkipInstruction (...) { // usage: skip threadnumber _script[atoi(argv[1]))->_iP++; return true; }
This could get messy fast, though.
What we have going on here, though, is a MVA sort of pattern, which is more or less like its more famous brother MVC, except that the +** is bidirectional – the adapter can fiddle with the model and vice versa – the reason is decoupling and ease of mantainance: http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93adapter
MVC:
(Image: https://www.palantir.com)
We have a middle layer called the DebuggerAdapter that works on the script engine for us, while we need to knownothing about the script engine itself in the console proper.
The header looks kinda like this:
namespace Wintermute { class DebuggerAdapter { public: DebuggerAdapter(WintermuteEngine *vm); // Called by Script (=~Model) bool triggerBreakpoint(ScScript *script); bool triggerStep(ScScript *script); bool triggerWatch(ScScript *script, const char *symbol); // Called by Console (~=View) int addWatch(const char *filename, const char *symbol); int addBreakpoint(const char *filename, int line); // ... int stepOver(); int stepInto(); int stepContinue(); // ... SourceFile *_lastSource; private: bool compiledExists(Common::String filename); void reset(); WintermuteEngine *_engine; int32 _lastDepth; ScScript *_lastScript; int32 _lastLine; }; }
An example of how it works:
// in debugger.cpp: bool Console::Cmd_AddBreakpoint(int argc, const char **argv) { /** * Add a breakpoint */ if (argc == 3) { int error = ADAPTER->addBreakpoint(argv[1], atoi(argv[2])); if (!error) { DebugPrintf("%s: OK\n", argv[0]); } else if (error == NO_SUCH_SCRIPT) { Common::String msg = Common::String::format("no such script: %s, breakpoint NOT created\n", argv[1]); debugWarning(argv[0], ERROR, msg); } else if (error == NO_SUCH_SOURCE) { Common::String msg = Common::String::format("no such source file: %s\n", argv[1]); debugWarning(argv[0], WARNING, msg); } else if (error == NO_SUCH_LINE) { Common::String msg = Common::String::format("source %s has no line %d\n", argv[1], atoi(argv[2])); debugWarning(argv[0], WARNING, msg); } else if (error == IS_BLANK) { Common::String msg = Common::String::format("%s:%d looks like a comment/blank line.\n", argv[1], atoi(argv[2])); debugWarning(argv[0], WARNING, msg); } else { Common::String msg = Common::String::format("Error code %d", error); debugWarning(argv[0], WARNING, msg); } } else { DebugPrintf("Usage: %s to break at line of file \n", argv[0]); } return true; } // in debugger_adapter.cpp int DebuggerAdapter::addBreakpoint(const char *filename, int line) { // TODO: Check if file exists, check if line exists assert(SCENGINE); if (!compiledExists(filename)) { return NO_SUCH_SCRIPT; } int isLegal = isBreakpointLegal(filename, line); if (isLegal == OK) { SCENGINE->addBreakpoint(filename, line); return OK; } else if (isLegal == IS_BLANK) { // We don't have the SOURCE. A warning will do. SCENGINE->addBreakpoint(filename, line); return IS_BLANK; } else if (isLegal == NO_SUCH_SOURCE) { // We don't have the SOURCE. A warning will do. SCENGINE->addBreakpoint(filename, line); return NO_SUCH_SOURCE; } else if (isLegal == NO_SUCH_LINE) { // No line in the source A warning will do. SCENGINE->addBreakpoint(filename, line); return NO_SUCH_LINE; } else { // Something weird? Don't do anything. return isLegal; } }
This is a somewhat trivial case since SCENGINE gives us addBreakpoint and we simply wrap it, but DebuggerAdapter does get more complex.
This way, when and if we want a prettier user interface, we don’t need to worry (not too much, at least) about breaking functionality, and if we decide to restructure the script engine and / or the way the adapter interacts with it, we don’t need to worry too much about rendering the interface unusable.
This could actually turn out useful if somebody decides to stitch together a graphical debugger – just code the actual GUI and add water.
One cool thing our interface does is displaying source files – we’ll talk about it in the next installment.