C語言異常與斷言介面的實現

codingwu的部落格發表於2014-09-25

程式中通常會出現三種錯誤:使用者錯誤、執行期錯誤以及異常

標準庫函式setjmp和longjmp

在C語言中,標準庫函式setjmp和longjmp形成了結構化異常工具的基礎。簡單的說就是setjmp例項化處理程式,而longjmp產生異常

setjmp和longjmp是C語言所獨有的,它們部分彌補了C語言有限的轉移能力。與刺激的abort()和exit()相比,goto語句看起來是處理異常的更可行方案。不幸的是,goto是本地的:它只能跳到所在函式內部的標號上,而不能將控制權轉移到所在程式的任意地點(當然,除非你的所有程式碼都在main體中)。

為了解決這個限制,C函式庫提供了setjmp()和longjmp()函式,它們分別承擔非區域性標號和goto作用。標頭檔案<setjmp.h>申明瞭這些函式及同時所需的jmp_buf資料型別。

函式說明:

int setjmp(jmp_buf env) 建立本地的jmp_buf緩衝區並且初始化,用於將來跳轉回此處。這個子程式儲存程式的呼叫環境於env引數所指的緩衝區,env將被longjmp使用。如果是從setjmp直接呼叫返回,setjmp返回值為0。如果是從longjmp恢復的程式呼叫環境返回,setjmp返回非零值。
void longjmp(jmp_buf env, int value) 恢復env所指的緩衝區中的程式呼叫環境上下文,env所指緩衝區的內容是由setjmp子程式呼叫所儲存。value的值從longjmp傳遞給setjmplongjmp完成後,程式從對應的setjmp呼叫處繼續執行,如同setjmp呼叫剛剛完成。如果value傳遞給longjmp零值,setjmp的返回值為1;否則,setjmp的返回值為value

成員型別:

jmp_buf 陣列型別,例如:struct int[16]struct __jmp_buf_tag,用於儲存恢復呼叫環境所需的資訊。

jmp_buf 的定義:

typedef struct _jmp_buf
{
    int _jp[_JBLEN+1];
} jmp_buf[1];

這個是 setjmp.h 裡的一行定義,把一個 struct 定義成一個陣列。這樣,在宣告 jmp_buf 的時候,可以把資料分配到堆疊上。但是作為引數傳遞的時候則作為一個指標。

原理非常簡單:

1.setjmp(j)設定“jump”點,用正確的程式上下文填充jmp_buf物件j。這個上下文包括程式存放位置、棧和框架指標,其它重要的暫存器和記憶體資料。當初始化完jump的上下文,setjmp()返回0值。

2. 以後呼叫longjmp(j,r)的效果就是一個非區域性的goto或“長跳轉”到由j描述的上下文處(也就是到那原來設定j的setjmp()處)。當作為長跳轉的目標而被呼叫時,setjmp()返回r或1(如果r設為0的話)。(記住,setjmp()不能在這種情況時返回0。)

通過有兩類返回值,setjmp()讓你知道它正在被怎麼使用。當設定j時,setjmp()如你期望地執行;但當作為長跳轉的目標時,setjmp()就從外面“喚醒”它的上下文。你可以用longjmp()來終止異常,用setjmp()標記相應的異常處理程式。

本文地址:http://www.cnblogs.com/archimedes/p/c-exception-assert.html,轉載請註明源地址。

一個簡單的例子:

#include <stdio.h>
#include <setjmp.h>

static jmp_buf buf;
void second(void) {
    printf("second\n");         // 列印
    longjmp(buf,1);             // 跳回setjmp的呼叫處 - 使得setjmp返回值為1
}
void first(void) {
    second();
    printf("first\n");          // 不可能執行到此行
}
int main() {   
    if ( ! setjmp(buf) ) {
        first();                // 進入此行前,setjmp返回0
    } else {                    // 當longjmp跳轉回,setjmp返回1,因此進入此行
        printf("main\n");       // 列印
    }
    return 0;
}

執行結果:

second
main

在下例中,setjmp被用於包住一個例外處理,類似trylongjmp呼叫類似於throw語句,允許一個異常返回給setjmp一個異常值。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>

void first(void);
void second(void);
static jmp_buf exception_env;
static int exception_type;

