I wanted to include some kind of debug or admin console in my game engine. Basically a simple command line that I can use to interact with the code while it runs; i.e. modify object properties, change the game state, and receive information about error handling. It begins with a stream buffer object.
ConsoleBuffer.hpp
#ifndef CONSOLE_BUFFER_HPP #define CONSOLE_BUFFER_HPP /***********/ /* Headers */ /***********/ #include <iostream> #include <streambuf> class ConsoleBuffer : public std::streambuf { public: ConsoleBuffer(); ~ConsoleBuffer(); private: virtual int overflow(int character); virtual int underflow(); virtual int sync(); static const int bufferSize = 64; char iBuffer[bufferSize]; char oBuffer[bufferSize]; }; #endif // CONSOLE_BUFFER_HPP
ConsoleBuffer.cpp
/***********/ /* Headers */ /***********/ #include <cstdio> #include <cstring> #include <io.h> ConsoleBuffer::ConsoleBuffer() { setp(iBuffer, iBuffer + (bufferSize - 1)); setg(oBuffer + bufferSize, oBuffer + bufferSize, oBuffer + bufferSize); } ConsoleBuffer::~ConsoleBuffer() { sync(); } int ConsoleBuffer::overflow(int character) { if((character != EOF) && (pptr() != epptr())) { return sputc(static_cast<char>(character)); } else if(character != EOF) { sync(); return overflow(character); } else { return sync(); } } int ConsoleBuffer::underflow() { if(gptr() != egptr()) return static_cast<int>(*gptr()); int putbackSize = gptr() - eback(); if(putbackSize > 32) putbackSize = 32; std::memmove(iBuffer + (32 - putbackSize), gptr() - putbackSize, putbackSize); int numOfChars = read(0, iBuffer + 32, bufferSize - 32); if(numOfChars <= 0) return EOF; setg(iBuffer + 32 - putbackSize, iBuffer + 32, iBuffer + 32 + numOfChars); return static_cast<int>(*gptr()); } int ConsoleBuffer::sync() { if(pbase() != pptr()) { std::size_t size = static_cast<int>(pptr() - pbase()); fwrite(pbase(), 1, size, stderr); setp(pbase(), epptr()); } return 0; }
Right off the bat, I’ll say I looked at a number of resources and examples on implementing a stream buffer and kind of used an amalgamation of those methods. I’m still a little unfamiliar with some of the C standard library stuff I used, and as a result it feels hacked together. I would really appreciate some good references on streambuf implementations.
Next is the Console class. This contains an iostream object which uses the ConsoleBuffer, and an std::map
which stores command names as keys and ConsoleCommand objects as values. We interact with the console primarily in two ways, the first is the overloaded <<
operator, which simply writes to the command line. The other is via the Console::listen()
function. This is non-blocking, and calls the Console::readFromStream()
in a separate thread, but executes commands back in the main thread so that we can manipulate any data without having to worry about more locks and mutexes. We also do a little parsing of the commands, it’s pretty rudimentary but supports nested commands and basic data types.
Console.hpp
#ifndef CONSOLE_HPP #define CONSOLE_HPP /***********/ /* Headers */ /***********/ #include <ConsoleBuffer.hpp> #include <ConsoleCommand.hpp> #include <map> #include <vector> #include <memory> #include <thread> class Console { public: Console(); ~Console(); void registerCommand(std::string id, std::shared_ptr<AbstractBase::ConsoleCommand> command); void listen(); void setPrompt(std::string prompt); bool usePrompt(bool value = -1); std::string getPrompt(); template<typename ValueType> std::ostream& operator<<(ValueType value) { return (_stream << value); } private: std::map<std::string, std::shared_ptr<AbstractBase::ConsoleCommand>> _commands; bool _usePrompt; std::string _prompt; ConsoleBuffer _buffer; std::iostream _stream; std::thread _thread; bool _threadIsActive; std::vector<std::string> _tokens; void readFromStream(); std::string parse(std::vector<std::string> tokens); }; Console& console(); #endif // CONSOLE_HPP
Console.cpp
/***********/ /* Headers */ /***********/ #include <Console.hpp> #include <iterator> Console::Console() : _buffer(), _stream(&_buffer), _prompt("> "), _usePrompt(true), _threadIsActive(false) { _stream.tie(&_stream); _thread = std::thread([]{ return; }); } Console::~Console() { if(_thread.joinable()) _thread.join(); } void Console::registerCommand(std::string id, std::shared_ptr<AbstractBase::ConsoleCommand> command) { _commands[id] = command; } std::string Console::parse(std::vector<std::string> tokens) { if(tokens.empty()) return ""; auto itr = tokens.begin(); while(itr != tokens.end()) { if(itr->front() == '"') { itr->erase(itr->begin()); if(itr->back() == '"') { itr->pop_back(); } else { for(auto subItr = itr + 1; subItr != tokens.end(); tokens.erase(subItr)) { itr->append(1, ' '); itr->append(*subItr); if(subItr->back() == '"') { tokens.erase(subItr); break; } } if(itr->back() != '"') { _stream << Alert("Missing terminating \" character", Alert::Code::ERROR); return ""; } itr->pop_back(); } } else if(itr->front() == '$ ') { itr->erase(itr->begin()); if(itr->front() != '{') { _stream << Alert("Invalid token, subexpression must begin with $ {", Alert::Code::ERROR); return ""; } else { itr->erase(itr->begin()); std::vector<std::string> expression; int nestedLevel = 0; for(auto subItr = itr; subItr != tokens.end(); tokens.erase(subItr)) { expression.push_back(*subItr); if(subItr->front() == '$ ') { nestedLevel++; } if(subItr->back() == '}') { if(nestedLevel != 0) { nestedLevel--; } else { tokens.erase(subItr); break; } } } if(expression.back().back() != '}') { _stream << Alert("Missing terminating } character", Alert::Code::ERROR); return ""; } expression.back().pop_back(); tokens.insert(itr, parse(expression)); } } itr++; } if(_commands.find(tokens.front()) != _commands.end()) { return _commands.find(tokens.front())->second->execute(tokens); } else { _stream << Alert("\"" + tokens.front() + "\" is not a valid command", Alert::Code::ERROR | Alert::Code::INVALID_COMMAND); return ""; } } void Console::listen() { if(!_threadIsActive) { if(_thread.joinable()) { _thread.join(); parse(_tokens); } else _thread = std::thread([=]{ readFromStream(); }); } } void Console::readFromStream() { _threadIsActive = true; _tokens.clear(); if(_usePrompt) _stream << _prompt; std::string input; getline(_stream, input); if(input == "") { _threadIsActive = false; return; } std::istringstream sstream(input); std::copy(std::istream_iterator<std::string>(sstream), std::istream_iterator<std::string>(), std::back_inserter(_tokens)); _threadIsActive = false; } void Console::setPrompt(std::string prompt) { _prompt = prompt; } std::string Console::getPrompt() { return _prompt; } bool Console::usePrompt(bool value) { if(value != -1) _usePrompt = value; return _usePrompt; } Console& console() { static Console _console; return _console; }
Before getting into the ConsoleCommand templates, I wanted to throw in this simple lexical cast which comes into play later. I know boost includes a pretty full-featured lexical cast but I will eventually need to develop a way of casting to more complex object types so I figured I’d just make my own.
LexicalCast.hpp
#ifndef LEXICAL_CAST_HPP #define LEXICAL_CAST_HPP /***********/ /* Headers */ /***********/ #include <string> #include <sstream> template<typename Type> Type lexical_cast(std::string str) { Type value; std::stringstream sstream(str); sstream >> value; return value; } template<> std::string lexical_cast<std::string>(std::string str); #endif // LEXICAL_CAST_HPP
LexicalCast.cpp
/***********/ /* Headers */ /***********/ #include <LexicalCast.hpp> template<> std::string lexical_cast<std::string>(std::string str) { return str; }
Now for the ConsoleCommand. The tricky part was finding a way to determine the quantity and types of arguments for each command in order to construct a tuple of usable variables from the separate tokens which we can forward to the function.
ConsoleCommand.hpp
#ifndef CONSOLE_COMMAND_HPP #define CONSOLE_COMMAND_HPP /***********/ /* Headers */ /***********/ #include <Alert.hpp> #include <LexicalCast.hpp> #include <string> #include <functional> #include <vector> namespace AbstractBase { class ConsoleCommand { public: virtual std::string execute(std::vector<std::string> tokens) = 0; }; } template<typename ReturnType, typename... ArgumentList> class ConsoleCommand : public AbstractBase::ConsoleCommand { public: ConsoleCommand(std::function<ReturnType(ArgumentList...)> func) { _function = func; } std::string execute(std::vector<std::string> tokens) { constexpr auto numberOfArguments = std::tuple_size<typename std::decay<std::tuple<ArgumentList...>>::type>::value; if(tokens.size() - 1 != numberOfArguments) { std::stringstream sstream; sstream << Alert("Command '" + tokens.front() + "' expects " + std::to_string(numberOfArguments) + " argument(s), " + std::to_string(tokens.size() - 1) + " provided", Alert::Code::ERROR | Alert::Code::INVALID_ARG_COUNT); return sstream.str(); } tokens.erase(tokens.begin()); auto parameters = buildParameterPack(tokens, std::make_index_sequence<numberOfArguments>()); return std::to_string(invoke(parameters, std::make_index_sequence<numberOfArguments>{})); } protected: std::function<ReturnType(ArgumentList...)> _function; template<std::size_t... N> decltype(auto) buildParameterPack(std::vector<std::string> tokens, std::index_sequence<N...>) { return std::make_tuple(lexical_cast<getArgumentType<N>>(tokens[N])...); } template<typename Tuple, std::size_t... index> ReturnType invoke(Tuple&& args, std::index_sequence<index...>) { return _function(std::get<index>(std::forward<Tuple>(args))...); } template<std::size_t N> using getArgumentType = typename std::tuple_element<N, std::tuple<ArgumentList...>>::type; }; template<typename... ArgumentList> class ConsoleCommand<void, ArgumentList...> : public AbstractBase::ConsoleCommand { public: ConsoleCommand(std::function<void(ArgumentList...)> func) { _function = func; } std::string execute(std::vector<std::string> tokens) { constexpr auto numberOfArguments = std::tuple_size<typename std::decay<std::tuple<ArgumentList...>>::type>::value; if(tokens.size() - 1 != numberOfArguments) { std::stringstream sstream; sstream << Alert("Command '" + tokens.front() + "' expects " + std::to_string(numberOfArguments) + " argument(s), " + std::to_string(tokens.size() - 1) + " provided", Alert::Code::ERROR | Alert::Code::INVALID_ARG_COUNT); return sstream.str(); } tokens.erase(tokens.begin()); auto parameters = buildParameterPack(tokens, std::make_index_sequence<numberOfArguments>()); invoke(parameters, std::make_index_sequence<numberOfArguments>{}); return ""; } protected: std::function<void(ArgumentList...)> _function; template<std::size_t... N> decltype(auto) buildParameterPack(std::vector<std::string> tokens, std::index_sequence<N...>) { return std::make_tuple(lexical_cast<getArgumentType<N>>(tokens[N])...); } template<typename Tuple, std::size_t... index> void invoke(Tuple&& args, std::index_sequence<index...>) { return _function(std::get<index>(std::forward<Tuple>(args))...); } template<std::size_t N> using getArgumentType = typename std::tuple_element<N, std::tuple<ArgumentList...>>::type; }; #endif // CONSOLE_COMMAND_HPP
And finally, below are a few examples of how you would register console commands.
Examples:
void exit() { while(Game::instance()->getActiveState() != nullptr) { Game::instance()->popState(); } } void echo(std::string str) { console() << str << std::endl; } int fps() { return Game::instance()->getFPS(); } void createSprite(std::string id, std::string textureFilePath) { auto texture = construct<TGE::Texture>(id, textureFilePath); auto sprite = construct<TGE::Sprite>(id, texture); Game::instance()->getActiveState()->drawableStack.push_back(sprite); } void destroySprite(std::string id) { destroy<Sprite>(id); } void registerFunctions() { auto _exit = std::make_shared<ConsoleCommand<void>>(exit); auto _echo = std::make_shared<ConsoleCommand<void, std::string>>(echo); auto _fps = std::make_shared<ConsoleCommand<int>>(fps); auto _createSprite = std::make_shared<ConsoleCommand<void, std::string, std::string>>(createSprite); auto _destroySprite = std::make_shared<ConsoleCommand<void, std::string>>(destroySprite); console().registerCommand("exit", _exit); console().registerCommand("echo", _echo); console().registerCommand("fps", _fps); console().registerCommand("createSprite", _createSprite); console().registerCommand("destroySprite", _destroySprite); }
This code is working, everything compiles and runs as expected. I do have some improvements that I plan to work on and additional functionality I want to add in order to make it more powerful, but for the time being I just want to know if there are any glaring mistakes or things I clearly should have done better. Thanks in advance for taking the time to look at my code!