Конструирование оснастки для обработки оконных сообщений Windows
Язык Форт большинству кажется наименее подходящим, чтобы программировать на нем, да еще и под Windows. Ведь в нем нет никакой графики, только унылая черная текстовая консоль.
Попробуем преодолеть этот миф.
Во-первых, программировать под Windows оказывается очень легко, достаточно открыть любую инструкцию по WinAPI.
Во-вторых, сама Windows управляет всей своей графикой, нам достаточно лишь вызывать нужные функции и правильно обрабатывать сообщения.
Перед созданием окна необходимо создать свой класс. В структуре WNDCLASS есть поле WNDPROC lpfnWndProc, которое содержит ссылку на процедуру обработки сообщений, поступающих от окон данного класса.
Требования у Windows к данной процедуре несложные:
1) Если сообщение не обрабатывается процедурой, необходимо вызвать функцию DefWindowProc
2) Сохранить содержимое регистров rdi rsi rbx
Сделаем ассемблерную вставку. Нам нужна согласующая заглушка, которая будет вызывать процедуру, написанную на Форте. Обратно, если из высокоуровневой процедуры приходит сигнал о том, что сообщение не было обработано, вызвать DefWindowProc.
winproc
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 и т.п. будут константами.
A wmsg @ и = объединим в одно слово.
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 немедленного исполнения. Вместо того, чтобы скомпилироваться, оно будет исполнено. Исполнено во время компиляции слова }}. А нам надо, чтобы оно исполнялось во время компиляции модуля обработки сообщений.
Взглянем на реализацию 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 мы встречаем в ассемблерной вставке. Это так называемое векторное слово. Оно почти обычная константа, но вместо того, чтобы положить значение на стек, исполняет его.
Теперь самое интересное
Определим слова WM_LBUTTONDOWN{{ и WM_LBUTTONUP{{
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
Подытожим. Удобно выделить общую, заголовочную часть в отдельный файл.
winuser.f
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 !
Файл
test.f
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
Послесловие
Заметьте, несмотря на использования языка Форт, мы ни разу не вспомнили про стек и не встретили ни одного слова манипуляции со стеком. А еще мы спрятали не самую кошмарную структуру управления. Её не видно, хоть она и есть. код получился более описательный, чем процедурный. Ко всему прочему, код не требует комментариев, он сам читается как комментарий. Форт-система, написанная на Форте является сама себе справочником. Еще один нюанс. Для разработки своей программы вы можете использовать средства любого уровня. От встроенного ассемблера, даже ниже, вы можете внести во встроенный ассемблер недостающие опкоды и мнемоники и до создания высокоуровневых, обобщающих инструментов, позволяющих создавать компактный выразительный код.