Автор:
mOleg Дата публикации: 2008-12-08
публикуется впервые
Сообщения Аннотация В статье обсуждаются методики организации, хранения и вывода текстовых сообщений, как принятых в существующих Форт-системах, так и новых. Так же обсуждаются преимущества и недостатки подходов к решению проблемы хранения сообщений.
Самый простой вариант Самый простой вариант – это литеральные строки в коде, например:
… S” простое сообщение” TYPE …
либо, как вариант сообщения вместе с ошибкой:
… ABORT” простое сообщение об ошибке” ...
Этот метод хорош тем, что проще него придумать практически не возможно, но имеет ряд недостатков:
-невозможно изменить текст сообщения без пересборки (перекомпиляции) кода,
-сообщение находится в статическом пространстве программы, поэтому отнимает место у кода и данных,
-одно и то же сообщение придется повторять в нескольких местах программы, если оно вызывается несколько раз,
-невозможно просмотреть все сообщения программы, так как они разбросаны по коду программы.
Тем не менее, использование литеральных строк для организации сообщений принятая практика.
Одно слово – одно сообщение Более универсальным вариантом, который является развитием предыдущего метода – оформлять каждое сообщение в отдельное слово, и уже это слово вызывать в случае необходимости. Причем, полученные таким образом сообщения могут самостоятельно принимать решения о необходимости вывода сообщения:
: Err001 ( flag --> ) IF .” сообщение в отдельном определении” -1 THROW THEN ;
Имя сообщения (в нашем случае Err001) не обязательно должно быть цифробуквенным, и может быть вполне осмысленным. Тем не менее, недостатки остаются:
- в собранном виде текст сообщения не изменить без перекомпиляции кода,
- сообщение так же продолжает отнимать место у кода и данных.
Тем не менее, все сообщения можно разместить в одном файле, и поэтому легче контролировать. Если сообщение вызывается из нескольких мест программы, дополнительного пространства на повторное включение текста, как это было в первом примере, не занимается.
Но появляются новые недостатки:
- расходуются имена в словаре на именование ошибок,
- ошибки в тексте программы становятся не очень читабельными, особенно, если имя ошибки не содержит в закодированном виде информации о типе сообщения.
Еще один классический метод Еще одним методом работы с сообщениями любили пользоваться Форт-системы во времена ДОСа. Выглядело это примерно так:
CREATE messages
S” первое сообщение” S”,
S” второе сообщение” S”,
S” n-тое сообщение” S”,
Поиск сообщений в таком текстовом массиве выглядел так:
: message ( u --> ) 0 messages 0 2SWAP DO + COUNT LOOP TYPE ;
Соответственно, чтобы вывести сообщение, необходимо было компилировать его код в виде литерала, а так же компилировать код, который бы умел находить по номеру сообщения само сообщение и выводить его куда необходимо:
: Error ( u --> ) COMPILE message COMPILE ABORT ; IMMEDIATE
и в программе это используется так:
… 123 Error …
Опять же, одни недостатки ушли, например, все сообщения находятся в одном месте, не расходуются имена слов. Основной недостаток у такого подхода заключается в том, что код сообщения не несет никакой информации о содержимом сообщения. И преимущества у нас следующие:
- сообщения хранятся в одном месте,
- сообщения могут храниться отдельно от кода.
А что же у нас с СПФ? Предыдущий вариант достаточно хорош, но его можно улучшить, например, сообщения можно хранить во внешнем файле, и подгружать в динамическую область памяти уже после запуска программы. Номер сообщения можно сделать произвольным, и хранить в теле самого сообщения. И в СПФ так и сделали:
\ найти ошибку с номером n в списке ошибок, вернуть строку сообщения
: (DECODE-ERROR) ( n --> c-addr u )
STATE @ >R STATE 0!
BEGIN
REFILL
WHILE ( n )
PARSE-NAME ['] ?SLITERAL CATCH
IF 2DROP DROP S" Error while error decoding!" R> STATE ! EXIT THEN
OVER = IF ( n )
DROP >IN 0! [CHAR] \ PARSE
TUCK SYSTEM-PAD SWAP CHARS MOVE
SYSTEM-PAD SWAP R> STATE ! EXIT
THEN
REPEAT ( n )
<# SOURCE SWAP CHAR+ SWAP 1 - HOLDS DUP 0< IF DUP S>D #(SIGNED) 2DROP THEN U>D #S #> R> STATE ! ;
\ Возвратить строку, содержащую расшифровку кода ошибки n при условии u.
: DECODE-ERROR ( n u --> c-addr u )
DROP
S" lib/SPF.ERR" +ModuleDirName 2DUP FILE-EXIST 0=
IF
2DROP
S" SPF.ERR" +ModuleDirName
THEN
R/O OPEN-FILE-SHARED
IF DROP DUP >R ABS 0 <# #S R> SIGN S" ERROR #" HOLDS #>
TUCK SYSTEM-PAD SWAP CHARS MOVE SYSTEM-PAD SWAP
ELSE
DUP >R
['] (DECODE-ERROR) RECEIVE-WITH DROP
R> CLOSE-FILE THROW
2DUP -TRAILING + 0 SWAP C!
THEN ;
А вот так: … 2 <> IF -2007 THROW THEN … выглядит вывод сообщения.
Итак, основной недостаток у такого подхода заключается в том, что код сообщения не несет никакой информации о содержимом сообщения, правда второй недостаток заключается в том, что при возникновении ошибки каждый раз файл со списком ошибок транслируется заново.
Но зато имеется много преимуществ:
- сообщения хранятся в одном месте во внешнем файле, а значит
- сообщения могут меняться независимо от кода программы и при новом запуске
- сообщения могут грузиться (но не грузятся) в произвольный участок памяти, а это значит, что
- количество сообщений может даже изменяться.
Такой список сообщений легко поддерживать, но достаточно сложно отслеживать в коде, на то ли сообщение сделана ссылка, либо что означает конкретное сообщение с номером n.
Вариант организации сообщений из Win32Forth Win32Forth близок к традиционной схеме с массивом сообщений, и выглядит следующим образом:
VARIABLE THROW_MSGS \ список сообщений
THROW_MSGS LINK, THROW_STACKUNDER , ," stack underflow"
THROW_MSGS LINK, THROW_UNDEFINED , ," is undefined"
THROW_MSGS LINK, THROW_COMPONLY , ," is compilation only"
THROW_MSGS LINK, THROW_NAMEREQD , ," requires a name"
\ кусок, отвечающий за поиск сообщения
… THROW_MSGS
BEGIN @ ?DUP
WHILE DUP CELL+ @ LAST-ERROR =
IF 2 CELLS+ ?TYPE PTRNULL THEN
REPEAT …
То есть все сообщения находятся в статической области памяти и разделяют пространство кода/данных, но все сообщения связаны в список, каждая запись в котором содержит: ссылку на предыдущее сообщение, код сообщения, и само сообщение. Таким подходом «убивается несколько зайцев»:
- сообщения можно добавлять в список по необходимости,
- сообщения можно перекрывать новыми с таким же кодом,
- нет необходимости специальным образом сохранять и загружать список сообщений.
Недостаток тот же, что и у СПФа – не читабельны самые важные места в коде, которые и вызывают сообщение.
А что, если у каждого метода взять лучшее? Собственно, ведь нам не так много нужно! Хочется, чтобы было удобно и понятно в тексте программы отслеживать сообщения, хочется, чтобы сообщения можно было менять, например, перевести на другой язык, в процессе работы с программой даже не имея ее исходных текстов, хочется, чтобы сообщения не загромождали код программы. И еще иногда все же удобно работать с номерами сообщений, например, в местах вызова сообщений.
USER msg-list \ ссылка на последнее сообщение в списке
USER-VALUE last-msg \ номер последнего созданного сообщения
0 \ формат записи сообщения
CELL -- off_msgPrev \ адрес предыдущего сообщения
CELL -- off_msgName \ номер текущего сообщения
1 -- off_msgBody \ строка сообщения вместе со счетчиком длины
CONSTANT /msgRecord
\ добавить новое сообщение в список сообщений
: new-msg ( asc # msg --> )
OVER /msgRecord + ALLOCATE THROW
TUCK off_msgName !
DUP >R off_msgBody SCOPY
R@ msg-list CHANGE
R> off_msgPrev ! ;
\ найти сообщение в списке сообщений по его номеру
: find-msg-num ( msg --> asc # true | msg false )
>R msg-list @
BEGIN DUP WHILE
DUP off_msgName @ R@ <> WHILE
off_msgPrev @
REPEAT RDROP off_msgBody COUNT TRUE EXIT
THEN R> SWAP ;
\ найти номер сообщения по содержимому сообщения
: find-msg-body ( asc # --> msg | asc # false )
2>R msg-list @
BEGIN DUP WHILE
DUP off_msgBody COUNT 2R@ COMPARE WHILE
off_msgPrev @
REPEAT off_msgName @ RDROP RDROP EXIT
THEN 2R> ROT ;
\ найти номер для следующего сообщения
: num-msg ( --> err )
last-msg BEGIN 1 + DUP find-msg-num WHILE 2DROP REPEAT TO last-msg ;
\ найти сообщение в списке msg-list, отобразить его текст
\ если сообщения с указанным индексом err не найдено, отобразить индекс
: ~MESSAGE ( err --> ) find-msg-num IF ELSE 0 (D.) THEN >STDERR ;
\ отобразить сообщение с номером msg если flag отличен от нуля,
\ выполнить THROW с кодом msg, если флаг = 0 действия не выполняются
: (ABORT) ( flag msg --> )
SWAP IF DUP ~MESSAGE THROW ELSE DROP THEN ;
\ по содержимому строки asc # определить номер сообщения
: reffered ( asc # --> msg )
find-msg-body DUP IF ELSE DROP num-msg DUP >R new-msg R> THEN ;
\ компилировать код, в случае ошибки выводящий сообщение message
: ABORT" ( / message" --> )
?COMP [CHAR] " PARSE reffered
[COMPILE] LITERAL POSTPONE (ABORT) ; IMMEDIATE
\ компилировать код, выводящий сообщение message
: MESSAGE" ( / message" --> )
[CHAR] " PARSE reffered
STATE @ IF [COMPILE] LITERAL POSTPONE ~MESSAGE
ELSE DROP \ в режиме интерпретации сообщение
\ добавляется к списку сообщений
THEN ; IMMEDIATE
Теперь в тексте можно везде писать так, как и в самом первом случае: … ABORT” какая-то там ошибка!” …
Причем, такое сообщение может многократно присутствовать в исходных текстах программы, но в коде в каждом таком месте будет скомпилирован только номер сообщения, а не само сообщение, для чего каждый раз само сообщение ищется в списке msg-list, и если находится, то номер уже существующего сообщения используется. Этим мы добиваемся читабельности текстов.
Все сообщения, добавленные в систему, необходимо будет сохранить на диск в отдельный файл таким образом, чтобы одна строка соответствовала одному сообщению, и в начале каждой строки присутствовал номер сообщения (то есть так же, как в СПФ).
\ сохранить все сообщения в файл с указанным именем
: save-messages ( asc # --> )
W/O CREATE-FILE THROW >R
msg-list
BEGIN @ DUP WHILE
DUP off_msgName @ 0 (D.) R@ WRITE-FILE THROW
S" " R@ WRITE-FILE THROW
DUP off_msgBody COUNT R@ WRITE-FILE THROW
LT LTL @ R@ WRITE-FILE THROW
off_msgPrev
REPEAT DROP R> CLOSE-FILE THROW ;
Так как номера сообщений фиксируются в теле самого сообщения, порядок следования сообщений в файле может быть произвольным, в приведенном случае – обратным.
Загрузка списка сообщений:
\ загрузить список сообщений в память из файла Asc #
: load-messages ( asc # --> ) FILE>HEAP
IF SAVE-SOURCE N>R SOURCE! 0 >IN !
BEGIN NextWord DUP WHILE
val >R 0x0D PARSE R> new-msg 1 >IN +!
REPEAT 2DROP
NR> RESTORE-SOURCE
ELSE -1 THROW
THEN ;
Необходимо учитывать, что для хранения строк сообщений используется хип, а это значит, что для каждого потока сообщения будут локальны, то есть в каждом потоке можно использовать собственный набор сообщений. Для хранения сообщений используется линейный список, а это значит, что для сотен тысяч сообщений лучше модифицировать методику хранения сообщений для более быстрого поиска сообщений, как по номеру сообщения, так и по содержимому. Причем, повышение скорости поиска по содержимому ускорит время компиляции кода, в то время, как по номеру – ускорит поиск во время работы готовой программы. Для пары сотен сообщений скорости поиска по линейному списку вполне достаточно для нормальной работы. Рабочую библиотеку можно взять в spf\devel\~mOleg\lib\strings\msg.f с cvs.
Вывод Все перечисленные методики часто встречаются с небольшими модификациями в программах, причем, может использоваться несколько вариантов одновременно, в зависимости от удобства применения.
Так что как всегда стоит в каждом конкретном случае выбирать наиболее удобный вариант и модифицировать под конкретные условия.
Литература:
1. spf\devel\~mOleg\lib\strings\messages.f , messages.dsc
2. spf\devel\~mOleg\lib\strings\msg.f
3. исходные тексты системы info-forth 4 версии text.scr
4. исходные тексты системы SPF версии 4.17 и выше
5. исходные тексты системы Win32Forth