Вступление.
Петр Соболев попросил меня написать статью о программировании для PalmPilot на примере CyrHack. Мне и самому было бы интересно рассказать об этом - глядишь, полку приложений для пилота прибудет :)
Почему именно CyrHack? Хотелось, с одной стороны, попробовать написать что-нибудь для PalmPilot; с другой - плохо представлялось, что именно. На тот момент (август-сентябрь 98) в конференции пользователей КПК шло активное обсуждение единственного программного продукта, выполнявшего локализацию пилота - PiLoc фирмы Парагон. При всех достоинствах PiLoc были у него и недостатки - например, в распространявшейся бесплатно версии PiLoc 2.0 присутствовала ошибка, благодаря которой активная работа аппаратными кнопками приводила к зависанию устройства с потерей всего содержимого ОЗУ (превращала Pilot в "калькулятор за $400"). Кроме того, раздражала позиция Парагона по отношению к поддержке своего продукта - несмотря на многочисленные жалобы пользователей PiLoc, фирма хранила молчание относительно ошибки и своих планов по ее устранению. Мол, мы монополисты - куда ж вы от нас денетесь.
Мне известны еще несколько проектов по локализации PalmOS - это CyRus (shareware; автор Сергей Меньшиков) и Russian III (sourceware; автор Константин Кляцкин). Именно Константину CyrHack обязан некоторыми важными решениями. Недавно появился еще один - PaPiRus от фирмы ФизТехСофт. Больше русификаторов, хороших и разных!
С точки зpения пpогpаммиста..
..PalmPilot мало чем отличается от обычного компьютера. Процессор, совместимый по системе команд с M68000, память (от 128К до 2М, с возможностью расширения до 16М), устройства ввода/вывода (ЖК-дисплей разрешением 160х160 с сенсорной панелью, несколько кнопок и последовательный порт) - что еще нужно, чтобы достойно встретить старость?
Средств для создания программ под PalmOS более чем достаточно - от бесплатных GNU SDK и ASDK до крутых коммерческих вроде CodeWarrior от Metrowerks. Существуют даже средства разработки прямо на пилоте (например, Pocket C, пара бейсиков, Forth и даже LISP). Писать можно на чем угодно - от ассемблера до C и Java. На http://www.palm.com можно бесплатно разжиться SDK 3.0, содержащим документацию по PalmOS 3.0, неплохой эмулятор PalmPilot для Windows 95 POSE (сделан на основе известного эмулятора CoPilot) и образ ROM для него. Существуют также эмуляторы для MacOS и X-Windows (xcopilot).
Мне оказалось удобней писать на ассемблере. Во-первых, код получается достаточно компактный; во-вторых, компилятор PILA входит в поставку бесплатного ASDK (альтернативный SDK), доступен в исходных текстах и легко собирается практически под любую платформу.
Более подробно о средствах разработки можно узнать на www.massena.com
Хаки
Хак (hack, patch) - небольшая программа (или кусочек кода), изменяющий или расширяющий функциональность ОС. Как правило, хак подменяет один или несколько системных вызовов, а также может выполнять некоторые не вполне стандартные с точки зрения системы действия, вроде прямого доступа к глобальным переменным системы или регистрам микропроцессора (какое кощунство!).
Чтобы несколько хаков, перехватывающих одни и те же вызовы (systraps), не подрались между собой (и не привели систему в состояние перманентного изумления), в 1996 году Edward Keyes предложил элегантный способ их организации - путем введения специального API по установке/удалению хаков, и разработал замечательную программу HackMaster. Благодаря HackMaster разработчику хака больше не нужно отвлекаться на обвязку своего кода, зачастую состоящего из двух-трех десятков команд, процедурами установки и удаления хака, отработки системных событий и тому подобной шелухой - все это берет на себя HackMaster.
На страничке, посвященной HackMaster, можно также найти статью о том, как самостоятельно написать хак. Статья настолько живая и интересная, что решение о том, что альтернативный русификатор для пилота должен быть выполнен в виде хака, окрепло окончательно.
Разумеется, реализация программы в виде хака имеет и свои недостатки. Первый - хаку нужен HackMaster; хотя он и является условно-бесплатным (и без ограничений), все же есть люди, которых необходимость ставить на пилот HackMaster несколько напрягает. Впрочем, пилотер, у которого нет в системе ни одного хака, на мой взгляд, не настоящий пилотер :)
Второй недостаток, весьма чувствительный - хаку недоступны глобальные переменные. Есть и третий - если хак подменяет некоторые системные значения своими, то в случае деактивации хака отыграть их обратно нет возможности (В HackMaster версии 0.9 не была реализована так называемая Custom Install Procedure; Ed Keyes обещал реализовать ее в версии 2.0, которая сейчас находится в стадии бета-тестирования, но, насколько мне известно, так и не выполнил обещания). Наконец, хаку недоступна возможность выполнить какие-либо действия сразу после сброса системы. Наверняка есть и еще недостатки.
О гpаффити
В CyrHack не была реализована собственная граффити, поскольку в PalmOS, предоставляющей программисту [относительно] удобный и [достаточно] хорошо сгруппированный набор вызовов API по работе с памятью, вводом-выводом, пользовательским интерфейсом и т.п., не были предусмотрены средства по модификации, расширению или полной замене Graffiti Manager. В принципе, ничего сложного в перехвате всех вызовов Graffiti Manager и подмене их своим кодом нет, но вот реализация собственного движка (engine) по распознаванию рукописного ввода - задача не из простых (и недешевых ;)). Поэтому представляется разумным подход "малой кровью", использованный в PiLoc, Russian III и CyrHack - использование сходных по начертанию штрихов для ввода кириллицы.
Как это работает
Условно функции, выполняемые CyrHackом, можно
разделить на три группы
- вывод кириллицы (шрифты)
- ввод кириллицы (Граффити, экранная
клавиатура)
- сервисные (сортировка, установка
значений по включению питания и проч.)
ВЫВОД
Замена встроенных шрифтов PalmOS на русифицированные осуществляется модификацией таблицы шрифтов. Делается это не от хорошей жизни - хотя в API имеется вызов FntGetFontPtr, который, казалось бы, позволяет обойтись без хирургического вмешательства в содержимое означенной таблицы, но практически ни один из вызовов Font Manager им не пользуется - все лезут напрямую в память.
(Механизм работы со шрифтами несколько изменился в PalmOS версии 3.0, в частности, появилась возможность регистрировать в системе свои шрифты (Custom fonts) с возможностью их последующего использования; в CyrHack не используется)
Собственно модификация таблицы шрифтов производится в перехватываемом вызове FntSetFont в рамках инициализации структур хака (начальные значения переключателей, указатели на таблицы перекодировок и тому подобное):
pSys equ 'psys' systrap FtrGet(#pSys.l, #1.w, &osVersion(a6).l) move.l osVersion(a6), Features.osversion(a3) cmp.b #03, osVersion(a6) beq.s PalmOS3 PalmOS12 lea Fonts(pc), a0 move.l a0, $01d2.l adda.l #2694.l, a0 move.l a0, $01d6.l adda.l #3002.l, a0 move.l a0, $01da.l adda.l #3598+3782.l, a0 move.l a0, $01de.l bra.s PalmOS99 PalmOS3 systrap MemPtrNew(#32.l) move.l a0,Features.fontptr(a3) move.l a0,a2 systrap MemPtrSetOwner(a2.l, #0.l) movea.l $01da.l, a0 systrap MemMove(a2.l, a0.l, #32.l) lea Fonts(pc), a0 move.l a0, (a2) adda.l #2694.l, a0 move.l a0, 4(a2) adda.l #3002.l, a0 move.l a0, 8(a2) adda.l #3598.l, a0 move.l a0, 28(a2) adda.l #3782.l, a0 move.l a0,12(a2) move.l a2, $01da.l PalmOS99
Для поддержки в CyrHack различных кодировок кириллицы был выбран довольно панковский подход. Самое простое решение - добавление в него шрифтов в соответствующих кодировках - сильно увеличило бы размер хака (в среднем на 10К для каждой кодировки), что не есть хорошо в условиях вечного дефицита памяти, особенно на устройствах с 512К и менее. Было решено перехватить вызов WinDrawChars и перекодировать выводимые на экран символы в соответствии с таблицами (128 байт на кодировку). Разумеется, при этом вывод на экран существенно замедляется, но, во-первых, он и так притормаживается вызовами Feature Manager, а во-вторых, мы же панки!
; 0 - 1251, 1 - koi8r, 2 - 866, 3 - Mac, etc tst.l Features.encoding(a4) beq.s Go movem.l d0-d2/a0-a3, -(a7) ; T r a n s l a t i o n move.l Features.chrtable(a4), a1 move.l Features.encoding(a4), d0 subi.l #1, d0 asl.l #7, d0 adda.l d0, a1 moveq #0, d0 ; счетчик цикла move.w wLen(a6), d0 move.l Features.buffer(a4),a2 move.l pChar(a6),a3 ; адрес процедуры трансляции move.l Features.translation(a4),a0 jsr (a0) ; e n d movem.l (a7)+,d0-d2/a0-a3 ; подмена исходной строки оттранслированной move.l Features.buffer(a4),pbuf(a6) Go move.w bY(a6), -(a7) move.w bX(a6), -(a7) move.w wLen(a6), -(a7) move.l pbuf(a6), -(a7) move.l pfnOldProc(a6), a0 jsr (a0)
Но шрифты в PalmOS пропорциональные - отсюда вытекает необходимость перехватывать еще и несколько вызовов Font Manager, в частности, FntCharsWidth, FntLineWidth и FntCharWidth. Судя по тому, что результат довольно коряв, стопроцентного успеха пока достичь не удается :-)
ВВОД
Что касается экранной клавиатуры, то здесь все довольно просто. Перехватывается DmGetResource и его параметры модифицируются в зависимости от текущей кодировки таким образом, что при отрисовке клавиатуры вызовом SysKeyboardDialog используются ресурсы в соответствующей кодировке:
cmpi.l #tkbd, type(a6) bne.s No cmpi.w #$2710, id(a6) bne.s No ; start fiddling move.l Features.encoding(a0), d0 add.w d0, id(a6) No movem.l (a7)+, d0-d2/a0-a2 move.w id(a6), -(a7) move.l type(a6), -(a7) move.l pfnOldProc(a6), a0 jsr (a0)
С Граффити дело обстоит немного сложнее. Перехват EvtEnqueueKey - очевидный способ для модификации кодов, помещаемых в Key Queue - несовместим с "тяжелыми" вызовами, вроде вызовов Feature Manager. Поэтому было решено перехватывать SysHandleEvent (весьма удобное место для полета фантазии, например, для переключения языка щелчком по индикатору RU/EN):
movea.l event(a6), a0 move.w EventType.eType(a0), d0 ; check if it is key event cmp.w #keyDownEvent, d0 bne PenDown move.w EventType.data + keyDown.chr(a3), d3 Toggle cmp.w #161, d3 ; Ext.shift + | bne.s Switch move.l Features.ruslat(a2), d0 eori.l #1, d0 move.l d0, Features.ruslat(a2) lea.l DrawIndicator(pc), a0 jsr (a0) moveq #0,d0 bra Change ; cycle encodings Switch cmp.w #165, d3 ; Ext.shift + Y bne Go addi.l #1, Features.encoding(a2) cmpi.l #4, Features.encoding(a2) blt.s Switch1 move.l #0,Features.encoding(a2) Switch1 systrap FrmGetActiveFormID() systrap FrmUpdateForm(d0.w, #frmRedrawUpdateCode.w)
Русское Граффити сделано аналогично PiLoc - так привычней и проще. Введенные с помощью Граффити коды перекодируются с помощью простой таблицы (64 байт на кодировку).
; remap stroke if RU is active cmp.l #1, Features.ruslat(a2) ; 0 - EN, 1 - RU bne.s Done0 Remap move.l Features.grftable(a2), a0 move.w d3, d0 subi.b #'A', d0 add.l d0, a0 move.b (a0), d0 Change move.w d0, EventType.data + keyDown.chr(a3)
GSI (Graffiti Shift Indicator) сделан с помощью двух шрифтов, различающихся только значками RU/Ru/ru и EN/En/en. Не совсем здорово в смысле объема (лишних ~600 байт), но зато прикольно. Шрифты просто переключаются в зависимости от активной раскладки (снова патчится многострадальная таблица шрифтов).
proc DrawIndicator() beginproc move.l d0,-(a7) ; patch GSI font entry cmp.b #03, Features.osversion(a2) beq.s PalmOS03 PalmOS12 move.l Features.gsien(a2), $01de.l tst.l Features.ruslat(a2) beq.s DrawGSI add.l #334,$01de.l bra.s DrawGSI PalmOS03 move.l Features.fontptr(a2), a0 move.l Features.gsien(a2), 12(a0) tst.l Features.ruslat(a2) beq.s DrawGSI add.l #334,12(a0) DrawGSI systrap GsiEnabled() systrap GsiEnable(d0.b) move.l (a7)+, d0 endproc
Переключение раскладки щелчком по индикатору EN/RU (красивая идея, правда?) сделано довольно просто. Так как ранее мы уже начали заигрывать с вызовом SysHandleEvent, то не составит труда получить (и использовать раньше других) координаты пера - достаточно обработать penDownEvent. Дальше остается только определить, не произошло ли опускание пера на область, занятую индикатором (ради этого пришлось перехватить вызов GsiSetLocation, потому что в PalmOS (сюрприз!) нет законного способа узнать координаты индикатора на экране. Попутно выяснилось, что в документации display- и window-relative координаты перепутаны. Поубивал бы, ей-богу).
PenDown move.l #0, handled(a6) ; check if it is penDown event cmp.w #penDownEvent, d0 bne Done ; get window coordinates of pen moveq #0, d0 move.w EventType.screenX(a0), d0 ; x cmp.w Features.gsix(a2), d0 bmi PenDown0 sub.w Features.gsix(a2), d0 cmpi.w #10, d0 bgt PenDown0 move.w EventType.screenY(a0), d0 ; y cmp.w Features.gsiy(a2), d0 bmi PenDown0 sub.w Features.gsiy(a2), d0 cmpi.w #8, d0 bgt PenDown0 ; if GSI is not enabled then we just skip along systrap GsiEnabled() tst.b d0 beq.s PenDown0 move.l #1, handled(a6) ; reverse ruslat move.l Features.ruslat(a2), d0 eori.l #1, d0 move.l d0, Features.ruslat(a2) lea.l DrawIndicator(pc), a0 jsr (a0) ; когда-нибудь дойдут руки приделать звуковую индикацию :) ; systrap SndPlaySystemSound(#1.b) PenDown0
Вот с чем пришлось повозиться, так это с Commands и Shortcuts. Похоже, в PalmOS опять-таки не существует нормального способа определить, находится ли система в настоящий момент в состоянии приема Command (во всяком случае, эксперименты с атрибутами меню, перехватом MenuEraseStatus и шаманским бубном результата не дали), и способ борьбы был позаимствован из исходных текстов RussianIII (использование значений TimGetSeconds). С шорткатами тоже пришлось прогнуться - помимо перехвата GrfGetAndExpandMacro - чтобы английское "Dinner", введенное с помощью шортката при активной русской раскладке, не превращалось в загадочное "Диннея".
СЕРВИС
Нормальная сортировка кириллицы достигается перехватом GetCharSortValue и GetCharCaselessValue и подменой "родных" таблиц сортировки "правильными":
proc MyGetCharCaselessValue() beginproc lea.l SortTable(pc), a0 endproc
Панель управления хака сделана больше из любопытства, чем из необходимости :-) все, что требуется от хакописателя - это обеспечить наличие в хаке формы, представляющей панель на экране, и обработчик событий к ней; об остальном позаботится HackMaster.
Установка Power On потребовала перехвата вызова HwrLCDWake - в качестве образца использовались исходные тексты LogoHack:
; check Power mode selector cmp.l #2, Features.powermode(a2) beq.s Go Lat move.l Features.powermode(a2), Features.ruslat(a2) Go
Заключение.
Как видно, программировать под PalmOS нетрудно. Обширный инструментарий, относительно подробная документация и большой выбор ресурсов Интернет, посвященных разработке приложений для Pilot, делают это занятие почти что развлечением.
Разумеется, PalmOS не лишена недостатков (а порой и просто маразмов). Например, в API отсутствует возможность определить, засвечена ли точка на экране; многие моменты неясно или просто неверно отражены в документации (вспомним путаницу с экранными и оконными координатами). Зато имеются зачатки здравого смысла, вроде FntGetFontPtr:
link a6, #0 movea.l $01d6.w, a0 unlk a6 rts
Всего-то ничего, но по каким-то соображениям разработчики постеснялись вставить вызовы FntGetFontPtr в остальные функции Font Manager и вместо этого старательно лезут напрямую в память, - остается только развести руками.
К чести 3Com надо отметить, что обращение в их службу поддержки не остается без ответа. Правда, ответ этот может воспоследовать через несколько недель (сразу выдается только специальный квиток-подтверждение о том, что-де "ваш номер в очереди 78675645"). Большим подспорьем является лист рассылки Palm Developers (palm-dev-forum@3com.com), где тусуются зубры из Palm Computing, 3Com и массы мелких компаний-разработчиков софта под PalmOS.
Что касается CyrHack, то после релиза я выдам исходные тексты на всеобщее растерзание и начну делать очередную версию в виде нормального приложения, а не хака.