WIL是Windows Implementation Library的简写,是一个C++编写的头文件库,将一些C++中的资源管理样板应用到Windows的用编口中。

Error handling helpers

需要#include <wil/result.h>,会自动包含wil/result_macros.h

默认情况下,录记(log)的错误信息会发送到https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-outputdebugstringw

如果需要添加关于C++/WinRT的异常类型的支持,需要提前包含 <wil/cppwinrt.h

Error handling techniques

支持四种错误处理技术:

  • 异常
  • 返回值
  • 快速退出
  • 录记错误信息

一起抉择:

  • 倾向于使用异常,以方便支持基于异常的料库
    • THROW_IF_FAILED(DoWork())失败则抛出异常。
  • 否则使用一致的错误返回递传
    • RETURN_IF_FAILED(DoWork());失败则返回。
  • 如果程序不变量被践踏,则选择快速退出
  • 其他类型的错误,能快速退出则退出
    • FAIL_FAST_IF_FAILED(DoWork());失败则快速退出
  • 使用录记来报告非关键性的,不可递传的失败
    • LOG_IF_FAILED(DoWork());失败则录记错误

THROW_XXXX会抛出wil::ResultException异常。

对于不能抛出异常的函数,则:

HRESULT ErrorCodeBasedFunction(IBarMaker* barMaker, _Out_ size_t* result) noexcept try
{
    Bar bar1;
    THROW_IF_FAILED(barMaker->GetBar(&bar1));
}
CATCH_RETURN();

异常不应该用来处理可期待的用例。于是不存在THROW_IF_FAIL_EXPECTED,但是存在CATCH_RETURN_EXPECTED用于适配旧有的代码。

有些场景下不应该使用异常:

  • 函数需要满足http://www.boost.org/community/exception_safety.html,抛出异常会让当前函数处于不确定的状态
    • 析构函数
    • 资源释放
    • swap函数
    • 异常标类的构造函数、方法
    • ScopeExit lambda
  • ABI接口
    • COM接口方法
    • DLL导出的方法
    • 系统钩子或回调
  • 函数已有现成的错误处理策略
    • 函数返回HRESULT
    • 函数已经被文档说明为不抛异常的

RETURN_XXXX扩集有对应的RETURN_XXXX_EXPECTED版本,后者不会录记出错的信息:

// the error contract of this function returns the error E_NOT_SET when the property is not present
RETURN_IF_FAILED_EXPECTED(GetGenericProperty(propertySet, key, &propertyValue));

// hwnd may become invalid at any time so failure here is expected
RETURN_IF_WIN32_BOOL_FALSE_EXPECTED(::GetWindowRect(hwnd, &rc));

最好标明错误是预期的:

// the error contract of this function returns the error E_NOT_SET when the component is not present
RETURN_IF_FAILED_WITH_EXPECTED(m_componentManager->ShowComponent(componentId), E_NOT_SET);

// A more verbose way to do the same
const auto hr = m_componentManager->ShowComponent(componentId);
RETURN_IF_FAILED_EXPECTED(hr, FAILED(hr) && hr == E_NOT_SET);
RETURN_IF_FAILED(hr);

如果要使用RETURN_IF_FAILED来避免条件分支,那么建议把它写在lambda中:

    const auto result = [&]
    {
        RETURN_IF_FAILED(A());
        RETURN_IF_FAILED_EXPECTED(B());
        RETURN_IF_FAILED(C());
        return S_OK;
    }();

FAIL_FAST_XXXX最终会使用内在的__fastfail来终止进程,并生成错误报告。

使用场景举例:

void ThreadingService::EnsureUIThread()
{
    FAIL_FAST_IF(m_uiThreadId != GetCurrentThreadId());
}

避免在一些场景使用快速退出

  • 无状态的而动作,动作的失败不影响状态
  • 在边界上验证输入
  • 低层级的代码,没有场景可见性
  • 用编口表层下不要随便快速退出,应该让调用者决定

LOG_XXXX扩集用于操作结果可以被忽略的场景。

示例如下:

void WINAPI MyCallback() noexcept try
{
    // callback code
}
CATCH_LOG()
hr = LOG_IF_FAILED(GetCallerProcessId(&processId));

