Главное меню
Мы солидарны с Украиной. Узнайте здесь, как можно поддержать Украину.

Исключения в Си

Автор Алексей Гринь, сентября 19, 2009, 12:46

0 Пользователи и 1 гость просматривают эту тему.

Алексей Гринь

Когда я поносил С++, единственное, что я на тот момент признавал ущербным в Си, так это отсутствие исключений. Казалось бы, это что-то на уровне ассемблера, генерируемым С++-компилятором, и всё, что можно сделать на голом Си, так это только жалкую пародию. Оказывается, чтобы реализовать полноценную систему исключений на Си, нужно всего лишь 4-5 часов.

Коденг
#include "System.h"

void innerfunc(void)
{
    THROW(Exception_new_void());
}

int main(void)
{
   TRY
   {
       printf("Before.\n");

       innerfunc();

       printf("After.\n");
   }
   CATCH(e)
   {
       if(TYPEOF(e) == typeof_Exception())
       {
           printf("Inside catch block!\n");

           CATCHED;
       }
   }
   END_TRY;

   printf("After try block.\n");

   return 0;
}


Аутпут:
ЦитироватьBefore.
Inside catch block!
After try block.

Process returned 0 (0x0)   execution time : 0.015 s
Press any key to continue.

Как видно, строка printf("After.\n"); внезапно не исполнилась, потому что кинулось исключение, и ход программы перескочил, наплевав на стек, из innerfunc в catch-блок.
Реализация такова, что спокойно обрабатывается любое количество вложенных try'ев (тут я вру, потому что конкретно в моей реализации предел стоит в 32, ибо мне было влом делать растягиваемый контекстный стек (что за стек, опишу ниже), но обычно больше двух-трёх вложенных try'ев никогда и не надо). Также реализация может маппить SEH-исключения, например Access Violation, в свои собственные и, соответственно, их ловить и обрабатывать. Также это всё потоко-безопасно (какой я молодец).

Реализуется это просто:

1. Создаётся один глобальный TLS-индекс (Thread Local Storage). Теперь каждый новый тред создаёт и заносит в этот индекс объект стека (обычная пользовательская структура типа MyStack<jmp_buf>, ничего низкоуровнего), то бишь каждый тред имеет по собственному локальному стеку контекстов (чтобы отличать от стека выполнения программы, буду называть "контекстный стек"). Тип jmp_buf (который в <setjmp.h>) нужен нам дальше.

2. Далее, макрос TRY разворачивается в "if(!setjmp(dotorg_push_env()))"

   Всё, что делает функция dotorg_push_env, это извлекает из TLS записанный туда контекстный стек (который из п. 1), push'ит его, и возвращает ссылку на буфер типа jmp_buf (память под него должна быть выделена уже)
   Функция setjmp (которая в <setjmp.h>) принимает ссылку на возвращённый объект типа jmp_buf, и записывает в этот буфер данные текущего контекста исполнения: каково текущее смещение программного стека (настоящего, не пользовательского), ещё что-то, в зависимости от платформы. Эта функция обычно compiler-intrinsic и реализуется через пару ASM-опкодов (EDIT: в msvcrt.dll это реальный вызов функци...)

3. Макрос THROW(x) разворачивается в "longjmp(dotorg_raise_exc((void*)(x)), 1)"

   Функция dotorg_raise_exc, опять, извлекает из TLS контекстный стек (который из п. 1), и pop'ит jmp_buf. Этот jmp_buf есть самый последний (most recent) контекст-буфер, записанный в макросе TRY.
   Функция longjmp получает этот буфер, и здесь-то и происходит самое интересное — unwind'ится (разматывается) стек. Происходит прыжок с текущего места в место, записанное в jmp_buf, наплевав на ход выполнения программы, и какая функция чего вызывала. Эта же функция сохраняет в TLS ссылку на x (это объект исключения, он нам нужен будет потом)

4. Далее, макрос CATCH(x) разворачивается в "else { System_Object* x = (System_Object*)dotorg_get_exc();".
    System_Object — это мой тип, он тут может быть любым, заострять внимание не надо. Функция dotorg_get_exc извлекает объект исключения, записанный by dotorg_raise_exc.

5. Далее, если ошибка поймана, нужно систему об этом предупредить. Макрос CATCHED разворачивается в "dotorg_exit_catch()". Эта функция просто затирает ссылку на текущий объект исключения в ноль (в моей реализации подсчёта ссылок заодно вызывается RemoveRef, чтобы сразу удалить).

