原 What Every Dev need

What Every Dev needs to Know About Exceptions in the Runtime

============================================================

Date: 2005

CLR中的异常有个重要区别,他是托管异常,通过诸如c#的的try/catch/finally的形式开放给应用程序。还有运行时内部异常。大多数运行时开发者很少考虑如何生成并公开托管异常模型。但是运行时开发者需要知道异常是如何实现的。为了保证区分两种异常。本文档使用managed exception标识一个托管应用程序的抛出或捕获。使用CLR‘s internal exceptions标识运行时内部错误。多数情况下,这份文档指的是CLR内部异常.

exceptions影响哪里

===========================

异常影响很广。最多的是抛出、捕获异常的函数。因为代码中必须明确的抛出异常和捕捉并正确的处理异常,甚至函数没有抛出异常,但是它可能调用了一个抛出异常的函数,因此函数必须正确的处理抛出的异常。(The judicious use of _holders_ can greatly ease writing such code correctly.)

为什么CLR内部异常不同

==========================================

CLR的内部异常和C++异常相似,但也并非完全相同。代码可以编译成Mac OSX,BSD和Windows程序。 操作系统和编译器的差异决定了我们不能仅仅使用标准的C++ try/catch。此外,CLR内部异常提供了管理的“finally”和“fault”类似的功能。

在一些宏的帮助下,可以写出和标准c++几乎一样的处理异常的代码。

捕获异常

=====================

EX_TRY

------

基础的宏是EX_TRY / EX_CATCH / EX_END_CATCH,使用方式如下

EX_TRY

// Call some function. Maybe it will throw an exception.

Bar();

EX_CATCH

// If we're here, something failed.

m_finalDisposition = terminallyHopeless;

EX_END_CATCH(RethrowTransientExceptions)

EX_TRY 简单的开始try块,和c++的try十分相似,也有一个{。

EX_CATCH

--------

EX_CATCH宏关闭try块,包括大括号},同时开始catch块,和EX_TRY相似,他也开始使用一个花括号开始catch块。

和c++异常的区别是:clr开发者不用特别指定需要捕获的异常,事实上,这组宏捕获所有异常包括非c++异常例如托管异常。如果有些代码需要捕获一个异常或一组异常, 那么它就需要捕获、检查异常, 并重新抛出其他异常。

EX_CATCH宏会捕捉所有,而函数通常并不需要,下面两节主要讨论如何处理你不关心的异常。

GET_EXCEPTION() & GET_THROWABLE()

---------------------------------

那么, clr 开发人员如何检查所捕获的内容, 并确定要执行的操作?CLR提供了有几种方法, 至于用什么取决于需求。

首先,无论捕获的是什么异常,都是一个继承值全局异常类的子类的示例。其中一些异常类含义很明显,如OutOfMemoryException,有些则是特定领域的,如EETypeLoadException,而有些则仅仅是系统异常的封装类如CLRException(有一个托管对象引用)和HRException(保存一个句柄)。如果初始异常没有继承值全局异常类。那么宏会在某些情况下封装起来。(所有的异常都应该是系统提供的,新的异常不必须经过CORE执行引擎的情况下)。

接下来, 总是有一个与CLR内部异常关联的HRESULT。有时, 与HRException一样, 该值来自某个com源, 但内部错误和Win32 API故障也有HRESULTS。

最后,因为几乎CLR中所有异常都有可能传递进托管代码,所以在内部异常和托管异常之间有相应的映射关系。不需要创建异常,也能获取这个异常。

CLR开发者是如果对异常进行分类的呢?

通常,所有的异常分类是根据异常的HRESULT:

HRESULT hr = GET_EXCEPTION()->GetHR();

通过托管异常对象便于获得更多信息。如果异常将被传递回托管代码, 则无论是立即还是稍后缓存, 托管对象都是必需的。而异常对象也同样容易得到。这是个对象引用, 所有常用的规则都适用:

