Особенности реализации SysGetKey() в REXX/Object REXX под OS/2

Попытался причесать свои мысли по поводу малоприятной особенности реализации SysGetKey(), создающей проблемы для русскоязычных пользователей. Впрочем, это касается вообще всех языков, использующих кириллицу, а также греческого языка, части восточноевропейских и некоторых западноевропейских языков, например, французского.
---
Проблема заключается в том, что для этих языков один из символов алфавита кодируется шестнадцатиричным значением "E0"x в соответствии с применяемой кодовой страницей. Например, для русского языка и кодовой страницы CP866 - это строчная буква кириллицы "р", для болгарского и кодовой страницы CP855 - это прописная буква кириллицы "Я", для греческого и кодовой страницы CP869 - это маленькая греческая буква "зета" и так далее.

Одновременно шестнадцатиричное значение "E0"x используется в качестве индикатора получения сканкода после нажатия какой-либо клавиши расширенной функциональности, например, одной из клавиш блока управления курсором. А при последовательном побайтовом считывании данных из буфера клавиатуры, реализованном в SysGetKey(), весьма затруднительно понять является ли полученный байт со значением "E0"x первым из последовательности байтов сканкода или отдельным символом национального алфавита.

Чтобы понять, что происходит на самом деле и можно ли бороться с этим явлением, протребовалось провести детальное исследование поведения функции SysGetKey().

Поведение, ожидаемое от SysGetKey(), можно узнать из документации.

Описание (согласно руководствам "OS/2 Procedures Language 2/REXX" и "Object REXX Reference"):

 >>---SysGetKey(---+------------+---)---><
                   +---option---+

Считывает из буфера клавиатуры и возвращает в качестве результата очередные данные о нажатии клавиши. Если буфер клавиатуры пуст, то ожидает нажатия клавиши. В отличие от встроенной функции CHARIN(), не требует завершения процесса ввода нажатием клавиши Enter. В руководстве "OS/2 Procedures Language 2/REXX" присутствует фраза: "Выполняется аналогично функции getch() языка Си."

Параметры:
   option    (необязательный) управляет процессом эхо-вывода на экран данных о нажатии клавиши. В руководстве "OS/2 Procedures Language 2/REXX" вместо термина "данные о нажатии клавиши" (key, keystroke) используется термин "символ" (character).
Возможные значения параметра:
   "ECHO"   разрешает эхо-вывод (по умолчанию),
   "NOECHO" запрещает эхо-вывод.

Возвращаемое значение:
Данные о нажатой клавише, полученные из буфера клавиатуры.


Какие данные получает SysGetKey() из буфера клавиатуры в документации не расшифровывается. Подсказка об аналогии с функцией getch() языка Си в руководстве "OS/2 Procedures Language 2/REXX" позволяет немного прояснить ситуацию: "Функция getch() считывает символ из входного потока консоли. При чтении данных о нажатии функциональных клавиш или клавиш расширенной функциональности каждая функция должна вызываться дважды; первый вызов возвращает 0x00 или 0xE0, а второй вызов возвращает действительный код клавиши."

Исходя из этого, можно ожидать, что SysGetKey() всегда возвращает однобайтовую строку, содержащую либо символ, соответствующий нажатой клавише, либо один из байтов сканкода клавиши. Сканкод идентифицируется получением байта со значением "00"x или "E0"x.

Приведённый ниже тест позволяет убедиться в этом.

Тест № 1:

 /* Sample1.cmd - test of SysGetKey() */
 parse version _ver
 say _ver
 if RxFuncQuery('SysLoadFuncs') then do
    _rc = RxFuncAdd('SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs');
    _rc = SysLoadFuncs();
 end;
 say SysVersion()
 say 'RexxUtil v.'||SysUtilVersion()
 say 'Press Esc to quit'
 _key = '';
 do while (_key \== '1B'x)
    _key = SysGetKey('NOECHO');
    say '_key="'||c2x(_key)||'"x'
 end;
 _rc = SysDropFuncs();
 exit;

Условия выполнения теста:
  • кодовая страница консоли - CP866
  • раскладка клавиатуры - RU441
  • клавиатура переключена на латиницу
  • CapsLock выключен
