Pull to refresh

Windows Forth +

Reading time 6 min
Views 7.1K

Конструирование оснастки для обработки оконных сообщений 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

Послесловие


Заметьте, несмотря на использования языка Форт, мы ни разу не вспомнили про стек и не встретили ни одного слова манипуляции со стеком. А еще мы спрятали не самую кошмарную структуру управления. Её не видно, хоть она и есть. код получился более описательный, чем процедурный. Ко всему прочему, код не требует комментариев, он сам читается как комментарий. Форт-система, написанная на Форте является сама себе справочником. Еще один нюанс. Для разработки своей программы вы можете использовать средства любого уровня. От встроенного ассемблера, даже ниже, вы можете внести во встроенный ассемблер недостающие опкоды и мнемоники и до создания высокоуровневых, обобщающих инструментов, позволяющих создавать компактный выразительный код.
Tags:
Hubs:
+26
Comments 13
Comments Comments 13

Articles