Конструирование оснастки для обработки оконных сообщений Windows
Язык Форт большинству кажется наименее подходящим, чтобы программировать на нем, да еще и под Windows. Ведь в нем нет никакой графики, только унылая черная текстовая консоль.
Попробуем преодолеть этот миф.
Во-первых, программировать под Windows оказывается очень легко, достаточно открыть любую инструкцию по WinAPI.
Во-вторых, сама Windows управляет всей своей графикой, нам достаточно лишь вызывать нужные функции и правильно обрабатывать сообщения.
Перед созданием окна необходимо создать свой класс. В структуре WNDCLASS есть поле WNDPROC lpfnWndProc, которое содержит ссылку на процедуру обработки сообщений, поступающих от окон данного класса.
Требования у Windows к данной процедуре несложные:
1) Если сообщение не обрабатывается процедурой, необходимо вызвать функцию DefWindowProc
2) Сохранить содержимое регистров rdi rsi rbx
Сделаем ассемблерную вставку. Нам нужна согласующая заглушка, которая будет вызывать процедуру, написанную на Форте. Обратно, если из высокоуровневой процедуры приходит сигнал о том, что сообщение не было обработано, вызвать DefWindowProc.
HEADER winproc HERE CELL+ ,
push_rcx push_rdx push_r8 push_r9 push_rbx push_rsi push_rdi
mov_rax,# hwnd , mov_[rax],rcx
mov_rax,# wmsg , mov_[rax],rdx
mov_rax,# wparam , mov_[rax],r8
mov_rax,# lparam , mov_[rax],r9
mov_rax,# ' inWinProc ,
mov_r11,# ' Push @ , call_r11
mov_r11,# ' EXECUTE @ , call_r11
mov_r11,# ' Pop @ , call_r11
test_rax,rax
jne forward>
pop_rdi pop_rsi pop_rbx pop_r9 pop_r8 pop_rdx pop_rcx
ret
>forward
pop_rdi pop_rsi pop_rbx pop_r9 pop_r8 pop_rdx pop_rcx
push_rcx push_rdx push_r8 push_r9 push_rbx push_rsi push_rdi
mov_r11,# ' DefWindowProcA CELL+ @ ,
sub_rsp,b# 0x 20 B,
call_r11
add_rsp,b# 0x 20 B,
pop_rdi pop_rsi pop_rbx pop_r9 pop_r8 pop_rdx pop_rcx
ret
Логика данного куска понятна без лишних комментариев.
1) Сохраним параметры в переменные
2) Вызовем высокоуровневую процедуру
3) Вызовем DefWindowProc, если получен не ноль
Теперь займемся высокоуровневой частью
Слово Форта само по себе является процедурой.
WORD: Messages
do_something
;WORD
Какое something мы должны do, сейчас и будем выяснять.
В ассемблерной вставке мы видим использование переменной wmsg. Она принимает параметр uMsg — номер сообщения Windows. Нам необходимо сравнить содержимое wmsg с номером нужного нам сообщения, и если это тот номер, обработать сообщение. Вернуть ноль, чтобы DefWindowProc не вызывалась.
WORD: Messages
wmsg @ hex, 201 (( WM_LBUTTONDOWN ) = If
1 Else
do_lbuttondown 0 Then
;WORD
Имеет право на существование. Но это приемлемо, когда надо обработать одно-два сообщения. Но неудобно, некрасиво, плохо сопровождаемо и не решает поставленную задачу. Ведь придется писать вложенные конструкции If Then, а это ужас-ужас в листинге.
WORD: Messages
wmsg @ hex, 201 (( WM_LBUTTONDOWN ) = If
wmsg @ hex, 202 (( WM_LBUTTONUP ) = If
1 Else
do_lbuttonup 0 Then
Else
do_lbuttondown 0 Then
;WORD
Всего два сообщения, а приходится напрягаться чтобы быть уверенным что написано правильно.
К счастью, конструкция Case… Of… EndOf… EndCase реализуется довольно легко и существенно украшает код.
WORD: Messages
Case
wmsg @ hex, 201 (( WM_LBUTTONDOWN ) = Of do_lbuttondown 0 EndOf
wmsg @ hex, 202 (( WM_LBUTTONUP ) = Of do_lbuttonup 0 EndOf
EndCase
;WORD
Гораздо приятнее читать и, если что, добавлять еще обработчики. Но все же можно лучше.
Во-первых, здесь постоянно повторяется wmsg @ и =.
Во-вторых, вставлять численные шестнадцатеричные значения констант как-то неэстетично. К тому же приходится писать комментарий, что это значение обозначает.
Пусть WM_LBUTTONDOWN, WM_LBUTTONUP и т.п. будут константами.
WORD: (?wm)
wmsg @ =
;WORD
WORD: Messages
Case
WM_LBUTTONDOWN (?wm) Of do_lbuttondown 0 EndOf
WM_LBUTTONUP (?wm) Of do_lbuttonup 0 EndOf
EndCase
;WORD
Стало гораздо красивее и понятнее. Но все равно слишком много лишних слов в листинге.
Messages{{
WM_LBUTTONDOWN{{ do_lbuttondown }}
WM_LBUTTONUP{{ do_lbuttonup }}
}}Messages
Решим эту задачу.
Самое простое — это реализовать слово }}. Оно — почти эквивалент слова EndOf, просто к нему мы присовкупим слово 0.
WORD: }}
0 EndOf
;WORD
А вот и нет. Слово EndOf немедленного исполнения. Вместо того, чтобы скомпилироваться, оно будет исполнено. Исполнено во время компиляции слова }}. А нам надо, чтобы оно исполнялось во время компиляции модуля обработки сообщений.
WORD: EndOf
COMPILE BRANCH HERE >R COMPILE 0 THEN R>
;WORD
Воспользуемся его Величеством Копипастом и напишем… Но для начала учтем, что слово }} должно быть немедленного исполнения.
IMMEDIATES CURRENT !
WORD: EndOf
COMPILE 0 COMPILE BRANCH HERE >R COMPILE 0 THEN R>
;WORD
FORTH32 CURRENT !
Вставим слово }} на место 0 EndOf, и убедимся в его работоспособности.
Разберемся со словом }}Messages
Оно должно:
1) компилировать не ноль
2) выполнять EndCase
3) заканчивать компиляцию, аналогично ;WORD
Заметим, это слово немедленного исполнения.
IMMEDIATES CURRENT !
WORD: }}Messages
COMPILE 1 (EndOf) ;Word quit ;WORD
;WORD
FORTH32 CURRENT !
А сейчас сконструируем открывающие слова. Начнем с Messages{{
Что оно должно делать?
4) запускать компиляцию
3) компилировать Case
2) делать адрес начала процедуры доступным вставке winproc
1) отмечать адрес, с которого начнется процедура обработки сообщений
Автоматическая компиляция запускается словом immediator. Оно заполняет поле параметров компилируемого слова соответственно исходному тексту. Полю параметров предшествует поле кода, которое в случае высокоуровневого определения должно содержать ссылку на адресный интерпретатор. Её нам дает константа interpret#. Слово Case суть синоним слова 0. Просто Case — немедленного исполнения, а 0 — обычное компилируемое слово.
WORD: Messages{{
HERE ['] inWinProc CELL+ ! 0 interpret# , immediator
;WORD
Вызов inWinProc мы встречаем в ассемблерной вставке. Это так называемое векторное слово. Оно почти обычная константа, но вместо того, чтобы положить значение на стек, исполняет его.
Теперь самое интересное
IMMEDIATES CURRENT !
WORD: WM_LBUTTONDOWN{{
COMPILE WM_LBUTTONDOWN COMPILE (?wm) COMPILE ?OF HERE COMPILE 0
;WORD
WORD: WM_LBUTTONUP{{
COMPILE WM_LBUTTONUP COMPILE (?wm) COMPILE ?OF HERE COMPILE 0
;WORD
FORTH32 CURRENT !
Неужели придется для каждого сообщения копипастить этот код, исправляя лишь константу? Посмотрим повнимательнее. Код в каждом определении один и тот же, они различаются лишь именем и используемой константой. Эта константа является для последующего кода параметром. Схематично выглядит как x do_something_with_x.
На наше счастье в Форте существует понятие определяющего слова. Которые и предназначены для таких случаев.
WORD: WM:
CREATE , DOES> @ COMPILE (?wm) COMPILE ?OF HERE COMPILE 0
;WORD
IMMEDIATES CURRENT !
WM_LBUTTONDOWN WM: WM_LBUTTONDOWN{{
WM_LBUTTONUP WM: WM_LBUTTONUP{{
FORTH32 CURRENT !
Эээ… А зачем мы повторяем один и тот же текст слева и справа? И даже трижды. (Мы же определили ранее константы). Может быть стоит не определять константы, а сразу определять слова?
0d 513 WM: WM_LBUTTONDOWN{{
0d 514 WM: WM_LBUTTONUP{{
И… Не работает. Посмотрим повнимательнее. Во-первых, все эти слова должны быть немедленного исполнения. То-есть они должны скомпилировать код после DOES> в тело Messages{{.
Эта часть: COMPILE (?wm) COMPILE ?OF HERE COMPILE 0 все делает правильно. Но сразу после DOES> мы получаем значение, скомпилированное во время создания слова WM_L… А оно нам нужно во время исполнения слова Messages{{.
Нам надо всего-лишь скомпилировать это значение как литерал уже в тело Messages{{.
WORD: WM:
CREATE , DOES> @ LIT, COMPILE (?wm) COMPILE ?OF HERE COMPILE 0
;WORD
Подытожим. Удобно выделить общую, заголовочную часть в отдельный файл.
WORD: Messages{{
HERE ['] inWinProc CELL+ ! 0 interpret# , immediator
;WORD
WORD: (?wm)
wmsg @ =
;WORD
WORD: WM:
CREATE ,
DOES> @ LIT, COMPILE (?wm) COMPILE ?OF HERE COMPILE 0
;WORD
IMMEDIATES CURRENT ! FORTH32 CONTEXT !
WORD: }}Messages
COMPILE 1 (EndCase) ;Word quit
;WORD
WORD: }}
COMPILE 0 COMPILE BRANCH HERE >R COMPILE 0 THEN R>
;WORD
0d 513 WM: WM_LBUTTONDOWN{{
0d 514 WM: WM_LBUTTONUP{{
0d 512 WM: WM_MOUSEMOVE{{
0d 15 WM: WM_PAINT{{
0d 16 WM: WM_CLOSE{{
FORTH32 CURRENT !
Файл
INCLUDE: winuser.f
WORD: do_on_lbuttondown
do on left button down
;WORD
WORD: do_on_lbuttondup
do on left button up
;WORD
do someting else
Messages{{
WM_LBUTTONDOWN{{ do_on_lbuttondown }}
WM_LBUTTONUP{{ do_on_lbuttondup }}
}}Messages
EXIT
Послесловие
Заметьте, несмотря на использования языка Форт, мы ни разу не вспомнили про стек и не встретили ни одного слова манипуляции со стеком. А еще мы спрятали не самую кошмарную структуру управления. Её не видно, хоть она и есть. код получился более описательный, чем процедурный. Ко всему прочему, код не требует комментариев, он сам читается как комментарий. Форт-система, написанная на Форте является сама себе справочником. Еще один нюанс. Для разработки своей программы вы можете использовать средства любого уровня. От встроенного ассемблера, даже ниже, вы можете внести во встроенный ассемблер недостающие опкоды и мнемоники и до создания высокоуровневых, обобщающих инструментов, позволяющих создавать компактный выразительный код.
Комментарии (13)
oco
16.03.2017 20:07Я бы выделил слово {{ отдельно
V2008n
16.03.2017 21:45Можно. Но… не нужно. Нет такого функционала, ради которого стоит заводить еще одно слово. Да еще с ничего не говорящим именем. {{ в конце слов, определенных через WM: в первую очередь украшательство. Можно вообще обойтись без них. Например сделать такой интерфейс: WM_LBUTTONDOWN do_smth ;WM
В предыдущей версии скобки были одинарные, но сразу выяснилось что их легко потерять или перепутать с круглыми.
Кроме того, код после DOES> в WM: существует в системе в единственном экземпляре, а не копируется в каждое слово, определенное через WM:. То-есть экономии никакой нет. Но я прям чувствую, что отдельно стоящие скобки так и будет тянуть или потерять, или прилепить.
Jef239
17.03.2017 07:21Приятно видеть, что форт продолжает жить.
А вообще форт намного удобней для интерактивной разработки. Например для тюнинга ядра ОС. Написал разбор таблицы — и сразу запомнил как функцию.
Beholder
17.03.2017 09:40+2В результате мы в статье так и не увидели никакой графики, а только
унылая черная текстовая консольчёрный текст на белом фоне.
ertaquo
Знаете, мне и с графикой Forth кажется не слишком подходящим для программирования. Не в этом веке и не для современных машин и задач.
oco
Для микроконтроллеров вполне себе подходит
kostus1974
собственно, идеи форта живут практически во всех плк. яркий пример — сименсовские s7 + stl в step7. всё через стек. то есть идея-то одна — постоянное изменение некоторого потока данных или некоторого состояния, некоторой переменной. стековые языки очень просты в плане реализации.
я несколько лет использовал именно stl (+ lad и fbd — тоже, в принципе, потоковая обработка переменных на стеке) для задач автоматизации.
но тот же сименсовский scl («обычный» яп, похожий на паскаль) в случаях не очень тривиальных — более или менее сложная логика, циклы (открытые, конечно!) — гораздо читабельнее. его гораздо проще поддерживать, анализировать глазами.
так что такие яп имеют право на жизнь именно там, где нужна простота, где нет сложных задач.
да, можно на форте писать сложные системы. можно также написать аналог офиса на баше. но зачем и кто это будет развивать?
… всё-таки не нужно.) уже нет.
V2008n
Понимаете, в чем дело. Идея Форта — не стек. Основное в Форте — расширяемость. Вы создаете сами инструменты для расширения системы под решаемую задачу. Причем можете создавать их на любом уровне, от машинного кода до самого верхнего уровня, причем на всех одновременно. Это и мощь и слабость. Чтобы писать на Форте, надо вывернуть мышление наизнанку. Это пугает «традиционалистов».
А на вечный вопрос «зачем?» ответ — хочу! :)
oco
removed
V2008n
На современных машинах он может обрести второе дыхание. С нынешней модой на виртуализацию.