Arlleex 173 22 апреля, 2019 Опубликовано 22 апреля, 2019 · Жалоба Знаю тема эта не нова, даже где-то обсуждалась, но вскользь. Хочу поделиться некоторыми соображениями по поводу механизма исключений на чистом Си, привести свою реализацию. Топик не для начинающих и, скорее, рассчитан на подготовленного читателя-фаната, поэтому за возможно причиненные морально-нравственные страдания автор ответственности не несет Также хочу отметить, что я не претендую на безоговорочную истину всего сказанного: все, что мне довелось получить - это результат пары-тройки часов колупания над готовыми решениями (которые, как думаю уже понятно, меня не сильно радовали). Итак, как мне довелось узнать, существует много различных реализаций исключений на Си, среди которых в первых гуглорядах стоят CException и т.д. И (не соврать бы) по-моему во всех реализациях механизма исключений главным рабочим инструментом являются функции setjmp() и longjmp(). Также существует два "скелета" для этих функций: if(!setjmp(...)) <- аналог оператора try { // потенциально опасный код ... if(...) longjmp(..., N); <- аналог оператора throw } else <- аналог оператора catch(...) { // обработчик } и switch(setjmp(...)) <- аналог оператора try { case 0: { // потенциально опасный код if(...) longjmp(..., N); <- аналог оператора throw } case 1: <- аналог оператора catch(int) (но тут явно задан номер исключения №1) { // обработчик 1 break; } case 2: <- аналог оператора catch(int) (но тут явно задан номер исключения №2) { // обработчик 2 break; } ... } Ну, как видно, на if-else, либо на switch-case (разницы-то, по сути, нет (пока что). Теперь, анализируя статью Exceptions in C with Longjmp and Setjmp, останавливаемся на втором варианте, так как фильтровать и ловить код ошибки от throw() гораздо удобнее по значению, нежели скопом ловить сразу все в одном месте (как в ветке else в первом варианте). Погоняем различными тестами окончательный вариант из статьи. 1. Конструкция try-catch полная: в блоке TRY явно кидается исключение 1, в CATCH-блоке оно ловится Спойлер #include <stdio.h> #include <setjmp.h> #define TRY do { jmp_buf ex_buf__; switch( setjmp(ex_buf__) ) { case 0: while(1) { #define CATCH(x) break; case x: #define FINALLY break; } default: { #define ETRY break; } } }while(0) #define THROW(x) longjmp(ex_buf__, x) int main(void) { printf("Code before try/catch\n"); TRY { THROW(1); } CATCH(1) { printf("Exception catched!\n"); } ETRY; printf("Code after try/catch\n"); while(1); return 0; } Спойлер Output: Code before try/catch Exception catched! Code after try/catch Работает правильно. 2. Затираем выброс исключения из предыдущего примера, блок FINALLY не добавляем Спойлер Output: Code before try/catch Code after try/catch Работает, но если бы этот код был в функции, а эта функция вызывалась из другой функции из, в свою очередь, своего try-catch-блока, поведение считалось бы ошибочным - ведь при отсутствии явного обработчика исключение должно быть "проброшено" в более глобальный обработчик (если он есть). Это есть первый косяк. 3. Выброс исключения из примера №1 оформляем в виде отдельной функции. В результате проект не собирается (ошибка компиляции), поскольку вызов THROW(...) предполагает видимым объект ex_buf__ из начала TRY-блока. Это есть второй косяк. 4. TRY-в-TRY: один TRY-блок (полноценный) внутри другого (более глобального) TRY-блока Спойлер int main(void) { printf("Code before try/catch\n"); TRY { TRY { THROW(1); } CATCH(1) { printf("Internal exception catched!\n"); } ETRY; } CATCH(1) { printf("External exception catched!\n"); } ETRY; printf("Code after try/catch\n"); while(1); return 0; } Спойлер Output: Code before try/catch Internal exception catched! Code after try/catch Правильно. Но если убрать CATCH-блок внутреннего TRY, не выведется сообщение "External exception catched!", а по логике наследования исключений должно. Третий косяк. 5. В предыдущем примере попробуем явно после выдачи сообщения "Internal exception catched!" сделать THROW(1): в try-catch C++ это привело бы к выдаче исключения на более глобальный уровень и обработке его там: в нашем случае последовательно должны были вывестись сообщения "Internal exception catched!", "External exception catched!" (между "Code before try/catch" и "Code after try/catch", конечно же), однако этого не произошло: вместо этого обнаружен бесконечный цикл вывода сообщения "Internal exception catched!". Это уже четвертый косяк. Короче, печаль. Нет схожести с поведением в более высокоуровневых языках типа C++. Ладно, напишем свое... с блэкджеком и автоматами Даффа Первым делом, подразумевая все изложенные проблемы кода выше, набросаем скелет программы, при котором убьются все эти проблемы сразу одним махом. Для этого определим некую структуру - блок управления исключениями // Exception Control Block typedef struct __sECB { struct __sECB *PrevECB; // предыдущий блок jmp_buf Context; // контекст текущего выполнения }__sECB; Структура содержит два параметра: PrevECB и Context. PrevECB указывает на предыдущий объект управления исключениями для организации односвязного списка исключений: это понадобится для наследования вызовов исключений при отсутствии явного локального обработчика. Буфер Context содержит контекст текущего выполнения и используется для локального обслуживания функциями setjmp() и longjmp(). Создадим "корень" в цепочке блоков управления исключениями __sECB *__ECBList = NULL; Значение NULL будет указывать, что в цепи вызовов try-catch при любом уровне вложенности функций, из которой произошел выброс исключения, обработчиков больше нет. Наглядно это будет выглядеть так main() { TRY {func1();} CATCH(1) {...} CATCH(2) {...} ... } func1() { TRY {func2();} } func2() { TRY {func3();} } func3() { TRY { if(...) THROW(1); <- выброс исключения №1 if(...) THROW(2); <- выброс исключения №2 } CATCH(1) {...} <- локальный обработчик для THROW(1) // а для THROW(2)-то нету!!! Поэтому этот выброс должен быть обработан на более глобальном уровне: // управление передастся в func2, там нет обработчиков, потом в func1, там тоже нет, // и затем - в main() как последнюю инстанцию (в main() __ECBList будет равен NULL - нет функций "выше" данной) } Теперь самое жирное: необходимо написать весь блок TRY-THROW-CATCH в своем естественном виде do { __sECB ECB; ECB.PrevECB = __ECBList; __ECBList = &ECB; u32 Reason = setjmp(__ECBList->Context); if(Reason) __ECBList = ECB.PrevECB; switch(Reason) { case 0: while(1) { { longjmp(__ECBList->Context, 1); } break; case 1: { printf("Exception catched!\n"); } break; } if(!Reason) __ECBList = ECB.PrevECB; break; default: { if(__ECBList != NULL) longjmp(__ECBList->Context, Reason); } } }while(0); Я не зря написал слово блок. Поскольку мы хотим уметь делать вложенные try-catch, то объект текущего блока управления исключениями выделяется локально на стеке. Именно поэтому в конструкции фигурирует do{}while(0). Теперь можно "разрисовать" кто есть кто. Спойлер do { __sECB ECB; ECB.PrevECB = __ECBList; __ECBList = &ECB; u32 Reason = setjmp(__ECBList->Context); if(Reason) __ECBList = ECB.PrevECB; switch(Reason) { case 0: while(1) { { longjmp(__ECBList->Context, 1); } break; case 1: { printf("Exception catched!\n"); } break; } if(!Reason) __ECBList = ECB.PrevECB; break; default: { if(__ECBList != NULL) longjmp(__ECBList->Context, Reason); } } }while(0); Красный цвет - начало блока TRY. Синий - THROW. Оранжевый - CATCH. Зеленый - ENDTRY. Начинается все с выделения на стеке блока управления текущими исключениями __sECB ECB; // создаем ECB для локальных исключений ECB.PrevECB = __ECBList; // запоминаем в TRY-листе последний активный блок __ECBList = &ECB; // устанавливаем текущий блок управления в качестве "верхушки" u32 Reason = setjmp(__ECBList->Context); // ловим исключения if(Reason) __ECBList = ECB.PrevECB; // если есть исключения - возвращаем верхушку обратно на один шаг назад - потому что в CATCH-блоке может быть повторный выброс исключения // или при отсутсвии подходящего локального CATCH управление должно передаться предыдущему блоку CATCH по стеку вызовов Автомат Даффа для того, чтобы "довыполнить" кое-какие дела после любых case switch(Reason) { case 0: while(1) { { longjmp(__ECBList->Context, 1); // выброс исключения } break; case 1: // локальный отлов исключения { printf("Exception catched!\n"); // пишем, что поймали локальное исключение } break; } if(!Reason) __ECBList = ECB.PrevECB; // финт ушами Даффа - возвращаем обратно указатель на текущий блок управления, если исключение не было сформировано break; Ну и окончание блока TRY default: // сюда попадаем, если возникло исключение, которое не было обработано локально { if(__ECBList != NULL) // если сверху по стеку вызовов есть "великий и могучий" блок TRY-CATCH, то... longjmp(__ECBList->Context, Reason); // управление передать ему } } }while(0); Обращаю внимание, что очень важно сохранить синтаксис THROW - longjmp(__ECBList->Context, N) - если возможно, исключение обработается в локальном CATCH, если же нет - в следующем по иерархии (размотка стека вызовов). Проследите (кому интересно) по коду, почему так получается. Теперь можно "обернуть" весь этот мусор в макровызовы #define TRY do{__sECB ECB; ECB.PrevECB = __ECBList; __ECBList = &ECB; u32 Reason = setjmp(__ECBList->Context); if(Reason) __ECBList = ECB.PrevECB; switch(Reason){case 0: while(1){ #define CATCH(X) break; case X: #define ENDTRY break;}if(!Reason) __ECBList = ECB.PrevECB; break; default:{if(__ECBList != NULL) longjmp(__ECBList->Context, Reason);}}}while(0) #define THROW(X) longjmp(__ECBList->Context, X) Вуаля, все проблемы из реализации по ссылке в начале топика исчезли Шаблон TRY { THROW(1); <- будет вызван локальный CATCH(1) (Extern exception catched!) TRY { f(); <- будет вызван объемлющий CATCH(1) (Extern exception catched!) } ENDTRY; } CATCH(1) { printf("Extern exception catched!\n"); } ENDTRY; f() { THROW(1); } и т.д. можно как угодно вкладывать друг в друга с и без обработчиков А вот теперь для меня пока что нерешенный вопрос... Это многопоточность. Я хочу иметь реентерабельные функции, которые могут пользоваться этими макросами потокобезопасно. Имея один глобальный список __ECBList, эта безопасность не обеспечивается. А хотелось бы в зависимости от потока, выполняемого в текущий момент времени, иметь свою копию __ECBList (при этом, для сохранения единообразия написания макросов, эта копия должна иметь одно имя - __ECBList). Вот как это бы сделать - вопрос открытый. Кому интересно покопаться - буду рад предложениям Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
jcxz 232 22 апреля, 2019 Опубликовано 22 апреля, 2019 · Жалоба 15 минут назад, Arlleex сказал: Итак, как мне довелось узнать, существует много различных реализаций исключений на Си, среди которых в первых гуглорядах стоят CException и т.д. И (не соврать бы) по-моему во всех реализациях механизма исключений главным рабочим инструментом являются функции setjmp() и longjmp(). Также существует два "скелета" для этих функций: Ой сколько Вы написали! Я столько читать не осилю. И честно непонятно - зачем столько? Вопрос же простейший. Я частенько пользуюсь такого рода приёмом. Только не через setjmp()/longjmp(), а пишу свою обёртку на асм. Дело выеденного яйца не стоит. Например так: JsonInputParse: PUSH {R0,R2,R4-R11,LR} PUSH {R1} LDR R3, pjmp STR SP, [R3] BL JsonInputParseExec ADD SP, SP, #8 POP {R2,R4-R11,PC} ;__noreturn void JsonInputError(uint); _Z14JsonInputErrorj: LDR R3, pjmp LDR SP, [R3] MOVS R3, R0 POP {R1} POP {R0,R2,R4-R11,LR} B JsonInputErrorExec JsonInputParse сохраняет состояние стека (вызывается сначала) и вызывает начало блока кода JsonInputParseExec из которого надо выходить в разных местах. JsonInputError - восстанавливает стек (вызывается после, в любой точке JsonInputParseExec или вложенных в него функций). Очень удобно, когда нужно зайти в какую-то развесистую обработку чего-то, со множеством уровней вложенности вызовов функций. А потом легко выйти по какому-то условию наружу из этого развесистого. PS: Если в родителе (откуда идёт вызов) и в внутри потомка используется плавучка, то и её контекст тоже нужно сохранить. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Rst7 5 22 апреля, 2019 Опубликовано 22 апреля, 2019 · Жалоба 52 minutes ago, Arlleex said: А вот теперь для меня пока что нерешенный вопрос... Это многопоточность. Я хочу иметь реентерабельные функции, которые могут пользоваться этими макросами потокобезопасно. Имея один глобальный список __ECBList, эта безопасность не обеспечивается. А хотелось бы в зависимости от потока, выполняемого в текущий момент времени, иметь свою копию __ECBList (при этом, для сохранения единообразия написания макросов, эта копия должна иметь одно имя - __ECBList). Добавьте переменную __ECBList в область данных ОС, которая хранит состояние текущего потока. Конкретика реализации зависит от типа ОС. Либо храните его в Thread Local Storage (например, та же FreeRTOS поддерживает это). Это может быть даже банальный массив с обращением к нему __ECBList[GetCurrentThreadId()], где GetCurrentThreadId() - функция, которая вернет номер текущего потока. Вариантов масса. Расскажите Вашу конкретику, какая ОС используется? Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Arlleex 173 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба 8 часов назад, jcxz сказал: Ой сколько Вы написали! Я столько читать не осилю. 8 часов назад, jcxz сказал: Только не через setjmp()/longjmp(), а пишу свою обёртку на асм. Например так: Примерно понял, только есть вопросы: 1. Можете привести пример использования в Си-коде этих оберток? 2. Кто такой pjmp? По этому адресу у Вас сохраняется положение SP перед выполнением JsonInputParseExec(). 3. После вызова JsonInputParseExec() делается ADD SP, SP, #8. Почему? 7 часов назад, Rst7 сказал: Либо храните его в Thread Local Storage (например, та же FreeRTOS поддерживает это). Это может быть даже банальный массив с обращением к нему __ECBList[GetCurrentThreadId()], где GetCurrentThreadId() - функция, которая вернет номер текущего потока. Вариантов масса. Расскажите Вашу конкретику, какая ОС используется? Код RTOS менять, конечно, не очень хотелось бы, но эксперимента ради - можно, думаю (хотя, кажется мне, этого не понадобится). Надо разузнать насчет TLS. Вот массив со списками нравится больше У меня FreeRTOS. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Rst7 5 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба 27 minutes ago, Arlleex said: У меня FreeRTOS. Масса вариантов тогда. TLS какой-никакой есть. Есть Task Tag - тоже можно использовать. Ну и да, опенсорсное ядро вполне можно и пропатчить. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Arlleex 173 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба Только что, Rst7 сказал: Масса вариантов тогда. TLS какой-никакой есть. Есть Task Tag - тоже можно использовать. Ну и да, опенсорсное ядро вполне можно и пропатчить. Спасибо! Появится снова немного свободного времени - займусь этим. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Kabdim 0 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба Осталось сравнить расходы на велосипед и на честные исключения и забить на велосипед. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Arlleex 173 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба 1 час назад, Kabdim сказал: Осталось сравнить расходы на велосипед и на честные исключения и забить на велосипед. Keil, ARMCC default version 5. Ключи: --cpp --exceptions. Оптимизация -О0. Стандартный код инициализации от microlib, при сравнении значения не имеет. #define REG *(volatile u32 *)0x20000000 int main(void) { try { if(REG) throw 1; } catch(int) { return 1; } return 0; } Program Size: Code=6456 RO-data=1164 RW-data=40 ZI-data=4608. int main(void) { TRY { if(REG) THROW(1); } CATCH(1) { return 1; } ENDTRY; return 0; } Program Size: Code=888 RO-data=460 RW-data=24 ZI-data=4096. Те же условия, только оптимизация -O3. Исключения C++: Program Size: Code=6268 RO-data=1164 RW-data=40 ZI-data=4608. Исключения-велосипеды: Program Size: Code=656 RO-data=460 RW-data=24 ZI-data=4096. По скорости даже проверять не буду. Скажите, конечно, что это жалкое подобие "настоящих" исключений - например, нет RTTI. Да оно мне и не нужно - мне нужна основная функциональность - уметь гибко бросать и ловить исключения. И, ИМХО, мне это удалось Осталось изобрести что-то попроще (подобно предложенному jcxz), либо взять как есть (примерно) Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Rst7 5 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба 9 hours ago, Arlleex said: Осталось изобрести что-то попроще (подобно предложенному jcxz), либо взять как есть (примерно) Если Вы посмотрите на внутренности setjmp/longjmp - то они ничем не отличаются, по-большому счету. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Arlleex 173 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба 50 минут назад, Rst7 сказал: Если Вы посмотрите на внутренности setjmp/longjmp - то они ничем не отличаются, по-большому счету. В общем-то да, сегодня смотрел под виртуальным отладчиком. Начал копать в сторону TLS. Не понятен один момент. Утверждается, что TLS - это некий участок памяти, отведенный для каждого потока. В нем хранятся переменные, специфичные для конкретного потока (потому как потоки могут, как в моем случае, пользоваться разными функциями, работающими с глобальными переменными, поэтому из конкретного потока должен быть обеспечен доступ к соответствующей копии этой переменной). Вроде бы все понятно. Дальше мне интересно, как связать с одним именем глобальной переменной две копии такой переменной. Вот пример int TickCounter; void TickFunc(void) { ++TickCounter; return; } void thread1(void) { TickFunc(); if(TickCounter == 100) { TickCounter = 0; // делаем что-то полезное в потоке 1 } ThreadDelay(1); } void thread2(void) { TickFunc(); if(TickCounter == 500) { TickCounter = 0; // делаем что-то полезное в потоке 2 } ThreadDelay(20); } В описании к компилятору фигурирует __declspec(thread), и написано, что при создании потока будет создана своя копия... бла бла бла. Как вообще компилятор может знать, где и какая функция вообще отвечает за создание потока? Реально ли сделать так, что при обращении к TickFunc() вне любой задачи FreeRTOS будет задействована глобальная переменная TickCounter, а при вызове этой функции внутри потока - будет использована соответствующая копия внутри TLS? Или это уже фантастика? Нашел функции vTaskSetThreadLocalStoragePointer() и vTaskGetThreadLocalStoragePointer(), которые устанавливают и получают указатель по индексу в TLS. Но не понятно что мне с этим делать? Создавать руками две глобальные переменные, для двух потоков, например, а потом в одном потоке вызывать vTaskSetThreadLocalStoragePointer() и назначить ему адрес одной переменной, а в другом потоке вызвать ее же и назначить адрес другой переменной-копии? Да тогда я и просто __ECBList[GetTaskID()] обошелся бы... P.S. Что-то под вечер уже не соображаю Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Rst7 5 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба Ну Вам просто надо сделать вот что. Вместо int TickCounter; написать #define TickCounter ((int*)pvTaskGetThreadLocalStoragePointer(NULL,TICK_COUNTER_INDEX)[0] И Ваш код заработает. Ну да, надо еще инициализацию сделать в каждом потоке. #define InitTickCounter vTaskSetThreadLocalStoragePointer(NULL,TICK_COUNTER_INDEX,(void*)malloc(sizeof(int))) Тут, конечно, круто использовать alloca() и инитить переменную прямо в стеке, если Ваш компилятор это позволяет. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Rst7 5 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба 35 minutes ago, Arlleex said: Или это уже фантастика? Если вот так сделать, как выше - то все зависит от поведения pvTaskGetThreadLocalStoragePointer. Если его можно позвать не в потоке - то вполне. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
jcxz 232 23 апреля, 2019 Опубликовано 23 апреля, 2019 · Жалоба 14 часов назад, Arlleex сказал: 1. Можете привести пример использования в Си-коде этих оберток? Я там случайно обрезал нижние строки из асм-вставки (уродский новый движок форума!). Полнее будет так: Спойлер PUBLIC JsonInputParse, _Z14JsonInputErrorj EXTERN JsonInputParseExec, JsonInputErrorExec THUMB ;u64 JsonInputParse(JsonInput *, void const *, void const *); JsonInputParse: PUSH {R0,R2,R4-R11,LR} PUSH {R1} LDR R3, pjmp STR SP, [R3] BL JsonInputParseExec ADD SP, SP, #8 POP {R2,R4-R11,PC} ;__noreturn void JsonInputError(uint); _Z14JsonInputErrorj: LDR R3, pjmp LDR SP, [R3] MOVS R3, R0 POP {R1} POP {R0,R2,R4-R11,LR} B JsonInputErrorExec EXTERN jsonInputJmp DATA pjmp DC32 jsonInputJmp где: extern "C" u64 JsonInputParse(JsonInput *, void const *, void const *); extern "C" u64 JsonInputErrorExec(JsonInput *, u8 const *, u8 const *, uint); extern "C" u64 JsonInputParseExec(JsonInput *, void const *, void const *); struct JsonInputJmp { u32 sp; }; extern "C" JsonInputJmp jsonInputJmp; Т.е. - JsonInputJmp просто хранит SP, к которому нужно будет вернуться. Начинается работа парсера с вызова: u64 q = JsonInputParse(&d.sh.jsi, s, se); s и se - указатели на начало и конец распарсиваемого фрагмента JSON-тела. Внутри выполняется развесистая обработка с множеством ветвлений, машиной состояний, вызовом множества функций. Которые парсят входящий JSON, в разных точках могут обнаруживать ошибки и при этом должны вызвать функцию JsonInputError() передав ей код и место ошибки (между s и se) в аргументе. Она восстановит исходное положение стека из JsonInputJmp, затем вызовет си-шную JsonInputError(), которая обработает ошибку (сформирует JSON-сообщение об ошибке с указанием её места в исходном фрагменте JSON) и вернётся в точку после вызова JsonInputParse() так, как будто был штатный выход из функции без ошибки (но вернув в q состояние ошибки). Стек не нарушится. Если же JsonInputParse() выполнила работу обработав фрагмент s...se без ошибок, то она просто выполнит return вернув в q состояние "Всё ок". В принципе - если внутри всей этой работы внутри JsonInputParse() JsonInput * (из 1-го аргумента) существует везде, то можно структуру JsonInputJmp хранить внутри JsonInput (или породить последнюю из первой). И таким образом избавиться от статической jsonInputJmp. Но мне это в данном случае не нужно. setjmp()/longjmp() неудобны тем, что требуют штатного выхода из функции тем же способом, что и при нештатном, резервируя под это одно значение аргумента. Кроме того: не позволяют передавать дополнительные аргументы. Да и вообще - накладывают кучу ненужных ограничений. Зачем эти ограничения, если мы всё это может сделать сами так, как наиболее удобно для данного конкретного места? Здесь это у меня реализовано так, в другом месте я реализовал по другому (как там удобнее). И не связан никакими искусственными ограничениями. Цитата 3. После вызова JsonInputParseExec() делается ADD SP, SP, #8. Почему? Снимаются со стека ненужные в точке возврата из JsonInputParse() значения первых двух её аргументов. Они туда сохраняются для функции JsonInputErrorExec(), которая по ним строит сообщение о локализации ошибки в входящем фрагменте JSON. А в точке возврата они уже не нужны - там и так знают что передавали внутрь. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться