![]() |
General Programming »
Macros and Add-ins »
Macros
Intermediate
License: The Code Project Open License (CPOL)
Design by Contract macros for C++ and link to DoxygenBy Antoine TandinMacros to write "Design by Contract" conditions in the header of the functions and inoculate it in your doxygen documentation automatically. |
VC6, VC7Win2K, WinXP, Win2003, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
"Design by Contract" is a very good method for programming. (Read Object Oriented Software Construction from Bertrand Meyer.)
For people who have never heard about DbC, I do a very simple and quick
introduction to the principle.
The idea is that between a function and the call of a function, there is a
contract.
To ensure that the function will do what it should do, every
condition of the contract has to be ok.
Those conditions can involve the function or the
caller.
When a condition
fails, one of the two contractors have not done his job. So the programmer has to choose to change the function or the way the function has
been called.
For example I choose to write a linear interpolation function.
/**@brief Float linear interpolation.
* @param _o lerp result.
* @param _t parametric value.
* @param _s start value.
* @param _e end value.
**/
void lerp( float& _o, float _t, float _s, float _e )
{
_o = _s + ((_e - _s) * _t);
}
We can add some preconditions like :
We can add as postcondition :
Imagine the same function but working with vectors or matrix. We should add
conditions saying that the input parameters should not be changed by the
function.
In C++ this condition can be check at compile time (in the case) using the
const keyword.
But for the others conditions we should use the assert function.
void lerp( float& _o, const float _t, const float _s, const float _e ){
assert((_t >= 0.f)&&(_t <= 1.f));
assert(!isnan(_s));
assert(!isnan(_e));
_o = _s + ((_e - _s) * _t);
assert(!isnan(_o));
}
For more specialized lerp functions you would probably add more sophisticated postconditions like insure othogolalization on matrix ...
Little personal trick : Don't ignore "stupid" conditions, because those are often the ones which appears. Most of the time it is because the code changes, late in the night, by copy paste block of code..., and the "stupid" condition which has been set becomes really relevant.
The Digital Mars C/C++ compiler is DbC compliant.
Usually, I write contract condition like this:
int foo( void* p )
{
assert( p ); // precondition : p cannot be null.
... code ...
if(...)
{
assert( post condition); // post condition documentation
return ...
}
... code ...
if(...)
{
assert( post condition ); // doc
// this condition didn't have to be check in the last return
assert( an other post condition); // documentation
return ...
}
... code ...
... once again check all post conditions ...
return ... ouf
}
So "debug" code, or "check" code really pollute my code, mainly because of repetitions. And I didn't want to have those stuff compiled for the the final release.
So I really want to be able to write the function in a better way .. like:
int foo( void* p )
{
#ifdef contract_check
PRECONDITION( p, "p can't be 0" ); // the phrase is to display to the user
// why the assertion fail.
POSTCONDITION( postcondition, "description of my post condition" );
POSTCONDITION( checkpostcondition2(), "an other post condition");
#endif // contract_check
... code ...
if(...)
return ...
... code ...
if(...)
return ...
... code ...
return ...
}
And, because I'm using a super code documentation generator (Doxygen), I wanted to see the documentation of my assertions (conditions) in the documentation. Of course, without writing it twice ... Do you follow me? (redundancy means future problems)
So the little set of macro I wrote gives you the ability to write something like:
int AClass::Test( int argA, int argB )
{
DBC_BEGIN
DBC_PRE_BEGIN
DBC_PRE( argA >= 0, precond 1 documentation and message )
DBC_PRE( argB != 0, precond 2 documentation and message )
DBC_PRE_END
DBC_POST_BEGIN
DBC_POST( argA + argB >= 0, post condition 1 documentation and message )
DBC_POST( ChechDataFct( argA, argB ), post cond 2 : cf. ChechDataFct() )
DBC_POST_END
DBC_END
... code ...
return ...;
}
And (if you use Doxygen) the documentation will be up to date.
Take a look at this picture taken from Doxygen documentation of the
AClass::Test function:

While compiling for WIN32, you have this following message when the condition fails:

