Introduction
Suppose you have written a function with a specific set of parameters. However, this function is designed in such a way that sometimes it needs more parameters. What does that mean? Wait, I'll explain.
If this function is called by your own functions, which in turn are also called by your functions and so on, then you can simply supply all possible parameters to all functions from which our "problematic" function can ever be called. Thus our function will always receive all the parameters it may ever need. But the disadvantage of this solution is that you have to supply all possible parameters to every function in the chain. This takes more stack, and makes the program totally unreadable. And also, you have to modify dozens of your code to add one single parameter to a single function, because you'll have to add it to all your functions.
There can also be another situation, where your function is called from within another function, which is not yours. In fact - your function is a callback function with a predefined set of parameters, which can't be changed. Well, usually in such situations, smart design always allows you to specify at least one parameter to your callback function (a context), but, alas, not all designs are smart :). Also, sometimes you'd like to have more parameters anyway.
So, what can be done in such situations?
- Use of global variables. Before your function could be called, you set some values to global variables, which are accessible from anywhere. This is the most straight and stupid way. Use of global variables should be avoided as much as possible. What if you call this function in more than one situation? And what if you call this function from within another pending call to the same function? And what will you do in the multi-tasking environment? Well, all that can be solved, but in very ugly ways.
- We can improve a bit the previous solution by using TLS. We won't have to bother about multi-tasking synchronization. However, it still has almost the same disadvantages. In addition, you must initialize the TLS (allocate a TLS index), hence your code needs a global initialization/uninitialization.
- And, finally, there is a way to supply a parameter to a function without the use of global variables. It uses SEH (Windows structured exception handling). Take a look at the following sample:
const DWORD EXCEPTION_FETCHPARAM_B = 0x800A0001L;
void Func_Nested(int a)
{
int b;
ULONG_PTR nExceptionParam = (ULONG_PTR) &b;
RaiseException(EXCEPTION_FETCHPARAM_B, 0, 1, &nExceptionParam);
}
void Func_Intermediate(int a)
{
Func_Nested(a);
}
void Func_Top()
{
EXCEPTION_POINTERS* pException;
__try
{
Func_Intermediate(12);
}
__except
(
pException = GetExceptionInformation(),
EXCEPTION_FETCHPARAM_B == pException->ExceptionRecord->ExceptionCode ?
(
ASSERT(1 == pException->ExceptionRecord->NumberParameters),
ASSERT(pException->ExceptionRecord->ExceptionInformation[0]),
*((int*) pException->ExceptionRecord->ExceptionInformation[0]) = 14,
EXCEPTION_CONTINUE_EXECUTION
) :
EXCEPTION_CONTINUE_SEARCH
)
{
}
}
In this sample, we start from Func_Top
, where we have all possible parameters, from there we call Func_Intermediate
, and this function eventually calls Func_Nested
. At some point, it realizes that it needs one more parameter. It raises a SEH exception with one parameter, which is a pointer to a placeholder for a missing parameter. Next, the OS looks for a suitable handler for this exception. Our handler detects this exception by querying its code, writes the missing parameter, and then reports to the OS that the exception is dismissed by returning EXCEPTION_CONTINUE_EXECUTION
code.
Next, this sample can be extended to an arbitrary number of 'missing' parameters. Take a look:
DWORD ParseException(EXCEPTION_POINTERS* pException)
{
switch (pException->ExceptionRecord->ExceptionCode)
{
case EXCEPTION_FETCHPARAM_B:
ASSERT(1 == pException->ExceptionRecord->NumberParameters);
ASSERT(pException->ExceptionRecord->ExceptionInformation[0]);
*((int*) pException->ExceptionRecord->ExceptionInformation[0]) = 14;
return EXCEPTION_CONTINUE_EXECUTION;
case EXCEPTION_FETCHPARAM_C:
ASSERT(1 == pException->ExceptionRecord->NumberParameters);
ASSERT(pException->ExceptionRecord->ExceptionInformation[0]);
*((int*) pException->ExceptionRecord->ExceptionInformation[0]) = 16;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
void Func_Top()
{
__try
{
Func_Intermediate(12);
}
__except(ParseException(GetExceptionInformation()))
{
}
}
Performance
It is believed that exception-handling mechanism is very slow and heavy. This is not accurate. It becomes really heavy only if the exception has been issued (handler returns EXCEPTION_EXECUTE_HANDLER
). In such a case takes place so-called unwind operation, which is a rapid return from all nested scopes, during which also guarded sections are executed (__finally
blocks). In our case, the exception is not issued. We just use SEH mechanism to communicate between some nested point and the __except
block. Such a trick can be compared to a function call by its complexity.
BTW: When you call RaiseException
function and your program runs under a debugger - it may write in the console something like "First-chance exception blablabla...". If you do this too frequently - the debugger can consume too much CPU for those outputs, but that doesn't mean that this method is heavy.
Where to use
This method is very powerful. It allows you to fetch parameters in almost any place in your application from up to WinMain
function.
General rule: you must be absolutely sure that the call to your function will take place in the same thread you put the __try
- __except
block, otherwise the exception will lead to a crash.
Another precaution: some people like to dismiss all exceptions in their code, to make buggy programs look more stable. So, if one of your nested functions uses such a dirty trick - this method won't be too useful. But I personally believe that is not a correct concept.
There seems to be a bit weird thing when raising such an exception from within a window procedure, where the handler resides out of the GetMessage
-> DispatchMessage
loop, or the DoModal
functions. It seems to work ok, but the User32 issues a page error exception (which it dismisses too).