| The
contents of the .zip file and the following text were not
created by and are not owned or copyrighted by mov
Software. The zip file containing
this page in .DOC format and the sample code can be
downloaded here.
The .zip file
contents are as follows:
|
EXCEMU.DOC
- Vladimir Belkin's article that provides an
overview of the exception handling emulation
mechanism. Translated by Fedor Sherstyuk. |
|
TCURTTI.* -
Implementation of RTTI for Windows CE. Written by
Fedor Sherstyuk. |
|
TCUEXC.* -
Platform-independent part of C++ exception
handling implementation. Written by Fedor
Sherstyuk. |
|
TCUXHAPI.*
- Windows CE Win32 APIs used by exception
handling emulation. Written by Fedor Sherstyuk. |
|
TCUWARN.H -
Helper file that eliminates irrelevant warnings
on /W4. Written by Fedor Sherstyuk. |
C++
exception handling emulation
1. Preface
This
text contains a translation of Vladimir Belkins
article "Exceptions processing in C++: what, when,
how".
Article
(chapter 2) is translated as is (only first 3 paragraphs
are slightly abridged). Some my comments are presented at
the end of this text (in chapter 3).
This
entire article was typed-in manually, so some code damage
is possible - code was NOT checked on compiler after
typing-in. Apart from possible typos there were no any
intentional modifications to presented code.
Also I
apologize for my imperfect English - Im not a
native speaker. English in code presented without any
modifications.
2. Article
C++
exceptions are essential part of a language.
Unfortunately some compilers have no support for them.
Additionally there are significant overhead, associated
with standard exception processing and this overhead
could be avoided by processing exceptions at a level of
C++ program.
Exception
handling mechanism described here was implemented as a
part of "CHARMS" product, intended for program
development under MS Windows on C++ (memory models
large and Win32). Source code
presented here was debugged by means of Symantec C++
6.11. Described exception handling mechanism is not
suited for multithreaded mode, but it could be modified
for it - to do this one will need to use threads
local storage (Tls).
Microsoft
tried to implement exception at C++ level in MFC. This
implementation is not good since their mechanism do not
perform destruction of local object during stack
unwinding and there is no way to throw exceptions of
arbitrary type (e.g. int, char). Exceptions handling
mechanism presented here implements both of these
features. Additionally it allows to organize exception
handling scheme that is somewhat more optimal than one,
described in ARM.
Unfortunately
presented implementation has no mechanism for exception
grouping, suggested by ARM: T type catch will not
intercept exceptions of types, derived from T.
Thats main drawback of this implementation.
For
description of exception handling scheme lets make
following definitions:
|
Automatic
object - object with automatic
storage class |
|
Automatic object
exists if its destructor should be
invoked during stack unwinding. |
Scheme
presented here diverge from one, presented in ARM in
following respect:
|
Per ARM scheme
object start existing only after it is completely
constructed. |
|
Per scheme
presented here object start existing from a
moment when explicit part of its constructor
starts executing. |
In practice
this means that if explicit part of objects
constructor throws the exception then per ARM scheme only
destructors for base and members will be called -
destructor of object itself will not be called. Per
scheme presented here objects destructor will be
called as well.
If exception
occurred during bases or members constructor
then according to both schemes only already constructed
parts of object will be destructed.
This
discrepancy stems from inability to implement ARM scheme
appropriately at a C++ level.
Lets look
at class "A" constructor that can throw the
exceptions, implemented per ARM scheme:
struct X_A { // Type
used for subsequent exception throwing
unsigned case_num;
X_A(unsigned
case_n):case_num(case_n) {}
};
class MemBlock {
char *p;
public:
MemBlock(size_t
sz,unsigned case_n) {
p=new char[sz];
if (p==NULL) throw
X_A(case_n);
}
operator char * ()
const {return p;}
~MemBlock() {delete
[] p;}
};
struct A {
MemBlock p1, p2, p3;
A():p1(100,1),p2(100,2),p3(100,3)
{
// do something
}
~A() {
// do something
}
};
This
class "A" could be implemented in a similar way
by means of exception handling scheme, presented here:
struct X_A { // Type
used for subsequent exception throwing
unsigned case_num;
X_A(unsigned
case_n):case_num(case_n) {}
};
DeclareException(X_A,101)
// Bind exception type with integer identifier
class MemBlock
COLON_CHECKED { // "COLON_CHECKED" macro means
that for
// automatic objects
of class destructor will
// be called during
stack unwinding.
char *p;
public:
MemBlock(size_t
sz,unsigned case_n) {
p=new char[sz];
if (p==NULL) xThrow
(X_A(case_n)); // "xThrow" macro is analogous
to
// C++
"throw" operator
}
operator char * ()
const {return p;}
~MemBlock() {if
(p!=NULL) delete [] p;}
};
struct A
COLON_CHECKED {
MemBlock p1, p2, p3;
A():p1(100,1),p2(100,2),p3(100,3)
{
resetChecked (); //
Necessary to prevent duplication of destructors
// calls for data
members
//
.....................
}
~A() {// do
something
};
};
If
class "A" uses default destructor then
implementation may be simplified:
struct A {
MemBlock p1, p2, p3;
A():p1(100,1),p2(100,2),p3(100,3)
{
//
.....................
}
};
Finally
there is one more way to implement class "A"
using presented scheme:
struct X_A { // Type
used for subsequent exception throwing
unsigned case_num;
X_A(unsigned
case_n):case_num(case_n) {}
};
DeclareException(X_A,101)
struct A
COLON_CHECKED {
char *p1,*p2,*p3;
A():p2(NULL),p2(NULL)
{ // From translator: Apparently thats bug
// and should be
p1(NULL),p2(NULL),p3(NULL)
if ((p1=new
char[100])==NULL) xThrow(X_A(1));
if ((p2=new
char[100])==NULL) xThrow(X_A(2));
if ((p3=new
char[100])==NULL) xThrow(X_A(3));
}
~A() {
if (p1!=NULL) delete
[] p1;
if (p2!=NULL) delete
[] p2;
if (p3!=NULL) {
delete [] p3;
// do something //
From translator: pretty funny code logic
}
}
};
For
comparison of schemes lets look at one more code
example, written according to ARM:
struct A {
char *p;
A() {
p=new char [100];
if (p==NULL) throw
ERR;
try {
// Exception could
be thrown here
} catch (...) {
delete [] p; throw;
}
}
~A() {delete [] p;}
}
Now
the same example, written according presented scheme;
struct A {
char *p;
A() {
p=new char [100];
if (p==NULL) xThrow
(ERR);
exception_maybe_throwed_here();
}
~A() {if (p!=NULL)
delete [] p;}
}
Exceptions
handling scheme presented here provides following macros
and functions to use for exceptions handling:
- "xTry"
- equivalent of C++ "try" operator.
- "xTryEnd"
- ends try-block.
- "xCatch(T,v)"
- equivalent of C++ "catch(T v)"
operator.
- "xCatchType(T)"
- equivalent of C++ "catch(T)"
operator.
- "xCatchAll"
- equivalent of C++ "catch(...)"
operator.
- "xThrow(v)"
- equivalent of C++ "throw v" operator.
- "xThrow()"
- equivalent of C++ no-parameters
"throw" operator.
- "void
terminate()" and "PFV
set_terminate(PFV)" functions analogous to
ones, defined in ARM.
- "Checked"
class, used as a base for those classes whose
automatic objects should be destructed during
stack unwinding (presented system can distinguish
automatic objects from all other). Such classes
hereinafter will be named Checked-classes and
their objects as Checked-objects.
- Macros:
 |