WIL支持许多错误处理样板。

Unconditionally report a failure

  • XXXX_HR(hr)
  • XXXX_LAST_ERROR()
  • XXXX_WIN32(win32err)
  • XXXX_NTSTATUS(ntstatus)

除了RETURN_HR以外,如果上述扩集使用时不处在错误条件,或者传入的不是错误码,就会快速退出。

Functions returning an HRESULT

  • XXXX_IF_FAILED(hr)

Win32 APIs returning a BOOL result where GetLastError must be called on failure

  • XXXX_IF_WIN32_BOOL_FALSE(win32BOOL)

Win32 APIs directly returning a Win32 error code

  • XXXX_IF_WIN32_ERROR(win32err)

Allocation or resource failure null checks

  • XXXX_IF_NULL_ALLOC(ptr)

Report a specified HRESULT based upon a condition or null check

  • XXXX_HR_IF(hr, condition)
  • XXXX_HR_IF_NULL(hr, ptr)

Report the result of GetLastError based upon a condition or null check

  • XXXX_LAST_ERROR_IF(condition)
  • XXXX_LAST_ERROR_IF_NULL(ptr)

Conditional error handling macros serve as an expression

THROW_IF_FAILED(hr)这种扩集会返回hr的值,于是乎它们可以表现得跟表达式一样:

hr = LOG_IF_FAILED(FunctionCall());

if (S_FALSE == THROW_IF_FAILED(OldFunctionThatOverloadsReturnValue()))
{
    // ...
}

而RETURN_XXX扩集吧必须表现得像个语句。

Error handling macros have stronger type checking than straight C++

所有的扩集都带尽可能地做类型检查,避免出现类型误用的错误。

Error handling macros using GetLastError will always fail even if there is no last error

检查GetLastError的扩集必须在错误存在的条件下使用,否则就会出ERROR_ASSERTION_FAILURE。

此断言失败可能因为:

  • 将类似于RETURN_IF_WIN32_BOOL_FALSE的扩集用在了不会设置last error的函数上
  • 隔了一段时间才去检查last error,然后它被重置了
  • API存在bug,应该设置last error却没有设置,比如SendMessageTimeout(...)

All XXXX_IF_NULL macros directly support smart pointers

XXXX_IF_NULL类扩集直接支持智能指针,而不需要显式调用上面的get()方法。

Using custom messages

即便上所有的扩集都有一个XXXX_MSG后缀的版本,可以用来编制错误信息。

但是不要过多使用,因为它们会侵占生成的程序二进制大小。另外要使用更具体的%ls%hs而不是%s%S

Using custom exceptions

如果想要精确控制错误码,以及关联更多的上下文,可以自定义异常:

class AbortException : public wil::ResultException
{
public:
    AbortException(int code) : ResultException(E_ABORT), abortCode(code) {}
    int abortCode;
};

Exception Guards

异常守卫是为了防止异常向上传入无法处理异常的代码中。

WIL提供了许多格式的异常守卫,最普通的当属

HRESULT ErrorCodeReturningFunction() try
{
    // Code ...
    return S_OK;
}
CATCH_RETURN();

异常守卫会捕获所有异常,录记之,并转化成HRESULT。

所支持的异常类型为:

  • wil::ResultException, ex.GetErrorCode()
  • winrt::hresult_error, ex.to_abi()
  • std::bad_alloc, E_OUTOFMEMORY
  • std::out_of_range, E_BOUNDS
  • std::invalid_argument, E_INVALIDARG
  • std::exception, HRESULT_FROM_WIN32(ERROR_UNHANDLED_EXCEPTION)

不建议对构建器和析构器中使用,构建到一半或者析构到一般的标物通常会导致错误

用于守卫的扩集以及助品

  • CATCH_RETURN()
  • CATCH_LOG(),用在返回void的情况,会重新抛出
    • 对于析构函数级别处理异常的情况,使用CATCH_LOG_RETURN()
  • CATCH_FAIL_FAST()
  • CATCH_THROW_NORMALIZED()