int main(void) {
    void *volatile mem_buffer;

    mem_buffer = NULL;

    if (setjmp(exception_env)) {
        /* 如果執行到這將產生一個異常*/
        printf("first failed, exception type %d\n", exception_type);
    } else {
        printf("calling first\n");
        first();
        mem_buffer = malloc(300); /* 分配記憶體 */
        printf("%s",strcpy((char*)mem_buffer, "first succeeded!")); /* ... 不會被執行 */
    }
    if (mem_buffer)
        free((void*) mem_buffer); /* 小心釋放記憶體 */
    return 0;
}

void first(void) {
    jmp_buf my_env;

    printf("calling second\n");
    memcpy(my_env, exception_env, sizeof(jmp_buf));
    switch (setjmp(exception_env)) {
        case 3:
            /* 如果執行到這,表示有異常 */
            printf("second failed with type 3 exception; remapping to type 1.\n");
            exception_type = 1;

        default: 
            memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
            longjmp(exception_env, exception_type); /* continue handling the exception */

        case 0:
            /* normal, desired operation */
            second();
            printf("second succeeded\n");  /* not reached */
    }
    memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
}

void second(void) {
    printf("entering second\n" ); 
    exception_type = 3;
    longjmp(exception_env, exception_type); 
    printf("leaving second\n"); 
}

執行結果:

calling first
calling second
entering second
second failed with type 3 exception; remapping to type 1.
first failed, exception type 1

介面

Except介面在一系列巨集指令和函式中包裝了setjmp和longjmp,它們一起提供了一個結構化異常處理工具

異常是Except_T型別的一個全域性或靜態變數:

#ifndef EXCEPT_INCLUDED
#define EXCEPT_INCLUDED
#include <setjmp.h>
#define T Except_T
typedef struct T {
    char *reason;
} T;

Except_T結構只有一個欄位,它可以初始化為一個描述異常的字串,當發生一個未處理的異常時,才把字串列印出來

異常處理程式處理的是異常的地址。異常必須是全域性的或靜態的變數,因此它們的地址唯一地標誌了它們,異常e由巨集指令引發或由函式引發:

#define RAISE(e) Except_raise(&(e), __FILE__, __LINE__)
void Except_raise(const T *e, const char *file,int line);

處理程式是由TRY-EXCEPT和TRY-FINALLY語句來例項化的,這兩個語句用巨集指令實現,這兩個語句可以處理巢狀異常,也可以管理異常狀態的資料

TRY-EXCEPT的語法是:

TRY

S

EXCEPT(e1)

S1

EXCEPT(e2)

S2

……

EXCEPT(en)

Sn

ELSE

S0

END_TRY

看下面的程式碼:

int Allocation_handle = 0;
jmp_buf Allocate_Failed;

void *allocate(unsigned n)
{
    void *new = malloc(n);
    if(new)
        return new;
    if(Allocation_handle)
        longjmp(Allocate_Failed, 1);
    assert(0);
}

char *buf;
Allocation_handle = 1;
if(setjmp(Allocate_Failed)) {
    fprintf(stderr, "cound't allocate the  buff\n");
    exit(EXIT_FAILURE);
}
buf = allocate(4096);
Allocation_handle = 0;

上面的程式碼沒有提供巢狀的處理程式,Allocation_handle標誌的使用也很麻煩。

把Allocation_Failed變成一個異常,該異常是在malloc返回一個空指標時由allocate引發:

Except_T Allocate_Failed = {"Allocation failed"};
void *allocate(unsigned n)
{
    void *new = malloc(n);
    if(new)
        return new;
    RAISE(Allocation_Failed);
    assert(0);
}

如果客戶呼叫程式程式碼想處理這個異常,那麼它需要在TRY-EXCEPT語句內呼叫allocate:

extern Except_T Allocate_Failed;
char *buf;
TRY
    buf = allocate(4096);
EXCEPT(Allocate_Failed)
    fprintf(stderr, "could't allocate the buff\n");
    exit(EXIT_FAILURE);
END_TRY;

TRY-EXCEPT語句是用setjmp和longjmp來實現的

TRY-FINALLY語句的語法是:

TRY

S

FINALLY

S1

END_TRY

如果S沒有產生任何異常,那麼執行S1,然後繼續執行END_TRY,如果S產生了異常,那麼S的執行被中斷,控制立即轉給S1。S1執行完後,引起S1執行的異常重新產生,使得它可以由前一個例項化的處理程式來處理。注意:S1是在兩種情況中都必須執行的,處理程式可以用RERAISE巨集指令顯示地重新產生異常