Использованы следующие цвета:
  • чёрный - текст, выводимый на экран во время выполнения
  • зелёный - комментарии
  • красный - символы кириллицы, соответствующие клавишам после переключения клавиатуры на кириллицу
Пустые строки вставлены для удобства.

Выполнение теста № 1:

 C:\>Sample1.cmd
 OBJREXX 6.00 18 May 1999
 OS/2 2.45
 RexxUtil v.2.00
 Press Esc to quit
              <--- нажата клавиша 2
 _key="32"x   => символ "2" (DIGIT TWO)
              <--- нажата комбинация клавиш Shift+2
 _key="40"x   => символ "@" (COMMERCIAL AT)
              <--- нажата комбинация клавиш Ctrl+2
 _key="00"x   => первый символ сканкода "0003"x
 _key="03"x   => второй символ сканкода "0003"x
              <--- нажата комбинация клавиш Alt+2
 _key="00"x   => первый символ сканкода "0079"x
 _key="79"x   => второй символ сканкода "0079"x
 
              <--- нажата клавиша F1
 _key="00"x   => первый символ сканкода "003B"x
 _key="3B"x   => второй символ сканкода "003B"x
              <--- нажата комбинация клавиш Shift+F1
 _key="00"x   => первый символ сканкода "0054"x
 _key="54"x   => второй символ сканкода "0054"x
              <--- нажата комбинация клавиш Ctrl+F1
 _key="00"x   => первый символ сканкода "005E"x
 _key="5E"x   => второй символ сканкода "005E"x
              <--- нажата комбинация клавиш Alt+F1
 _key="00"x   => первый символ сканкода "0068"x
 _key="68"x   => второй символ сканкода "0068"x
 
              <--- нажата клавиша GrayArrowUp
 _key="E0"x   => первый символ сканкода "E048"x
 _key="48"x   => второй символ сканкода "E048"x
              <--- нажата комбинация клавиш Shift+GrayArrowUp
 _key="E0"x   => первый символ сканкода "E048"x
 _key="48"x   => второй символ сканкода "E048"x
              <--- нажата комбинация клавиш Ctrl+GrayArrowUp
 _key="E0"x   => первый символ сканкода "E08D"x
 _key="8D"x   => второй символ сканкода "E08D"x
              <--- нажата комбинация клавиш Alt+GrayArrowUp
 _key="00"x   => первый символ сканкода "0098"x
 _key="98"x   => второй символ сканкода "0098"x
 
              <--- нажата клавиша h
 _key="68"x   => символ "h" (LATIN SMALL LETTER H)
              <--- нажата комбинация клавиш Shift+h
 _key="48"x   => символ "H" (LATIN CAPITAL LETTER H)
              <--- нажата комбинация клавиш Ctrl+h
 _key="08"x   => управляюший символ BS (BACKSPACE)
              <--- нажата комбинация клавиш Alt+h
 _key="00"x   => первый символ сканкода "0023"x
 _key="23"x   => второй символ сканкода "0023"x
 
              <--- переключаем клавиатуру на кириллицу
 
              <--- нажата клавиша р
 _key="E0"x   => символ "р" (CYRILLIC SMALL LETTER ER)
              <--- нажата комбинация клавиш Shift+р
 _key="90"x   => символ "Р" (CYRILLIC CAPITAL LETTER ER)
              <--- нажата комбинация клавиш Ctrl+р
 _key="08"x   => управляюший символ BS (BACKSPACE)
              <--- нажата комбинация клавиш Alt+р
 _key="00"x   => первый символ сканкода "0023"x
 _key="23"x   => второй символ сканкода "0023"x
 
              <--- нажата клавиша GrayArrowUp
 _key="E0"x   => первый символ сканкода "E048"x
 _key="48"x   => второй символ сканкода "E048"x
              <--- нажата клавиша р
 _key="E0"x   => символ "р" (CYRILLIC SMALL LETTER ER)
              <--- переключаем клавиатуру на латиницу
              <--- нажата комбинация клавиш Shift+h
 _key="48"x   => символ "H" (LATIN CAPITAL LETTER H)
              <--- нажата клавиша Enter
 
 _key="0D"x   => управляюший символ CR (CARRIAGE RETURN)
              <--- нажата клавиша Esc
 _key="1B"x   => управляюший символ ESC (ESCAPE)
 C:\>

