Pages

Wednesday, May 4, 2011

Using SEH with C++ exceptions

The problem is common for all Windows developers: you are allowed to use either SEH (Structured Exception Handling) or C++ exceptions, or both simultaneously, but not everyone knows how to mix two kinds of exception handling correctly.

By default it’s not allowed to mix SEH and C++ exceptions in one function, so a code like the this will not get compiled:

    __try
    {
        try
        {
            LPTSTR str = TEXT("Hello World!");
            CharLower(str);
        }
        catch(const _com_error& ex)
        {
            _putws(ex.ErrorMessage());
        }
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        // Process exception
    }

VC++ compiler tells me: error C2713: Only one form of exception handling permitted per function

There is a well known workaround, but it doesn’t look pretty elegant because it separates the code:

void Foo()
{
    try
    {
        LPTSTR str = TEXT("Hello World!");
        CharLower(str);
    }
    catch(const _com_error& ex)
    {
        _putws(ex.ErrorMessage());
    }
}

int main(int argc, char* argv[])
{
    __try
    {
        Foo();
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        // Process exception
    }

    return 0;
}

So, how to catch SEH exception as C++ exception?

The trick is called ‘CRT SE translators’. At the root of the mechanism lies the function declared in <eh.h> and called _set_se_translator which takes a pointer to a function of the following form:

typedef void (__cdecl *_se_translator_function)(unsigned int, struct _EXCEPTION_POINTERS*);

The SE translator function is called when SEH exception occurs and can throw C++ exceptions depending on the exception code caught.

Using _set_se_translator function also requires the ‘/EHa’ (Enable C++ exceptions with SEH) compiler option to be set.

Here is the working example of how to use both C++ and SEH exception in one function in pretty elegant manner:

#include <Windows.h>
#include <Winternl.h>
#include <comdef.h>
#include <stdio.h>
#include <eh.h>

class CScopedSehTranslator
{
public:

    // Constructs a scoped SEH translator object with the translator function specified.
    // If no translator function is specified, default translator function is used.
    explicit CScopedSehTranslator(_se_translator_function translator = NULL)
        : m_newTranslator(translator != NULL ? translator : DefaultSeh2Cpp)
    {
        m_oldTranslator = _set_se_translator(m_newTranslator);
    }

    // Restores previously set SEH translator.
    ~CScopedSehTranslator()
    {
        _set_se_translator(m_oldTranslator);
    }

private:

    _se_translator_function m_newTranslator;
    _se_translator_function m_oldTranslator;

    typedef ULONG (NTAPI* FnRtlNtStatusToDosError)(NTSTATUS Status);
    static FARPROC m_pfnRtlNtStatusToDosError;

    static void DefaultSeh2Cpp(
        unsigned int u, 
        EXCEPTION_POINTERS* excp)
    {
        // If can convert exception code to win32 error code,
        if(m_pfnRtlNtStatusToDosError != NULL)
        {
            // then convert
            const ULONG win32Code = reinterpret_cast<FnRtlNtStatusToDosError>
                (m_pfnRtlNtStatusToDosError)(excp->ExceptionRecord->ExceptionCode);
            // and issue _com_error exception with this code.
            _com_issue_error(HRESULT_FROM_WIN32(win32Code));
        }
        else
        {
            // Otherwise thwo E_UNEXPECTED.
            _com_issue_error(E_UNEXPECTED);
        }
    }

};

FARPROC CScopedSehTranslator::m_pfnRtlNtStatusToDosError = GetProcAddress(
    GetModuleHandle(L"ntdll.dll"), 
    "RtlNtStatusToDosError");

int main(int argc, char* argv[])
{
    // Construct SEH translator object with default translator function.
    // On destruction it will restore SEH translator function.
    CScopedSehTranslator sehTranslator;

    try
    {
        // This will cause EXCEPTION_ACCESS_VIOLATION because string literals
        // are read-only but allow referring to via non const pointer.
        LPTSTR str = TEXT("Hello World!");
        CharLower(str);
    }
    catch(const _com_error& ex)
    {
        // Here the SEH exception is caught.
        _putws(ex.ErrorMessage());
    }

    return 0;
}

Thanks. AS-IS

6 comments:

  1. Nice Idea, I made something similar.

    I think there is a problem with the if(m_oldTranslator != NULL) check within your ~CScopedSehTranslator().

    I mean what happens when there is no old translator and the object is destructed. Probably the previously set translator is still used.

    John

    ReplyDelete
  2. Good catch! Fixed in the original post.

    ReplyDelete
  3. // If can convert exception code to win43 error code,

    Did you mean win32? instead of win43.

    ReplyDelete
  4. ... GetModuleHandle(L"ntdll.dll"), ...

    maybe: GetModuleHandle("ntdll.dll") -> without L?

    ReplyDelete
    Replies
    1. not really, prefix L is for unicode charset which is OS default starting from WinNT, though it'd be more correct to write GetModuleHandle(TEXT("ntdll.dll")) in order to make more compatible ;)

      Delete