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.
参考:
- https://github.com/microsoft/wil/wiki/Internals-Local-data
- https://github.com/microsoft/wil/wiki/Shutdown-aware-objects
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)
其中
do { ... } while (0)
是为了避免问题https://stackoverflow.com/q/154136while ((void)0, 0)
是为了避免编译器警告__WI_SUPPRESS_4127_S
是为了压制https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4127?view=msvc-160,因为some_condition(...)
编译之后可能会被转为常量__RETURN_SOMETHING(...)
是另一个扩集,用于录记并返回相应值。
(本篇完)
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
。
(更新完)