Результаты теста однозначно демонстрируют соответствие реализации ожиданиям:
  • возврат значений из функции SysGetKey() происходит при КАЖДОМ нажатии на значащую клавишу (или комбинацию клавиш),
  • возвращаемые символы соответствуют выбранной раскладке клавиатуры и кодовой странице,
  • возвращаемые сканкоды соответствуют функциональным клавишам, клавишам расширенной функциональности и комбинациям клавиш,
  • нажатие комбинации клавиш Ctrl+C ожидаемо прерывает выполнение, вызывая обработку условия HALT.
Хорошо видно, что при таком подходе невозможно отличить, например, нажатие клавиши GrayArrowUp ("E048"x) от последовательного нажатия двух клавиш - со строчной буквой кириллицы "р" ("E0"x) и с прописной буквой латиницы "H" ("48"x).

Поскольку в описании функции getch(), лежащей в основе SysGetKey(), речь идёт о чтении символов из входного потока консоли, то поиск решения, которое может помочь в данной ситуации, естественно приводит к встроенной функции CHARS().

Описание (согласно документации OS/2 Procedures Language 2/REXX и Object REXX Reference):

 >>---CHARS(---+----------+---)---><
               +---name---+

Возвращает в качестве результата общее количество непрочитанных символов, оставшееся в указанном входном потоке. Сюда включаются все символы-разделители, находящиеся во входном потоке. В случае статичных потоков (например, файлов) возращает общее количество символов, начиная с текущей позиции чтения до конца потока. Если имя потока не указано, то используется STDIN.

Для потоков, у которых невозможно определить общее количество непрочитанных символов (например, STDIN), в качестве результата возвращается значение 1 при наличии данных и 0, если данные отсутствуют. Для устройств OS/2 в качестве результата всегда возращается значение 1.

Параметры:
   name    (необязательный) имя входного потока (по умолчанию STDIN).

Возвращаемое значение:
Общее количество непрочитанных символов, оставшееся в указанном входном потоке.
Если количество символов в потоке определить невозможно, то возвращается 1, если данные в потоке есть, и 0, если данные отсутствуют.
Для устройств - всегда 1.


Исходя из данного описания, логично предположить, что поскольку после нажатия клавиши расширенной функциональности требуется считать два символа из входного потока, то функция CHARS() будет сигнализировать о наличии символов во входном потоке не только после нажатия клавиши, но и после прочтения первого символа сканкода.

Приведённый ниже тест позволяет проверить это.

Тест № 2:

 /* Sample2.cmd - test of SysGetKey() with Chars() */
 parse version _ver
 say _ver
 if RxFuncQuery('SysLoadFuncs') then do
    _rc = RxFuncAdd('SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs');
    _rc = SysLoadFuncs();
 end;
 say SysVersion()
 say 'RexxUtil v.'||SysUtilVersion()
 say 'Press Esc to quit'
 _delay = 0.01;
 _sleep = 0;
 _key = '';
 do while (_key \== '1B'x)
    _rc = Chars();
    if (_rc > 0) then do
       _key = SysGetKey('NOECHO');
       say 'Chars()='||_rc||'   _key="'||c2x(_key)||'"x'
       _sleep = 0;
    end; else do
       if (\_sleep) then do
          say 'Chars()='||_rc||'   SysSleep()'
          say ''
          _sleep = 1;
       end;
       _rc = SysSleep(_delay);
    end;
 end;
 _rc = SysDropFuncs();
 exit;

Условия выполнения теста:
  • кодовая страница консоли - CP866
  • раскладка клавиатуры - RU441
  • клавиатура переключена на латиницу
  • CapsLock выключен
Использованы следующие цвета:
  • чёрный - текст, выводимый на экран во время выполнения
  • зелёный - комментарии
  • красный - символы кириллицы, соответствующие клавишам после переключения клавиатуры на кириллицу
Пустые строки вставлены для удобства.

