Практическое использование setjmp и longjmp в C
Может кто-нибудь объяснить мне, где именно setjmp()
а также longjmp()
функции можно использовать практически во встроенном программировании? Я знаю, что это для обработки ошибок. Но я хотел бы знать некоторые варианты использования.
7 ответов
Обработка ошибок
Предположим, что есть ошибка глубоко внутри функции, вложенной во многие другие функции, и обработка ошибок имеет смысл только в функции верхнего уровня.
Было бы очень утомительно и неловко, если бы все промежуточные функции возвращались нормально и оценивали возвращаемые значения или глобальную переменную ошибки, чтобы определить, что дальнейшая обработка не имеет смысла или даже будет плохой.
Это ситуация, в которой setjmp/longjmp имеет смысл. Эти ситуации аналогичны ситуации, в которой имеет место исключение в других языках (C++, Java).
Сопрограммы
Помимо обработки ошибок, я могу подумать и о другой ситуации, когда вам нужен setjmp/longjmp в C:
Это тот случай, когда нужно реализовать сопрограммы.
Вот небольшой демонстрационный пример. Я надеюсь, что он удовлетворит запрос Sivaprasad Palas для некоторого примера кода и ответит на вопрос TheBlastOne о том, как setjmp/longjmp поддерживает реализацию сопрограмм (насколько я вижу, он не основан на каком-либо нестандартном или новом поведении).
РЕДАКТИРОВАТЬ:
Может случиться так, что на самом деле это неопределенное поведение, чтобы сделать longjmp
вниз по стеку вызовов (см. комментарий MikeMB; хотя у меня еще не было возможности проверить это).
#include <stdio.h>
#include <setjmp.h>
jmp_buf bufferA, bufferB;
void routineB(); // forward declaration
void routineA()
{
int r ;
printf("(A1)\n");
r = setjmp(bufferA);
if (r == 0) routineB();
printf("(A2) r=%d\n",r);
r = setjmp(bufferA);
if (r == 0) longjmp(bufferB, 20001);
printf("(A3) r=%d\n",r);
r = setjmp(bufferA);
if (r == 0) longjmp(bufferB, 20002);
printf("(A4) r=%d\n",r);
}
void routineB()
{
int r;
printf("(B1)\n");
r = setjmp(bufferB);
if (r == 0) longjmp(bufferA, 10001);
printf("(B2) r=%d\n", r);
r = setjmp(bufferB);
if (r == 0) longjmp(bufferA, 10002);
printf("(B3) r=%d\n", r);
r = setjmp(bufferB);
if (r == 0) longjmp(bufferA, 10003);
}
int main(int argc, char **argv)
{
routineA();
return 0;
}
На следующем рисунке показан ход выполнения:
Предупреждение примечание
При использовании setjmp/longjmp следует помнить, что они влияют на достоверность локальных переменных, которые часто не учитываются.
Ср мой вопрос по этой теме.
Теория заключается в том, что вы можете использовать их для обработки ошибок, чтобы вы могли выпрыгнуть из глубоко вложенной цепочки вызовов, не сталкиваясь с обработкой ошибок в каждой функции в цепочке.
Как и любая умная теория, она разваливается при встрече с реальностью. Ваши промежуточные функции будут распределять память, захватывать блокировки, открывать файлы и выполнять различные действия, требующие очистки. Так на практике setjmp
/longjmp
Обычно это плохая идея, за исключением очень ограниченных обстоятельств, когда у вас есть полный контроль над вашей средой (некоторые встроенные платформы).
По моему опыту, в большинстве случаев, когда вы думаете, что с помощью setjmp
/longjmp
сработает, ваша программа достаточно ясна и проста, так что каждый промежуточный вызов функции в цепочке вызовов может выполнять обработку ошибок, или это настолько грязно и невозможно исправить, что вы должны сделать exit
когда вы сталкиваетесь с ошибкой.
Я написал Java-подобный механизм обработки исключений в C, используя setjmp()
, longjmp()
и системные функции. Он ловит пользовательские исключения, но также и сигналы, как SIGSEGV
, Он имеет бесконечное вложение блоков обработки исключений, который работает через вызовы функций и поддерживает две наиболее распространенные реализации потоков. Это позволяет вам определить древовидную иерархию классов исключений, которые имеют наследование во время соединения, и catch
оператор проходит по этому дереву, чтобы увидеть, нужно ли его поймать или передать.
Вот пример того, как код выглядит с использованием этого:
try
{
*((int *)0) = 0; /* may not be portable */
}
catch (SegmentationFault, e)
{
long f[] = { 'i', 'l', 'l', 'e', 'g', 'a', 'l' };
((void(*)())f)(); /* may not be portable */
}
finally
{
return(1 / strcmp("", ""));
}
И вот часть включаемого файла, который содержит много логики:
#ifndef _EXCEPT_H
#define _EXCEPT_H
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include "Lifo.h"
#include "List.h"
#define SETJMP(env) sigsetjmp(env, 1)
#define LONGJMP(env, val) siglongjmp(env, val)
#define JMP_BUF sigjmp_buf
typedef void (* Handler)(int);
typedef struct _Class *ClassRef; /* exception class reference */
struct _Class
{
int notRethrown; /* always 1 (used by throw()) */
ClassRef parent; /* parent class */
char * name; /* this class name string */
int signalNumber; /* optional signal number */
};
typedef struct _Class Class[1]; /* exception class */
typedef enum _Scope /* exception handling scope */
{
OUTSIDE = -1, /* outside any 'try' */
INTERNAL, /* exception handling internal */
TRY, /* in 'try' (across routine calls) */
CATCH, /* in 'catch' (idem.) */
FINALLY /* in 'finally' (idem.) */
} Scope;
typedef enum _State /* exception handling state */
{
EMPTY, /* no exception occurred */
PENDING, /* exception occurred but not caught */
CAUGHT /* occurred exception caught */
} State;
typedef struct _Except /* exception handle */
{
int notRethrown; /* always 0 (used by throw()) */
State state; /* current state of this handle */
JMP_BUF throwBuf; /* start-'catching' destination */
JMP_BUF finalBuf; /* perform-'finally' destination */
ClassRef class; /* occurred exception class */
void * pData; /* exception associated (user) data */
char * file; /* exception file name */
int line; /* exception line number */
int ready; /* macro code control flow flag */
Scope scope; /* exception handling scope */
int first; /* flag if first try in function */
List * checkList; /* list used by 'catch' checking */
char* tryFile; /* source file name of 'try' */
int tryLine; /* source line number of 'try' */
ClassRef (*getClass)(void); /* method returning class reference */
char * (*getMessage)(void); /* method getting description */
void * (*getData)(void); /* method getting application data */
void (*printTryTrace)(FILE*);/* method printing nested trace */
} Except;
typedef struct _Context /* exception context per thread */
{
Except * pEx; /* current exception handle */
Lifo * exStack; /* exception handle stack */
char message[1024]; /* used by ExceptGetMessage() */
Handler sigAbrtHandler; /* default SIGABRT handler */
Handler sigFpeHandler; /* default SIGFPE handler */
Handler sigIllHandler; /* default SIGILL handler */
Handler sigSegvHandler; /* default SIGSEGV handler */
Handler sigBusHandler; /* default SIGBUS handler */
} Context;
extern Context * pC;
extern Class Throwable;
#define except_class_declare(child, parent) extern Class child
#define except_class_define(child, parent) Class child = { 1, parent, #child }
except_class_declare(Exception, Throwable);
except_class_declare(OutOfMemoryError, Exception);
except_class_declare(FailedAssertion, Exception);
except_class_declare(RuntimeException, Exception);
except_class_declare(AbnormalTermination, RuntimeException); /* SIGABRT */
except_class_declare(ArithmeticException, RuntimeException); /* SIGFPE */
except_class_declare(IllegalInstruction, RuntimeException); /* SIGILL */
except_class_declare(SegmentationFault, RuntimeException); /* SIGSEGV */
except_class_declare(BusError, RuntimeException); /* SIGBUS */
#ifdef DEBUG
#define CHECKED \
static int checked
#define CHECK_BEGIN(pC, pChecked, file, line) \
ExceptCheckBegin(pC, pChecked, file, line)
#define CHECK(pC, pChecked, class, file, line) \
ExceptCheck(pC, pChecked, class, file, line)
#define CHECK_END \
!checked
#else /* DEBUG */
#define CHECKED
#define CHECK_BEGIN(pC, pChecked, file, line) 1
#define CHECK(pC, pChecked, class, file, line) 1
#define CHECK_END 0
#endif /* DEBUG */
#define except_thread_cleanup(id) ExceptThreadCleanup(id)
#define try \
ExceptTry(pC, __FILE__, __LINE__); \
while (1) \
{ \
Context * pTmpC = ExceptGetContext(pC); \
Context * pC = pTmpC; \
CHECKED; \
\
if (CHECK_BEGIN(pC, &checked, __FILE__, __LINE__) && \
pC->pEx->ready && SETJMP(pC->pEx->throwBuf) == 0) \
{ \
pC->pEx->scope = TRY; \
do \
{
#define catch(class, e) \
} \
while (0); \
} \
else if (CHECK(pC, &checked, class, __FILE__, __LINE__) && \
pC->pEx->ready && ExceptCatch(pC, class)) \
{ \
Except *e = LifoPeek(pC->exStack, 1); \
pC->pEx->scope = CATCH; \
do \
{
#define finally \
} \
while (0); \
} \
if (CHECK_END) \
continue; \
if (!pC->pEx->ready && SETJMP(pC->pEx->finalBuf) == 0) \
pC->pEx->ready = 1; \
else \
break; \
} \
ExceptGetContext(pC)->pEx->scope = FINALLY; \
while (ExceptGetContext(pC)->pEx->ready > 0 || ExceptFinally(pC)) \
while (ExceptGetContext(pC)->pEx->ready-- > 0)
#define throw(pExceptOrClass, pData) \
ExceptThrow(pC, (ClassRef)pExceptOrClass, pData, __FILE__, __LINE__)
#define return(x) \
{ \
if (ExceptGetScope(pC) != OUTSIDE) \
{ \
void * pData = malloc(sizeof(JMP_BUF)); \
ExceptGetContext(pC)->pEx->pData = pData; \
if (SETJMP(*(JMP_BUF *)pData) == 0) \
ExceptReturn(pC); \
else \
free(pData); \
} \
return x; \
}
#define pending \
(ExceptGetContext(pC)->pEx->state == PENDING)
extern Scope ExceptGetScope(Context *pC);
extern Context *ExceptGetContext(Context *pC);
extern void ExceptThreadCleanup(int threadId);
extern void ExceptTry(Context *pC, char *file, int line);
extern void ExceptThrow(Context *pC, void * pExceptOrClass,
void *pData, char *file, int line);
extern int ExceptCatch(Context *pC, ClassRef class);
extern int ExceptFinally(Context *pC);
extern void ExceptReturn(Context *pC);
extern int ExceptCheckBegin(Context *pC, int *pChecked,
char *file, int line);
extern int ExceptCheck(Context *pC, int *pChecked, ClassRef class,
char *file, int line);
#endif /* _EXCEPT_H */
Есть также модуль C, который содержит логику для обработки сигналов и некоторый учет.
Это было чрезвычайно сложно реализовать, я могу вам сказать, и я почти ушел. Я действительно старался сделать его как можно ближе к Java; Мне показалось удивительным, как далеко я продвинулся только с C.
Дайте мне крик, если вам интересно.
Сочетание setjmp
а также longjmp
это "супер сила goto
". Используйте с особой осторожностью. Однако, как объяснили другие, longjmp
очень полезно выйти из неприятной ситуации с ошибками, когда вы хотите get me back to the beginning
быстро, вместо того, чтобы выводить сообщение об ошибке для 18 уровней функций.
Тем не менее, так же, как goto
, но хуже того, вы должны быть ДЕЙСТВИТЕЛЬНО осторожны, как вы используете это. longjmp
просто вернет вас к началу кода. Это не повлияет на все остальные состояния, которые могли измениться между setjmp
и вернуться туда, где setjmp
началось. Таким образом, распределения, блокировки, полуинициализированные структуры данных и т. Д. По-прежнему распределяются, блокируются и частично инициализируются, когда вы возвращаетесь туда, где setjmp
назывался. Это означает, что вы должны действительно заботиться о местах, где вы делаете это, что это действительно нормально звонить longjmp
не вызывая БОЛЬШЕ проблем. Конечно, если следующая вещь, которую вы делаете, это "перезагрузка" [после сохранения сообщения об ошибке, возможно] - во встроенной системе, где вы обнаружили, например, что оборудование находится в плохом состоянии, тогда все в порядке.
Я также видел setjmp
/longjmp
используется для обеспечения очень основных механизмов потоков. Но это довольно частный случай - и определенно не то, как работают "стандартные" потоки.
Изменить: Конечно, можно добавить код для "очистки", точно так же, как C++ сохраняет точки исключения в скомпилированном коде, а затем знает, что дало исключение и что нужно очистить. Это может включать в себя какую-то таблицу указателей на функции и сохранение "если мы выпрыгнем снизу, вызовите эту функцию с этим аргументом". Что-то вроде этого:
struct
{
void (*destructor)(void *ptr);
};
void LockForceUnlock(void *vlock)
{
LOCK* lock = vlock;
}
LOCK func_lock;
void func()
{
ref = add_destructor(LockForceUnlock, mylock);
Lock(func_lock)
...
func2(); // May call longjmp.
Unlock(func_lock);
remove_destructor(ref);
}
С помощью этой системы вы можете выполнить "полную обработку исключений, как в C++". Но это довольно грязно, и полагается на хорошо написанный код.
setjmp
а также longjmp
может быть очень полезным в модульном тестировании.
Предположим, мы хотим протестировать следующий модуль:
#include <stdlib.h>
int my_div(int x, int y)
{
if (y==0) exit(2);
return x/y;
}
Обычно, если функция для тестирования вызывает другую функцию, вы можете объявить функцию-заглушку для ее вызова, которая будет имитировать то, что реальная функция делает для проверки определенных потоков. Однако в этом случае функция вызывает exit
который не возвращается. Заглушка должна как-то подражать этому поведению. setjmp
а также longjmp
могу сделать это для вас.
Чтобы протестировать эту функцию, мы можем создать следующую тестовую программу:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <setjmp.h>
// redefine assert to set a boolean flag
#ifdef assert
#undef assert
#endif
#define assert(x) (rslt = rslt && (x))
// the function to test
int my_div(int x, int y);
// main result return code used by redefined assert
static int rslt;
// variables controling stub functions
static int expected_code;
static int should_exit;
static jmp_buf jump_env;
// test suite main variables
static int done;
static int num_tests;
static int tests_passed;
// utility function
void TestStart(char *name)
{
num_tests++;
rslt = 1;
printf("-- Testing %s ... ",name);
}
// utility function
void TestEnd()
{
if (rslt) tests_passed++;
printf("%s\n", rslt ? "success" : "fail");
}
// stub function
void exit(int code)
{
if (!done)
{
assert(should_exit==1);
assert(expected_code==code);
longjmp(jump_env, 1);
}
else
{
_exit(code);
}
}
// test case
void test_normal()
{
int jmp_rval;
int r;
TestStart("test_normal");
should_exit = 0;
if (!(jmp_rval=setjmp(jump_env)))
{
r = my_div(12,3);
}
assert(jmp_rval==0);
assert(r==4);
TestEnd();
}
// test case
void test_div0()
{
int jmp_rval;
int r;
TestStart("test_div0");
should_exit = 1;
expected_code = 2;
if (!(jmp_rval=setjmp(jump_env)))
{
r = my_div(2,0);
}
assert(jmp_rval==1);
TestEnd();
}
int main()
{
num_tests = 0;
tests_passed = 0;
done = 0;
test_normal();
test_div0();
printf("Total tests passed: %d\n", tests_passed);
done = 1;
return !(tests_passed == num_tests);
}
В этом примере вы используете setjmp
перед входом в функцию для проверки, потом в заглушку exit
ты звонишь longjmp
чтобы вернуться обратно к вашему тестовому кейсу.
Также обратите внимание, что переопределено exit
имеет специальную переменную, которая проверяет, действительно ли вы хотите выйти из программы и вызывает _exit
сделать это. Если вы этого не сделаете, ваша тестовая программа может завершиться некорректно.
Поскольку вы упоминаете встроенный, я думаю, что стоит отметить случай неиспользования: когда ваш стандарт кодирования запрещает это. Например, MISRA (MISRA-C:2004: правило 20.7) и JFS (AV-правило 20): "Макрос setjmp и функция longjmp не должны использоваться".
Несомненно, наиболее важным применением setjmp/longjmp является то, что он выполняет "нелокальный переход". Команда Goto (и есть редкие случаи, когда вам нужно будет использовать циклы goto over for и while) наиболее безопасно и безопасно используется в той же области. Если вы используете goto для перехода между областями видимости (или автоматическим распределением), вы, скорее всего, повредите стек своей программы. setjmp/longjmp позволяет избежать этого, сохраняя информацию стека в том месте, куда вы хотите перейти. Затем, когда вы прыгаете, он загружает эту информацию о стеке. Без этой функции программистам на C, скорее всего, пришлось бы обратиться к программированию на ассемблере для решения проблем, которые мог решить только setjmp/longjmp. Слава богу, есть. Все в библиотеке C чрезвычайно важно. Вы будете знать, когда вам это нужно.
Помимо обработки ошибок, еще одна вещь, которую вы можете сделать и о которой ранее не упоминалось, - это разумно реализовать хвостовое рекурсивное вычисление в C.
Именно так реализованы продолжения в C без преобразования входного кода в стиль передачи продолжения.
setjmp() и longjmp() полезны для обработки ошибок и прерываний, встречающихся в низкоуровневой подпрограмме программы.