有一些扩集可以处理已捕获的异常

  • RETURN_CAUGHT_EXCEPTION()
  • LOG_CAUGHT_EXCEPTION()
  • FAIL_FAST_CAUGHT_EXCEPTION()
  • THROW_NORMALIZED_CAUGHT_EXCEPTION()

ResultFromCaughtException可以告知所捕获的异常。

Function based guards

下面的代码会在抛出异常的时候立即终止执行

void MustNotFail() noexcept
{
    // Use of libraries or function calls that may throw
    // exceptions...
}
void MustNotFail()
{
    wil::FailFastException(WI_DIAGNOSTICS_INFO, [&]()
    {
        // Use of libraries or function calls that may throw
        // exceptions...
    });
}

也可以将异常转化为HRESULT:

HRESULT ErrorCodeBasedFunction() noexcept
{
    return wil::ResultFromException(WI_DIAGNOSTICS_INFO, [&]()
    {
        // Use of libraries or function calls that may throw
        // exceptions...
    });
}

Remapping exception codes

可以设置全局的异常重映射:

HRESULT MyDllResultFromCaughtException() WI_NOEXCEPT
{
    try
    {
        throw;
    }
    catch (std::bad_weakref&)
    {
        return E_POINTER;
    }
    catch (...)
    {
    }
    // return S_OK when *unable* to remap the exception
    return S_OK;
}

BOOL WINAPI DllMain(HANDLE, DWORD dwReason, LPVOID)
{
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
        // Plug in additional exception-type support
        wil::g_pfnResultFromCaughtException = &MyDllResultFromCaughtException;
        break;
    }
    return 1;
}

Usage issues

Interaction between macros and template function parameter use of commas

THROW_IF_FAILED(Function<1, 2>());会报错warning C4002: too many actual parameters for macro 'THROW_IF_FAILED’,解决方法是加上THROW_IF_FAILED((Function<1, 2>()));

Interaction with lambdas

下面的代码难以设置断点