Выполнение теста № 2:

 C:\>Sample2.cmd
 OBJREXX 6.00 18 May 1999
 OS/2 2.45
 RexxUtil v.2.00
 Press Esc to quit
 Chars()=0   SysSleep()
                          <--- нажата клавиша 2
 Chars()=1   _key="32"x   => символ "2" (DIGIT TWO)
 Chars()=0   SysSleep()
                          <--- нажата клавиша F1
 Chars()=1   _key="00"x   => первый символ сканкода "003B"x
 Chars()=0   SysSleep()
                          <--- нажата клавиша Enter
 Chars()=1   _key="3B"x   => второй символ сканкода "003B"x
 Chars()=1   _key="0D"x   => управляюший символ CR (CARRIAGE RETURN)
 Chars()=0   SysSleep()
                          <--- нажата клавиша GrArrowUp
 Chars()=1   _key="E0"x   => первый символ сканкода "E048"x
 Chars()=0   SysSleep()
                          <--- нажата клавиша Enter
 Chars()=1   _key="48"x   => второй символ сканкода "E048"x
 Chars()=1   _key="0D"x   => управляюший символ CR (CARRIAGE RETURN)
 Chars()=0   SysSleep()
                          <--- переключаем клавиатуру на кириллицу
                          <--- нажата клавиша р
 Chars()=1   _key="E0"x   => символ "р" (CYRILLIC SMALL LETTER ER)
 Chars()=0   SysSleep()
                          <--- переключаем клавиатуру на латиницу
                          <--- нажата комбинация клавиш Shift+h
 Chars()=1   _key="48"x   => символ "H" (LATIN CAPITAL LETTER H)
 Chars()=0   SysSleep()
                          <--- нажата клавиша Esc
 Chars()=1   _key="1B"x   => управляюший символ ESC (ESCAPE)
 C:\>

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

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

Возникшая терминологическая путаница в описаниях, вызвавшая ложные ожидания, связана как с историческими причинами, так и с многоплатформенностью языка Си. Действительно, если в MS-DOS и в OS/2 v1.x буфер клавиатуры - это очередь из одиночных байтов, то в OS/2, начиная с v2.0, в очередь ставятся элементы типа KBDKEYINFO. И если для MS-DOS и OS/2 v1.x ожидаемое поведение полностью соответствует описаниям, то для понимания того, что следует ожидать в OS/2 v2.0 и выше информации в документации REXX явно недостаточно.

Описание (согласно документации Control Programming Guide and Reference из пакета OS/2 ToolKit):

 typedef struct _KBDKEYINFO {
    UCHAR   chChar;
    UCHAR   chScan;
    UCHAR   fbStatus;
    UCHAR   bNlsShift;
    USHORT  fsState;
    ULONG   time;
 } KBDKEYINFO;
 typedef KBDKEYINFO *PKBDKEYINFO;

где:

   chChar    если бит 1 поля fbStatus сброшен - символ, соответствующий нажатой клавише в текущей раскладке клавиатуры и кодовой странице;
если бит 1 поля fbStatus установлен - 0x00 или 0xE0, информируя, что поле chScan содержит сканкод функциональной клавиши или клавиши расширенной функциональности
   chScan    сканкод нажатой клавиши
   fbStatus    флаги статуса события, вызванного нажатием клавиши
   bNlsShift    зарезервировано, должно быть равно 0
   fsState    флаги состояния переключателей и дополнительных клавиш, участвующих в комбинации нажатых клавиш
   time    отметка времени в миллисекундах


Исходя из вышеизложенного, следует признать, что для Classic REXX поставленная задача не имеет решения без привлечения сторонних библиотек. А вот в Object REXX можно попытаться воспользоваться тем фактом, что чтение второго символа сканкода происходит без обращения к буферу клавиатуры и, следовательно, без задержек, а для нажатия клавиши всё таки требуется некоторое время.

Что и демонстрирует приведённый ниже пример.

