Ограничение, согласно которому goto не может использоваться для перехода между функциями, накладывается в языке C из-за того, что все функции в C находятся на одном и том же уровне области видимости (то есть стандарт языка C не предусматривает вложенных объявлений функций, хотя gcc допускает такую возможность в качестве расширения). Следовательно, если взять две функции, X и Y, то у компилятора не будет возможности узнать, может ли стековый фрейм для функции X быть в стеке ко времени вызова функции Y, а стало быть, возможен ли переход из функции Y в функцию X.
В таких языках, как Pascal, где объявления функций могут быть вложенными и переход из вложенной функции на уровень выше разрешен, статическая область видимости функции позволяет компилятору определить информацию о динамической области видимости этой функции. Таким образом, если функция Y лексически вложена в функцию X, то компилятор знает, что стековый фрейм для X должен уже быть в стеке ко времени вызова функции Y, и может сгенерировать код для перехода из функции Y в какое-либо место внутри функции X.
#include <setjmp.h>
int setjmp(jmp_buf env);
Возвращает 0 при первом вызове, ненулевое значение при возвращении через longjmp()
void longjmp(jmp_buf env, int val);
Вызов setjmp() устанавливает цель для последующего перехода, выполняемого функцией longjmp(). Этой целью является та самая точка в программе, откуда вызывается функция setjmp(). С точки зрения программирования после longjmp() это выглядит абсолютно так же, как будто мы только что вернулись из вызова setjmp() во второй раз. Способ, позволяющий отличить второе «возвращение» от первого, основан на целочисленном значении, возвращаемом функцией setjmp(). При первом вызове setjmp() возвращает 0, а при последующем, фиктивном возвращении предоставляется то значение, которое указано в аргументе val при вызове функции longjmp(). Путем использования для аргумента val различных значений можно отличать друг от друга переходы к одной и той же цели из различных мест программы.
Если бесконтрольно указать для функции longjmp() аргумент val, равный нулю, то это вызовет фиктивное возвращение из setjmp(), которое будет выглядеть, как будто это первое возвращение. По этой причине, если для val указано значение 0, longjmp() фактически применяет значение 1.
Используемый обеими функциями аргумент env предоставляет связующий элемент, позволяющий осуществить переход. При вызове setjmp() в env сохраняется различная информация о среде текущего процесса. Это позволяет выполнить вызов longjmp(), которому для осуществления фиктивного возвращения нужно указать ту же переменную env. Поскольку вызовы setjmp() и longjmp() — различные функции (в противном случае мы могли бы обойтись и простой инструкцией goto), env объявляется глобально или, что менее распространено, передается в качестве аргумента функции.
На момент вызова setjmp() в env наряду с другой информацией хранятся копии регистра счетчика команд (он указывает на тот машинный код, который выполняется в данный момент) и регистра указателя стека (где отмечается вершина стека). Эта информация позволяет осуществить последующий вызов longjmp(), чтобы выполнить два основных действия.
• Удалить из стека стековые фреймы для всех промежуточных функций между функцией, вызвавшей longjmp(), и функцией, которая перед этим вызвала setjmp(). Эту процедуру иногда называют «раскруткой стека», и она выполняется путем сброса регистра указателя стека и присвоением ему значения, сохраненного в аргументе env.
• Установить такое значение регистра, чтобы выполнение программы продолжалось с места предварительного вызова setjmp(). Это действие также выполняется с использованием значения, сохраненного в env.
Пример программы
Применение функций setjmp() и longjmp() показано в листинге 6.5. Эта программа с помощью предварительного вызова setjmp() устанавливает цель перехода. Дальнейшее использование инструкции switch (на основе значения, возвращаемого setjmp()) позволяет различить первоначальный возврат из функции setjmp() и возврат после longjmp(). Если возвращаемое значение равно 0, значит, только что был сделан первоначальный вызов setjmp() — мы вызываем функцию f1(), которая либо сразу же вызывает longjmp(), либо переходит к вызову f2(), в зависимости от значения argc (то есть количества аргументов командной строки). Если управление перешло в f2(), в ней тут же происходит вызов longjmp(). Вызов функции longjmp() из любой функции возвращает нас назад, к тому месту, из которого была вызвана setjmp(). В двух вызовах longjmp() используются разные аргументы val, поэтому инструкция switch в main() может определить функцию, из которой произошел переход, и вывести на экран соответствующее сообщение.