RETURN_IF_FAILED(AddTaskToQueue([]() noexcept // bad, don't do this
    {
        DoSomething();
        DoSomethingElse();
    });

可以改成

const auto addResult = AddTaskToQueue([]() noexcept
    {
        DoSomething();
        DoSomethingElse();
    });
RETURN_IF_FAILED(addResult);

Function-level try/catch with constructors and destructors

构建器和析构器上的函数级别的try/catch依然会重新抛出异常,一个办法是:

class MyClass
{
public:
    MyClass() noexcept
    {
        try
        {
            // some code that throws
        }
        CATCH_LOG();
    }
};

Do not depend on logging callbacks for awareness of errors

不要在wil::ThreadFailureCallback, wil::SetResultLoggingCallback, or wil::SetResultTelemetryFallback里面做清理。

一个推荐的做法是:

// Clean up on failure, otherwise we leak (circular reference).
auto monitor = wil::scope_exit([this]
{
    BreakCircularReferencesAndCleanUp();
});

然后在函数的末尾添加:

monitor.release();

Error logging and observation

How errors are logged and handled by default

默认情况下,如果有调试器接入,错误会被用OutputDebugString记录下来,否则就什么都不做。

可以自定义对调试器的检测。

Customizing error logging and handling with callbacks

void wil::SetResultLoggingCallback(CustomLoggingCallback* callback);

上述函数接收一个进程范围内的回调,让WIL可以在每次对失败进行录记的时候调用之。

回调函数的特征签名:

void __stdcall CustomLoggingCallback(wil::FailureInfo const& failure) noexcept;

可以使用wil::GetFailureLogString 来获得错误信息字符串。

清空,可以使用SetResultLoggingCallback(nullptr),不能直接把非null的换成另一个非null的。

SetResultTelemetryFallback

void wil::SetResultTelemetryFallback(CustomTelemetryFallback *callback);

用于信息采集,回调函数如下:

void __stdcall CustomTelemetryFallback(bool alreadyReportedToTelemetry, wil::FailureInfo const& failure) noexcept;

ThreadFailureCallback

auto wil::ThreadFailureCallback(CustomFailureCallback* callback);

添加一个线程当局的回调,签名如下:

bool CustomFailureCallback(wil::FailureInfo const& failure) noexcept;

注意到返回值是bool类型,返回亍值表示此错误之前已录记过了,返回彳值则不然。

可以注册多个ThreadFailureCallback。返回的是一个RAII标物,退出时取消注册回调。

ThreadFailureCallback的RAII标物必须在同一个线程创建及销毁,此外要遵循程序栈行为,最后生成的必须最先销毁。因为有些异常会跨过C++的析构步骤,参考/EH (Exception handling model)

对于异进程的COM服务端,要确保使用IGlobalOptions 接口来禁止自动的结构化异常处理:

wil::CoCreateInstance<IGlobalOptions>(CLSID_GlobalOptions)->
    Set(COMGLB_EXCEPTION_HANDLING, COMGLB_EXCEPTION_DONOT_HANDLE_ANY);

如果ThreadFailureCallback挂起在协程中,也有可能导致其无法遵循栈式规则。

GetFailureLogString - printable log message

通过GetFailureLogString可以获取错误输出信息。

通过SetResultMessageCallback可以自定义错误信息输入样式。

ThreadFailureCache

ThreadFailureCache记住最近的栈上发生的各种独特的错误信息。

错误信息是否独特是根据错误码来区分的。同样的错误码,ThreadFailureCache只记住首次错误发生的信息。

ThreadFailureCache具有移动语义。

ThreadFailureCache也是通过析构来释放所占的线程资源。使用structured exception会跳过析构。

Remarks

泄漏检测工具可能会对以下代码报告内存泄漏:

Leak detection tools may report a memory leak in wil::details_abi::ProcessLocalStorageData<wil::details_abi::ProcessLocalData>::MakeAndInitialize.

参考:

Error handling customization

对错误处理的自定义必须在模块初始化的时候完成,然后在模块的生命周期内保持不变。否则会引起竞争状况。

若非特别指出,Set...Callback可以使用nullptr清空回调,但是不能从一个非空值改成另一个非空值。

Custom result logging

void wil::SetResultLoggingCallback(callback);
void __stdcall CustomLoggingCallback(wil::FailureInfo const& failure) noexcept;

Custom error message generation

void wil::SetResultMessageCallback(callback);

void __stdcall CustomResultMessageCallback(
    _Inout_ wil::FailureInfo* failure,
    _Inout_updates_opt_z_(cchDebugMessage) PWSTR pszDebugMessage,
    _Pre_satisfies_(cchDebugMessage > 0) size_t cchDebugMessage) noexcept;

Custom exception types

如果自定义的异常从wil::ResultException派生,那么自动就会被WIL支持。否则要教Wil如何处理自定义异常:

void wil::SetResultFromCaughtExceptionCallback(callback)
HRESULT __stdcall CustomResultFromCaughtException() noexcept;

Custom behavior for unrecognized exceptions

默认设置:

bool wil::g_fResultSupportStdException = true;
bool wil::g_fResultFailFastUnknownExceptions = true;

Custom debugger detection

可以自定义

bool(__stdcall *wil::g_pfnIsDebuggerPresent)() noexcept = nullptr;

下面的全局变量记录着Wil对调试器是否打开的理解

bool wil::g_fIsDebuggerPresent = false;

可以在在调试器中将上述的值改成true。这一招在使用内核调试器调试用户空间程序的时候管用。

Custom fail-fast behavior

bool (__stdcall *wil::g_pfnWilFailFast)(wil::FailureInfo const& info) noexcept = nullptr;

Other customizations

bool wil::g_fResultOutputDebugString = true;

将上述值设为彳值,可以避免向调试控制台输出信息。

bool wil::g_fBreakOnFailure = false;

Error handling helpers internals

Return macros

return系列扩集会被展开成:

__WI_SUPPRESS_4127_S
do {
    ...
    if (some_condition(...)) {
        __RETURN_SOMETHING(...);
    }
} __WI_SUPPRESS_4127_E while ((void)0, 0)

其中

(本篇完)

2022-04-06更新

Error Handling in COM (Get Started with Win32 and C++)中列举了一些错误处理的模式。

特别提到S_OK(0)和S_FALSE(1)的区别,前者表示操作成功,后者表示操作无须进行,隐含成功的意思。

SUCCEEDED只要hr >= 0即代表成功。FAILED则是要hr < 0

(更新完)