这是一个C语言机制和基于库的实现的参考实现,用于错误处理和延迟清理,改编自GO编程语言中的类似功能。该机制改进了。
其目的是通过为建议名称为<;stddefer.h>;的新标头提供一个新的库子句,将这里提供的基本工具集成到C标准中。有关通用方法的说明可在以下文档中找到:
Guard{void*const p=malloc(25);if(!p)Break;defer free(P);void*const q=malloc(25);if(!q)Break;defer free(Q);if(mtx_lock(&;mut)==thrd_error)Break;推迟mtx_unlock(&;mut);//获取的所有资源}。
其想法是,我们使用DEFER关键字指示语句(例如,对free的调用)仅在受保护块的末尾执行,并且我们希望此操作无条件地发生,无论以哪种方式离开受保护块。对于三个一起延迟的语句,它说它们应该以与遇到的顺序相反的顺序执行。因此,此代码示例的控制流可以如下所示:
这里,虚线表示当某些资源不可用或执行被信号中断时可能出现的环境控制流。
清理代码(FREE或MTX_UNLOCK)被编码到离需要它的地方很近的地方。
清理代码不会隐藏在某些先前定义的函数(如atexit处理程序)或构造函数/析构函数对(C++)中。
对于正常控制流(无中间返回、退出、…)。具有类似属性的代码可以使用现有工具进行编码。上面的内容等同于以下内容
{void*const p=malloc(25);if(!p)Goto DEFER0;if(False){DEFER1:free(P);goto DEFER0;}void*const q=malloc(25);if(!q)goto DEFER1;if(False){DEFER2:free(Q);goto DEFER1;}if(mtx_lock(&;mut)==thrd_error)goto DEFER2;if(False){DEFER3:mtx_unlock(&;mut);转到DEFER2;}//获取的所有资源转到DEFER3;DEFER0:;}。
在这里,if(False)子句保证延迟语句在第一次遇到时被跳过,标签和goto语句实现从后到前的跳跃以最终执行延迟语句。
显然,大多数C程序员不会这样编写代码,但他们更愿意写下上述内容的线性化,这是在C:
{void*const p=malloc(25);if(!p)goto DEFER0;void*const q=malloc(25);if(!q)goto DEFER1;if(mtx_lock(&;mut)==thrd_error)goto DEFER2;//获取的所有资源MTX_UNLOCK(&;mut);DEFER2:释放(Q);DEFER1:释放(P);DEFER0:;}。
这样做的优点是只显式地显示环境控制流(三个*=goto=),但是这样做的代价是接近;清理代码远离需要它的地方。
然而,即使是这种线性化也需要某种形式的标签命名约定。对于更复杂的代码,这些跳转的维护可能很棘手,而且容易出错。这显示了延迟方法的另一个优势:
清理规范不依赖于任意命名,如标签(C)或RAII类(C++),并且在添加或删除DEFER或BREAK语句时不会更改。
在C中更难实现的另一个重要属性(需要C++中的try/catch块)是,受保护块中的所有出口都会被检测到并执行操作:Break、Return、thrd_exit、Exit、Panic或信号中断。也就是说,除非有鼻神飞来飞去,否则我们还有第四个重要的属性。
这与C++对析构函数的处理不同,析构函数只有在下面有try/catch块时才能保证执行。
这种延迟执行的原则以一种自然的方式扩展到嵌套保护的块,即使它们堆叠在不同的函数调用中。
在上面的示例中,作为延迟语句结果的控制流是静态的,也就是说,延迟语句本身没有放入条件或循环中。例如,能够将它们放在if中给了程序员更大的灵活性,特别是它只允许在确实需要相应资源的情况下延迟语句。
//创建一个小的本地缓冲区作为复合文字Double*p=(len>;MAXLEN)?0:(Double[MAXLEN]){0};//如果len太大,则转到堆,如果(P){p=defer_calloc(len,sizeof double);defer free(P);}//现在使用p作为缓冲区,直到我们离开函数。
在这里,只有在pi引用的缓冲区是动态分配的情况下,才会在受保护的块终止时计算延迟语句。为了支持这样的构造,ourimplementation确定每个延迟语句在运行时的执行顺序和次数。此方法需要在运行时分配存储以维护延迟状态。DEFER机制通过在DEFER语句失败的情况下继续正常操作来提供容错能力,方法是忍受执行DEFERED语句来释放资源。
DEFER_CALLOC和DEFER语句都可能耗尽内存。如果调用成功(因此分配了资源),但延迟机制失败,则立即评估对FREE的调用,并通过死机终止封闭的受保护块的执行,错误代码为DEFER_ENOMEM。
在我们对C标准化的建议中,当前的想法是让每个函数体实现一个受保护的块,这样对于许多常见的用法来说,显式的保护语句是不必要的。一般来说,这不是宏奇迹可以做到的,而需要编译器的魔力。
引用实现通过跟踪堆栈指针,通过一些内部魔术为GCC系列编译器实现了这一点。这有一些注意事项,请参见下面的内容。然而,在本文档中,我们将始终谈论受保护的块,而不管是否显式地给出了防护语句,或者函数体是否就是这样的。
有三个严重程度级别用于终止受保护的块,Break或类似的级别仅终止受保护的块,return终止同一函数的所有受保护的块,以及在所有函数调用中退出或死机同一线程。
显式:通过在结束时离开受保护的块或使用BREAK、DEFER_BREAK、退出…。从内部。
隐式:来自信号处理程序。提供了两个信号处理程序DEFER_SIG_FLAG和DEFER_SIG_JUMP。他们的行为顾名思义。第一种方法只设置一个标志,然后用户代码通常会调查该标志并采取相应的行动。这通常用于用户中断程序执行的SIGINT这样的信号。第二个处理程序跳转到延迟堆栈上的第一个DEFER子句,并触发死机。这个更适合与故障操作相关的信号,这些故障操作本身是不可恢复的。
一旦在延迟语句中开始执行,就可以通过RECOVER来调查导致执行的条件。此返回指示发生了什么情况的整数错误代码。如果它是0,那么在中断、返回或退出时,这个DEFER子句的执行是正常的。如果它是任何其他值不寻常的事情可能已经发生,通常会在调用堆栈中向下发生死机。
一旦恢复了错误条件,处理错误的责任就会传回给用户。如果他们不想要,他们可以通过调用`Panic(code,0)`重新发出死机,其中0表示使用与恢复之前相同的最终操作(例如退出)。
作为C库如何使用此函数的示例,还有一些存储分配函数的增强版本,如DEFER_MALLOC。如果它们遇到内存不足的情况,它们会恐慌的想法。这有几个影响。
用户通过DEFER子句安装的清理操作将被调用,例如关闭文件或释放大量分配。
用户代码可以通过RECOVER或类似方式建立恢复机制。这可能很少被普通的应用程序使用,但是这里的安全关键应用程序将获得一个句柄来避免灾难。
对为这些函数创建的汇编器的检查表明,所有这些对快速执行路径产生的开销非常小。通常,此技术可以提供一种可行的方式来提供与附件K相同的错误检测功能,但在某种程度上。
EXIT展开线程的所有活动函数调用的所有DEFER子句并正常退出。
一旦包含此标头,特征EXIT、QUICK_EXIT和THRD_EXIT就会被重载以进行展开。当这样的代码链接到以不同方式编译的遗留目标文件时,为了保持一致,您应该包装系统调用。这通常可以通过向链接器提供`-print=exit`等参数来完成。(你必须查阅你当地的手册来了解这一点。)。通过向构建提供环境变量DEFER_NO_WRAP,可以(但不应该)关闭此功能。
以类似的方式,该实现还提供了包装C库的分配函数,但是这里的缺省设置是相反的:默认情况下,malloc和Co没有包装。您可以通过设置DEFER_MALLOC_WRAP来启用此功能。否则,您可以使用函数defer_malloc和类似函数来捕获分配错误。
在紧急情况下发送)始终为正,且系统错误代码通常为否定错误号。为了以可移植的方式处理此类错误代码,我们为所有POSIX错误代码提供了一个前缀为DEFER_的等效代码。例如,在存在DEFER_ENOMEM的平台上有与ENOMEM相同的DEFER_ENOMEM,另外还有一些其他未使用的数字。
对于信号号,甚至在更小的负值中也有复杂的编码。因为系统信号号可能与errno号冲突,所以我们不能直接使用它们。相反,对于所有POSIX信号号,都有常数用DEFER_PREFIX替换SIG前缀。例如,存在DEFER_HUP来表示信号SIGHUP,而不管该信号编号是否实际存在于平台上。
有两个方便的宏简化了此功能的使用,DEFER_IF作为伪选择语句,RECOVER_SIGNAL只恢复信号,如果没有信号,则不执行任何操作。
C++对于无序代码执行、namelydestructor和catch子句具有类似的功能。DEFER语句将这两个功能组合为一个。
此实现提供了在这些机制之间进行转换的功能,以便可以在混合使用C和C++的程序中可靠地执行堆栈展开。具体地说,如果从C++代码调用,C功能本身就会将边界处的死机和退出请求转换为C++异常。要做到这一点,C++应用程序唯一要做事情就是建立一个标记,这实际上就是当前的语言是C++。当C++代码包含<;stddefer_codes.h>;时,对于任何包含函数main的翻译单元都会这样做(通过一些黑客攻击)。
然而,要真正确保资源由这样的ac++程序员释放,最好捕获所有异常。这将确保所有的破坏者,包括Main在内,在解开时都会被召回.。实现这一点的一种简单方法是将main建立为try-catch块:
Int main(int argc,char*argv[])尝试{...。做好你的事…}接住(…){扔;}。
要为可能从C调用的C++代码建立互操作性,需要更多的内容。可以创建用于功能测试的包装器Testing_cpp,如下例所示:
#include";stddefer_codes.h";#include";stddefer_cpp.h++";int Testing_cpp(Int Rec){std::defer_bORDARY;//必须是第一个声明的变量try{return test(Rec);}catch(...){bilary.Panic();}return 0;}。
STD::DEFER_BOLDORY类型的局部变量,构造函数为其存储调用前的状态,
一个try/catch子句,它保证异常被捕获,并且在展开时所有析构函数都被调用。
对方法Panic()的调用,它要么传播捕获的任何异常(如果调用方也是C++),要么将异常转换为异常(如果调用方是C)。
当一致地这样使用时,将执行C延迟和C++异常之间的仲裁来回转换,但延迟错误代码的表达能力有其局限性。我们翻译一些标准代码和异常:
此实现使用setjmp/long jmp作为主要功能来跳转到同一函数中的延迟语句或展开调用堆栈。我们区分了已知以同一函数(_Defer_Shrtjmp)为目标的";个跳转和那些已知跳到调用堆栈上的另一个函数(_Defer_Long Jmp)的跳转。返回的展开通常都是短跳转,而退出和调用堆栈的其他展开总是会启动长跳转。
这个实现相当黑客,因为它使用嵌套的forloop来实现主宏、延迟和保护。这样做是为了确保在原则上可以与任何C编译器一起使用的宏来实现主要只有库的实现。这种技术会导致难以阅读的代码,因此不推荐使用。
此外,我们使用称为shnell的中间处理器来生成C(ANDC++)代码。它将#杂注注释用于复杂宏的定义和代码展开。因此,如果您想要修改此实现,您必须下载shnell并修改真正的源代码。
该实现可以区分基本上所有跳跃最终都被实现为长跳跃的情况,或者可以采用一些短跳跃捷径的平台。目前,这只适用于GCC和那些实现了所谓的计算转到的朋友,即可以获取地址的标签,然后这些地址可以用于扩展的转到功能中。这样一个专门的实现可以在展开端获得很多好处(然后它们大多是简单的跳转),但是它们仍然必须跟踪所有受保护的块,并使用setjmp延迟子句,因为这些子句可以从其他函数或信号处理程序跳转到这些子句。
当执行延迟语句时,在延迟语句中使用的局部变量必须是活动的,因此通常在受保护的块的末尾。为了能够在处理过程中对变量的更改做出反应,此策略是必需的。例如,要释放的指针值可以通过调用realloc来重写以调整对象的大小。
也就是说,我们可以说,DEFER接收不带参数的lambda,并在离开警卫时执行它。捕获的默认值是&;,也就是说,所有变量都是通过引用&34;捕获的。
在某些情况下,按值延迟捕获局部变量是有意义的。在λ表示法中,这有点像
其中变量TOTO将被冻结为它在DEFER语句的点上具有的值,然后当在保护结束时执行延迟语句时将提供TOTO的那个特定值。在参考实现中,可以通过使用替代宏DEFER_CAPTURE来实现相同的功能。
对于GCC和朋友来说,该实现能够检测不同函数调用之间的边界。对于没有放在防护内的延期,这一点很重要,这样我们就不会在另一个功能中将其附加到防护上。这只适用于跟踪堆栈指针的编译器魔术。
Setjmp/long jmp提供的对局部变量状态的保证比我们上下文中需要的要弱。因此,我们使用其他同步特性来保证值是最新的,即原子变量和栅栏。如果这些不可用,这些实施仍将有效,但必须按照针对延迟的说明进行预防。
这个实现是特殊的,因为它几乎只是一个";头";实现,重载使用amacro返回(加上其他一些)。这并不理想,可能会出现一些性能问题,甚至系统提供的内联函数会导致编译失败。经验法则是,尽量晚一点包含<;stddefer.h>;头文件,这样就可以尽可能少地进行交互。
2.1*=DEFER=:确保在受保护块的末尾执行延迟语句。
延迟语句本身不能包含受保护的块或其他DEFER子句,并且不能调用除死机之外可能导致执行终止的函数。此外,只有在使用RECOVER测试了当前死机状态之后,才能调用死机。
延迟语句可以使用任何可见的局部变量,该局部变量在放置延迟的位置是可见的,并且在执行延迟语句时仍然有效,即在周围的卫士或函数主体的末尾。此属性在编译时是可检查的,违反该属性通常会导致编译中止。这里的简化实现将一切就绪,因此延迟语句使用所有变量的最后一次写入的值,但成功与否取决于是否存在某些同步特性。
如果这样的同步功能不可用,则必须将在DEFER本身和DEFERED语句执行之间可能发生变化的本地变量以及在DEFERED语句中使用的本地变量声明为易失性变量,以便将最新的值考虑在内。因此,根据经验,您在延迟语句中使用的变量应该是限定的:const限定用于DEFER子句应该使用原始值的那些变量,而Volatile限定用于那些应该与其最新更改一起使用的变量。
该实现使用分配(As Of Calloc)和释放(Free)来维护延期子句的列表。如果calloc失败,将执行DEFER子句,然后通过死机和错误参数-DEFER_ENOMEM展开整个执行过程。
参数列表必须为空,或者包含变量列表。其效果是对这些变量求值,并将值存储在一个秘密位置。当最终执行延迟语句时,名称和类型相同(但常量限定)的无地址局部变量放在延迟语句之前,并使用这些冻结值进行初始化。
如果这样的块正常终止,或者使用BREAK或CONTENTATION终止,则所有已注册为DEFER语句的延迟语句将以与其注册顺序相反的顺序执行。还有一个宏DEFER_BREAK,可用于BREAK或CONTINUE语句引用内部循环或SWITCH语句的上下文中。此外,RETURN、EXIT、QUICK_EXIT和THRD_EXIT都会触发延迟语句的执行,直到它们各自的受保护块嵌套级别。
其他非线性控制流出或流入块的标准方式(goto、long_mp、_exit、about)不会调用该机制,并且当在这种受保护的块中使用时可能会导致内存泄漏或其他损坏。
对于其中一些构造,可以使用DEFER_GOTO、DEFER_ABORT等替代方法。
2.3*=死机=:展开整个调用堆栈,并在下行过程中执行延迟语句。
在当前线程的所有延迟语句以遇到DEFER语句的反向顺序执行之后,在具有DEFER的最后一个堆栈帧中。
.