catch-and-throw

Table of Contents

CATCH и THROW

Аннотация

CATCH и THROW механизм обработки исключений (нелокальный или многоуровневый выход (EXIT)) наиболее глубокомысленная фундаментальная концепция добавленная в язык Форт, с момента его создания Чаком Муром. Одновременно эта идея устройства и реализации элегантна и органична Forth. Этот механизм так же прост в использовании при правильном его понимании. Эта статья описывает историю создания CATCH и THROW , их реализацию, синтаксис, семантику, и рациональные методы эффективного использования в сложных многоуровневых Forth-системах, подобных OpenFirmaware.

Благодарности

Специальные благодарности Вильяму Митч Бредли из FirmWorks за предоставление истории его участия в CATCH и THROW, и за разрешение его включения в эту статью. Так же благодарности Паулю Томасу из SunMicrosystems за его поддержку и разъяснения ANS Форт определений слов ABORT и ~ABORT"~, и Роберту Хоуку из SunMicrosystems за тщательную техническую рецензию статьи.

История

Когда форт был изобретен, обработка исключений осталась за рамками языка. Механизм поддержки исключений, определенный ANS, был предложен Вильямом Митч Бредли в 1990 году. В это время Бредли руководил созданием OpenBoot проекта в SunMicrosystems, и так же являлся председателем комитета X3J14 ANS Форт технического комитета.

Перед тем, как он пришел с идеей, Бредли изучил все возможные схемы обработки исключений в форте, опубликованные в то время. Он так же изучил схемы из различных других языков (Си, Modula, Cedar, Lisp, PostScript). Обработка исключений в других языках стилистически непротиворечива с ними. Например, обработка исключений Цедаре включена в синтаксис языка и требует серьезной поддержки компилятора, не смотря на то, что в Си setjmp() и longjmp() не требуют синтаксической поддержки компилятора, они требуют от линкера умения разбирать буфер адресов переходов, который эффективнее реализуется через глобальные переменные. Оба варианта близки общей идеологии этих языков.

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

Бредли хорошо помнит, что место в котором к нему в конце концов пришло озарение было ванной в номере отеля. Это было утро встречи технического комитета создающего стандарт, проходящего в NASA Goddard помещении в парке колледжа в 1990. Позднее в тот же день Бредли объявил тему обработки исключений, которая тогда была одной из больных тем комитета. Все знали, что стандарт должен содержать тему, но никто в реальности не имел подходящих предложений. Проблема со сложными схемами, которая всегда поднималась в комитетских дискуссиях, заключалась в возникновении проблематичных взаимодействий с другими аспектами форта. В таких случаях дискуссии стремились сбиться в сторону и затихнуть.

Позднее Бредли разработал детали реализации и показал, как просто вышло. Он начал использовать механизм в различных местах OpenBoot, и опыт обнаружил, что механизм работает очень хорошо. Он был особенно доволен тем, как гибка и масштабируема схема применения CATCH и THROW получилась, но гораздо больше ожидал схода с пути, потому что ключевое свойство в Forth - избежание синтаксической сложности приводит к максимальной гибкости. После успешного опыта использования CATCH и THROW в OpenBoot, который должен обеспечивать высокую надежность на широком спектре задач, Бредли предложил свою модель обработки исключений ANS комитету. По его отзывам механизм был поддержан сразу, по сравнению с аналогичными предложениями: без обычных перебранок, криков и зубовного скрежета. Единственное изменение, внесенное комитетом в оригинальную схему Брадли, заключается в том, что 0 THROW работает как NOOP (то есть ничего не делает). Это было решено после того, как решили использовать THROW после слов, возвращающих результат завершения ввода/вывода ior ( код ior зависит от реализации системы, в которой работает Forth). Фразы типа OPEN-FILE THROW очень удобны. Чуточку позже комитет окончательно зафиксировал специфические значения кодов ошибок для THROW.