OBJECTREF throwable = NULL;

GCPROTECT_BEGIN(throwable);

// . . .

EX_TRY

// . . . do something that might throw

EX_CATCH

throwable = GET_THROWABLE();

EX_END_CATCH(RethrowTransientExceptions)

// . . . do something with throwable

GCPROTECT_END()

有时, 无法避免c++异常对象, 尽管这主要是在异常基底实现中。如果c++异常确实是十分重要的, 则有一组轻量级的RTTI-like函数, 可帮助对异常进行分类。例如,

Exception *pEx = GET_EXCEPTION();

if (pEx->IsType(CLRException::GetType())) {/* ... */}

这段代码能判断异常是(或者继承自)CLRException。

EX_END_CATCH(RethrowTransientExceptions)

----------------------------------------

上面的栗子中,"RethrowTransientExceptions"是EX_END_CATCH宏的一个参数;它是三个预定义的宏中的一个,这三个宏反映了异常的处理方法。下面列出了三个宏及其含义:

- _SwallowAllExceptions_: 见名思意,会吞掉所有的异常,虽然简单好用,但是常常不能满足需求。

- _RethrowTerminalExceptions_. 再抛出。

- _RethrowTransientExceptions_."transient" 异常的最佳定义是, 如果再次尝试, 可能不会发生, 可能是在不同的上下文中。这些是”transient“异常:

- COR_E_THREADABORTED

- COR_E_THREADINTERRUPTED

- COR_E_THREADSTOP

- COR_E_APPDOMAINUNLOADED

- E_OUTOFMEMORY

- HRESULT_FROM_WIN32(ERROR_COMMITMENT_LIMIT)

- HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY)

- (HRESULT)STATUS_NO_MEMORY

- COR_E_STACKOVERFLOW

- MSEE_E_ASSEMBLYLOADINPROGRESS

CLR开发人员在不确定的情况下一般应该使用RethrowTransientExceptions.

不同情况下,开发则需要考虑捕获何种异常,也只应该捕获这种异常,因为宏能捕获所有的异常,唯一不捕获的方法是重新抛出异常。

如果EX_CATCH / EX_END_CATCH块正确的分类异常并在必要的时候抛出,那么SwallowAllExceptions就是告诉宏不必在此抛出异常

## EX_CATCH_HRESULT

有时需要异常对应的HRESULT,特别是在COM接口中是,这种情况下EX_CATCH_HRESULT比EX_CATCH块简单,一个经典案例如下

HRESULT hr;

EX_TRY

// code

EX_CATCH_HRESULT (hr)

return hr;

然而,虽然看着简单诱人,但并不保证正确。EX_CATCH_HRESULT捕获所有的异常,保存句柄,并吞掉所有异常,因此除非你确信函数确实需要吞掉所有异常,EX_CATCH_HRESULT通常是不合适的用法。

正如上面提到的,异常宏捕获所有的异常;捕获特定异常的唯一办法是捕获所有异常,重新抛出其他的异常。如果异常被捕获,检查,写日志等等之后,如果不需要,会被重新抛出。EX_RETHROW会重写抛出相同的异常

Not catching an exception

=========================

有些代码不会抛出异常,但是需要做一些清理,修正工作,Holers使用与这种场景,但并非所有的地方,当holders不适用的时候,CLR有两种"finally"块处理。

EX_TRY_FOR_FINALLY

------------------

当函数退出时需要进行一些操作,finally使用于这种情况。CLR有一组宏来实现try/finally:

EX_TRY_FOR_FINALLY

// code

EX_FINALLY

// exit and/or backout code

EX_END_FINALLY

重点:EX_TRY_FOR_FINALLY宏使用SEH而不是c++的EH,C++编译器不允许在同一个函数中混合使用SEH和EH.具有自动析构的局部变量需要c++EH来执行析构函数。因此,使用EX_TRY_FOR_FINALLY的函数不能使用EX_TRY,也不能使用带有自动析构的局部变量。