#define RERAISE Except_raise(Except_frame.exception, \
    Except_frame.file, Except_frame.line)

介面中的最後一個巨集指令是:

#define RETURN switch (Except_stack = Except_stack->prev,0) default: return

RETURN巨集指令用在TRY語句的內部,用來代替return語句

實現

Except介面中的巨集指令和函式一起維護了一個記錄異常狀態以及例項化處理結構的堆疊。結構中的欄位env就是setjmp和longjmp使用的某個jmp_buf,這個堆疊可以處理巢狀的異常

typedef struct Except_Frame Except_Frame;
struct Except_Frame {
    Except_Frame *prev;
    jmp_buf env;
    const char *file;
    int line;
    const T *exception;
};
extern Except_Frame *Except_stack;

Except_stack指向異常棧頂的異常幀,每個幀的prev欄位指向它的前一幀,產生一個異常就是將異常的地址存在exception欄位中,並分別在file和line欄位中儲存異常的附屬資訊–異常產生的檔案以及行號

TRY從句將一個新的Except_Frame壓入異常棧,並呼叫setjmp,由RAISE和RERAISE呼叫Except_raise填充棧頂幀的欄位exception、file和line,從異常棧中彈出棧頂Exception_Frame,然後呼叫longjmp,EXCEPT從句檢查該幀中的exception欄位,決定應該用哪個處理程式。FINALLY從句執行清除程式碼,並重新產生已彈出的異常幀中儲存的異常。

巨集指令TRY、EXCEPT、ELSE、FINALLY_TRY一起將TRY-EXCEPT語句轉化成如下形式的語句:

do {

creat and push an Except_Frame

if(first return from setjmp) {

S

} else if (exception is e1) {

S1

……

} else if (exception is en) {

Sn

} else {

S0

}

if(an exception occurrend and wasn’t handled)

RERAISE;

} while(0)

Exception_Frame的空間分配很簡單,在由TRY開始的do-while主體中的複合語句內部宣告一個該型別的區域性變數即可:

#define TRY do { \
    volatile int Except_flag; \
    Except_Frame Except_frame; \
    Except_frame.prev = Except_stack; \
    Except_stack = &Except_frame;  \
    Except_flag = setjmp(Except_frame.env); \
    if (Except_flag == Except_entered) {

在TRY語句內有四種狀態,由下面的列舉識別符號給出

enum { Except_entered=0, Except_raised,
       Except_handled,   Except_finalized };

setjmp的第一個返回值將Except_flag設定為Except_entered,表示進入TRY語句,並且將某個異常幀壓入異常棧,Except_entered必須為0,因為setjmp首次呼叫的返回值為0,隨後,setjmp的返回值將被設為Except_raised,表示發生了異常,處理程式將Except_flag的值設成Except_handled,表示處理程式已經對異常進行了處理。

#define TRY do { \
    volatile int Except_flag; \
    Except_Frame Except_frame; \
    Except_frame.prev = Except_stack; \
    Except_stack = &Except_frame;  \
    Except_flag = setjmp(Except_frame.env); \
    if (Except_flag == Except_entered) {
#define EXCEPT(e) \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } else if (Except_frame.exception == &(e)) { \
        Except_flag = Except_handled;
#define ELSE \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } else { \
        Except_flag = Except_handled;
#define FINALLY \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } { \
        if (Except_flag == Except_entered) \
            Except_flag = Except_finalized;
#define END_TRY \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
        } if (Except_flag == Except_raised) RERAISE; \
} while (0)

最後實現程式碼如下:

#include <stdlib.h>
#include <stdio.h>
#include "assert.h"
#include "except.h"
#define T Except_T
#ifdef WIN32
__declspec(thread)
#endif
Except_Frame *Except_stack = NULL;
void Except_raise(const T *e, const char *file,
    int line) {
    Except_Frame *p = Except_stack;
    assert(e);
    if (p == NULL) {
        fprintf(stderr, "Uncaught exception");
        if (e->reason)
            fprintf(stderr, " %s", e->reason);
        else
            fprintf(stderr, " at 0x%p", e);
        if (file && line > 0)
            fprintf(stderr, " raised at %s:%d\n", file, line);
        fprintf(stderr, "aborting...\n");
        fflush(stderr);
        abort();
    }
    p->exception = e;
    p->file = file;
    p->line = line;
    Except_stack = Except_stack->prev;
    longjmp(p->env, Except_raised);
}

相關文章