Click "OK" to continue (break when using a debugger).
Click "Cancel" to ignore (means that it will not popup a message (or break) if the condition fails again).
It works with VC6 and VC7.
The code is composed of 2 parts:
For example, you can choose to send an exception when a condition fails.
The demo zip file is exactly the same as the source zip file, plus a compressed HTML doc.
This documentation file can be generated from the source zip file if you have Doxygen.
Because 100% of the feedback I receive (...2) asks me for explanation about the trick I have used to make the tool working, I added this section to the article.
The Doxygen documentation works using the Doxygen preprocessor :
In source file, the line:
DBC_PRE( argA >= 0, precond 1 )
Using in the Doxygen preprocessor:
DBC_PRE(a,b) =\pre b \code a \endcode
the code becomes for Doxygen:
\pre precond 1 \code argA >= 0 \endcode
which is a valid command for Doxygen parser.
Note: I assume you know what are functions prolog and
epilog, how the compilers use the stack in the calling
function process.
The main problem is ... post condition of course. You see easily that calling the preconditions at the begin of a function is very easy ... because you have written it at the beginning :)
But I use a little trick to call the post conditions after returning the function.
The process is very simple: Meanwhile the epilog of the function (so when the function ends/returns/leaves), we go to the start of the postcondition, execute postcondition block, and finish the epilog.
Before continuing to explain the trick, you should see that there is another problem: do not call POSTCONDITION block before the end of the function. As it is written just before the body of the function, we have to skip it.
The algorithm to execute the function becomes:
- execute precondition block
- jump to body
when return :
- goto postcondition block
- execute postcondition block
- return back to epilog of the function.
So the first step is to tag the poscondition block. It is very easy because
it is enclosed by 2 macros: DBC_POST_BEGIN and
DBC_POST_END.
We also need to know where the start of the body of the function is. The
answer is ... after the "Design by Contract" block ... enclosed by
DBC_BEGIN and DBC_END.
The pseudo macros become something like :
define DBC_BEGIN // declare start of body label. define DBC_END __dbc_body : define DBC_PRE_BEGIN define DBC_PRE_END define DBC_POST_BEGIN if(not in epilog) __asm je __dbc_body define DBC_POST_END continue epilog (means jump after caller instruction in epilog ...)
I often use C++ local variable declaration, or even local static variable declaration to do many things. I've plan to write something about that in another article because it is a powerful feature of C++, and I never read anything about taking benefits of that.
Here, I just use the fact that when we declare a local instance of a class, the destructor will be called during the epilog of the function. So declaring an instance of a class with destructor gives you an "epilog trap", which is the destructor of the class.
I also use the fact that constructors are called where the variable is declared... but it is not very relevant for this tool. I mean, it can be implemented differently I suppose.
Here is the code of the class declaration used by this tool:
class structDBC { public : structDBC(); // constructor ~structDBC(); // destructor // store caller address of post condition block // (means instruction address which call destructor in epilog). long dbc_postblockRETaddr; // store start address of postcondition block. long dbc_postblockaddr; };
Add our class in the macros:
define DBC_POST_BEGIN structDBC autoDBC; if(!autoDBC.dbc_postblockRETaddr) goto __dbc_body; define DBC_POST_END goto autoDBC.dbc_postblockRETaddr;
So post condition block is enclosed by:
dbc_postblockRETaddr is not set (we are not called by
epilog) we goto the start of the body. ELSE (called by epilog) we execute the
post condition block.
dbc_postblockaddr). Note:
call instruction (no
jmp). According to compilers, it is possible to add some
special declaration like __noinline to insure that. I think that
usually compilers will implement call of those functions with call because they are implemented in a separate object.
__declspec(naked) indicates Visual C compiler to not create
epilog et prolog to this function. __declspec(naked) structDBC::structDBC(){
// standard prolog (__asm ENTER)
__asm push ebp;
__asm mov ebp, esp;
// Put in eax the stack pointer where return address is store
__asm mov eax, ebp;
__asm add eax, 4; // because we have done a push on the first instruction.
// store address effective return adress in dbc_postblockaddr
// made in 2 lines because we can't write
// __asm mov dword ptr [ecx+4], [eax]
__asm mov eax, [eax]; // store effective address in eax.
__asm mov dword ptr [ecx+4], eax; // copy to dbc_postblockaddr
// standard epilog (__asm LEAVE )
__asm mov esp, ebp;
__asm pop ebp;
__asm mov dword ptr [ecx], 0 // reset dbc_postblockRETaddr
__asm ret;
}
__declspec(naked) structDBC::~structDBC(){
// standard prolog
__asm push ebp;
__asm mov ebp, esp;
// put return address in dbc_postblockRETaddr
__asm pusha;
__asm mov eax, ebp;
__asm add eax, 4;
__asm mov eax, [eax];
__asm mov [ecx], eax; // copy to dbc_postblockRETaddr
__asm popa;
// standard epilog
__asm mov esp, ebp;
__asm pop ebp;
__asm add esp, 4; // skip next line.
// goto dbc_postblockaddr (start of post condition block).
__asm jmp dword ptr [ecx+4];
}
Note: if you compare with the code of the source, you will notice that
I don't use a class, but a struct ... This is just because I want
to ensure that no virtual table will be implemented (or other stuff the C++
compiler wanted to add)... according to assembler code. Writing this explanation
paragraph, I notice that I never use the attributes names of the class... It is
just because ... I don't need it while coding in assembly.
#define DBC_POST_BEGIN structDBC autoDBC; \ __asm cmp dword ptr [autoDBC], 0 \ __asm je __dbc_body #define DBC_POST_END __asm jmp dword ptr [autoDBC];
It is the first article I publish and ... sorry for my English.
I hope it will be useful to others.
Unfortunately, I haven't tested it very much, just on a few targets:
the code depends on compiler.
But, the idea is here, everybody can adapt and improve the stuff (please
share your add-on or improvements :) )
I tried to do those stuff using only C/C++, for example using
longjump. But I didn't find a way to get what I wanted.
If somebody can do it without assembly: please send me your code :)
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 27 Oct 2004 Editor: Nishant Sivakumar |
Copyright 2004 by Antoine Tandin Everything else Copyright © CodeProject, 1999-2009 Web22 | Advertise on the Code Project |