Бредли заимствовал имена CATCH THROW из Lisp, в котором обработка ошибок ведется методами очень похожими на ANS-евые CATCH и THROW Форта. Лисповские catch и throw "теггированные", что означает, если вы скажете (throw foo) это вызовет только переключение catch, который определяет тот же тэг foo. Бредли раздумывал, делая подобное с CATCH и THROW в Forth, где конкретный CATCH будет отвечать только на конкретные значения THROW, но он его отверг. Он написал примеры кода использующего различную семантику, и нашел, что "ловим все, и делаем повторные THROW при необходимости" семантика обеспечивает более чистый код почти в любом случае, особенно в соединении с CASE. С тех пор CATCH и THROW продолжали распространяться в исходных OpenBoot текстах. OpenBoot использует то оригинальное определение CATCH и THROW, которое Бредли написал в 1990. CATCH и THROW так же стандарт в FCode. Эти FCode все больше используются в диагностических прошивках для устройств ввода/вывода на платформе SunMicrosysyems.

Синтаксис и семантика.

В сущности, THROW - это ABORT (многоуровневый выход) и CATCH это "подтверждающая ABORT" версия EXECUTE. Если слово исполняется с помощью CATCH, и во время исполнения этого слова происходит THROW , стек возвратов остается нетронутым, стек данных очищается, и управление передается в точку CATCH. Если CATCH не был использован, THROW откатится на самый верхний уровень , и программа будет выброшена без шанса вмешаться в процесс восстановления. ANS Форт представляет CATCH и THROW, как часть опционального набора слов исключений:

9.6.1.0875 CATCH ( i*x xt – j*x 0 | i*x n )

Поверхностное описание: исполнить команду определяемую xt. Вернуть код THROW n .

ANS Форт описание: Создать фрейм исключения в стеке исключения и исполнить указанный токен xt как с помощью EXECUTE таким образом, что контроль выполнения должен быть передан в точку точно за CATCH, если THROW произойдет во время исполнения xt.

9.6.1.2275 THROW ( k*x n – k*x | i*x n )

Поверхностное описание: Вернуться назад в CATCH обработчик, если код n не равен нулю.

ANS Форт описание: Если любые биты числа n не равны нулю, вытолкнуть верхний фрейм исключения с вершины стека исключений вместе со всем, находящимся на стеке возвратов выше этого фрейма. Затем восстановить входной поток на тот, что использовался во время соответствующего CATCH и скорректировать глубины всех стеков, определенных стандартом, так чтобы они имели те же глубины, что сохранены в фрейме исключений (i это то же число входных аргументов, соответствующих началу выполнения CATCH), положить n на вершину стека данных, и передать управление в точку за CATCH который сохранил фрейм исключения. Если вершина стека содержит отличное от нуля число и на стеке исключений нет фрейма исключений, выполнить следующее: Если n = -1, выполнить функцию ABORT. Если n = -2 выполнить функцию ~ABORT"~. В других случаях система может отображать зависимые от реализации сообщения, дающие информацию о состоянии ассоциируемую с кодом n. Затем система должна выполнить функцию ABORT.

Реализация CATCH и THROW.

Реализация Бредли слов CATCH и THROW очень проста для понимания.

CATCH сохраняет указатель стека данных, адрес предыдущего фрейма исключений на стеке возвратов, и запоминает адрес текущего фрейма в глобальной переменной handler. После чего слово, описанное своим исполнимым адресом, исполняется. Если это слово исполняется нормально (без исключений), управление возвращается к CATCH через обычный механизм возврата, CATCH удаляет данные, положенные на стек возвратов, восстанавливает переменную handler, кладет нуль на вершину стека данных на (примечание mOleg: место xt), и передает управление коду, стоящему за CATCH.

