When beginning work on any emulator there
are often details left forgotten except at the critical junctures
when they are needed. More often than not these items are basic,
and not only that but they are details which are required by any
decent emulator. These are of course facilities for error handling
and facilities for logging. In this article I'll describe a generic
object-oriented approach to error handling. This approach involves
the use of C++ exceptions as well as C++ file IO, so if these components
are not familiar to you then I suggest you learn C++ a bit more
before attempting any of this.
Firstly,
exceptions are objects to be thrown only when the condition which
triggers this is truly exceptional. This is a fairly unclear statement,
but one which you'll find in many books. Basically what I have taken
this to mean is that exceptions should not be used to pass information
around a system, but instead their purpose is solely handling errors.
In the following sections I'll describe a generic exception class
which will report enough information about an error that it may
be tracked down easily by anyone who uses it, but first let's look
at some code:
class EFException
{
public:
///////////////////////////////////////////////////////////////////////////
// Enumerations
/// An enumeration with individual exception categorizations.
enum ExceptionCode
{
EC_FATAL_ERROR,
EC_GENERAL_ERROR,
EC_INVALID_PARAMETERS,
EC_ILLEGAL_ADDRESS,
EC_ILLEGAL_OPCODE,
EC_ILLEGAL_PACKET
};
///////////////////////////////////////////////////////////////////////////
// Constructors
/** The basic constructor.
*/
EFException(const ExceptionCode& code, const std::string& message, const std::string& file,
const std::string& function, long line);
/** The assignment operator.
*/
EFException operator = (EFException& right);
///////////////////////////////////////////////////////////////////////////
// Exception Information Functions
/** Returns the exception code for this message.
*/
ExceptionCode GetExceptionCode() const throw();
/** Returns detailed information about the error.
*/
std::string GetDetailedInformation() throw();
protected:
///////////////////////////////////////////////////////////////////////////
// Exception Code Function
/** Returns the name of the exception code for this exception.
*/
std::string GetExceptionCodeStr() throw();
///////////////////////////////////////////////////////////////////////////
// Variables
long mLine;
ExceptionCode mCode;
std::string mFile;
std::string mMessage;
std::string mFunction;
};
|
As
you can see this class will be our exception class. This is
the class which will be thrown every time an exception occurs.
This exception describes every aspect of the area where the
exception occurred, most notably the name of the file, the function
in that file, the line in that file, and also a message describing
the error that occurred. This is very useful for getting important,
specific information about the error that occurred, as this
information is invaluable when you are writing an emulator and
tracking down bugs. In addition to all that information you
may notice that I have also included an error code. The error
codes create a simple way in which a fatal error can be distinguished
from a non-fatal error at any point in your program. In addition
to this ability to distinguish differences, they also help to
categorize an error into a group, making it easier to asses
the error when it is thrown.
Now lets explain some of the more advanced
C++ concepts presented here for those who don't know about them.
Firstly the word "const" following a function is used
to show the fact that within the body of this function no values
are modified, this means that the function is only returning
a value - nothing more and nothing less. Secondly the word "throw()"
following a function is used to create a contract between the
function caller and the function itself meaning that this function
will not throw any object of any kind any time that it is called.
This is very important, especially in an exception class. Also
you may have noticed that we specified an "operator =",
this is because the compiler (Microsoft Visual C++ .NET) was
unable to generate this function at compilation time. Anyways
now lets move on to what the functions themselves are supposed
to do.
The purpose of the constructor should
be obvious given its parameter names and the names of our protected
variables, as should the purpose of the assignment operator,
so I will not delve into these as these are necessary knowledge
at this point. However let's
discuss the function "GetExceptionCode". This function's
purpose is to retrieve the error code that categorizes this
exception. Inside the body of this function nothing will be
modified or thrown, and in fact all the internals of this function
do is return the value "mCode" which is a protected
member variable. Moving onto the next function, we see "GetDetailedInformation".
This function's purpose is to return an std::string containing
detailed information about the exception. This function in fact
returns a message formatted as follows:
---------------------------------------------
Exception Details:
---------------------------------------------
Code : EC_ILLEGAL_OPCDE
Line : 387
Function: NeoPSX::PSXInterpreter::ExecuteNext
File : PSXInterpreter.cpp
Message : Current CPU opcode is invalid or unknown.
|
As
you can see this formatting makes the error very easy to recognize
and even possibly solve, if this is not in fact just an invalid
CPU opcode but possibly an unimplemented one. This is one of the
main reasons why I believe exceptions can be very beneficial when
working on an emulator. Also by giving you the exact line, function,
and file at which this error occurred it is much easier to track
down and eventually fix. This function also returns the exception
code for the the exception that was thrown, this is done through
a call to the protected function "GetExceptionCodeStr".
This function translates the numbered exception code from the enumeration
into an std::string version of the exception code. Anyways now that
we're all familiar with what this function does let's
examine the implementation of all these functions:
EFException::EFException(const ExceptionCode& code, const std::string& message,
const std::string& file, const std::string& function, long line)
: mCode( code ),
mLine( line ),
mFile( file ),
mMessage( message ),
mFunction( function )
{ }
EFException EFException::operator = (EFException& right)
{
mCode = right.mCode;
mLine = right.mLine;
mFile = right.mFile;
mMessage = right.mMessage;
mFunction = right.mFunction;
return *this;
}
////////////////////////////////////////////////////////////////////////////////////////////////
EFException::ExceptionCode EFException::GetExceptionCode() const throw()
{
return mCode;
}
std::string EFException::GetDetailedInformation() throw()
{
// Use a static buffer to avoid unncessary allocations on every call.
static char sBuffer[20];
// This is the header of our detailed exception description.
std::string mDetailed = "Exception Has Been Caught!\r\n";
mDetailed += std::string("---------------------------------------\r\n
Exception Details:\r\n
---------------------------------------\r\n");
// Name the exception code which triggered this.
mDetailed += "Code : " + GetExceptionCodeStr() + "\r\n";
// Use snprintf because its safer, and add the line number.
_snprintf(sBuffer, 20, "%d", mLine);
mDetailed += "Line : " + std::string( sBuffer ) + "\r\n";
// Print the name of the function from which the exception was thrown.
mDetailed += "Function: " + mFunction + "\r\n";
// Print the name of the file in which the offending code is found.
mDetailed += "File : " + mFile + "\r\n";
// Print our detailed error message.
mDetailed += "Message : " + mMessage;
return mDetailed;
}
////////////////////////////////////////////////////////////////////////////////////////////////
std::string EFException::GetExceptionCodeStr() throw()
{
switch( mCode )
{
case EC_FATAL_ERROR: return "EC_FATAL_ERROR";
case EC_GENERAL_ERROR: return "EC_GENERAL_ERROR";
case EC_INVALID_PARAMETERS: return "EC_INVALID_PARAMETERS";
default: return "UNKNOWN CODE";
}
}
|
And
there you have it, a generic exception which gives enough information
to be used as a replacement for assert. While I did not mention this
as one of the main goals, it should be apparent now that not only
can this exception give us more information than assert, but it is
a more object oriented approach to the problem. Hopefully this article
has helped many of you budding emulator developers, and hopefully
you can employ it to better facilitate error-handling in your own
emulators. In the next article I'll deepen our discussion of error
handling when I explain how to write a flexible logging framework
which you can reuse in all your emulators both past, present, and
future.
|