#define
COLON_CHECKED :protected Checked |
 |
#define
CHECKED_COMMA protected Checked, |
 |
#define
COLON_VCHECKED :protected virtual Checked |
 |
#define
VCHECKED_COMMA protected virtual Checked, |
- "void
Checked::resetChecked()" method that
Checked-class constructor should call if
this class contain Checked-objects as data
members.
- Macros:
 |
"DeclareTypeId(T,id)",
|
 |
"DeclareException(T,id)". |
| These
macros bind types to integer identifiers. To use
class T as exception type one need to use one of
these macros.
|
- "void
SetTheStackBottom(void __ss *p)" function
used for exception handling system
initialization. This function need to be called
to enable distinguishing automatic
Checked-objects. In "CHARMS" program
this function is called during "CHARMS"
initialization. In other programs this function
may be used in a following way:
| void
main(int argc,...) { SetTheStackBottom(&argc);
//.................................
}
or in a following way:
int PASCAL
WinMain(HINSTANCE hInst,...) {
SetTheStackBottom(&argc);
//.................................
}
|
Mentioned in
item 11 "resetChecked();" function requires
special explanation. Suppose we have Checked-class
"A" with data member of Checked-class
"B". In this case "resetChecked();"
should be the first operator in any "A"-class
constructor - otherwise destructor of class "B"
will be called twice during stack unwinding.
Before
description of exception handling mechanism
implementation lets look at one example of its
usage:
struct X_DC {
HWND hwnd;
X_DC(HWND
wnd):hwnd(wnd) {}
};
DeclareException(X_DC,8801)
// Exceptions of
this type will be thrown by constructors of classes
// for responsible
window DC. For example:
class
DC:CHECKED_COMMA public Rect {
DECL_ASN(DC)
HDC dc;
HWND hwnd;
public:
DC(HWND wnd) {
dc=GetDC(hwnd=wnd);
if (dc==NULL)
xThrow(X_DC(hwnd));
CetClientRect(hwnd,(RECT
*)this);
}
operator HWND
()const{return hwnd;}
operator HDC
()const{return dc;}
~DC () {
if (dc!=NULL)
ReleaseDC(hwnd,dc);
}
};
// try-block usage
sample
xTry {
DC dc1(hwnd);
// Perform drawing
}
xCatch(X_DC,except) {
char buf[60];
sprintf(buf,"can
not create DC for window %x\n", except.hwnd);
OutputDebugString(buf);
xThrow();
}
xTryEnd
Now
lets discuss the implementation.
System
uses two stacks for its operation: try-blocks stack and
Checked-objects stack. Stacks are implemented as lists.
Stack of checked objects also is referred to as
destruction list. Objects on top of these stacks are
referred to as current. Pointers to stack tops and to
bottom of current execution stack are stored in global
variables that form a context of exception handling
system. So if one will want to adapt this system to
multithreaded mode hell need to use Tls. At the
beginning of program execution both stacks are empty.
When control is passed to try-block this block creates
"TryToExecute" class object which became a top
of try-blocks stack. At exit from try-block this object
is removed from stack. "TryToExecute"
constructor saves context of exception handling system.
Destructor restores saved context and rethrows not
processed exception (if any) in restored context.
struct TryToExecute
{
jmp_buf buf;
Checked __ss *chk;
TryToExecute __ss
*tr;
int type;
void *lvalue;
void OnError(int
type,void *ptr);
TryToExecute();
~TryToExecute();
};
Following
global variables form a context of exception handling
system:
Checked _ss
*TheLastChecked=NULL; // Top of Checked-objects stack
TryToExecute __ss
*TheLastTryToExecute=NULL; // Top of try-blocks stack
int
TheLastExceptionType=~0; // Index of exception type
void
*TheLastExceptionLvalue=NULL; // Pointer to buffer that
// contain exception
object
unsigned
TheStackBottom=NULL; // Bottom of execution stack
The
last variable is used to check whether some address
belongs to stack:
int
StackLocated(void __ss *p) {
return
(unsigned(p)>SP) &&
(unsigned(p)<=TheStackBottom);
}
Following
macros are used for try-block organization:
#define xTry
{TryToExecute _TTE_; if (!setjmp(_TTE_.buf)) {
#define
xCatch(t,val) } else if (typeTypeId(t)==_TTE_.type) { \
t &val=*(t
*)(_TTE_.lvalue);_TTE_.type=0;
#define xCatchAll }
else {_TTE_.type=0;
#define
xCatchType(t) } else if (typeTypeId(t)==_TTE_.type)
{_TTE_.type=0;
#define xTryEnd }}
"TryToExecute"
constructor stores current destruction list and assigns
NULL to global variable that points to top of execution
list. Destructor of this class restores value of this
variable and if there is a not processed exception it is
being rethrown in new context.
Checking
for presence of not processed exception performed as
follows: constructor of "TryToExecute" zeroes
the "type" field, at execution throwing this
field is filled with exception type and if catch-block
processed the exception it zeroes "type" field
as well. Thus if destructor reveals that
"type!=0" it concludes that there is not
processed exception of type "type". This means
that type identifier must be nonzero for any type.
To
throw an exception following function is being used:
template<class
T> void xThrow(T value)
{static T x=value;
OnException(typeId(value),&x);
};
However
because there is a bug in Symantec 6.xx that do not allow
compiler to precompile headers with template functions
and because there are compilers that do not process
templates at all there is a following macro:
#define
DeclareException(T,Id) DeclareTypeId(T,Id) \
inline void
xThrow(const T &value) \
{
checkExceptionSize(sizeof(T),Id); \
new(exceptionBuffer)
T(value); \
OnException(Id,ExceptionBuffer);
\
}
There
are following routines that perform stack unwinding and
catch-blocks processing:
void OnException(int
type,void *p) {
TheLastExceptionType=type;
TheLastExceptionLvalue=p;
if
(TheLastTryToExecute==NULL) {
if
(TheLastChecked!=NULL) {
Checked __ss
*p=TheLastChecked;
TheLastChecked=TheLastChecked->prev;
p->DestroyChecked();
} terminate ();
}
TheLastTryToExecute->OnError(type,p);
}
void
TryToExecute::OnError(int _type,void *ptr)
{ if
(TheLastChecked!=NULL) {
Checked __ss
*p=TheLastChecked;
TheLastChecked=TheLastChecked->prev;
p->DestroyChecked();
}
type=_type;lvalue=ptr;
TheLastChecked=chk;
TheLastTryToExecute=tr;
longjmp(buf,~0);
}
void
Checked::DestroyChecked() {
this->~Checked();
if
(TheLastChecked!=NULL)
TheLastChecked->DestroyChecked();
}
Checked::~Checked()
{
if
(StackLocated((void __ss *)this)) TheLastChecked=prev;
}
"DeclareTypeId(T,id)"
and "typeTypeId(T)" macros and a
"typeId(...)" functions are defined as follows:
#define
DeclareTypeId(type,xx) \
inline int
typeId(const type &) {return (xx);} \
inline int
typeId(const type *) {return (xx);}
#define
typeTypeId(T) ptypeId((T *)NULL)
Some
header file declares integer identifiers for all C++
primitive types and pointers to them up to
"***".
DeclareTypeId(short,1);
DeclareTypeId(unsigned
short,2);
//
...............................
DeclareTypeId(unsigned
long***,42);
DeclareTypeId(double***,62);
DeclareTypeId(char***,72);
Here
is a definition of "Checked" class:
class Checked {
friend class
TryToExecute;
friend void
OnException(int type, void *p);
Checked __ss *prev;
// Points to previous Checked-object
public:
NULL_ASN(Checked) //
Neutralizes assignment operator.
Checked(const
Checked &) {new(this) Checked ();} // Copy
constructor
void
DestroyChecked(); // This method calls destructors of
this object
// and destructors
of those automatic
// Checked-objects
that are back in the list
// inside current
try-block. If there is no
// enclosing
try-block then all Checked objects are
// being destructed.
virtual ~Checked();
Checked();
void resetChecked();
// If Checked-object contains another
// Checked-objects
then its constructor should call
// this method. This
method removes from the list
// all objects,
placed after this object.
};
Constructor
of "Checked" class detects whether object being
constructed is located on stack and includes it into
destruction list only if it is.
Checked::Checked() {
#if M_I86LM ||
M_I86CM || M_I86VM
if
(FP_SEG(this)==getSS())
#endif
if
(StackLocated(this))
{
prev=TheLastChecked;
if
(TheLastChecked!=(Checked __ss *)this)
TheLastChecked=(Checked
__ss *)this;
}
}
Other
parts of exception handling system are quite simple and
any C++ programmer may implement them by himself.
3. One possible
improvement
Exception
handling system presented in article above is rather well
thought-out. It seems, however that it has two
limitations that could be avoided:
|
There should be
a global static mechanism (header file) for
binding types to unique integer identifiers. |
|
As it was
mentioned in the article it is impossible for
"xCatch(T,v)" to catch exception of
type derived from "T". |
It seems that
both of these problems stems from the common root: desire
to throw exceptions of primitive types (like
"int", "double *", etc.). Alternative
approach is to require that all exception types should be
derived from some specific base class (after all -
isnt it a C++ style?)
Having all
exception types derived from single class we have a
possibility for class self-identification (with type
identifier being "void *" pointer to some
static class member - kind of pseudo-RTTI) and for
checking inheritance relationships between
catch-operators class and exception objects
class (thats even better than RTTI!) This technique
is described in detail in John Lacos book
"Large-Scale C++ Software Design" in appendix,
devoted to protocol hierarchy design pattern (by the way
- this book by itself is very good albeit rather large).
|