В другом случае, если THROW исполняется (с ненулевым аргументом), THROW определяет ближайший вложенный CATCH фрейм (адрес которого лежит в handler), очищает стек возвратов до запомненного с помощью CATCH уровня, восстанавливает указатель стека данных в сохраненное состояние в этом фрейме, копирует значение, вызвавшее THROW на вершину стека данных, и возвращается на точку после CATCH (примечание mOleg: то есть управление всегда и в любом случае возвращается в одну и ту же точку, расположенную за CATCH). Когда CATCH возвращает нуль (имеется ввиду, что 0 THROW было исполнено, или что THROW не выполнялся вообще), состояние стеков такое же, как если бы вместо CATCH использовалось слово EXECUTE. Когда CATCH возвращает ненулевое значение (то есть THROW был вызван с ненулевым значением), глубина стека данных, не считая кода throw , сохраняется таким же, как до выполнения слова CATCH c его аргументом было исполнено. Должно быть акцентировано, что только глубина стека восстанавливается, а не его содержимое.

ANS Форт не требует использовать стек возвратов для реализации CATCH и THROW, но использование стека возвратов выглядит натурально и используется в большинстве систем. Предлагаемая реализация CATCH и THROW написана Митчем Бредли и сейчас используется во всех работах OpenFirmware и SunMicrosystems OpenBoot системах. Реализация использует нестандартные слова, описанные ниже. Эти слова или их эквиваленты имеются во многих системах: sp@ rp@ sp! rp! (слова работают с указателями вершин стеков данных и возвратов).

variable handler \ последний объявленный обработчик исключений

