Thursday 11 September 2014

Defensive Programming - BSLS_ASSERT

One of the tenants of the BDE is its approach to Defensive Programming which tries to address how you deal with undefined behaviour, those inputs that fall outside of the preconditions for a method.

The basic idea is that the production code should in fact not cater for undefined behaviour, since if it did, then one could argue that the behaviour is now defined. However actually doing nothing would be of no value to the exercise of debugging, or more directly exposing latent bugs.

The good old ASSERT has been around to solve exactly this problem, as it disappears in release mode builds.

The BDE however, offers its own set of Assert macros that try to take the existing ASSERT concept to a different level by offering more flexibility to both the function author and the function consumer regarding the behaviour over the asserts :

There two dimensions of this behavioural control that are offered:

  1. When should the Assert should take affect?
  2. What the behaviour of the affect should be?

When should the Assert should take affect?

To look at this dimension of control, we need to introduce the Assert macros that are supported for the function author to choose from:

BSLS_ASSERT(X)
When the cost of running the assert is within 5-10% of the cost of the raw production code
BSLS_ASSERT_SAFE(X)
When the cost of running the assert is in orders of magnitude compared to the raw production code
BSLS_ASSERT_OPT(X)
When the cost of the assert is negligible compared to the raw production code

So why do we give the author of the code the choice to make these calls about the relative cost of the ASSERT macros, especially if the idea is to have the assert code go away in release builds?

That is the point... we don't want to have the assert choice be that granular! Especially when you are writing library code like the BDE. As the BDE you have absolutely no knowledge about where you are being used, and what sort of tolerance your consuming application has for runtime checks. Some applications might have a high degree of tolerance for CPU cycle being spent on runtime validation - others might be so seriously constrained that even in debug mode, performing aggressive runtime checks would be prohibitive.

The onus is left to the application developer to chose which of these asserts to accept with the following preprocessor directives

  1. BSLS_ASSERT_LEVEL_ASSERT_SAFE
  2. BSLS_ASSERT_LEVEL_ASSERT
  3. BSLS_ASSERT_LEVEL_ASSERT_OPT
  4. BSLS_ASSERT_LEVEL_NONE

Lets look at an example of when you might use these as the function developer by looking at a somewhat random method in the BDE - highlighted comments in the method are mine

int systemProtect(void *address, int pageSize)
    // Protect from read/write access the page of memory at the specified
    // 'address' having the specified 'pageSize' (in bytes).  The behavior is
    // undefined unless 'pageSize == getSystemPageSize()'.
{
    BSLS_ASSERT(address);
        //Notice that our precondition here is assumed for production code, and not validated
        //so this is a typical candidate for validation... and further we know that relative to
        //our function the overhead of this check is quite low
        
    BSLS_ASSERT_SAFE(pageSize == getSystemPageSize());
        //Notice again, our function simply assume that you will pass it in the correct page size, 
        //which means it is a good candidate to assert.
        //However, in this case the relative cost of this function is quite expensive, and in bug 
        //free production code, those would be massively wasted cycle -- so we use SAFE

#ifdef BSLS_PLATFORM_OS_WINDOWS
    DWORD oldProtect;
    return !VirtualProtect(address, pageSize, PAGE_NOACCESS, &oldProtect);
#else
    return mprotect(static_cast<char*>(address), pageSize, PROT_NONE);
#endif
}

On the other hand there might be a function like this

int restoreData(int dataIndex)
{
    BSLS_ASSERT_OPT(validateIndex(dataIndex));
        //do some validation on this index, but since we are about to make a call out to 
        //some persistence layer.. we know that the relative cost of this validation is
        //entirely negligible 
        
    return getDataFromDeepStorage( dataIndex );
}
Someone might now argue that if you know that OPT type asserts are so relatively cheap in relation to the function, why even put them in an assert - and if you were thinking that, then I would argue that you are falling into the trap of conflating relative performance of a method to that of an application, and this speaks directly to the fact that a function developer (especially a library developer) has no knowledge of the domain it is running it.

While in relation to this method the cost might be small, in the context of the application, this method might be being called 100's of orders of magnitude's more than any other method, which in turn might mean the second most expensive part of the application would be the "negligible" run time check.. which is entirely redundant ( assuming a bug free application )

What the behaviour of the affect should be?

Lets now look at the second dimension.. what exactly should the assert do... should we write to the console ( on embedded devices that might be an issue ).. should we pop up a dialog box ( as a service without a UI that sucks )... throw an exception... write to a log file... send an email ?

The answer to all of these is yes... because well it depends -- it depends on the type of application you are, are you running on the developers machine, or are you in embedded in medical hardware, are you under load and performance testing etc.

Again we are back to the position where only at the application level do we know the answer to this, so we can only make this call then.

The BDE Assert Macros are all configured to invoke a method that matches this signature, and is setup like so :

static void assertHandler(const char *text, const char *file, int line)
{
 //.. do something appropriate
}

int main()
{
 bsls::Assert::setFailureHandler(&assertHandler); 
        //Tell the BDE that should any assert fire off in this program, it should 
        //invoke this handler
    
    doProgram(); 
 return 0;
}
Like all good things though, the BDE will come with a default handler, and a few other handlers that you might find handy
    bsls::Assert::setFailureHandler(&bsls::Assert::failThrow); 
        //will throw bsls::AssertTestException 
    bsls::Assert::setFailureHandler(&bsls::Assert::failAbort); 
        //The Default -- aborts the application with std::abort()
    bsls::Assert::setFailureHandler(&bsls::Assert::failSleep);
        //Will simply go into a repeating loop of sleeps to give you the chance to attach a debugger 
        //into the live crash
All in all, some nice extensions to the simple little Assert, to allow you the freedom to extend your capabilities of doing runtime checks deeper into the life cycle of your application -- even live -- without taking on the innocent by ignorant judgement call of a library developer.

Also something you will need to know how to control if you are going to be developing with the BDE libraries.

Happy defensive programming!

No comments:

Post a Comment