6. Теперь настало время макроса END_TRY. Он у меня разворачивается в "} dotorg_pop_env()".

   Эта функция убивает двух зайцев:
         а) pop'ит контекстный стек. Проверяет текущий объект исключения. Если он затёрт через CATCHED, то ошибка поймана, всё прекрасно, ничего более не делать.
         б) Но если текущий объект исключения не равен нулю —  то это значит, что ошибка до сих пор не поймана! Тогда функция пытается pop'ить контекстный стек ещё раз. Если не получается, т.е. контекстов больше нет (т.е. больше нет TRY-блоков), значит исключение unhandled. Программа завершается. Но, если в контекстном стеке есть ещё один контекст — значит выше по программному стеку есть ещё один TRY-block. Далее происходит магия:

                           void* higher_env = (void*)&(((&exst->items)[--exst->sp]).buf);
                           longjmp(higher_env, 1);


Вуаля. Стек unwind'ится дальше. Здесь что-то типа рекурсии, но такая рекурсия не ест программный стек. Разматывая стек, постоянно будет псевдорекурсивно вызываться dotorg_pop_env в вышележащих stackframe'ах. Когда-то ошибка словится. А когда-то контексты в контекстном стеке кончатся, тогда выведется ошибка "Program encountered unhandled exception of type 'blabla'".

7. Если исключения и вовсе не было, то CATCH-блок попросту проскакивается (а TRY исполняется до конца). Вся магия в том, что макросы в конце концов разворачиваются в такую конструкцию:

Цитироватьif(!setjmp(dotorg_push_env())) /* TRY */
      {
             
      }
      else /* CATCH */
      {
      }
      dotorg_pop_env(); /* END_TRY */

   Функция setjmp возвращает 1, если она вызвалась после перехода по longjmp, и 0 — если в ходе обычного хода программы. В первом случае будет перескок в else-block, во втором случае else-block, напротив, будет проигнорирован.

8. Теперь как мапятся SEH-исключения. При создании TLS-индекса под контекстный стек, мы пишем такую шляпу:

     <..> SetUnhandledExceptionFilter(&m_SEH_handler); <..>

Самая функция примерно такая:

static LONG WINAPI m_SEH_handler(LPEXCEPTION_POINTERS pp)
{
    switch(pp->ExceptionRecord->ExceptionCode)
    {
        case EXCEPTION_ACCESS_VIOLATION:
            /* Тут можно выкидывать какой угодно собственный объект. */
            THROW(System_AccessViolationException_new(S("No-o-o!")));
            break;

        /* ... etc. */

        default:
            return EXCEPTION_CONTINUE_SEARCH;
    }

    return EXCEPTION_CONTINUE_EXECUTION;
}



Пример:
#include "System.h"

void innerfunc(void)
{
    /* Намеренно вызываем SEGFAULT, чтобы получить SEH-исключение. */
    int *a = 0;
    a = *a;
}

int main(void)
{
   TRY
   {
       printf("Before.\n");

       innerfunc();

       printf("After.\n");
   }
   CATCH(e)
   {
       printf("OMG! Access violation!\n");

       CATCHED;
   }
   END_TRY;

   return 0;
}


Аутпут:
ЦитироватьBefore.
OMG! Access violation!

Process returned 0 (0x0)   execution time : 0.015 s
Press any key to continue.

Вот так. Написал всё это, чтобы не забыть реализацию, и, может, пригодится кому.

Теперь думаю, как лучше реализовать RTTI. Наверное, не обойтись без DSL.
肏! Τίς πέπορδε;

Алексей Гринь

О! Я забыл упомянуть такой случай, что THROW может быть вызван внутри CATCH-блока (т. н. rethrow). В текущей реализации такой поворот событий приводил бы к зацикливанию.

Поэтому в dotorg_raise_exc (который маскируется by THROW) я добавил совсем ничего:

Цитировать/* Проверяем, есть ли для треда текущее исключение. Если есть, значит мы ещё в CATCH-блоке, ошибка не поймана, а уже вызывается THROW. */
    if(exst->data)
    {
        /* Релизим предыдущий объект исключения. */
        RELEASE(exst->data);

        /* Объявляем текущим исключением новое (которое кинуто в catch-блоке). */
        exst->data = data;

        /* Перескакиваем на высший контекст. Без этой строки зацикливалось бы на текущем контексте. */
        dotorg_pop_env();
    }
肏! Τίς πέπορδε;




Алексей Гринь

Нет. Я не понимаю «Ээээ...». Ваше Ээээ... может означать от «Не поэл...» до «У вас ошибка».
肏! Τίς πέπορδε;


аноним

а я вот поэл и даже почти разобрался. Спасибо Алексей.

Быстрый ответ

Обратите внимание: данное сообщение не будет отображаться, пока модератор не одобрит его.

Имя:
Имейл:
Проверка:
Оставьте это поле пустым:
Наберите символы, которые изображены на картинке
Прослушать / Запросить другое изображение

Наберите символы, которые изображены на картинке:

√36:
ALT+S — отправить
ALT+P — предварительный просмотр