: CATCH ( xt -- exception# | 0)
\ адрес возврата уже на стеке
sp@ >r        ( xt) \ сохранить указатель стека данных
handler @ >r  ( xt) \ сохранить предыдущий обработчик
rp@ handler ! ( xt) \ установить текущий обработчик
execute       ( )   \ выполнить указанное xt
r> handler !  ( )   \ восстановить предыдущий обработчик
r> drop       ( )   \ удалить сохраненный указатель стека данных
0             ( 0)  \ обозначить нормальное завершение
;

: THROW ( ??? exception# -- ??? exception# ) \ возвращается в сохраненный контекст
dup 0= if drop exit then \ выходим в случае, если флаг = 0
handler @ rp!   ( exc#) \ восстанавливается указатель стека возвратов
r> handler !    ( exc#) \ восстанавливается предыдущий обработчик
r> swap >r  ( saved-sp) \ запомнить номер исключения на стек возвратов перед изменением стека данных
sp! drop r>     ( exc#) \ восстановить указатель стек данных, вернуть код исключения
;

Коды исключений.

Коды исключений согласно ANS могут выбираться только из определенных диапазонов для избегания конфликтов при переносе кода между разными форт-системами и приложениями. Значения {-255..-1} могут быть использованы только так, как описано в стандарте. Значения {-4095..-256} могут использоваться только так, как определено в системе. Программы не должны определять значения для использования вместе с THROW в перечисленных диапазонах. Стандарт фиксирует коды исключений в диапазоне {-58 … -1} предназначенные для различных обработчиков исключений и ошибок (-1 зарезервирован за ABORT, а -2 за ABORT" , -3 за переполнением стека данных, -4 за переопустошением стека данных и так далее).

ABORT и ABORT".

ABORT и ~ABORT"~ существовали в форте до появления механизма обработки исключений, который появился в 1990. (примечание mOleg: строго говоря в старых фортах слово ABORT - было бесконечным циклом, внутри которого работал цикл QUIT, поэтому многоуровневым выходом его можно на мой взгляд считать с натяжкой), в то время как ~ABORT"~ многоуровневый выход с ассоциируемым сообщением. Синтаксис и семантика для них в стандарте следующие:

6.1.0670 ABORT ( i*x – ) ( R: j*x – )

ANS: Очистить стек данных и выполнить функцию QUIT, которая включает очистку стека возвратов без отображения сообщения.

6.1.0680 ~ABORT"~ ( i*x flag – | i*x ) ( R: j*x – |j*x ) Compilation: ( “ ccc” – )

ANS: удалить флаг с вершины стека данных, если любой из битов значения flag отличны от нуля, отобразить строку ccc и выполнить зависящую от реализации последовательность ABORT.

В дополнение, ANS Форт определяет две другие версии этих слов, для систем, которые вобрали в себя слова CATCH и THROW. Эти "более мощные" версии ABORT , ~ABORT"~ присутствуют как часть опциональной поддержки исключений:

9.6.2.0670 ABORT ( i*x – ) ( R: j*x – )

ANS: выполнить функцию -1 THROW.

Мы должны согласиться, что это не очень чистое определение слова ABORT. Что оно означает ABORT заменяется до -1 THROW , и упрощает восстановление ошибок. Использование CATCH и THROW позволяет программе выполнять более разумные действия при возникновении ошибок, а так же позволяет различное поведение ABORT в зависимости от того, как оно определено.

9.6.2.0680 ABORT” ( i*x flag – | i*x ) ( R: j*x – |j*x ) Compilation: ( “ ccc” – )

ANS: убрать flag, если любой из битов флага отличны от нуля, выполнить -2 THROW , отображающее строку ccc если ошибка дошла до базового обработчика исключений (CATCH).

Стандарт определяет ~ABORT"~ потому что программисты хотят его использовать, но не всегда хотят видеть текст сообщения, особенно для встраиваемых приложений, где не всегда дисплей имеется. Для отладки на больших системах отображение сообщений приветствуются. Так же сообщения могут сохраняться в историю (log), или могут отображаться отложено, или могут быть переведены на другой язык, после чего отображены. Комитет пришел к выводу, что в случае использования механизма исключений возможно вмешательство в процесс вывода сообщений, идея понравилась.

В дополнение к 9.6.2.0680 определению слова ~ABORT"~ возможны три различные методики реализации ABORT":

  • ~ABORT"~ сначала отображает сообщение, затем выполняет -2 THROW
  • ~ABORT"~ сохраняет строку сообщения в буфер, затем выполняет -2 THROW, CATCH-ер отображает текст
  • ~ABORT"~ сохраняет строку сообщения в буфер, затем выполняет -2 THROW, CATCH-ер не отображает текст

Эффективное использование CATCH и THROW в программе.

Как упоминалось ранее, Бредли рассматривал создание THROW только для реагирования на особенные коды исключений, но решился отказаться от этого. Он обнаружил, что "ловить все и передавать дальше при необходимости" семантика обеспечивает более прозрачный код практически в любом случае, особенно в сочетании с конструкцией CASE. Это должно быть легко понятно. Вспомните, что CATCH всегда ловит все THROW, инициированные как программным кодом так и самой системой. Поэтому, ловля и повторный вызов исключений с тем же кодом исключения может заменить запланированную обработку исключений в Форт системе, в случае ошибки или исключения. В дополнение, перехват системных кодов или абортов позволяет для некоторых Форт-систем не отображать при необходимости сообщения:

: foo ( -- ) ... 10 THROW ; \ где-то в коде
['] foo CATCH ?dup if ... then \ дырявый обработчик

Хотя этот пример может выглядеть корректным, в реальности это не правило кодирования. Вступление было сделано, что только тот THROW будет пойман, который находится в слове foo, и имеет код 10. С этой предпосылкой программа только проверяет, отлично ли значение от нуля, и предполагает, что, если отлично от нуля, то это значение 10 из foo. Неверно! Что, если во время исполнения слова foo, Форт система сгенерирует системный THROW с одним из зарезервированных кодов THROW? В таком случае системный THROW, инициированный системой (например, переполнение стека код: -3) будет пойман в приложении вызывающем foo, и выполнение программы будет продолжено с точки CATCH (с, возможно, подпорченным стеком данных), в замену выхода на более высокий уровень для вывода сообщения об ошибке, как это раньше предполагалось в системе. Учитывая описанное, корректное решение будет в проверке пойманного кода THROW на знакомство этого кода программе. В последствии повторно вызвать исключение с любым неизвестным программе кодом, передавая исключение выше:

['] foo CATCH dup 10 = if ( 10|n) \ проверка, равен ли код 10
... ( 10) \ если равен, обрабатываем известный код
else ( n) \ иначе
THROW ( ) \ передаем исключение выше
then \ 0 THROW выполнит noop

Если необходимо обработать больше значений кодов, лучший выход использовать CASE структуру следующим образом:

['] foo CATCH
case
10 of .... endof \ реакция на код = 10
20 of .... endof \ реакция на код = 20
40 of .... endof \ реакция на код = 40
dup THROW \ передача неизвестного кода выше
endcase \ 0 THROW выполнит noop

С этой предлагаемой программной техникой использование CATCH в приложении никогда не навредит. Другое требование для эффективного использования CATCH и THROW - никогда не использовать значения кодов исключений зарезервированные в системе для внутренних нужд. В большинстве случаев приложение должно использовать только маленькие положительные числа для кодов исключений, только согласно предписанию стандарта. Интересным вариантом использования THROW - использование адреса памяти как кода исключения. Этот адрес памяти может содержать сжатую строку (текстовое сообщение), соответствующее ошибке:

ok : foo ( -- ) ... p” XXX” THROW ;
ok ['] foo CATCH ( paddr ) .error
ok XXX

В этом примере значение THROW, возвращаемое CATCH - это адрес строки, содержащей сообщение "XXX". Слово .error должно определить, является ли код исключения действительным адресом в памяти, и выполняет COUTN TYPE, которые выводят сообщение. Количество различных типов значений кодов может быть использовано в программе: (1) положительные коды ошибок, (2) положительные коды ошибок для выбора с помощью CASE, (3) маленькое положительное число для выбора сообщения из списка сообщений, (4) адрес в памяти, содержащий строку сообщения, (5) исполнимый адрес или другой адрес в памяти.

Другое наблюдение - не стоит очищать стеки данных и возвратов в большинстве систем перед исполнением THROW:

ok : foo ( -- ) 1 >r 2 >r 3 4 5 THROW ; ( )
ok [‘] foo CATCH ( 5 )

Часто предполагается, что CATCH и THROW механизм должен использоваться только для обработки ошибок. Хотя, конечно, обработка ошибок - это главное назначение механизма, CATCH и THROW можно использовать так же для эффективного программирования, когда многоуровневый выход делает программы проще.

Выводы.

Не смотря на многочисленные механизмы появлявшиеся до и после создания CATCH и THROW, по нашему мнению именно этот механизм отвечает идеологии Forth. Механизм доказал свою эффективность в различных Forth-системах и приложениях (таких как OpenFirmware, OpenBoot и FCODE драйверах). Очень важно следовать стандарту при обработке кодов исключений в программе, и осторожно обрабатывать каждое CATCH, помня, что любое исключение может быть поймано любым обработчиком в программе. Стандарт определяет два набора слов: ABORT , ABORT"~ заботясь о обработке аварийных прекращений. Один является частью ~THE CORE WORD SET (которые не используют THROW), и другой является частью THE OPTIONAL EXCEPTION WORD SET (с использованием THROW). Последний набор позволяет гибко и просто обрабатывать ошибки, используя CATCH для обработки поведения ABORT и ~ABORT"~.

Ссылки.

[ 1] ANSI X3.215-1994 ANS for Information Systems - Programming Languages - Forth [ 2] IEEE Standard 1275-1994; Standard for Boot Firmware: Core Requirements & Practices [ 3] Gassanenko, M.L., Extension of the Exception Handling Mechanism, Euroforth95 [ 4] Rodriguez, B.J., A Forth Extension Handler, SIGForth, Vol. 1, Summer’89, p. 11-13 [ 5] Wejgaard, W., TRY: A Simple Exception Handler, EuroFORML ‘91, p.4 [ 6] Clifton, G., Terry, R., Exception Handling in Forth, Rochester Forth Conference ‘85 [ 7] Woehr, J., Forth: The New Model, M&T Books, 1992

(примечание mOleg: - так в скобках обозначены коментарии переводчика)

Яндекс.Метрика
Home