Jump to content

    

Механизм исключений на чистом Си

Знаю тема эта не нова, даже где-то обсуждалась, но вскользь.

Хочу поделиться некоторыми соображениями по поводу механизма исключений на чистом Си, привести свою реализацию.

Топик не для начинающих и, скорее, рассчитан на подготовленного читателя-фаната, поэтому за возможно причиненные морально-нравственные страдания автор ответственности не несет:biggrin:

Также хочу отметить, что я не претендую на безоговорочную истину всего сказанного: все, что мне довелось получить - это результат пары-тройки часов колупания над готовыми решениями (которые, как думаю уже понятно, меня не сильно радовали).

 

Итак, как мне довелось узнать, существует много различных реализаций исключений на Си, среди которых в первых гуглорядах стоят 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++. Ладно, напишем свое... с блэкджеком и автоматами Даффа:biggrin:

Первым делом, подразумевая все изложенные проблемы кода выше, набросаем скелет программы, при котором убьются все эти проблемы сразу одним махом. Для этого определим некую структуру - блок управления исключениями

// 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)

 

Вуаля, все проблемы из реализации по ссылке в начале топика исчезли:dance2:

Шаблон

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). Вот как это бы сделать - вопрос открытый. Кому интересно покопаться - буду рад предложениям:hi:

Share this post


Link to post
Share on other sites
15 минут назад, Arlleex сказал:

Итак, как мне довелось узнать, существует много различных реализаций исключений на Си, среди которых в первых гуглорядах стоят CException и т.д. И (не соврать бы) по-моему во всех реализациях механизма исключений главным рабочим инструментом являются функции setjmp() и longjmp(). Также существует два "скелета" для этих функций:

Ой сколько Вы написали! Я столько читать не осилю. И честно непонятно - зачем столько? Вопрос же простейший.

Я частенько пользуюсь такого рода приёмом. Только не через setjmp()/longjmp(), а пишу свою обёртку на асм. Дело выеденного яйца не стоит.  :wink2:

Например так:

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: Если в родителе (откуда идёт вызов) и в внутри потомка используется плавучка, то и её контекст тоже нужно сохранить.

Share this post


Link to post
Share on other sites
52 minutes ago, Arlleex said:

А вот теперь для меня пока что нерешенный вопрос... Это многопоточность. Я хочу иметь реентерабельные функции, которые могут пользоваться этими макросами потокобезопасно. Имея один глобальный список __ECBList, эта безопасность не обеспечивается. А хотелось бы в зависимости от потока, выполняемого в текущий момент времени, иметь свою копию __ECBList (при этом, для сохранения единообразия написания макросов, эта копия должна иметь одно имя - __ECBList).

Добавьте переменную __ECBList в область данных ОС, которая хранит состояние текущего потока. Конкретика реализации зависит от типа ОС.

 

Либо храните его в Thread Local Storage (например, та же FreeRTOS поддерживает это).

 

Это может быть даже банальный массив с обращением к нему __ECBList[GetCurrentThreadId()], где GetCurrentThreadId() - функция, которая вернет номер текущего потока.

 

Вариантов масса. Расскажите Вашу конкретику, какая ОС используется?

Share this post


Link to post
Share on other sites
8 часов назад, jcxz сказал:

Ой сколько Вы написали! Я столько читать не осилю.

:biggrin:

 

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.

Вот массив со списками нравится больше:smile:

У меня FreeRTOS.

Share this post


Link to post
Share on other sites
27 minutes ago, Arlleex said:

У меня FreeRTOS.

Масса вариантов тогда. TLS какой-никакой есть. Есть Task Tag - тоже можно использовать. Ну и да, опенсорсное ядро вполне можно и пропатчить.

Share this post


Link to post
Share on other sites
Только что, Rst7 сказал:

Масса вариантов тогда. TLS какой-никакой есть. Есть Task Tag - тоже можно использовать. Ну и да, опенсорсное ядро вполне можно и пропатчить.

Спасибо!

Появится снова немного свободного времени - займусь этим.

Share this post


Link to post
Share on other sites

Осталось сравнить расходы на велосипед и на честные исключения и забить на велосипед.

Share this post


Link to post
Share on other sites
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. Да оно мне и не нужно - мне нужна основная функциональность - уметь гибко бросать и ловить исключения. И, ИМХО, мне это удалось:bb:

 

Осталось изобрести что-то попроще (подобно предложенному jcxz), либо взять как есть (примерно):wink:

Share this post


Link to post
Share on other sites
9 hours ago, Arlleex said:

Осталось изобрести что-то попроще (подобно предложенному jcxz), либо взять как есть (примерно)

Если Вы посмотрите на внутренности setjmp/longjmp - то они ничем не отличаются, по-большому счету.

Share this post


Link to post
Share on other sites
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. Что-то под вечер уже не соображаю:to_take_umbrage:

Share this post


Link to post
Share on other sites

Ну Вам просто надо сделать вот что.

Вместо 

int TickCounter;

написать

#define TickCounter ((int*)pvTaskGetThreadLocalStoragePointer(NULL,TICK_COUNTER_INDEX)[0]

И Ваш код заработает. Ну да, надо еще инициализацию сделать в каждом потоке.

#define InitTickCounter vTaskSetThreadLocalStoragePointer(NULL,TICK_COUNTER_INDEX,(void*)malloc(sizeof(int)))

Тут, конечно, круто использовать alloca() и инитить переменную прямо в стеке, если Ваш компилятор это позволяет.

 

Share this post


Link to post
Share on other sites
35 minutes ago, Arlleex said:

Или это уже фантастика?

Если вот так сделать, как выше - то все зависит от поведения  pvTaskGetThreadLocalStoragePointer. Если его можно позвать не в потоке - то вполне.

Share this post


Link to post
Share on other sites
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. А в точке возврата они уже не нужны - там и так знают что передавали внутрь.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now