Тест № 3:

 /* Sample3.cmd - test of SysGetGetKey() with check timeout */
 say 'Sample3 application started'
 parse version _ver
 say _ver

 if RxFuncQuery('SysLoadFuncs') then do
    _rc = RxFuncAdd('SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs');
    _rc = SysLoadFuncs();
 end;
 say SysVersion()
 say 'RexxUtil v.'||SysUtilVersion()
 say 'Press Esc to quit'

 signal on halt
 _kbd = .ownKbd~new;
 _kbd~run;

 _wait = 0.05;
 _delay = 0.01;
 _key = '';
 do while (_key \== '1B'x)
    _key = SysGetKey('NOECHO');
    if (_key == '00'x) then do
       _key = _key||SysGetKey('NOECHO');
    end;
    _kbd~keys~queue(_key);
    if (_key \== 'E0'x) then do
       _rc = SysSleep(_delay);
    end;
 end;

 halt:
    _kbd~stop = .true;
    _rc = SysSleep(_wait);
    drop _kbd;
    say 'Sample3 application stopped'
    _rc = SysDropFuncs();
    exit;

 ::class ownKbd
 ::method stop attribute unguarded
 ::method keys attribute unguarded

 ::method init
    self~stop = .false;
    self~keys = .queue~new;
    say 'Instance of '||self~defaultname||' created'
    return;

 ::method uninit
    self~stop = .true;
    say 'Instance of '||self~defaultname||' destroyed'
    return;

 ::method run
    say 'Run() method of '||self~defaultname||' processed'
    reply
    _delay = 0.001;
    _key = '';
    do while (\self~stop)
       if (self~keys~items > 0) then do
          _key = self~keys~pull;
          if ((_key == 'E0'x) & (self~keys~items > 0)) then do
             _key = _key||self~keys~pull;
          end;
          say '_key="'||_key~c2x||'"x'
       end;
       _rc = SysSleep(_delay);
    end;
    say 'Run() method of '||self~defaultname||' terminated'
    return;

Условия выполнения теста:
  • кодовая страница консоли - CP866
  • раскладка клавиатуры - RU441
  • клавиатура переключена на латиницу
  • CapsLock выключен
Использованы следующие цвета:
  • чёрный - текст, выводимый на экран во время выполнения
  • зелёный - комментарии
  • красный - символы кириллицы, соответствующие клавишам после переключения клавиатуры на кириллицу
Пустые строки вставлены для удобства.

Выполнение теста № 3:

 C:\>Sample3.cmd
 Sample3 application started
 OBJREXX 6.00 18 May 1999
 OS/2 2.45
 RexxUtil v.2.00
 Press Esc to quit
 Instance of an OWNKBD created
 Run() method of an OWNKBD processed
                <--- нажата клавиша 2
 _key="32"x     => символ "2" (DIGIT TWO)
                <--- нажата клавиша F1
 _key="003B"x   => сканкод "003B"x
                <--- нажата клавиша GrArrowUp
 _key="E048"x   => сканкод "E048"x
                <--- переключаем клавиатуру на кириллицу
                <--- нажата клавиша р
 _key="E0"x     => символ "р" (CYRILLIC SMALL LETTER ER)
                <--- переключаем клавиатуру на латиницу
                <--- нажата комбинация клавиш Shift+h
 _key="48"x     => символ "H" (LATIN CAPITAL LETTER H)
                <--- нажата клавиша Enter
 _key="0D"x     => управляюший символ CR (CARRIAGE RETURN)
                <--- нажата клавиша Esc
 _key="1B"x     => управляюший символ ESC (ESCAPE)
 Run() method of an OWNKBD terminated
 Instance of an OWNKBD destroyed
 Sample3 application stopped
 C:\>

Этот способ, конечно, не идеален. Возможны ошибки распознавания, например, при высокой скорости набора или для сильно нагруженных приложений. Таким образом потребность в функции, которая позволяла бы уверенно отличать нажатия клавиш расширенной функциональности от нажатия указанной клавиши национального алфавита, всё равно остаётся.
---

Смотри также:
Особенности реализации SysGetKey() в Open Object REXX под Windows

Библиотека Yet Another GetKey for REXX
Пользователь akhceloo сослался на вашу запись в своей записи «Библиотека Yet Another GetKey for REXX» в контексте: [...] Разборки с функцией SysGetKey (см. Особенности реализации SysGetKey() в REXX/Object REXX под OS/2 [...]