Когда я поносил С++, единственное, что я на тот момент признавал ущербным в Си, так это отсутствие исключений. Казалось бы, это что-то на уровне ассемблера, генерируемым С++-компилятором, и всё, что можно сделать на голом Си, так это только жалкую пародию. Оказывается, чтобы реализовать полноценную систему исключений на Си, нужно всего лишь 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.
Нет. Я не понимаю «Ээээ...». Ваше Ээээ... может означать от «Не поэл...» до «У вас ошибка».
а я вот поэл и даже почти разобрался. Спасибо Алексей.