- PVSM.RU - https://www.pvsm.ru -
Пока писал эту сугубо техническую статью, Хабр успел превратиться в местное отделение ВОЗ и теперь мне даже стыдно ее публиковать… но в душе теплится надежда, что айтишники еще не разбежались и она найдет своего читателя. Или нет?
Меня всегда восхищала стандартная библиотека Си, да и сам Си — при всей своей минималистичности от них так и веет духом тех самых первых красноглазиков хакеров [1]. В черновике [2] первого официального стандарта (ANSI C, он же C89, он же [3] ANS X3.159-1989, он же, позднее, C90 и IEC 9899:1990) определяется 145 функций и макросов, из них около 25 — это вариации (ввиду отсутствия в языке перегрузок), а 26 чисто математических. K&R во второй редакции [4]² приводят 114 функций (плюс математические), считая остальные за экзотику. В черновике³ C11 [5] функций уже 348, но больше сотни — математика, а еще штук 90 это «перегрузки». А теперь посмотрим на Boost, где одних только библиотек — 160 [6]. Чур меня…
И среди этой сотни-полутора функций всегда были: обработка сигналов, вариативные функции (которые до интерпретируемого PHP дошли 25 лет спустя, а в Delphi, бурно развивавшемся одно время, их нет до сих пор) и порядка 50 строковых функций вроде printf() (м-м-м… JavaScript), strftime() (…) и scanf() (дешевая альтернатива регуляркам).
А еще всегда были setjmp()/longjmp(), которые позволяют реализовать привычный по другим языкам механизм исключений, не выходя за рамки переносимого Си. Вот о них и поговорим — Quake World [7], стеки, регистры, ассемблеры и прочая матчасть [8], а вишенкой будет занятная статистика (спойлер [9]: Visual Studio непостоянна, как мартовский заяц, а throw saneex.c
в два раза быстрее всех).
¹ По результатам [9] замеров [10] в статье.
² Кстати, книга великолепная. 270 страниц, из которых 80 — это краткий пересказ стандарта. Или в то время еще не умели растекаться мыслью по древу и конвертировать это в гонорар, или авторы были выше этого. K&R [11] — старая школа, чо.
³ Из особо достоверных источников [12] известно, что финальные версии стандартов ANSI и ISO продаются за деньги, а черновики бесплатны. Но это не точно.
⁴ Да, я тоже не люблю «сокращалки» вроде TinyURL, но парсер Хабра считает URL частью текста и ругается на длинный текст до ката, яко Твіттер поганий. Дальше этого не будет, честно-честно. Параноикам могу посоветовать urlex.org [13].
Оглавление:
Итак, герои нашей программы — setjmp() [22]/longjmp() [23], определенные в setjmp.h [24], которые любят вместе сокращать как «SJLJ» (хотя мне это слово не нравится, напоминает одну печально известную аббревиатуру). Они появились в C89 и, в общем-то, уходить не собираются, но про них не все знают (знать не значит использовать — знание полезно, а использование — как повезет).
Справедливости ради надо сказать, что на Хабре уже [25] были [26] статьи, посвященные этой теме, в особенности отличная статья [27] от zzeng [28]. В англоязычной Сети, конечно, тоже имеется [29], плюс можно найти реализации вроде такой [30] или даже вот такой [31]¹, но, на мой взгляд, у них есть фатальный недостаток результат или не до конца привычен (к примеру, нельзя выбрасывать исключения повторно), или используются механизмы не по стандарту.
¹ CException [31] хочется отметить особо — всего 60 строчек, пишут, что работает быстро, тоже ANSI C, но у него нет finally и текстовых сообщений, что для меня принципиально важно.
Вообще, использовать исключения или нет — вечный спор тупоконечников с остроконечниками [32] в любом языке, и я призываю тех, кто по другую сторону баррикад, или пройти мимо, или прочитать материал и отложить его в свою копилку знаний, пусть даже на полку «чего только не тащат в нашу уютненькую сишечку». (Главное, чтобы спорщики не забывали, что ни одна программа на Си по-настоящему от «исключений» не свободна, ибо проверка errno не спасет при делении на ноль. Сигналы — те же яйца, только в профиль.)
Для меня лично исключения это инструмент, который позволяет:
if (error) return -1;
)Но обо всем по порядку. Как это у нас принято [33], начнем с матчасти.
В двух словах, longjmp() — это нелокальный goto, а setjmp() — пророк его способ задания метки этому goto в run-time. Короче, «goto на стероидах». И, как и любые стероиды, то бишь, goto, они могут нанести непоправимый вред вашему коду — превратить его в такую лапшу, которая для goto просто вне досягаемости. Посему лучше всего их использовать не напрямую, а внутри какой-нибудь обертки, задающей четкую иерархию переходов (как то исключения — вверх по стеку в пределах явно обозначенных блоков «try»).
Помните, я говорил в начале, что от Си и, конкретно, от setjmp.h
прямо веет черт^W юниксовщиной? Так вот, вы вызываете setjmp() один раз, а она возвращается сколько угодно раз (но, как минимум, один). Да, в обычном мире смузихлебы вызывают функцию и она возвращается один раз, а в Советской России функция вызывает вас сама, сколько раз ей хочется и когда ей этого хочется. Такие дела.
Эта концепция, кстати, воплотилась не только в setjmp() — fork() [35] в POSIX делает нечто очень похожее. Я помню, когда я впервые знакомился с *nix’овыми API после десятка лет работы исключительно с WinAPI, мне просто сносило крышу — в моих ментальных шаблонах не укладывалось, что функции могут вот так себя вести. Как метко говорят — «а что, так можно было?»… Но мы отвлеклись.
Думаю, все читающие в курсе, что основной элемент рантайма — это стек [36], на котором лежат параметры и (некоторые) локальные переменные данной функции. Вызываешь новую функцию — стек растет (причем у Intel’а — вниз), выходишь — тает (у Intel’а — да-да, вверх). Вот примерчик:
void sub(int s) {
char buf[256];
sub(2);
}
int main(int m) {
sub(1);
}
Есть такой занятный компилятор — tcc (Tiny C Compiler [37]) от известного программиста-парохода Ф. Беллара [38]. tcc практически не делает оптимизаций и код после него очень приятно смотреть в дизассемблере. Он генерирует такое тело для sub() (в нотации Intel [39], опуская пролог и эпилог):
sub esp, 100h ; выделяем место под локальную переменную
mov eax, 2 ; передаем параметр
push eax
call sub_401000 ; вызываем sub()
add esp, 4 ; очищаем стек после возврата (= cdecl)
Вот схемка происходящего со стеком:
Вот эти оранжевые цифры по центру — это указатель на вершину стека (который у Intel… ну, вы поняли). Указатель хранится в регистре ESP (RSP на x86_64). setjmp() сохраняет текущее значение ESP/RSP, плюс другие служебные регистры, в область памяти jmp_buf, которую вы ему передаете. Если происходит вызов longjmp() далее по курсу (из этой же функции или из подфункции) — указатель восстанавливается и получается, что следом автоматически восстанавливается и окружение функции, где был вызван setjmp(), а все вызванные ранее подфункции моментально завершаются (возвращаются). Эдакий откат во времени, «undo» для рантайма (конечно, с большой натяжкой).
В следующем примере setjmp() поместит в jmp значение указателя FEF8h
(FDF0h
и т.д. — красные стрелки на схеме выше) и функция продолжит выполнение, как обычно:
void sub(int s) {
char buf[256];
jmp_buf jmp;
setjmp(jmp);
sub(2);
}
Но, конечно, есть нюанс™:
Последний момент [41] особенно интересен. Пример:
#include <stdlib.h>
#include <stdio.h>
#include <setjmp.h>
int main(void) {
int i;
jmp_buf jmp;
i = rand();
if (setjmp(jmp) == 0) {
i = rand();
printf("%dn", i);
longjmp(jmp, 1);
} else {
printf("%dn", i);
}
}
Вопрос залу: будут ли числа в консоли совпадать?
Правильный ответ: зависит от воли звезд. Так-то!
Посмотрим, что происходит на примере gcc. Если скомпилировать с -O0, то числа будут совпадать, а в дизассемблере мы увидим вот это:
; int main(void) {
push ebp ; пролог (создается stack frame)
mov ebp, esp ; EBP указывает на стек ниже ESP (если по схеме)
sub esp, E0h
...
call _rand ; результат возвращается в EAX
mov [ebp-D4h], eax ; это i = rand(); где i на стеке (EBP-D4h)
...
; if (... == 0) { ; вызов setjmp() и возврат из нее до прыжка
call _rand
mov [ebp-D4h], eax ; снова i = rand(); на стеке
; printf("%dn", i);
mov eax, [ebp-D4h] ; передаем i со стека как параметр
mov esi, eax
lea edi, format ; передаем строку "%dn"
mov eax, 0
call _printf
...
; } else { ; вторичный возврат из setjmp() после прыжка
mov eax, [ebp-D4h] ; снова передаем i, как в ветке выше
mov esi, eax
lea edi, format ; "%dn"
mov eax, 0
call _printf
Как видно, компилятор не заморачивался и поместил переменную i в стек (по адресу EBP - D4h
). Если смотреть на всю ту же схемку [42], то:
FFF8h - E0h = FF18h
(вместо FEF8h
), это значение и сохраняется в jmp
FF18h
)FF18h
, но, так как переменная i не выходит за эти границы, она по-прежнему доступна, равно как и другая переменная (jmp), и параметры main() (буде они есть)
А вот если включить хотя бы -O1, то картина изменится:
; пролога и stack frame больше нет, используется значение ESP напрямую
sub esp, E8h
...
call _rand
mov [esp+E8h-DCh], eax ; i = rand(); в стеке, как и с -O0
...
; -O1 почему-то решило, что выполнение else более вероятно, чем
; if (setjmp() == 0) (хотя по-моему наоборот), и переставило
; их местами; здесь я вернул прежний порядок для понятности
; if (... == 0) {
call _rand
mov esi, eax ; ВНИМАНИЕ! запись i в регистр
; printf("%dn", i);
lea edi, format ; "%dn"
mov eax, 0
call _printf
...
; } else {
mov esi, [esp+E8h-DCh] ; ВНИМАНИЕ! чтение i со стека
lea edi, format ; "%dn"
mov eax, 0
call _printf
Вдобавок, с -O1 gcc при компиляции ругается страшными словами:
test.c:6:11: warning: variable ‘i’ might be clobbered by ‘longjmp’ or ‘vfork’ [-Wclobbered]
Что мы здесь видим? Вначале i помещается в регистр, но в первой ветке (внутри if) gcc, видимо сочтя i не используемой после первого printf(), помещает новое значение сразу в ESI, а не в стек (через ESI оно передается дальше в printf(), см. ABI, стр. 22 [43] — RDI (format), RSI (i), …). Из-за этого:
ESP + E8h - DCh
остается старое значение rand()Или, если переписать это обратно на Си:
stack[i] = rand(); // i = rand(); изменение стека (1)
if (setjmp(jmp) == 0) {
ESI = rand(); // i = rand(); изменение регистра (2)
printf("%dn", ESI); // печать значения (2)
longjmp(jmp, 1); // прыжок
} else {
printf("%dn", stack[i]); // печать значения (1)
// или могло бы быть так:
printf("%dn", ESI); // использование регистра, где уже кто-то
// "побывал" (первый printf() или longjmp())
}
Честно говоря, мне не понятно, почему gcc результат первого rand() не помещает сразу в ESI или в другой регистр (даже при -O3). На пишут [44], что в режиме x86_64 (под который я компилировал пример) сохраняются все регистры, кроме EAX. Зачем промежуточное сохранение в стек? Я предположил, что gcc отследил printf() в else после longjmp(), но если убрать второй rand() и этот printf() — результат не меняется, i так же вначале пишется в стек.
Если кто может пролить свет на сию тайну — прошу в комментарии.
Решение проблемы [15] «летучих переменных» — квалификатор volatile (дословно — «летучий»). Он заставляет компилятор всегда помещать переменную в стек, поэтому наш код будет работать, как ожидается, при любом уровне оптимизаций:
volatile int i;
Единственное изменение при -O1 будет в теле if:
; было:
call _rand
mov esi, eax
; стало:
call _rand
mov [rsp+E8h-DCh], eax
mov esi, [rsp+E8h-DCh]
; или можно переписать так:
call _rand
mov esi, eax
mov [rsp+E8h-DCh], eax
Как видим, компилятор продублировал присвоение в стек (сравните [45]):
if (setjmp(jmp) == 0) {
ESI = stack[i] = rand();
Итак, если соблюдать меры предосторожности — не прыгать между потоками и между завершившимися функциями и не использовать изменившиеся не-volatile переменные после прыжка, то SJLJ позволяет нам беспроблемно перемещаться по стеку вызовов в произвольную точку. И не обязательно быть адептом секты свидетелей исключений — сопротивление бесполезно, ибо SJLJ уже давно заполонили всю планету среди нас:
Последний пример, на мой взгляд, наиболее хрестоматийный — это обработка ошибок и других состояний, когда нужно выйти «вот прямо сейчас», с любого уровня, при этом вставлять везде проверки на выход утомительно, а где-то и не возможно (библиотеки). Кстати, еще один пример [70] был описан в проекте DrMefistO [71].
Конкретно в Quake World запускается бесконечный цикл в WinMain(), где каждая новая итерация устанавливает jmp_buf, а несколько функций могут в него прыгать, таким образом реализуя «глубокий continue»:
// WinQuake/host.c
jmp_buf host_abortserver;
void Host_EndGame (char *message, ...)
{
...
if (cls.demonum != -1)
CL_NextDemo ();
else
CL_Disconnect ();
longjmp (host_abortserver, 1);
}
void Host_Error (char *error, ...)
{
...
if (cls.state == ca_dedicated)
Sys_Error ("Host_Error: %sn",string); // dedicated servers exit
CL_Disconnect ();
cls.demonum = -1;
inerror = false;
longjmp (host_abortserver, 1);
}
void _Host_Frame (float time)
{
static double time1 = 0;
static double time2 = 0;
static double time3 = 0;
int pass1, pass2, pass3;
if (setjmp (host_abortserver) )
return; // something bad happened, or the server disconnected
...
}
// QW/client/sys_win.c
int WINAPI WinMain (...)
{
...
while (1)
{
...
newtime = Sys_DoubleTime ();
time = newtime - oldtime;
Host_Frame (time);
oldtime = newtime;
}
/* return success of application */
return TRUE;
}
Один из доводов, который приводят против использования исключений — их отрицательное влияние на производительность. И действительно, в исходниках setjmp() в glibc [72] видно, что сохраняются почти все регистры общего назначения ЦП. Тем не менее:
saneex.c
в частности и не предполагаются к применению во внутренностях числодробилок«Честные» zero-cost exceptions особенно полезны в том плане, что избавляют от более медленных volatile [16]-переменных, которые иначе размещаются в стеке, а не в регистрах (именно поэтому они и не затираются longjmp()). Тем не менее, их поддержка это уже задача для компилятора и платформы:
И, хотя saneex.c
не претендует на пальму zero-cost (ее пальма — это переносимость), так ли уж страшен setjmp(), как его малюют? Может, это суеверие? Чтобы не быть голословными — померяем.
Я набросал два бенчмарка «на коленке», которые в main() в цикле 100 тысяч раз входят в блок try/catch и делают или не делают throw().
Исходник бенчмарка на C:
#include <stdio.h>
#include <time.h>
#include "saneex.h"
int main(void) {
for (int i = 0; i < 100000; i++) {
try {
// либо ("выброс" = да):
throw(msgex("A quick fox jumped over a red dog and a nyancat was spawned"));
// либо ("выброс" = нет):
time(NULL);
} catchall {
fprintf(stderr, "%sn", curex().message);
} endtry
}
}
Исходник на С++ (я адаптировал пример с Википедии [79], вынеся объявление вектора за цикл и заменив cerr <<
на fprintf()):
#include <iostream>
#include <vector>
#include <stdexcept>
#include <time.h>
int main() {
std::vector<int> vec{ 3, 4, 3, 1 };
for (int i = 0; i < 100000; i++) {
try {
// либо ("выброс" = да):
int i{ vec.at(4) };
// либо ("выброс" = нет):
time(NULL);
}
catch (std::out_of_range & e) {
// << вместо fprintf() вызывает замедление цикла на 25-50%
//std::cerr << "Accessing a non-existent element: " << e.what() << 'n';
fprintf(stderr, "%sn", e.what());
}
catch (std::exception & e) {
//std::cerr << "Exception thrown: " << e.what() << 'n';
fprintf(stderr, "%sn", e.what());
}
catch (...) {
//std::cerr << "Some fatal errorn";
fprintf(stderr, "Some fatal error");
}
}
return 0;
}
Тестировалось все на одной машине в двух ОС (обе 64-битные):
Measure-Command { test.exe 2>$null }
Также я попробовал замерить исключения в Windows через расширения __try/__except, взяв другой пример с Википедии [81]:
#include <windows.h>
#include <stdio.h>
#include <vector>
int filterExpression(EXCEPTION_POINTERS* ep) {
ep->ContextRecord->Eip += 8;
return EXCEPTION_EXECUTE_HANDLER;
}
int main() {
static int zero;
for (int i = 0; i < 100000; i++) {
__try {
zero = 1 / zero;
__asm {
nop
nop
nop
nop
nop
nop
nop
}
printf("Past the exception.n");
}
__except (filterExpression(GetExceptionInformation())) {
printf("Handler called.n");
}
}
}
Однако вектор включить в цикл не вышло — компилятор сообщил, что:
error C2712: Cannot use __try in functions that require object unwinding
Так как накладываемые ограничения на код идут вразрез с принципом привычности, о котором я говорил в начале [82], я не внес эти результаты в таблицу ниже [9]. Ориентировочно это 1100-1300 мс (Debug или Release, x86) — быстрее, чем стандартные исключения в VS, но все равно медленнее, чем они же в g++.
№ Компилятор Конфиг Платф Механизм Выброс Время (мс)¹ saneex медленнее
1. VS 2019 v16.0.0 Debug x64 saneex.c да 9713 / 8728 = 1.1 в 1.8 / 1.8
2. VS 2019 v16.0.0 Debug x64 saneex.c нет 95 / 46 = 2 в 4.5 / 2.3
3. VS 2019 v16.0.0 Debug x64 C++ да 5449 / 4750² = 1.6
4. VS 2019 v16.0.0 Debug x64 C++ нет 21 / 20 = 1
5. VS 2019 v16.0.0 Release x64 saneex.c да 8542³ / 182 = 47 в 1.8 / 0.4
6. VS 2019 v16.0.0 Release x64 saneex.c нет 80³ / 23 = 3.5 в 8 / 1.8
7. VS 2019 v16.0.0 Release x64 C++ да 4669³ / 420 = 11
8. VS 2019 v16.0.0 Release x64 C++ нет 10³ / 13 = 0.8
9. gcc 9.2.1 -O0 x64 saneex.c да 71 / 351 = 0.2 в 0.2 / 0.6
10. gcc 9.2.1 -O0 x64 saneex.c нет 6 / 39 = 0.2 в 1.5 / 1.1
11. g++ 9.2.1 -O0 x64 C++ да 378 / 630 = 0.6
12. g++ 9.2.1 -O0 x64 C++ нет 4 / 37 = 0.1
13. gcc 9.2.1 -O3 x64 saneex.c да 66 / 360 = 0.2 в 0.2 / 0.6
14. gcc 9.2.1 -O3 x64 saneex.c нет 5 / 23 = 0.2 в 1 / 0.6
15. g++ 9.2.1 -O3 x64 C++ да 356 / 605 = 0.6
16. g++ 9.2.1 -O3 x64 C++ нет 5 / 38 = 0.1
¹ В столбце Время добавлены замеры одного из читателей на Windows 7 SP1 x64 с VS 2017 v15.9.17 и gcc под cygwin.
² Крайне странный факт: если fprintf() заменить на cerr <<
, то время выполнения сократится в 3 раза: 1386/1527 мс.
³ VS в релизных сборках на моей системе выдает очень непостоянные результаты, поэтому в дальнейших рассуждениях я использую цифры читателя.
Результаты получились… интересные:
cerr <<
вместо fprintf() в паре с выбросом исключения в VS в отладочной сборке ускоряет цикл в 3-4 раза (строка 3). ЧЯДНТ?saneex.c
быстрее, чем во встроенных языковых конструкциях (в 2.3 раза быстрее VS, в 5 раз быстрее gcc/g++), а try без throw — помедленнее, но речь идет о единицах миллисекунд. Вот это поворот!Что тут можно сказать… Есть о чем похоливарить. Добро пожаловать в комментарии!
Для меня самый важный use-case — это много блоков try с крайне редкими throw («лови много, бросай мало»), а он зависит практически только от скорости setjmp(), причем производительность последнего, судя по таблице, далеко не так плоха, как часто думают. Косвенно это подтверждается и вот этой статьей [83], где автор после замеров делает вывод, что один вызов setjmp() равен двум вызовам пустых функций в OpenBSD и полутора (1.45) — в Solaris. Причем эта статья от 2005 года. Единственное «но» — сохранять нужно без сигнальной маски, но она обычно и не интересна.
Ну, а напоследок…
saneex.c
Библиотека, чей пример был на КДПВ:
Интересующиеся могут найти ее исходники на GitHub [86]. Ниже я кратко на одном примере покажу, как ей пользоваться и какие есть подводные камни [20]. Код примера из saneex-demo.c
в репозитории:
01. #include <stdio.h>
02. #include "saneex.h"
03.
04. int main(void) {
05. sxTag = "SaneC's Exceptions Demo";
06.
07. try {
08. printf("Enter a message to fail with: [] [1] [2] [!] ");
09.
10. char msg[50];
11. thrif(!fgets(msg, sizeof(msg), stdin), "fgets() error");
12.
13. int i = strlen(msg) - 1;
14. while (i >= 0 && msg[i] <= ' ') { msg[i--] = 0; }
15.
16. if (msg[0]) {
17. errno = atoi(msg);
18. struct SxTraceEntry e = newex();
19. e = sxprintf(e, "Your message: %s", msg);
20. e.uncatchable = msg[0] == '!';
21. throw(e);
22. }
23.
24. puts("End of try body");
25.
26. } catch (1) {
27. puts("Caught in catch (1)");
28. sxPrintTrace();
29.
30. } catch (2) {
31. puts("Caught in catch (2)");
32. errno = 123;
33. rethrow(msgex("calling rethrow() with code 123"));
34.
35. } catchall {
36. printf("Caught in catchall, message is: %sn", curex().message);
37.
38. } finally {
39. puts("Now in finally");
40.
41. } endtry
42.
43. puts("End of main()");
44. }
Программа выше читает сообщение, бросает исключение и обрабатывает его в зависимости от пользовательского ввода:
End of try body
Now in finally
End of main()
catch (1)
(26.), а на экране появится: Caught in catch (1)
Your message: 1 hello, habr!
...at saneex-demo.c:18, code 1
Now in finally
End of main()
Caught in catch (2)
Now in finally
Uncaught exception (code 123) - terminating. Tag: SaneC's Exceptions Demo
Your message: 2 TM! kak tam blok4ain?
...at saneex-demo.c:18, code 2
calling rethrow() with code 123
...at saneex-demo.c:33, code 123
rethrown by ENDTRY
...at saneex-demo.c:41, code 123
!
, то исключение получится «неуловимым» (uncatchable; 20.) — оно пройдет сквозь все блоки try выше по стеку, вызывая их обработчики (как catch, так и finally), пока не дойдет до внешнего и не завершит процесс — гуманный аналог abort(): Caught in catch (1)
Your message: ! it is a good day to die
...UNCATCHABLE at saneex-demo.c:18, code 0
Now in finally
Uncaught exception (code 0) - terminating. Tag: SaneC's Exceptions Demo
Your message: ! it is a good day to die
...UNCATCHABLE at saneex-demo.c:18, code 0
UNCATCHABLE rethrown by ENDTRY
...at saneex-demo.c:41, code 0
Caught in catchall, message is: Your message: 3 we need more gold
Now in finally
End of main()
Потокобезопасность. По умолчанию ее нет, но если у вас нормальный компилятор (не MSVC¹), то C11 спасет отца народов за счет помещения важных переменных в локальную область потока (TLS [87]):
#define SX_THREAD_LOCAL _Thread_local
¹ Последние годы у Microsoft имеются [88] какие-то [89] подвижки [90] на почве open source, но всем по дело [91] идет медленно, хотя и лучше, чем 8 лет назад [92], так что мы пока держимся [93].
sxTag (05.) — строка, которая выводится вместе с непойманным исключением в stderr. По умолчанию — дата и время компиляции (__DATE__ __TIME__).
Создание SxTraceEntry (записи в stack trace). Есть несколько полезных макросов — оберток над (struct SxTraceEntry) {...}
:
newex()
— этот был в примере [94]; присваивает __FILE__, __LINE__ и код ошибки = errno (что удобно после проверки результата вызова системной функции, как в примере после fgets(); 11.)
catch (0)
никогда не сработаетmsgex(m)
— как newex(), но также устанавливает текст ошибки (константное выражение)exex(m, e)
— как msgex(), но также прицепляет к исключению произвольный указатель; его память будет освобождена через free() автоматически: try {
TimeoutException *e = malloc(sizeof(*e));
e->elapsed = timeElapsed;
e->limit = MAX_TIMEOUT;
errno = 146;
throw(exex("Connection timed out", e));
} catch (146) {
printf("%s after %dn", curex().message,
// читаем через void *SxTraceEntry.extra:
((TimeoutException *) curex().extra)->elapsed);
} endtry
И, конечно, есть мои любимые designated initializers [95] из все того же C99 (работают в Visual Studio 2013+ [88]):
throw( (struct SxTraceEntry) {.message = "kaboom!"} );
Выброс исключения:
throw(e)
— бросает готовый SxTraceEntryrethrow(e)
— аналогично throw(), но не очищает текущий stack trace; может использоваться только внутри catch/catchallthrif(x, m)
— макрос; при if (x)
создает SxTraceEntry с текстом x + m и «выбрасывает» егоthri(x)
— как thrif(), только с пустым mМакросы нужны для удобного «преобразования» результата типичного библиотечного вызова в исключение — как в примере [94] с fgets() (11.), если функция не смогла прочитать ничего. Конкретно с fgets() это не обязательно обозначает ошибку (это может быть просто EOF: ./a.out </dev/null
), но других подходящих функций в том примере не используется. Вот более жизненный:
thri(read(0xBaaD, buf, nbyte));
// errno = 9, "Bad file descriptor"
// Assertion error: read(0xBaaD, buf, nbyte);
Их всего две с половиной (но зато какие!):
{
, а endtry их закрывает</sarcasm>
Что касается «половины», то это уже разобранный ранее [16] volatile. «Прием» исключения — это повторный вход в середину функции (см. longjmp()), поэтому, если значение переменной было изменено внутри тела try, то такая переменная не должна использоваться в catch/catchall/finally и после endtry, если она не объявлена как volatile. Компилятор заботливо предупредит о такой проблеме. Вот наглядный пример:
int foo = 1;
try {
foo = 2;
// здесь можно использовать foo
} catchall {
// а здесь уже нет!
} finally {
// и здесь тоже!
} endtry
// и здесь нельзя!
С volatile переменную можно использовать где угодно:
volatile int foo = 1;
try {
...
У каждого потока есть два статически-выделенных (глобальных) массива:
struct SxTryContext
— информация о блоках try, внутри которых мы сейчас находимся — в частности, jmp_buf на каждый из них; например, здесь их два: try {
try {
// мы здесь
} endtry
} endtry
struct SxTraceEntry
— текущий stack trace, то есть объекты, переданные кодом снаружи для идентификации исключений; их может быть больше или меньше, чем блоков try: try { // один SxTryContext
try { // два SxTryContext
// ноль SxTraceEntry
throw(msgex("Первый пошел!"));
// один SxTraceEntry
} catchall {
// один SxTraceEntry
rethrow(msgex("Второй к бою готов!"));
// два SxTraceEntry (*)
} endtry
} endtry
Если в коде выше вместо rethrow() использовать throw(), то объектов SxTraceEntry (*)
будет не два, а один — предыдущей будет удален (stack trace будет очищен). Кроме того, можно вручную добавить элемент в цепочку через sxAddTraceEntry(e)
.
try и другие элементы конструкции суть макросы (— ваш К. О.). Скобки { }
после них не обязательны. В итоге, все это сводится к следующему псевдокоду:
try { int _sxLastJumpCode = setjmp(add_context()¹);
bool handled = false;
if (_sxLastJumpCode == 0) {
throw(msgex("Mama mia!")); clearTrace();
sxAddTraceEntry(msgex(...));
if (count_contexts() == 0) {
fprintf(stderr, "Shurik, vsё propalo!");
sxPrintTrace();
exit(curex().code);
} else [
longjmp(top_context());
}
} catch (9000) { } else if (_sxLastJumpCode == 9000) {
handled = true;
} catchall { } else {
handled = true;
} finally { }
// здесь действия в finally { }
} endtry remove_context();
if (!handled) {
// как выше с throw()
}
¹ Имена с _ в библиотеке не используются, это абстракции.
Думаю, после подробных объяснений [8], как работает SJLJ, что-то еще здесь комментировать излишне, а потому позвольте откланяться и предоставить слово уже вам.
Автор: Павел
Источник [96]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/351361
Ссылки в тексте:
[1] хакеров: https://tinyurl.com/tj364je
[2] черновике: https://tinyurl.com/m4hdlab
[3] он же: https://tinyurl.com/djfxz6
[4] K&R во второй редакции: https://tinyurl.com/y6z5z989
[5] черновике³ C11: https://tinyurl.com/444uvtg
[6] одних только библиотек — 160: https://tinyurl.com/qqu853s
[7] Quake World: https://habr.com/ru/post/491084/#irl
[8] матчасть: https://habr.com/ru/post/491084/#sjlj
[9] спойлер: https://habr.com/ru/post/491084/#benchres
[10] замеров: https://habr.com/ru/post/491084/#bench
[11] K&R: https://ru.wikipedia.org/wiki/%D0%AF%D0%B7%D1%8B%D0%BA_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F_%D0%A1%D0%B8_(%D0%BA%D0%BD%D0%B8%D0%B3%D0%B0)
[12] особо достоверных источников: https://stackoverflow.com/questions/17014835/where-can-i-find-the-c89-c90-standards-in-pdf-format
[13] urlex.org: https://urlex.org
[14] Регистры, стек и все-все-все: https://habr.com/ru/post/491084/#matan
[15] Затирание переменных или, по-русски, clobbering: https://habr.com/ru/post/491084/#clob
[16] Квалификатор volatile: https://habr.com/ru/post/491084/#vol
[17] Производительность: https://habr.com/ru/post/491084/#perf
[18] Виновник торжества — saneex.c: https://habr.com/ru/post/491084/#saneex
[19] Остальные «фичи»: https://habr.com/ru/post/491084/#sexfeat
[20] И «особенности реализации»: https://habr.com/ru/post/491084/#sexpit
[21] Итог: как это работает: https://habr.com/ru/post/491084/#sexside
[22] setjmp(): https://www.opennet.ru/man.shtml?topic=setjmp&category=3&russian=2
[23] longjmp(): https://www.opennet.ru/man.shtml?topic=longjmp&category=3&russian=2
[24] setjmp.h: https://www.opennet.ru/man.shtml?topic=setjmp.h&category=3&russian=5
[25] уже: https://habr.com/ru/post/324642
[26] были: https://habr.com/ru/post/50985
[27] отличная статья: https://habr.com/ru/post/208006
[28] zzeng: https://habr.com/ru/users/zzeng/
[29] тоже имеется: http://groups.di.unipi.it/~nids/docs/longjump_try_trow_catch.html
[30] вроде такой: https://github.com/Jamesits/CTryCatch
[31] вот такой: https://sourceforge.net/projects/cexception
[32] тупоконечников с остроконечниками: https://ru.wikipedia.org/wiki/%D0%9F%D1%83%D1%82%D0%B5%D1%88%D0%B5%D1%81%D1%82%D0%B2%D0%B8%D1%8F_%D0%93%D1%83%D0%BB%D0%BB%D0%B8%D0%B2%D0%B5%D1%80%D0%B0#%D0%A7%D0%B0%D1%81%D1%82%D1%8C_1._%D0%9F%D1%83%D1%82%D0%B5%D1%88%D0%B5%D1%81%D1%82%D0%B2%D0%B8%D0%B5_%D0%B2_%D0%9B%D0%B8%D0%BB%D0%B8%D0%BF%D1%83%D1%82%D0%B8%D1%8E
[33] Как это у нас принято: https://habr.com/ru/post/224955
[34] все-все-все: http://lib.ru/ANEKDOTY/9600.txt
[35] fork(): https://www.opennet.ru/man.shtml?topic=fork&category=3&russian=5
[36] стек: https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA_%D0%B2%D1%8B%D0%B7%D0%BE%D0%B2%D0%BE%D0%B2
[37] Tiny C Compiler: https://bellard.org/tcc/
[38] Ф. Беллара: https://ru.wikipedia.org/wiki/%D0%91%D0%B5%D0%BB%D0%BB%D0%B0%D1%80,_%D0%A4%D0%B0%D0%B1%D1%80%D0%B8%D1%81
[39] нотации Intel: https://en.wikipedia.org/wiki/X86_assembly_language#Syntax
[40] undefined behavior: https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D1%91%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D0%BE%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5
[41] Последний момент: https://habr.com/ru/post/491084/#r1
[42] схемку: https://habr.com/ru/post/491084/#r6
[43] ABI, стр. 22: https://raw.githubusercontent.com/wiki/hjl-tools/x86-psABI/x86-64-psABI-1.0.pdf
[44] пишут: https://stackoverflow.com/questions/2535989/what-are-the-calling-conventions-for-unix-linux-system-calls-on-i386-and-x86-6
[45] сравните: https://habr.com/ru/post/491084/#r2
[46] предлагает: https://en.wikipedia.org/wiki/Setjmp.h#Cooperative_multitasking
[47] ldir: https://habr.com/ru/users/ldir/
[48] упоминал: https://habr.com/ru/post/143318
[49] alexkalmuk: https://habr.com/ru/users/alexkalmuk/
[50] юнит-тесты в Эльбрусе на основе SJLJ: https://habr.com/ru/post/447704
[51] вторая статья: https://habr.com/ru/post/239387
[52] dzeban: https://habr.com/ru/users/dzeban/
[53] профилирование в Linux: https://habr.com/ru/post/261003
[54] писали про быстрый интерпретатор: https://habr.com/ru/post/261665
[55] Atakua: https://habr.com/ru/users/atakua/
[56] обработку ошибок в x86emu: https://habr.com/ru/post/176707
[57] NWOcs: https://habr.com/ru/users/nwocs/
[58] в libpng: https://habr.com/ru/post/176163
[59] libjpeg-turbo: https://libjpeg-turbo.org
[60] аналогично: https://raw.githubusercontent.com/libjpeg-turbo/libjpeg-turbo/master/example.txt
[61] Skapix: https://habr.com/ru/users/skapix/
[62] писал про pthreads: https://habr.com/ru/post/339698
[63] kutelev: https://habr.com/ru/users/kutelev/
[64] прыжки из обработчиков сигналов: https://habr.com/ru/post/332626
[65] тут пишут: https://stackoverflow.com/questions/819864/what-are-some-good-ways-to-use-longjmp-setjmp-for-c-error-handling
[66] Symbian: https://ru.wikipedia.org/wiki/Symbian_OS
[67] перевод археологических раскопок: https://habr.com/ru/post/324804
[68] PatientZero: https://habr.com/ru/users/patientzero/
[69] исходники: https://github.com/id-Software/Quake/blob/master/WinQuake/host.c
[70] еще один пример: https://habr.com/ru/post/434992
[71] DrMefistO: https://habr.com/ru/users/drmefisto/
[72] glibc: http://ftp.gnu.org/gnu/glibc/
[73] В Windows: https://en.wikipedia.org/wiki/Microsoft-specific_exception_handling_mechanisms
[74] тыц: https://habr.com/ru/post/267771
[75] dwarfstd.org: http://dwarfstd.org
[76] комментарии: https://habr.com/ru/post/50985#comment_12975751
[77] nuit: https://habr.com/ru/users/nuit/
[78] libunwind: http://www.nongnu.org/libunwind/
[79] Википедии: https://en.wikipedia.org/wiki/C%2B%2B#Exception_handling
[80] Ubuntu: https://www.ubuntu.com
[81] другой пример с Википедии: https://en.wikipedia.org/wiki/Exception_handling_syntax#Microsoft-specific
[82] в начале: https://habr.com/ru/post/491084/#r4
[83] вот этой статьей: https://tratt.net/laurie/blog/entries/timing_setjmp_and_the_joy_of_standards.html
[84] опционально: https://habr.com/ru/post/491084/#sexmt
[85] CC0: https://creativecommons.org/publicdomain/zero/1.0/
[86] найти ее исходники на GitHub: https://github.com/ProgerXP/SaneC
[87] TLS: https://en.wikipedia.org/wiki/Thread-local_storage
[88] имеются: https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2013/hh409293(v=vs.120)#compiler
[89] какие-то: https://docs.microsoft.com/en-us/previous-versions/hh409293(v=vs.140)#c-runtime-library
[90] подвижки: https://docs.microsoft.com/ru-ru/cpp/build/reference/experimental-preprocessor
[91] дело: https://docs.microsoft.com/ru-ru/cpp/c-language/ansi-conformance
[92] 8 лет назад: https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-and-c99/
[93] пока держимся: https://coub.com/view/cl1z0
[94] примере: https://habr.com/ru/post/491084/#r5
[95] designated initializers: https://en.wikipedia.org/wiki/C99#Design
[96] Источник: https://habr.com/ru/post/491084/?utm_source=habrahabr&utm_medium=rss&utm_campaign=491084
Нажмите здесь для печати.