不管是哪门编程语言,在coding过程中,我们都会遇到一个很头痛的问题,就是如何处理函数的返回值和异常。函数返回值传统的处理的方法是使用If/else。但对于有返回码的函数,需要写“无数”的if分支,代码很难看,也不好维护。异常处理更是头痛,一旦发生,只有查看resetlog,对于网络类异常,或者库中出现的异常,调用者也不知道如何处理。高级语言中,都有try/catch/finally的语法,用于异常处理。但是在C语言中,没有相应的支持。通过抛出异常,并让能处理该异常的上层函数处理,是比较符合自然逻辑的方法。如果能够在C语言中支持,就非常好了。下面的文章,介绍了如何实现一个自定义的c异常处理支持的过程。值得参考。该文章几个要点值得注意:
1. 目前针对C的异常处理机制,已经有开源(比如GitHub)实现,如果项目中有需要,可以下载并根据项目定制。
2. C/C++都借用的setjump/longjump实现异常处理。可能不准确,因为C++中还有很多runtime支持。C++的异常处理具体实现方法还有语言级别的支持。后续会进一步学习。
3. 异常处理时,需要跳转到异常处理函数。当前栈内的内容如何处理,需要视情况而定。C++中,由于支持析构,所以栈中的内容要逐步清理。特别是变量。针对函数,要看需求,是否需要逐级返回,还是直接跳转到顶层。以下文章,由于不再需要栈中内容,所以直接出栈,抛弃了栈中的内容。针对Java,由于需要打印调用栈,估计还需要拷贝栈中的内容。
4. setJump可以保存函数调用前上下文是机制的关键,因为只有这样,当调用longjump时,可以返回到调用前的状态,并根据setjump的不同返回值,调用异常处理函数。
以下内容转载自:http://www.on-time.com/ddj0011.htm,避免国内用户无法查看,故复制如下。如果原作者有版权问题,请联系我删除。
by Tom Schotland and Peter Petersen
Error handling is an important issue in embedded systems, and it can account for a substantial portion of a project's code. We were faced with this issue during the design of RTFiles, the embedded filesystem component of On Time RTOS-32, our Win32-compatible RTOS for 32-bit x86 targets. The core filesystem is portable with a C function API to the application and a device-driver interface below it. Typically, errors can occur in device drivers and must be reported to the application with suitable return codes, so errors must travel through the complete core filesystem.
The classic C approach to this problem is return codes. Each function returns a value indicating success or failure. However, with a nontrivial function call hierarchy, this approach clutters the code significantly. Every function must check the return code of every function call it makes and take care of errors. In most cases, the function will merely pass any errors back up to its caller. RTFiles has several hundred internal functions and a call hierarchy up to about 15 levels deep, so this approach would have been a nightmare to maintain.
Programming languages such as Ada or C++ address this issue with exceptions. Exceptions make it easy to separate error handling from the rest of the code. Intermediate functions can completely ignore errors occurring in functions they call, if they can't handle them anyway. Exceptions are much easier to maintain than error return codes, so we definitely wanted to use them for RTFiles. Unfortunately, we had to write RTFiles in C, and not C++ or Ada, for portability. RTFiles must support compilers without C++ support.
Another issue is overhead and reliability. C++ exception handling needs a lot of run-time system support routines, which might add too much code to a small embedded system. C++ exceptions are objects dynamically allocated from the heap, but many embedded systems do not want to use any dynamic memory allocation to avoid heap fragmentation and out-of-heap-space problems. For example, what would happen if an RTFiles device driver throws a disk-write-protection exception, and the heap allocation called by throw throws an out-of-memory exception?
The solution to the problem is to implement a simple exception-handling library in C with the following goals:
In this article, we describe how we designed and implemented our exception-handling library, demonstrate how it is used, and compare it to C++ exception handling. The complete source code is available through the DDJ Resource Center accessible to registered users of www.ddj.com.
jmp_buf jumper; int SomeFunction(int a, int b) { if (b == 0) // can't divide by 0 longjmp(jumper, -3); return a / b; } void main(void) { if (setjmp(jumper) == 0) { int Result = SomeFunction(7, 0); // continue working with Result } else printf("an error occured\n"); }
Example 1: A simple error-handling approach based on setjmp() and longjmp().
The basic function of exception handling is to transfer control to an exception-handler when an error occurs, where the handler resides somewhere higher up in the current function call hierarchy. Standard C has a mechanism to accomplish this: setjmp() and longjmp(). Example 1 shows a simple implementation of error handling based on setjmp()/longjmp(). However, this example is a little too simple. It relies on a single global variable called "jumper," which contains the information where the exception handler is. However, we need many different exception handlers in different functions, so how will SomeFunction() know which jumper to use? Another issue is multitasking. Each task will have its own call hierarchy and thus needs its own jumper or set of jumpers.
What we really need is a dynamically linked list of exception handler records. Such a record will contain a jmp_buf structure and supplemental information (for example, whether an exception has been raised, has it been handled, what is its error code, and so on). Each function that defines an exception-handler adds such a record to the list and removes it from the list when it returns. The exception-handler records are allocated in the function's stack frame since we do not want to use the heap.
To make the whole thing reentrant, a separate list root is maintained per task. Multitasking operating systems will usually provide some mechanism to maintain per-task values. For example, Win32 has Task Local Storage (TLS), and RTKernel-32, the real-time kernel component of On Time RTOS-32, has both Win32 TLS and its own Task User Data. TLS allows an application to allocate a pointer value for every task (even for those that have not been created yet). At run time, this value can be set and retrieved by the current task. Our exception handling library uses TLS to store the exception-handler list root pointers. If used in a single-task environment, the list root can simply be implemented as a single global variable.
#define DIVIDE_BY_ZERO -3 int SomeFunction(int a, int b) { if (b == 0) // can't divide by 0 XRaise(DIVIDE_BY_ZERO); return a / b; } void main(void) { XRecord XData; XLinkExceptionRecord(&XData); switch (setjmp(XData.Context)) { case 0: // this is the code block { int Result = SomeFunction(7, 0); // continue working with Result } break; case DIVIDE_BY_ZERO: printf("a division by zero occurred\n"); break; default: printf("some other error occurred\n"); break; case XFINALLY: printf("cleaning up\n"); } XUnLinkExceptionRecord(&XData); }
Example 2: setjmp()/longjmp()-based error handling with exception records allocated on the stack.
void main(void) { XTRY case XCODE: // this is the code block { int Result = SomeFunction(7, 0); // continue working with Result } break; case DIVIDE_BY_ZERO: // handler for a specific exception printf("a division by zero occurred\n"); break; default: // default handler printf("some other error occurred\n"); break; case XFINALLY: // finally handler printf("cleaning up\n"); XEND }
Example 3: This program is functionally identical to Example 2, but the details of setting up the exception handler block have been moved to macros XTRY and XEND.
Example 2 shows an improved version using exception handler records on the stack. XLinkExceptionRecord() puts the given exception-handler record onto the current task's exception-handler list and initializes it. XUnLinkExceptionRecord() removes the exception-handler record from the list. XRaise() throws an exception, which is a negative error code in RTFiles. XRaise() retrieves the top-level exception-handler record of the current task and then calls longjmp() with the given error code. This transfers control to the correct handler.
To simplify the syntax, the exception-handler library's header file defines a few macros to encapsulate building and destroying an exception-handler block. Example 3 shows the same program using these macros.
One big advantage of a home-grown exception handling library is that we can define the semantics to best fit the needs of our application. For example, the most common case in RTFiles is that a function cannot handle an error, but some cleanup action is required. Typically, some global data structures of the filesystem are protected on entry to the filesystem using semaphores. We must ensure that such semaphores are released no matter how the function is left (through a normal return statement or through an exception). We thus reserved a special exception code (-1, defined as XFINALLY), which shall always be raised exactly once when an XTRY block is left. Such a finally-handler is not supported by C++ exception handling.
Another difference from C++ exceptions is that executing an exception-handler does not automatically complete handling of the exception. If the handler does not explicitly call function XHandled(), the exception-handling library will continue to pass the exception to handlers higher up in the list (called "exception propagation" or "stack unwinding"). We decided on these semantics because they reflect the typical case in RTFiles. However, in other applications, this could be handled differently, such as by using C++ semantics where an exception is considered handled once an exception handler has been invoked.
The RTFiles API consists of C functions that all return integer values. Positive values indicate success and negative values indicate errors. For example, the function to open a file, RTFOpen(), will either return a valid, positive file handle or an error code such as RTF_FILE_NOT_FOUND, which is defined as -9. To keep things simple, we use the standard RTFiles error codes as exception values. This lets us automate returning correct error codes to the application. Because all RTFiles API functions contain a top level XTRY block, when leaving the block, we can simply return the current exception value. Basically, we propagate exceptions out of RTFiles by simply returning the exception value. This is implemented by the macro XEND, which returns the error code if it finds that the last exception-handler record has been removed and that a still unhandled exception is being propagated.
To implement these semantics, the exception-handling library must know the current state of processing, stored in the current top-level exception-handling record. Three states are distinguished:
When an XTRY block is entered, the initial state is set to XCode by function XLinkExceptionRecord(). If the code section completes without raising any exceptions, XUnLinkExceptionRecord() is called next, which will then set the state to XFinally and execute the finally-handler. If, however, an exception is raised, XRaise() sets the state to XHandling and calls the appropriate handler. If the handler does not call XHandled() to indicate that the exception is now handled, XUnLinkExceptionRecord() executes the finally-handler and moves on to the next handler in the handler chain. This means that any code following the XTRY block will never get executed, and the current execution frame is abandoned.
Of course, this process has to stop somehow. If an exception-handler can handle the error, it will call XHandled() and normal execution will continue after the current XTRY block. If the outermost exception-handler has not handled the exception, we just pass the exception code (which is an RTFiles error code) back to the application. However, most applications will propably prefer to abort the program with a fatal error (just like C++, for example).
As long as the OS-dependent functions for TLS data are defined (in our example, we just use a single global variable), the exception library can be compiled with any ANSI C compiler. However, our goal to become independent of the C++ run-time systems has not been reached yet. Looking at the source code of typical implementations of longjmp() reveals that longjmp() references a lot of the C++ exception-handling support routines. This makes sense because C++ must ensure that all objects with a local scope are destroyed when the respective scope is left. As longjmp() can leave a local scope, it must also call the destructors of all objects going out of scope. Because we only use C in RTFiles, this functionality is not required, and we do not want longjmp() to pull in so much code we would never need. Thus, we implemented our own versions of setjmp() and longjmp(); see Listing One. The two functions, XSaveContext() and XRestoreContext(), only have to save/restore the stack frame registers, the return address, and any registers the compiler might use for register variables. By defining symbol XWIN32, our alternate functions are used by the exception-handling library instead of setjmp()/longjmp().
XTRY case XCODE // code body break; [case ERROR_1: // handler for ERROR_1 break; // more handlers go here ...] default: // handle all other errors break; case XFINALLY: // finally handler XEND or XENDX
Example 4: The basic structure of an XTRY block.
XTRY blocks can be nested to any depth, either within a single function or across function calls. The XTRY block has a code body, any number of exception-handlers to handle specific exceptions, a single default handler to catch all other exceptions, and a finally-handler. Example 4 shows the basic structure of of an XTRY block.
The XTRY block is closed with macro XENDX or XEND. XENDX is used in functions that cannot return the error code as a return value. This is frequently the case for functions internal to RTFiles, declared as static void. If XENDX finds no outer exception-handler, it reports a fatal error. Basically, this means that a function containing an XENDX block may only be called while an XTRY block already resides on the call stack. This is the case in RTFiles, since all API functions (the only entry points to RTFiles) have XTRY blocks.
Within an XTRY block, a few exception-management functions are available. XRaise(int XValue) raises an exception with the given error code. XValue must be a value less than or equal -2, because positive values are not considered errors and -1 is reserved for the finally-handlers. XHandled() stops propagating an exception. It may only be called from an exception-handler. XReExecute() can be called by an exception-handler to execute the code body of the current XTRY block again. For example, this can be used for retries. Finally, macro XVALUE returns the value of the exception currently being processed.
There are also a few restrictions that must be observed. Due to the implementation of setjmp() and longjmp(), all local variables used within more than one part of an XTRY block (code body, exception handler, finally handler, or outside the XTRY block) must be declared as volatile. This is required by the C ANSI standard, which is explained in more detail in "Register Variables and longjmp()". Additionally, this C exception-handling library should not be mixed with C++ code within the same source file, because it could cause destructors not to be called.
Although C++ was never an option for RTFiles, we do want to check that our goals have been met. In particular, our C exception-handling library should have little run-time overhead, and it should need less code space than C++ exceptions. Listings Two and Three show a C and a C++ version of a benchmark program, respectively. One thing difficult to compare is the finally-handler, which is not supported by C++. In the benchmark, the finally-handler should merely increment an integer. In the C++ version, this statement has been placed in the destructor of a local class object. C++ guarantees that such a destructor is called when its object goes out of scope, regardless of the method to leave the scope.
Program | Code Size | Time (no throws) | Time (with throws) |
---|---|---|---|
XBench.c | 4.6k | 1392 ms | 1362 ms |
CPPBench.cpp | 35.3k | 1492 ms | 71343 ms |
Table 1: Code sizes and benchmark results for C and C++ exception-handling compiled with Borland C++ Builder 4.0, run under Windows NT.
Table 1 lists the execution times and code sizes of both programs. Execution times are given for the case where no exceptions are thrown and for one throw per iteration. The C program has been linked with a stripped-down run-time system with C++ exception-handling support removed while the C++ version is linked with an unmodified run-time system. The tests were performed with Borland C++ Builder 4.0 for Win32 under Windows NT. While there is only a small difference in execution times when no exceptions occur, the C exception library easily outperforms C++ in all other disciplines.
This simple exception-handling library has been a great help in implementing RTFiles. The mechanism is easy to use, portable, uses no dynamic memory allocation, and is efficient. When used correctly (that is, when you do not call XRaise() while no XTRY block is present on the call stack), it cannot fail. In particular for embedded systems, where low resources preclude the use of C++, using the C exception-handling library can radically reduce the complexity of error handling.
Care must be taken with local variables in functions using setjmp(). In Example 5, for instance, you would expect the following program output:
1: 1 2: 2 3: 3
However, with most optimizing compilers, it will be:
1: 1 2: 2 3: 2
void main(void) { jmp_buf jumper; int LocalVar = 1; printf("1: %i\n", LocalVar); if (setjmp(jumper) == 0) { LocalVar++; printf("2: %i\n", LocalVar); longjmp(jumper, 1); } LocalVar++; printf("3: %i\n", LocalVar); }
Example 5: Sample Program.
In particular, the incorrect result will be produced whenever the compiler chooses to allocate LocalVar as a register variable. setjmp() will save all registers used for register variables in the given jmp_buf. Even though the register variable has been incremented after the call to setjmp(), longjmp() will restore the value it had at the time of setjmp(). The only way to prevent this problem is to declare such variables as volatile. This is even required by the ANSI C standard in section 7.6.2.1.
Since the C exception-handling library uses setjmp() in its XTRY macro and calls longjmp() in macro XEND and XRaise(), all parameters and local variables of functions with XTRY blocks must be declared as volatile if their values must be preserved across the XTRY and XEND macros. This is even the case when alternate functions for context saving/restoring are used instead of setjmp()/longjmp(), since they also can only restore register variable values in effect when the context was saved.
Listing One:
; RTFEX32.ASM ; Copyright (c) 1998,99 On Time Informatik ; http://www.on-time.com/ ; Custom context save/restore functions for C Exception Handling Library ; This is what we want to implement: ; typedef struct { ; unsigned long esi, edi, ebx, ret, ebp, esp; ; } XContext; ; int __stdcall XSaveContext(XContext * C); ; void __stdcall XRestoreContext(XContext * C, int Value); .386 XContext STRUC _esi DD ? _edi DD ? _ebx DD ? _ret DD ? _ebp DD ? _esp DD ? XContext ENDS _TEXT SEGMENT DWORD USE32 PUBLIC 'CODE' ASSUME CS:_TEXT PUBLIC XSaveContext PUBLIC _XSaveContext@4 PUBLIC XRestoreContext PUBLIC _XRestoreContext@8 XSaveContext proc near _XSaveContext@4 label near pop ecx ; ret address pop edx ; parameter C mov [edx+_esi], esi ; save all required registers mov [edx+_edi], edi mov [edx+_ebx], ebx mov [edx+_ret], ecx mov [edx+_ebp], ebp mov [edx+_esp], esp xor eax, eax ; return code is zero jmp ecx ; and return XSaveContext endp XRestoreContext proc near _XRestoreContext@8 label near mov edx, [esp+4] ; parameter C mov eax, [esp+8] ; parameter Value, set as return value mov esi, [edx+_esi] ; restore all required registers mov edi, [edx+_edi] mov ebx, [edx+_ebx] mov ebp, [edx+_ebp] mov esp, [edx+_esp] jmp [edx+_ret] ; and jump to saved context XRestoreContext endp _TEXT ENDS END
Listing Two:
// XBENCH.C // Copyright (c) 1998,99 On Time // http://www.on-time.com/ // Benchmark for the C Exception Handling Library #include#include #include #include #define DIVIDE_BY_ZERO -3 // an error code // some global variables to count // iterations, exceptions, and cleanups int Calls, Exceptions, Finallys; /////////////////////////////////////// int SomeFunction(int a, int b) // functiom which can raise an exception { if (b == 0) XRaise(DIVIDE_BY_ZERO); return a / b; } /////////////////////////////////////// int TestFunction(int a, int b) // test function containing an XTRY block { int volatile Result; Calls++; XTRY case XCODE: Result = SomeFunction(a, b); break; case DIVIDE_BY_ZERO: Exceptions++; XHandled(); break; default: printf("unknown exception!\n"); break; case XFINALLY: Finallys++; break; XENDX return Result; } /////////////////////////////////////// void Bench(int a, int b, int Iterations) // benchmark function to call TestFunction in a loop // and print timing and statistics { DWORD T0, T1; int i; Calls = Exceptions = Finallys = 0; T0 = GetTickCount(); for (i=0; i
Listing Three:
// CPPBENCH.CPP // Copyright (c) 1998,99 On Time // http://www.on-time.com/ // Benchmark for C++ Exception Handling #include#include #include #define DIVIDE_BY_ZERO -3 // an error code // some global variables to count // iterations, exceptions, and cleanups int Calls, Exceptions, Finallys; /////////////////////////////////////// int SomeFunction(int a, int b) // functiom which can raise an exception { if (b == 0) throw DIVIDE_BY_ZERO; return a / b; } /////////////////////////////////////// int TestFunction(int a, int b) // test function containing try block { int Result; class Finally { public: Finally() { Calls++; } ~Finally() { Finallys++; } } FinallyHandler; try { Result = SomeFunction(a, b); } catch (int& ErrorCode) { switch (ErrorCode) { case DIVIDE_BY_ZERO: Exceptions++; break; default: printf("unknown exception!\n"); throw; } } catch (...) { printf("non integer exception!\n"); throw; } return Result; } /////////////////////////////////////// void Bench(int a, int b, int Iterations) // benchmark function to call TestFunction in a loop // and print timing and statistics { DWORD T0, T1; int i; Calls = Exceptions = Finallys = 0; T0 = GetTickCount(); for (i=0; i