Эмуляция обработки исключений в Ansi C'89

Тема в разделе "Общие вопросы по С и С++", создана пользователем sergey_kovtunenko, 26 авг 2007.

Статус темы:
Закрыта.
  1. sergey_kovtunenko

    sergey_kovtunenko Гость

    Привет! Пришла идея написать эмуляцию обработки исключительных ситуаций для ANSI C. Это должна быть высокопортируемая библиотека макроопределений, использующая достоинства (и недостатки нелокальных переходов в С -- setjmp()\longjmp()). Насколько мне известно, готовых решений на pure ANSI C на данный момент нет. По крайней мере гугление ни к чему не привело.
    При написании библиотеки столкнулся с рядом проблем, которые мне трудно решить. Поэтому публикую исходный код тестовой версии библиотеки. Я использую ANSI C'99 совместимый компилятор пока (в конце вычищу код для C'89), поэтому по ходу могут встречаться комментарии в стиле С++.
    В тестовой версии библиотеки все глобальные переменные прямо лежат внутри заголовочного файла. Это сделано для упрощения отладки. Т.е. грубо говоря, эта версия всего лишь модель. После получения работоспособной модели все глобальные переменные будут локализованы в исходном файле библиотеки, будут статическими и доступ к ним будет осуществляться только через функции.

    Цель создания библиотеки:​

    * Получить преимущества обработки исключений в языке С. О различиях между SEH и обычным подходом проверки возвращаемых значений функцией можно почитать у Б. Страуструпа в 14 главе русского "Язык программирования С++"
    * Получить высокопортируемую библиотеку, которую можно будет использовать на любой платформе, где есть стандартная библиотека С. Классно иметь средство, которое будет компилироваться на любом ANSI C совместимом компиляторе!

    Из недостатков можно выделить:​

    * _Синтаксическую_ (но не идеологическую) несовместимость с С++ ;(
    * Незащищённость от многопоточночти из-за применения статических переменных. В планах, после завершения варианта для однопоточных переменных приступить к реализации версии, корректно работающей с многопоточными приложениями

    Общая Концепция библиотеки состоит в следующем:
    Код (Text):
    //..............
    TRY {
    // тестируемый код1
    TRY {
    // вложенный тестируемый код2
    THROW (100);
    // продолжение вложенного кода, который не выполнится
    }
    CATCH (200) {
    // обработчик 1
    }
    CATCH (100) {
    // обработчик 2, в который мы попадём
    THROW (10);
    // код, который никогда не выполнится
    }
    CATCHALL {
    // код, обрабатывающий все остальные ситуации
    }
    TRYEND; // конец вложенного обработчика
    }
    CATCHALL {
    // обработка всех исключительных ситуаций. Сюда мы попадём по строчке THROW (10);
    }
    TRYEND;
    // ................

    Код (Text):
    /* excpt.h -- тестовая версия заголовочного файла */

    #ifndef N0PEXCEPT_H_
    #   define N0PEXCEPT_H_

    #   ifdef __cplusplus
    extern "C" {
    #   endif

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

    /* Включение или выключение кода обработки исключений. Для выключения
    необходимо закоментировать строчку */
    #   define N0PEXCEPT_ON_

    /* Максимальный уровень вложений обработчика исключений */
    #   define MAX_NESTED_LEVEL_ 40

    //------------------------------------------------------------------------------

    /* Состояние текущего прерывания */
    typedef struct EXCPT_T EXCPT_T;
    struct EXCPT_T {
    /* Буффер для сохранения состояния */
    jmp_buf jbuf;
    /* Код возврата из setjmp() */
    int ret;
    }; /* EXCPT_T */

    /* Текущий глобальный уровень вложенности обработчиков исключений */
    int i = -1;

    /* Текущий локальный уровень вложенности обработчиков исключений */
    int j = -1;

    /* Массив структур состояний */
    EXCPT_T jbuf[MAX_NESTED_LEVEL_];

    //------------------------------------------------------------------------------




    #   ifdef N0PEXCEPT_ON_

    #define DBGTRYIN(func, num)         \
    printf("i=%d, j = %d >>> " #func ".TRY." #num " >>>\n", i, j)
    #define DBGTRYOUT(func, num)            \
    printf("i=%d, j = %d <<< " #func ".TRY." #num " <<<\n", i, j)
    #define DBGCATCHIN(func, num, val)  \
    printf("\ti=%d, j = %d >>> " #func ".CATCH(" #val ")." #num " >>>\n", i, j)
    #define DBGCATCHOUT(func, num, val) \
    printf("\ti=%d, j = %d <<< " #func ".CATCH(" #val ")." #num " <<<\n", i, j)
    #define DBGCATCHALLIN(func, num)        \
    printf("\ti=%d, j = %d >>> " #func ".CATCHALL." #num " >>>\n", i, j)
    #define DBGCATCHALLOUT(func, num)   \
    printf("\ti=%d, j = %d <<< " #func ".CATCHALL." #num " <<<\n", i, j)

    /* Попытка выполнить кусок кода */
    #       define TRY \
    do { \
    j = i; \
    j++; \
    jbuf[j].ret = setjmp(jbuf[j].jbuf); \
    if (0 == jbuf[j].ret) {

    #       define TRYEND \
    i = j; \
    } \
    } while (0)

    /* Перехват исключительной ситуации */
    #       define CATCH(x) \
    i = j; \
    } else if ((x) == jbuf[j].ret) {

    #       define CATCHALL \
    i = j; \
    } else {

    /* Выброс исключительной ситуации */
    #       define THROW(x) \
    longjmp(jbuf[j].jbuf, (x))

    #   else
    #       define TRY
    #       define TRYEND
    #       define CATCH(x) if (0)
    #       define CATCHALL if (0)
    #       define THROW(x)
    #   endif /* N0PEXCEPT_ON_ */

    #   ifdef __cplusplus
    }
    #   endif
    #endif /* N0PEXCEPT_H_ */
    Код (Text):
    /* main.c -- тестирующий библиотеку файл */

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

    #include "excpt.h"



    void
    foo(void);

    void
    func1(void);



    void
    foo(void)
    {
    printf("foo()\n");
    //  THROW(1);
    printf("end foo()\n");
    }

    int
    main(void)
    {
    printf("start main() : i=%d, j=%d\n", i, j);
    /*  TRY {
    DBGTRYIN(main, 1);
    TRY {
    DBGTRYIN(main, 2);
    THROW (100);
    DBGTRYOUT(main, 2);
    }
    CATCH (100) {
    DBGCATCHIN(main, 2, 100);
    DBGCATCHOUT(main, 2, 100);
    }
    CATCHALL {
    DBGCATCHALLIN(main, 2);
    DBGCATCHALLOUT(main, 1);
    }
    TRYEND;
    //      THROW (10);
    DBGTRYOUT(main, 1);
    }
    CATCH (10) {
    DBGCATCHIN(main, 1, 10);
    //      THROW (5);
    DBGCATCHOUT(main, 1, 10);
    }
    CATCHALL {
    DBGCATCHALLIN(main, 1);
    DBGCATCHALLOUT(main, 1);
    }
    TRYEND; */
    func1();
    printf("end main() : i=%d, j=%d\n", i, j);
    return 0;
    }



    // Функция, в которой используются уже развёрнутые макроопределения
    void
    func1(void)
    {
    printf("start func1() : i=%d, j=%d\n", i, j);
    do {
    //      j = i; // Это логическая ошибка
    j++;
    jbuf[j].ret = setjmp(jbuf[j].jbuf);
    if (0 == jbuf[j].ret) {
    {
    DBGTRYIN(func1, 1);
    // вложенный TRY
    do {
    j = i;
    j++;
    jbuf[j].ret = setjmp(jbuf[j].jbuf);
    if (0 == jbuf[j].ret) {
    {
    DBGTRYIN(func1, 2);
    longjmp(jbuf[j].jbuf, 10);
    DBGTRYOUT(func1, 2);
    }
    //                      i = j; // если ты дошел до этой строчки, значит успешно (без THROW'ов, return'ов и raise()) выполнил TRY
    } else if (10 == jbuf[j].ret) {
    {
    DBGCATCHIN(func1, 2, 10);
    DBGCATCHOUT(func1, 2, 10);
    }
    //                      i = j;
    //                      j = i;
    } else {
    {
    DBGCATCHALLIN(func1, 2);
    DBGCATCHALLOUT(func1, 2);
    }
    //                      i = j;
    //                      j = i;
    }
    } while (0);
    // конец вложенному TRY
    longjmp(jbuf[j].jbuf, 100);
    DBGTRYOUT(func1, 1);
    }
    i = j;
    } else if (100 == jbuf[j].ret) {
    {
    DBGCATCHIN(func1, 1, 100);
    DBGCATCHOUT(func1, 1, 100);
    }
    //          i = j;
    //          j = i;
    } else {
    {
    DBGCATCHALLIN(func1, 1);
    DBGCATCHALLOUT(func1, 1);
    }
    //          i = j;
    //          j = i;
    }
    } while (0);
    printf("end func1() : i=%d, j=%d\n", i, j);
    }

    Для тех, кто дочитал до этого момента и не закрыл окно браузера, пояснение алгоритма и подводные камни.
    Пояснение и различия с С++ подходом:
    • Блоки обработчиков могут быть вложенными один в один. Аналогично с С++
    • Если THROW(n) никто не обрабатывает, то программа вылетает с ошибкой. В либе реализуется за счёт передачи неверного значения в longjmp(), что крэшит программу на большинстве платформ. Полная аналогия с С++, за исключением невозможности (пока) зарегистрировать свой обработчик подобной ситуации.
    • Если внутри блока выбрасывается THROW() исключение, то оно передаётся внешнему обработчику. Почти полная аналогия с С++, кроме того, что в либе передаётся аргумент, а в С++ тот же самый аргумент передаётся во внешний блок.
    • Либа должна корректно обрабатывать ситуации, когда внутри блоков TRY\CATCH()\CATCHALL встречаются локальные переходы: goto\return()\raise(). И это одна из самых больших проблем при реализации алгоритма ;(((

    В либе реализован массив значений {буффер_сохранения_состояния, код возвращаемого значения} размером в максимальную глубину вложенности. При заходе в новый блок TRY индекс массива увеличивается и запоминается новое состояние. При выходе из блока TRY индекс массива декрементируется.

    Неинформативные глобальные имена индексов i, j выбраны чисто для наглядности и удобства набора. :)

    Переменная j была нужна для защиты от локальных переходов и возбуждения сигналов goto\return()\raise(). Она работала так:

    Прошу помощи в реализации манипуляций с индексами массива и, возможно, логикой работы. Цель: сделать библиотеку "пуленепробиваемой" с некоторыми ограничениями.
     
  2. North

    North Гость

    Ввы видать гозу. А здесь таких мало.
    Это, как я понял, есть попытка эмуляции/моделирования стека, который должен раскручиваться при выбрасывании исключения. А записи в этом массиве вроде node`ов которые помещает в реальный стек настоящий SEH.

    Я так понял что, пользователь может написать return при обработке исключения, а библиотеке необходимо выполнить некие действия для поддержиния своего корректного состояния (декремент стека и тп.) ? И очень не хочется вводить в библиотеку особый макрос типа RETURN() для обработки подобных ситуаций ?

    Эта, проект коммерческий или где ?
     
  3. sergey_kovtunenko

    sergey_kovtunenko Гость

    По сути так, идеологически это стек. Физически -- обычный массив вложенности, где индекс массива играет роль стек_поинтера. Насколько я понимаю, SEH чаще всего понимают как майкрософтовскую поблажку
    Код (Text):
    __try
    {
    // guarded code
    }
    __except(expression)
    {
    // exception handler
    }
    Если подходить с этой позиции, то моя либа должна быть платформенно независимой, а значит никаких SEH, никаких ассемблерных вставок, никаких левых инклудов...

    Да, в блоке TRY юзер может сделать либо goto за пределы блока, либо return() сразу из функции, содержащей TRY {...} CATCHALL {...} ENDTRY; и при таком раскладе теряется вся последовательность вызовов. Думаю, если это невозможно будет обойти, то проще в мануале написать "не делайте return() из блока TRY, чем вводить макрос RETURN()

    Либа абсолютно некоммерческая с открытым исходным кодом :( Такие либы будут очень полезны программистам для встроенных систем ;) Ведь у них нет С++ ;)
     
  4. North

    North Гость

    Код (Text):
    //-----------------------------------------------------------------------------------
    #ifndef EXCEPTIONS_H
    #define EXCEPTIONS_H
    //-----------------------------------------------------------------------------------
    //-----------------------------------------------------------------------------------
    #include <stdio.h>
    #include <setjmp.h>

    #define MAX_NESTED_LEVEL 10 /* Максимальный уровень вложенности */

    /* Состояние текущего прерывания */
    typedef struct EXCPT_T EXCPT_T;
    struct EXCPT_T
    {
    jmp_buf jbuf;   /* Буффер для сохранения состояния */
    int id;         /* Код возврата из setjmp() он же идентификатор исключения */
    };

    static int eup = -1;                    /* индекс верхнего элемента */
    static EXCPT_T estack[MAX_NESTED_LEVEL];/* exception state stack */

    #       define TRY \
    do { \
    ++eup; \
    estack[eup].id = setjmp(estack[eup].jbuf); \
    if (0 == estack[eup].id) {\
    printf("open try-block %i\n", eup);

    /* Выброс исключительной ситуации */
    #       define THROW(x) \
    longjmp(estack[eup].jbuf, (x))

    /* Перехват исключительной ситуации */
    #       define CATCH(x) \
    } else if ((x) == estack[eup].id) {\
    printf("catch %i stack %i\n", estack[eup].id, eup); \
    estack[eup].id = 0; /* говорим что обработали исключение */

    #       define CATCHALL \
    } else { \
    printf("catch all %i stack %i\n", estack[eup].id, eup); \
    estack[eup].id = 0; /* говорим что обработали исключение */

    /* Проверка перехвата исключения и обработка ошибок*/
    #       define TRYEND \
    }\
    if ((eup <= 0) && (0 != estack[eup].id)) \
    terminate(estack[eup].id); \
    else {\
    printf("leave try-block %i\n", eup); \
    --eup;  \
    }\
    } while(false)

    //-----------------------------------------------------------------------------------
    /* Проверяет наличие не обработанного исключения PL C++ 14.5 (стр. 427)*/
    static bool uncaught_exception(){ return eup >=0 ? bool(estack[eup].id) : false; }

    /*  Выполняется при возникновении неперехваченного исключения
    code - код завершения приложения
    */
    void default_terminate(int code)
    {
    printf("uncatched exception ID: %i\n", code);
    /*печать стека*/
    for(int i=0; i<MAX_NESTED_LEVEL; i++)
    {
    if( estack[i].id )
    {
    printf("state[%i]: exception ID: %i\n", i, estack[i].id);
    }
    else
    {
    printf("state[%i]: exception ID: NO\n", i);
    }
    }
    /*exit(code);*/
    }

    typedef void (*terminate_func)(int);
    static terminate_func terminate = default_terminate;

    static void set_new_terminate(terminate_func func){ terminate = func; }
    //-----------------------------------------------------------------------------------
    #endif//EXCEPTIONS_H
    //-----------------------------------------------------------------------------------




    #include "exceptions.h"

    int main(void)
    {
    TRY {
    //   DBGTRYIN(main, 1);

    //  THROW (100);
    TRY {
    //       DBGTRYIN(main, 2);
    THROW (100);
    //       DBGTRYOUT(main, 2);
    }
    CATCH (100) {

    //        DBGCATCHIN(main, 2, 100);
    //        DBGCATCHOUT(main, 2, 100);
    }
    CATCHALL {
    }
    TRYEND;
    THROW (200);
    //   DBGTRYOUT(main, 1);
    }
    CATCH (10) {
    //    DBGCATCHIN(main, 1, 10);
    //      THROW (5);
    //    DBGCATCHOUT(main, 1, 10);
    }
    TRYEND;
    //  func1();
    //  printf("end main() : i=%d, j=%d\n", i, j);

    getch();
    return 0;
    }
    Мой вариант. Прикручена парочка стандартных функций. Обработка goto, return действительно является проблемой. Есть вариант с макросами
    #define return
    ...
    #undef return
    Но вроде как препроцессор вложенные макросы не понимает ;) . Надо думать. В частности о возможности выразить это в терминах ANSI C.
     
Загрузка...
Статус темы:
Закрыта.

Поделиться этой страницей