Эмуляция обработки исключений в ANSI C'89

Модераторы: Hawk, Romeo, Absurd, DeeJayC, WinMain

sergey_kovtunenko
Сообщения: 6
Зарегистрирован: 26 авг 2007, 15:35

26 авг 2007, 15:43

Привет! Пришла идея написать эмуляцию обработки исключительных ситуаций для ANSI C. Это должна быть высокопортируемая библиотека макроопределений, использующая достоинства (и недостатки нелокальных переходов в С -- setjmp()\longjmp()). Насколько мне известно, готовых решений на pure ANSI C на данный момент нет. По крайней мере гугление ни к чему не привело.
При написании библиотеки столкнулся с рядом проблем, которые мне трудно решить. Поэтому публикую исходный код тестовой версии библиотеки. Я использую ANSI C'99 совместимый компилятор пока (в конце вычищу код для C'89), поэтому по ходу могут встречаться комментарии в стиле С++.
В тестовой версии библиотеки все глобальные переменные прямо лежат внутри заголовочного файла. Это сделано для упрощения отладки. Т.е. грубо говоря, эта версия всего лишь модель. После получения работоспособной модели все глобальные переменные будут локализованы в исходном файле библиотеки, будут статическими и доступ к ним будет осуществляться только через функции.
Цель создания библиотеки:
* Получить преимущества обработки исключений в языке С. О различиях между SEH и обычным подходом проверки возвращаемых значений функцией можно почитать у Б. Страуструпа в 14 главе русского "Язык программирования С++"
* Получить высокопортируемую библиотеку, которую можно будет использовать на любой платформе, где есть стандартная библиотека С.
Из недостатков можно выделить:
* _Синтаксическую_ (но не идеологическую) несовместимость с С++ ;(
* Незащищённость от многопоточночти из-за применения статических переменных. В планах, после завершения варианта для однопоточных переменных приступить к реализации версии, корректно работающей с многопоточными приложениями

Общая Концепция библиотеки состоит в следующем:

Код: Выделить всё

//..............
TRY {
   // тестируемый код1
   TRY {
      // вложенный тестируемый код2
      THROW (100);
      // продолжение вложенного кода, который не выполнится
   }
   CATCH (200) {
      // обработчик 1
   }
   CATCH (100) {
      // обработчик 2, в который мы попадём
      THROW (10);
      // код, который никогда не выполнится
    }
    CATCHALL {
      // код, обрабатывающий все остальные ситуации
    }
    TRYEND; // конец вложенного обработчика
}
CATCHALL {
   // обработка всех исключительных ситуаций. Сюда мы попадём по строчке THROW (10);
}
TRYEND;
// ................

Код: Выделить всё

/* excpt.h -- тестовая версия заголовочного файла */

#ifndef N0PEXCEPT_H_
#	define N0PEXCEPT_H_

#	ifdef __cplusplus
extern "C" {
#	endif

#	include <stdio.h>
#	include <setjmp.h>

	/* Включение или выключение кода обработки исключений. Для выключения
		необходимо закоментировать строчку */
#	define N0PEXCEPT_ON_

	/* Максимальный уровень вложений обработчика исключений */
#	define MAX_NESTED_LEVEL_ 40

//------------------------------------------------------------------------------

/* Состояние текущего прерывания */
typedef struct EXCPT_T EXCPT_T;
struct EXCPT_T {
	/* Буффер для сохранения состояния */
	jmp_buf jbuf;
	/* Код возврата из setjmp() */
	int ret;
}; /* EXCPT_T */

/* Текущий глобальный уровень вложенности обработчиков исключений */
int i = -1;

/* Текущий локальный уровень вложенности обработчиков исключений */
int j = -1;

/* Массив структур состояний */
EXCPT_T jbuf[MAX_NESTED_LEVEL_];

//------------------------------------------------------------------------------




#	ifdef N0PEXCEPT_ON_

#define DBGTRYIN(func, num)			\
	printf("i=%d, j = %d >>> " #func ".TRY." #num " >>>\n", i, j)
#define DBGTRYOUT(func, num)			\
	printf("i=%d, j = %d <<< " #func ".TRY." #num " <<<\n", i, j)
#define DBGCATCHIN(func, num, val)	\
	printf("\ti=%d, j = %d >>> " #func ".CATCH(" #val ")." #num " >>>\n", i, j)
#define DBGCATCHOUT(func, num, val)	\
	printf("\ti=%d, j = %d <<< " #func ".CATCH(" #val ")." #num " <<<\n", i, j)
#define DBGCATCHALLIN(func, num)		\
	printf("\ti=%d, j = %d >>> " #func ".CATCHALL." #num " >>>\n", i, j)
#define DBGCATCHALLOUT(func, num)	\
	printf("\ti=%d, j = %d <<< " #func ".CATCHALL." #num " <<<\n", i, j)

		/* Попытка выполнить кусок кода */
#		define TRY \
			do { \
				j = i; \
				j++; \
				jbuf[j].ret = setjmp(jbuf[j].jbuf); \
				if (0 == jbuf[j].ret) {

#		define TRYEND \
					i = j; \
				} \
			} while (0)

		/* Перехват исключительной ситуации */
#		define CATCH(x) \
					i = j; \
				} else if ((x) == jbuf[j].ret) {

#		define CATCHALL \
					i = j; \
				} else {

		/* Выброс исключительной ситуации */
#		define THROW(x) \
			longjmp(jbuf[j].jbuf, (x))

#	else
#		define TRY
#		define TRYEND
#		define CATCH(x) if (0)
#		define CATCHALL if (0)
#		define THROW(x)
#	endif /* N0PEXCEPT_ON_ */

#	ifdef __cplusplus
}
#	endif
#endif /* N0PEXCEPT_H_ */
sergey_kovtunenko
Сообщения: 6
Зарегистрирован: 26 авг 2007, 15:35

26 авг 2007, 15:44

Код: Выделить всё

/* main.c -- тестирующий библиотеку файл */

#include <stdio.h>
#include <setjmp.h>

#include "excpt.h"



void
foo(void);

void
func1(void);



void
foo(void)
{
	printf("foo()\n");
//	THROW(1);
	printf("end foo()\n");
}

int
main(void)
{
	printf("start main() : i=%d, j=%d\n", i, j);
/*	TRY {
		DBGTRYIN(main, 1);
		TRY {
			DBGTRYIN(main, 2);
			THROW (100);
			DBGTRYOUT(main, 2);
		}
		CATCH (100) {
			DBGCATCHIN(main, 2, 100);
			DBGCATCHOUT(main, 2, 100);
		}
		CATCHALL {
			DBGCATCHALLIN(main, 2);
			DBGCATCHALLOUT(main, 1);
		}
		TRYEND;
//		THROW (10);
		DBGTRYOUT(main, 1);
	}
	CATCH (10) {
		DBGCATCHIN(main, 1, 10);
//		THROW (5);
		DBGCATCHOUT(main, 1, 10);
	}
	CATCHALL {
		DBGCATCHALLIN(main, 1);
		DBGCATCHALLOUT(main, 1);
	}
	TRYEND; */
	func1();
	printf("end main() : i=%d, j=%d\n", i, j);
	return 0;
}



// Функция, в которой используются уже развёрнутые макроопределения
void
func1(void)
{
	printf("start func1() : i=%d, j=%d\n", i, j);
	do {
//		j = i; // Это логическая ошибка
		j++;
		jbuf[j].ret = setjmp(jbuf[j].jbuf);
		if (0 == jbuf[j].ret) {
			{
				DBGTRYIN(func1, 1);
				// вложенный TRY
				do {
					j = i;
					j++;
					jbuf[j].ret = setjmp(jbuf[j].jbuf);
					if (0 == jbuf[j].ret) {
						{
							DBGTRYIN(func1, 2);
							longjmp(jbuf[j].jbuf, 10);
							DBGTRYOUT(func1, 2);
						}
//						i = j; // если ты дошел до этой строчки, значит успешно (без THROW'ов, return'ов и raise()) выполнил TRY
					} else if (10 == jbuf[j].ret) {
						{
							DBGCATCHIN(func1, 2, 10);
							DBGCATCHOUT(func1, 2, 10);
						}
//						i = j;
//						j = i;
					} else {
						{
							DBGCATCHALLIN(func1, 2);
							DBGCATCHALLOUT(func1, 2);
						}
//						i = j;
//						j = i;
					}
				} while (0);
				// конец вложенному TRY
				longjmp(jbuf[j].jbuf, 100);
				DBGTRYOUT(func1, 1);
			}
			i = j;
		} else if (100 == jbuf[j].ret) {
			{
				DBGCATCHIN(func1, 1, 100);
				DBGCATCHOUT(func1, 1, 100);
			}
//			i = j;
//			j = i;
		} else {
			{
				DBGCATCHALLIN(func1, 1);
				DBGCATCHALLOUT(func1, 1);
			}
//			i = j;
//			j = i;
		}
	} while (0);
	printf("end func1() : i=%d, j=%d\n", i, j);
}

Для тех, кто дочитал до этого момента и не закрыл окно браузера, пояснение алгоритма и подводные камни.
Пояснение и различия с С++ подходом:
  • Блоки обработчиков могут быть вложенными один в один. Аналогично с С++
  • Если THROW(n) никто не обрабатывает, то программа вылетает с ошибкой. В либе реализуется за счёт передачи неверного значения в longjmp(), что крэшит программу на большинстве платформ. Полная аналогия с С++, за исключением невозможности (пока) зарегистрировать свой обработчик подобной ситуации.
  • Если внутри блока выбрасывается THROW() исключение, то оно передаётся внешнему обработчику. Почти полная аналогия с С++, кроме того, что в либе передаётся аргумент, а в С++ тот же самый аргумент передаётся во внешний блок.
  • Либа должна корректно обрабатывать ситуации, когда внутри блоков TRY\CATCH()\CATCHALL встречаются локальные переходы: goto\return()\raise(). И это одна из самых больших проблем при реализации алгоритма ;(((

В либе реализован массив значений {буффер_сохранения_состояния, код возвращаемого значения} размером в максимальную глубину вложенности. При заходе в новый блок TRY индекс массива увеличивается и запоминается новое состояние. При выходе из блока TRY индекс массива декрементируется.

Переменная j была нужна для защиты от локальных переходов и возбуждения сигналов goto\return()\raise(). Она работала так:
1. Сейчас мы находимся в i-том состоянии.
2. Делаем предположение, что наш блок TRY дойдёт нормально, без goto\return()\raise()\THROW() до самого конца. Для этого j = i+1;
3. Выполняем блок TRY.
3.1 Если блок TRY завершился успешно без исключений и переходов, то мы устойчиво стоим на новом состоянии, которое нужно "застолбить" в i, т.е. i=j
3.2 Если блок TRY завершился неудачей и исключением, то наша попытка окончилась неудачей, но мы всё равно остались стоять на i-том состоянии и переброска i = j не выполняется

Прошу помощи в реализации манипуляций с индексами массива и, возможно, логикой работы. Цель: сделать библиотеку "пуленепробиваемой" с некоторыми ограничениями.
Absurd
Сообщения: 1213
Зарегистрирован: 26 фев 2004, 13:24
Откуда: Pietari, Venäjä
Контактная информация:

27 авг 2007, 11:24

Идея плохая - когда пишешь в С никаких нарушений Control Flow быть не должно. Почется по2.7183баться - генерируй Pure C код из коделетов с помощью программы на OcaML или Haskell. Пример - fftw
2B OR NOT(2B) = FF
sergey_kovtunenko
Сообщения: 6
Зарегистрирован: 26 авг 2007, 15:35

28 авг 2007, 21:52

Absurd писал(а):Идея плохая - когда пишешь в С никаких нарушений Control Flow быть не должно. Почется по2.7183баться - генерируй Pure C код из коделетов с помощью программы на OcaML или Haskell. Пример - fftw
А мне видится отличной идеей. Если оно безопасно работает, то в чём проблема? Для чего по твоему в ANSI C присутствует хедер setjmp.h ? Для реализации нелокальных переходов при отлове ошибок глубоковложенных функций )) Так что этот хедер находит своё применение в таких библиотеках.
Оцени труд:
Реализация ООП в С
=> http://ldeniau.web.cern.ch/ldeniau/html/oopc.html

Реализации тех идей, что я хотел воплотить
=> http://ldeniau.web.cern.ch/ldeniau/html ... ption.html
=> http://www.nicemice.net/cexcept/
Absurd
Сообщения: 1213
Зарегистрирован: 26 фев 2004, 13:24
Откуда: Pietari, Venäjä
Контактная информация:

29 авг 2007, 11:10

Ну так и пиши на С++ : кто мешает? Не нравится С++ - есть Digital Mars D.
2B OR NOT(2B) = FF
sergey_kovtunenko
Сообщения: 6
Зарегистрирован: 26 авг 2007, 15:35

29 авг 2007, 16:13

Absurd писал(а):Ну так и пиши на С++ : кто мешает? Не нравится С++ - есть Digital Mars D.
Мне не нравиться С++, но очень нравиться С. К тому же не у всех есть компилятор С++. Его нет для встроенных систем ;) А на Д писать безперспективно, имхо.

Так что лучше добавить функционал в язык С, тем более что я уже определился с библиотекой чужой. Осталось только ошибки в ней исправить ;)
Absurd
Сообщения: 1213
Зарегистрирован: 26 фев 2004, 13:24
Откуда: Pietari, Venäjä
Контактная информация:

29 авг 2007, 16:49

К тому же не у всех есть компилятор С++. Его нет для встроенных систем
Для какой конкретно нет? Многие компиляторы С++ генерируют промежуточный код, достаточно сделать мини-порт.
А на Д писать безперспективно, имхо
Да ну? Человеку практическому который находится в рамках традиций и менстрима идея переделывать С не могла прийти в голову. А непрактический мог бы и попробовать D.
Так что лучше добавить функционал в язык С, тем более что я уже определился с библиотекой чужой.
Я например из кода на Pure С (Без SEH) вызвал твою библиотеку, возможно даже не напрямую а опосредованно, а ты сделал "экзепшен" через нелокальный переход например или через SEH. Были бы коды возврата я бы мог закрыть хандлы файлов и освободить память. А так - Resourse Leak. В системе которая должна работать в режиме 24/7 это неизбежно приведет к отказу через несколько дней или месяцев.
2B OR NOT(2B) = FF
sergey_kovtunenko
Сообщения: 6
Зарегистрирован: 26 авг 2007, 15:35

29 авг 2007, 22:28

Absurd писал(а):Для какой конкретно нет? Многие компиляторы С++ генерируют промежуточный код, достаточно сделать мини-порт.
Это изврат, а не С++. Компиляторы не могут поддерживать стандарт С++ хотя бы на каком-то уровне выше среднего, приходится бороться с особенностями конкретного компилятора С++, приходится отказываться от каких-то приёмов к которым привык в нормальных компиляторах (GCC, например). Уверен, что и код на С++ не слишком эффективный для встраиваемых систем. А для них очень критично быстродействие и затраты ресурсов. Короче куча НО.
Absurd писал(а): Да ну? Человеку практическому который находится в рамках традиций и менстрима идея переделывать С не могла прийти в голову. А непрактический мог бы и попробовать D.
У меня другая точка зрения. Для больших проектов в которых требуется высокая отказоустойчивость я бы использовал Java. А для малых и средних -- С.
Использовать язык Д считаю непрактичным, т.к. у него нет большого будущего а в настоящем, имхо, та ниша на которую он претендует уже успешно занята.
Absurd писал(а): Я например из кода на Pure С (Без SEH) вызвал твою библиотеку, возможно даже не напрямую а опосредованно, а ты сделал "экзепшен" через нелокальный переход например или через SEH. Были бы коды возврата я бы мог закрыть хандлы файлов и освободить память. А так - Resourse Leak. В системе которая должна работать в режиме 24/7 это неизбежно приведет к отказу через несколько дней или месяцев.
Ты не совсем прав. Подход который ты описал для очень неквалифицированных программистов "тяп-ляп".
Во первых, почему ты не допускаешь варианта разработки какой-то части большого проекта с использованием SEH внутри компонента с интерфейсом, который возвращает привычные коды ошибок? Это самый очевидный вариант.

Далее, в одной из либ есть специальные макросы protectPtr(ptr, func) и unprotectPtr(ptr).
Если происходит выброс из блока try, то автоматом освобождаются все ресурсы и мемлика не наблюдается.

В другой либе попроще, просто не разрешаются return()\goto из блока try.

А использование обычных кодов возврата ещё не гарантирует работу программы 24/7 . Ведь соблазнительно не анализировать их? В случае SEH если не анализировать throw's, то программа будет тупо завершаться. При хорошем тестировании, такие "забывания" делать catch легко выявляются ;) ) Так что это очень спорный момент что ещё лучше использовать.
Absurd
Сообщения: 1213
Зарегистрирован: 26 фев 2004, 13:24
Откуда: Pietari, Venäjä
Контактная информация:

30 авг 2007, 13:34

sergey_kovtunenko писал(а):Это изврат, а не С++. Компиляторы не могут поддерживать стандарт С++ хотя бы на каком-то уровне выше среднего, приходится бороться с особенностями конкретного компилятора С++, приходится отказываться от каких-то приёмов к которым привык в нормальных компиляторах (GCC, например). Уверен, что и код на С++ не слишком эффективный для встраиваемых систем. А для них очень критично быстродействие и затраты ресурсов. Короче куча НО.
Да ну. Насколько я знаю, проблема была только с MCVC++ 6 и давно мертвым Watcom. Все остальные компиляторы - Intel C++, g++, Metrowerk CodeWarrior, Digital Mars C++, KAI C++ поддреживали практически весь стандарт за исключением export template. Насчет Борланд не уверен, но насколько я знаю он тоже поддерживал стандарт намного лучше MSVC++ 6.
sergey_kovtunenko писал(а): У меня другая точка зрения. Для больших проектов в которых требуется высокая отказоустойчивость я бы использовал Java. А для малых и средних -- С.
Использовать язык Д считаю непрактичным, т.к. у него нет большого будущего а в настоящем, имхо, та ниша на которую он претендует уже успешно занята.
А если система реального времени? Под Джаву есть система реального времени с возможностью явного управления памятью (без уборки мусора), но она AFAIK работала на двухпроцессорном SPARC - один камень выполняет программу, другой ее шедюлит. Для i386 есть QNX, ей хратает одного процессора, но писать под нее надо на С.
sergey_kovtunenko писал(а): Ты не совсем прав. Подход который ты описал для очень неквалифицированных программистов "тяп-ляп".
Какой тяп-ляп? Я не хочу тянуть кучу нестандартных парадигм вместе с каждой библиотекой которую я использую. Если пишешь библиотеку, будь добр экспортируй символы через extern "C" и отлавливай все исключения на выходе, рапортуя об ошибках через возвращаемые значения.
sergey_kovtunenko писал(а): Во первых, почему ты не допускаешь варианта разработки какой-то части большого проекта с использованием SEH внутри компонента с интерфейсом, который возвращает привычные коды ошибок? Это самый очевидный вариант.
Если я пишу под винду на MSVC, то использую SEH явно если нужно. Например, я не оборачиваю объекты GDI в классы с деструкторами и поэтому произвожу clenup в блоке __finally.
sergey_kovtunenko писал(а): Далее, в одной из либ есть специальные макросы protectPtr(ptr, func) и unprotectPtr(ptr).
Если происходит выброс из блока try, то автоматом освобождаются все ресурсы и мемлика не наблюдается.
Я всегда освобождаю память там же где и выделяю, поэтому функции h_rb_tree create_rb_tree(...) всегда аккомпанирует destroy_rb_tree(h_rb_tree rb_tree). Удалять сущность типа h_rb_tree с помощью free() конечно же нельзя, по меньшей мере потому что это сложная структура содержащая много элементов выделенных в динамической памяти.
sergey_kovtunenko писал(а): А использование обычных кодов возврата ещё не гарантирует работу программы 24/7 . Ведь соблазнительно не анализировать их?
Тем кому соблазнительно системы работающие в режиме 24/7 не пишут.
sergey_kovtunenko писал(а): В случае SEH если не анализировать throw's, то программа будет тупо завершаться.
Этого избежать элементарно - ловить их всех в корне цикла обработки сообщений, логировать и давить.
2B OR NOT(2B) = FF
sergey_kovtunenko
Сообщения: 6
Зарегистрирован: 26 авг 2007, 15:35

30 авг 2007, 15:55

Absurd писал(а):Да ну. Насколько я знаю, проблема была только с MCVC++ 6 и давно мертвым Watcom. Все остальные компиляторы - Intel C++, g++, Metrowerk CodeWarrior, Digital Mars C++, KAI C++ поддреживали практически весь стандарт за исключением export template. Насчет Борланд не уверен, но насколько я знаю он тоже поддерживал стандарт намного лучше MSVC++ 6.
Речь шла о компиляторах для AVR, PIC, 8051... Я не знаю нормальных С++ компиляторов для них.
Absurd писал(а): Какой тяп-ляп? Я не хочу тянуть кучу нестандартных парадигм вместе с каждой библиотекой которую я использую. Если пишешь библиотеку, будь добр экспортируй символы через extern "C" и отлавливай все исключения на выходе, рапортуя об ошибках через возвращаемые значения.
Никто и не заставляет использовать нестандартные парадигмы. Если разрабатывать некий компонент так:

1. Внешние интерфейсы (для подключения библиотеки к другим компонентам. Возвращают обычные коды ошибок)
^
|
2. Абстрактный уровень, который отлавливает и обрабатываетошибки на уровне компонента.
^
|
3. Уровень реализации (на этом уровне выбрасываются исключения)

То программер, который использует библиотеку получает обычные функции с обычными кодами возврата.
Absurd писал(а): Если я пишу под винду на MSVC, то использую SEH явно если нужно. Например, я не оборачиваю объекты GDI в классы с деструкторами и поэтому произвожу clenup в блоке __finally.
__finally есть только в винде и такой код непереносим на другие платформы.
Absurd писал(а): Я всегда освобождаю память там же где и выделяю, поэтому функции h_rb_tree create_rb_tree(...) всегда аккомпанирует destroy_rb_tree(h_rb_tree rb_tree). Удалять сущность типа h_rb_tree с помощью free() конечно же нельзя, по меньшей мере потому что это сложная структура содержащая много элементов выделенных в динамической памяти.
Не всегда это возможно, освобождать память там, где выделялась. Это ведь одна из ключевых проблем языков вроде С++: Кто будет выделять память, а кто будет её освобождать?
Absurd писал(а): Тем кому соблазнительно системы работающие в режиме 24/7 не пишут.
Да, но полностью самому написать систему 24\7 практически сейчас нереально, приходится использовать сторонние компоненты. А кто даёт гарантию, что там проверялись все коды ошибок, возвращаеммые функциями?
Ответить