EX_HOOK

-------

有时有些只有当异常发生时才会执行的代码,这些情况下EX_HOOK是适用的,EX_HOOK和EX_FINALLY像是,但是"hook"语句只会在异常发生时运行,hook语句结束时异常会自动抛出。

EX_TRY

// code

EX_HOOK

// code to run when an exception escapes the “code” block.

EX_END_HOOK

这方法比 EX_CATCH/EX_RETHROW好些,非栈溢出的异常直接抛出,但是会捕获栈溢出异常(并释放栈)接着抛出一个新的栈溢出异常。

Throwing an Exception

=====================

在CLR中简单的调用即可抛出异常

COMPlusThrow ( < args > )

它有很多重载,实现的思路是向COMPlusThrow传递异常类型。这些异常是有一组宏生成的([Rexcep.h](https://github.com/dotnet/coreclr/blob/master/src/vm/rexcep.h)),这些类别为kAmbiguousMatchException,等等。重载还有写额外的参数特别制订了资源文件,一般通过代码报告的错误类型分来选择。

COMPlusThrowOOM();

------------------

ThrowOutOfMemory()抛出c++ OOM异常。为了避免内存溢出,这会抛出一个预先实例化的异常。

当获取到一个托管内存溢出异常时,运行时首先会尝试分配一个新的托管对象[1],如果分配失败,会返回一个预先分配的,共享的,全局的内存溢出异常对象。

[1]如果是个2gb数组请求失败,那么一个简单的对象仍旧能够分配

COMPlusThrowHR(HRESULT theBadHR);

---------------------------------

有一些重载,比如有个IErrorInfo等。有一些十分复杂的代码来确定异常种类对应的特定HRESULT。

COMPlusThrowWin32(); / COMPlusThrowWin32(hr);

---------------------------------------------

抛出win32类型的错误。

COMPlusThrowSO();

-----------------

抛出栈溢出异常,这并非一个真正的栈溢出,但是可能导致一个真正的栈溢出。

和OOM一样,会抛出一个预先定义的C++栈溢出异常对象,和OOM不同的时,检索托管对象时,运行时i总是i返回预定义的,共享全局的栈溢出异常。

COMPlusThrowArgumentNull()

--------------------------

参数null异常

COMPlusThrowArgumentOutOfRange()

--------------------------------

参数超出异常

COMPlusThrowArgumentException()

-------------------------------

参数错误异常

COMPlusThrowInvalidCastException(thFrom, thTo)

----------------------------------------------

给定类型句柄用于类型之间的转换,帮助器将创建一个格式良好的错误消息。

EX_THROW

--------

代码中通常不要抛出底层异常,很多COMPlusThrowXXX异常内部和函数ThrowXXX一样使用EX_THROW,尽量减少直接使用EX_THROW,尽量封装异常的底层细节。但是如果没有使用的高层函数,使用EX_THROW也可以。

宏有两个参数,一个是抛出异常的类型(c++异常的子类),还有一个异常构造函数的参数列表。

Using SEH directly

==================

There are a few situations where it is appropriate to use SEH directly. In particular, SEH is the only option if some processing is needed on the first pass, that is, before the stack is unwound. The filter code in an SEH __try/__except can do anything, in addition to deciding whether to handle an exception. Debugger notifications is an area that sometimes needs first pass handling.

Filter code needs to be written very carefully. In general, the filter code must be prepared for any random, and likely inconsistent, state. Because the filter runs on the first pass, and dtors run on the second pass, holders won't have run yet, and will not have restored their state.

PAL_TRY / PAL_EXCEPT, PAL_EXCEPT_FILTER, PAL_FINALLY / PAL_ENDTRY

-----------------------------------------------------------------

When a filter is needed, the PAL_TRY family is the portable way to write one in the CLR. Because the filter uses SEH directly, it is incompatible with C++ EH in the same function, and so there can't be any holders in the function.

Again, these should be rare.

__try / __except, __finally

---------------------------

There isn't a good reason to use these directly in the CLR.

Exceptions and GC mode

======================

使用 COMPlusThrowXXX() 引发异常不会影响gc模式, 并且在任何模式下都是安全的。当异常展开返回到EX_CATCH时, 堆栈上的任何持有者都将被解除, 释放他们的资源并重新设置他们的状态。当执行在 EX_CATCH 恢复时, 持有人保护的状态将被恢复到它是在 EX_TRY 的时候。

Transitions

===========

考虑到托管代码、clr、com 服务器和其他native code, 在调用约定、内存管理以及异常处理机制之间可能有许多转换。对于异常, CLR开发人员很幸运, 大多数这些转换要么完全位于运行时之外, 要么被自动处理。 有三个转换是CLR开发人员关心的问题。其他任何东西都是一个高级主题, 那些需要了解它们的人, 都清楚地知道他们需要了解!

Managed code into the runtime

-----------------------------

这是 "fcall"、"jit helper" 等等。运行时通过托管异常将错误报告回托管代码。 如果fcal 函数(直接或间接)引发托管异常。正常的 clr 托管异常实现将查找适当的托管处理程序。

另一方面, 如果 fcall 函数可以执行可能引发CLR内部异常的任何事情 (其中一个 c++ 异常), 则不能让该异常泄漏到托管代码。为了处理此情况,CLR具有UnwindAndContinueHandler (UACH), 它是捕获 c++ RH异常, 并抛出托管异常。

任何从托管代码调用的运行时函数,都可能引发 c++ EH异常, 都必须将抛出异常的代码包裹在 INSTALL_UNWIND_AND_CONTINUE_HANDLER/UNINSTALL_UNWIND_AND_CONTINUE_HANDLER 中。 使用HELPER_METHOD_FRAME 将自动 使用UACH。 使用UACH 的开销不小, 所以不应该到处使用。在性能关键要求比较高的代码不使用UACH,而在引发异常之前使用一个方法。

如果抛出一个c++异常,却没有UACH,典型的错误结果就是和CPFH_RealFirstPassHandler中的"GC_TRIGGERS called in a GC_NOTRIGGER region"约定发生冲突。为了修复这个问题,请查找托管到运行时转换, 并检查 INSTALL_UNWIND_AND_CONTINUE_HANDLER 或 HELPER_METHOD_FRAME_BEGIN_XXX。

Runtime code into managed code

------------------------------

从运行时到托管代码的转换具有高度平台相关。在32位 windows 平台上, clr的托管异常代码要求在输入托管代码之前使用"COMPlusFrameHandler"。这些转换由高度专门化的helpe 函数处理, 这些功能负责相应的异常处理程序。任何典型的新的都不可能使用任何其他方式。在 COMPlusFrameHander 丢失的情况下, 最可能的后果是, 目标托管代码中的异常处理代码根本不会执行--没有 finally 块, 也没有 catch 块。

Runtime code into external native code

--------------------------------------

从运行时调用其他native code (os、crt 和其他 dll)时可能需要特别注意。外部代码可能导致异常的情况。这一个问题的原因来自于 EX_TRY 宏的实现, 特别是它们如何将非异常转换或包装为异常。使用 c++ EH, 可以捕获所有异常 (通过 "catch (...)"), 但只能通过放弃有关已捕获内容的所有信息。捕获exception* 时, 宏要检查异常对象, 但在捕获其他内容时, 没有任何要检查的内容, 宏必须猜测实际的异常是什么。但是当异常来自于运行时的外部时, 宏总是会猜测错误。

当前的解决方案是在标注筛选器中包装对外部代码的调用。筛选器将捕获外部异常, 并将其转换为 SEHException, 这是运行时的内部异常之一。此筛选器是预定义的, 使用起来很简单。但是, 使用筛选导致无法使用SEH, 这当然会在同一函数中排除使用 c++ EH。若要将标注筛选器添加到使用 c++ EH 的函数, 需要将一个函数一分为二。

To use the callout filter, instead of this:

length = SysStringLen(pBSTR);

write this:

BOOL OneShot = TRUE;

struct Param {

BSTR* pBSTR;

int length;

};

struct Param param;

param.pBSTR = pBSTR;

PAL_TRY(Param*, pParam, &param)

{

pParam->length = SysStringLen(pParam->pBSTR);

}

PAL_EXCEPT_FILTER(CallOutFilter, &OneShot)

{

_ASSERTE(!"CallOutFilter returned EXECUTE_HANDLER.");

}

PAL_ENDTRY;

在引发异常的调用中缺少标注筛选器将导致在运行时中产生错误的异常。不正确的类型甚至是不确定的;如果已经有一些托管异常存在, 那么托管异常将被抛出。如果没有当前异常, 则将报告OOM。在已检查的生成中, 断言通常会触发缺少的标注筛选器。这些断言消息将包括文本 "The runtime may have lost track of the type of an exception"。

Miscellaneous

=============

还有许多其他宏参与EX_TRY。大多数不会在宏定义之外使用

一组, BEGIN_EXCEPTION_GLUE/END_EXCEPTION_GLUE, 值得特别提及。这些原来是过渡宏, 并将在 whidbey 项目中替换为更合适的宏。他们的工作很好, 所以他们都没有被取代。理想情况下, 所有实例都将在 "清理" 期间进行转换, 并删除宏。同时, CLR开发这不提倡使用 EX_TRY/EX_CATCH/EX_CATCH_END和EX_CATCH_HRESULT。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Jimoer

jvm学习记录-对象的创建、对象的内存布局、对象的访问定位

简述 今天继续写《深入理解java虚拟机》的对象创建的理解。这次和上次隔的时间有些长,是因为有些东西确实不好理解,就查阅各种资料,然后弄明白了才来做记录。 (此...

2757
来自专栏小二的折腾日记

day5(面向对象2)

用来将文件或文件夹封装成对象。 方便对文件与文件夹的属性信息进行操作。 File对象可以作为参数传递给

621
来自专栏欧阳大哥的轮子

深入解构objc_msgSend函数的实现

熟悉OC语言的Runtime(运行时)机制以及对象方法调用机制的开发者都知道,所有OC方法调用在编译时都会转化为对C函数objc_msgSend的调用。

1072
来自专栏java架构师

Unit断言学习

[TestMethod]—用于把一个方法标记为一个测试方法。当你运行你的测试时,仅标记有这个属性的方法才能够运行。 [TestClass]—用于把一个类标记为...

27411
来自专栏Java技术分享圈

Java的数据库连接工具类的编写

1074
来自专栏java一日一条

50个常见的 Java 错误及避免方法(第二部分)

System.out.println("Whatdo you want to do?");

1103
来自专栏学海无涯

20.Swift学习之协议

协议为方法、属性、以及其他特定的任务需求或功能定义一个大致的框架。协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现。满足了协议中需求的任意类型都叫做遵...

822
来自专栏积累沉淀

Linux之grep和egrep命令总结

grep / egrep 语法: grep  [-cinvABC]  'word'  filename -c :打印符合要求的行数 -i :忽略大小写 ...

18510
来自专栏全华班

java学习手册-JAVA程序员笔试题(一)

JAVA程序员笔试题(一) 一、选择题: 1、类的成员变量要求仅仅能够被同一package下的类访问,应该使用哪个修辞词 A. Protected、B. Pub...

3825
来自专栏python爬虫日记

转载:python的编码处理(一)

最近业务中需要用 Python 写一些脚本。尽管脚本的交互只是命令行 + 日志输出,但是为了让界面友好些,我还是决定用中文输出日志信息。 

972

扫码关注云+社区