Для кого предназначена эта книга?
Что ждет вас в WWW-библиотеке?
Благодарность
Введение
Мой подход
Мои предположения относительно вас
Что вы найдете в этой книге
Приложенный код
Чего вы не найдете в этой книге
Нахальная вставка
Не оставайтесь в стороне
Часть I. Основы
Публичные и приватные программы
Этика и метафоры
Знакомьтесь: это ваши пользователи
Игры, в которые играют люди
Любовь приходит и уходит, а программы остаются
Как не следует относиться к пользователям-новичкам
Как нужно относиться к пользователям — новичкам и гуру
Глава 2. Философия разработки программного обеспечения для Windows: макропроблемы
Математики и ювелиры
Три источника и три составные части качества программы
Разумность
Услужливость
Примеры с придирками: что такое «хорошо» и что такое «плохо»
Десять макроуровневых рекомендаций
1. Стремитесь к балансу во всех решениях
2. Сделайте принцип KISS вашей религией
4. Уважайте ресурсы вашего пользователя
5. Используйте DLL осторожно
6. Придерживайтесь широкого взгляда на инструментарий
7. Не забывайте о производительности
8. Не забывайте о тестировании
9. Думайте о повторном использовании кода
Глава 3. Философия разработки программного обеспечения для Windows: микропроблемы
11. Применяйте оборонительное программирование
Что такое «оборонительное программирование»?
Природа предположений
«Доверяй, но проверяй»
Оборонительное программирование и повторное использование кода
12. Выделяйте код, отвечающий за пользовательский интерфейс вашей программы
13. Используйте умные файлы данных
14. Элегантно реагируйте на сюрпризы со стороны
15. Остерегайтесь противоестественных действий
Глава 4. Инструменты
Рекомендации по выбору и использованию инструментов
1. Добивайтесь симбиоза ваших инструментов
2. Не разбрасывайтесь и не переоценивайте свои силы
3. Не будьте слишком доверчивы к вашим поставщикам
4. Разумно эксплуатируйте публичные исходные коды
5. Не влюбляйтесь в ваш новый блестящий молоток
Типы инструментов
Компиляторы и языки программирования
C/C++: системный язык, используемый в качестве прикладного
C/C++ и определяемые пользователем типы
Аттестации, аттестации и еще раз аттестации
И чтобы не забыть
Если Pascal так прекрасен
Каркасные библиотеки
Добавления и аксессуары
Третьесторонние библиотеки и компоненты
Третьесторонние отладчики
Системы управления версиями
Инсталляционные программы
Часть II. Практика
Шаг 1: грубый перенос на С
Шаг 2: C++ и идиома локальной функции
Реальное применение идиомы
Альтернативы и надежды
Глава 6. Как жить с DLL
Кто загружается первым?
Спасет ли нас Win32?
Этикет разумного применения динамических библиотек
Правильно располагайте DLL
Старые времена
Новые времена
Загружайте все DLL явно
Пример: Hello
Глава 7. Минимизированные Windows-программы: вселенная внутри значка
...и как Windows 95 поменяла правила игры
DROP1 и DROP2
Не бойтесь и минимизируйте
Глава 8. Защита программ от искажений
Спасение в CRC
В поисках места для тайника
SELFCHCK и PATCHER
Реализация самопроверки в вашей программе
Вперед, вычисляйте CRC
Глава 9. Умные файлы данных
Настройки приложения
Документы приложения
Основные формы постоянного хранения данных
Реестр Windows
Специальные бинарные файлы
Умные файлы данных
Пример хранения конфигурационных данных
Глава 10. Абстрагирование в действии: создание своих виртуальных машин
«Доверять, но проверять» или «асфальтировать»?
Три степени абстрагирования
Искусство Абстрагирования
Е Pluribus Unum
Глава 11. Сделайте вашу программу замкнутой
Базовая модель, дубль первый
Как сделать замкнутой существующую программу
Проблемы безопасности
Что бы еще замкнуть сегодня?
Часть III. Великий перевал
Изменение типов данных
Что значит изменение размеров типов для программистов
Перенос 16-битового кода на Windows 95
Выбор стратегии переноса
Великие неизвестные
«Ужастики»
GDI обрезает координаты до двух байт
Ограничения значений cbWndExtra и cbClsExtra
Формат результата GetOpenFileNameO
Где искать ассоциации?
Проблемы с перетаскиванием
Арр Paths срабатывает для 16-разрядных программ лишь наполовину
Функция GetFreeSystemResourcesO пропала в Win32
Ау, кто стянул мой значок?
Диалоги масштабируются по-разному
Доступ к lNl-файлам
Norton Navigator 1.0 и вы
Глава 13. Не слишком краткая лекция о длинных именах файлов
Хранение длинных имен
«Длинные имена файлов как параметры командной строки. ЕХЕ»
Становится только хуже и хуже
Что делать?
Крайние меры
Хорошие новости
Ждите ошибок, ждите ошибок
Ну что ж, умники, и как же, по-вашему, Microsoft должна была реализовать длинные имена файлов?
Глава 14. Разновидности Win32
Даже Microsoft иногда пьет из этого колодца
Проверка версии и платформы
«Ужастики»
Окна-списки обрезают значения строковых индексов
Окна-списки не могут обрабатывать длинные имена файлов в Windows 95
Функция GetLastErrorO возвращает непригодные для работы значения
Windows 95: проблема с SUBST и GetShortPathNameO
Странности функции GetShortPathNameO
Типы данных в реестре
Ограничения для cbWndExtra и cbClsExtra
Диалоги масштабируются по-разному
Ау, кто стянул мой значок?
Графические траектории не функционируют иод Windows 95
Win32s может свихнуться, когда виртуальной памяти слишком много
Win32s + RICHEDIT = катастрофа
Окна RICHEDIT по умолчанию не посылают уведомление EN_CHANGE
Рекомендации
Windows NT 4.0: свет в конце туннеля?
Часть IV. Ресурсы
allCLEAR Version III от Clear Software, Inc
Bounds Checker Professional 3.0 от Nii-Mega Technologies, Inc
Drag and File 1.0 for Windows 95 and Windows NT от Canyon Software
ISYS for Windows 4.0 от Odyssey Development
Microsoft Development Network CD-ROM от Microsoft
Multimedia ToolBook 3.0 от Asymetrix
Orpheus 1.00 от TurboPower Software Company
Partition Magic 1.02.177 от PowerQuest Corporation
SourceSafe 3.1 от Microsoft
System Commander 2.11 от V Communications
Windows 95 Device Driver Kit
Windows NT 3.51
YAHU: еще одна утилита для работыс заголовочными файлами
Глава 16. Дополнительная литература
Приложения
Приложение В. Конспект программистских небылиц
Приложение С. Библиотека Win32u
Что включено в Win32u
Обертки для сообщений окон-списков
Обертки для GDI-функций
Вспомогательные обертки и функции
Проверка платформ и отладочный режим
Эпилог
Текст
                    ринзоу


www-symbol ru ближайший книжный магазин Все заказы выполняются исправно и в срок i Быстрый недорогой 9 надежный способ приобрести лучшие компьютерной тематике С вопросами и предложениями обращайтесь по телефонам: (812) 310-5661, 311-0626 или по с-тail: trade@symboLnt
Lou Grinzo of Windows Programming 1CORIOLIS GROUP BOOKS
ПРОФЕ ИОНАЛЬНО Лу Гринзоу программирования для Windows 95/NT an Ьл-91б ете. 4997 г
Лу Гринзоу Философия программирования для Windows 95/NT Перевод Я. Межерицкого Издатель Главный редактор Художник Редактирование и верстка С. Леднев А. Галунов О. Гор су но в А. Денисов ББК 32 973 УДК 681.3.06 Г85 Гринзоу Л. Философия программирования для Windows 95/NT — Пер с англ. — СПб.:Символ-Плюс, 1997. 640 с * ил ISBN 5-89051-005-3 Эта книга — не краткий обзор Windows 95 и не полный справочник но Windows 95 API. Это совершенно удивительное сочетание философского взгляда на программирование и практических советов по разработке Windows-приложений и ведению крупных проектов. Программист любого уровня, пишущий под Windows, прочтет эту книгу с наслаждением. С се помощью вы постигнете тайну «Великого Каньона», который пролег между Windows 3 1, Windows NT, Win32 и Windows 95, и увидите как на ладони подводные камни при переходе от 16-разрядной к 32- разрядной Windows. Изложенный здесь материал принадлежит перу Лу Гринзоу, одного из ведущих специалистов в области программирования для Windows. Профессиональный подход и способность проникнуть в суть дела часто делают его публикации центральной темой таких изданий, как PC WEEK, PC TECHNIQUES, Dr Dobbs Journal, Software Development и Windows Tech Journal. Original English language edition published by The Coriolis Group, Inc., 7339 E. Acoma Drive, Suite 7, Scottsdalc, AZ 85260. Tel: 602-483-0192, Fax: 602-483-0193 Copyright © 1996, by The Coriolis Group, Inc. All rights reserved © Издательство «Символ-Плюс», 1997 ISBN 5-89051-005-3 ISBN i-883577-58-6 (англ.) Все права на данное издание защищены законодательством Российской Федерации, включая право на полное или частичное воспроизведение в любой форме. Все товарные знаки и зарегистрированные товарные знаки, упоминаемые в настоящем издании, являются собственностью соответствующих фирм. Издательство «Символ Плюс». 190000, Санкт-Петербург, Черноморский пер., 7. Лицензия ЛР № 064154 от 04.07.95. Подписано в печать 22.04.97. Формат 70ХЮ0 Vis- Бумага •1И етная. Печать офсетная. Объем 40 печ. л. Тираж 5000 экз. Заказ № 577. Отпечатано с диапозитивов в ГПП «Печатный Двор» Комитета РФ по печати. 197110, Санкт-Петербург, Чкаловский пр., 15.
Для кого предназначена эта книга? 13 Что мне необходимо знать и иметь? 13 Что ждет вас в WWW-библиотеке? 13 Благодарность 14 Введение Что такое «программирование»? 17 Мой подход 20 Мои предположения относительно вас 21 Что вы найдете в этой книге 23 Приложенный код 25 Чего вы не найдете в этой книге 25 Нахальная вставка 26 Не оставайтесь в стороне 26 Часть I. Основы Глава 1. Добро пожаловать в реальный мир 31 Windows 95 крупным планом 32 Публичные и приватные программы 33 Насколько публичное публично? 34 Этика и метафоры 35 Знакомьтесь: это ваши пользователи 36 Игры, в которые играют люди 37 Любовь приходит и уходит, а программы остаются 40 Как не следует относиться к пользователям-новичкам 41
Солержание Как нужно относиться к пользователям — новичкам и гуру . . 43 Глава 2. Философия разработки программного обеспечения для Windows: макропроблемы 47 Стиль 47 Математики и ювелиры 49 Три источника и три составные части качества программы 51 Корректность 51 Разумность 53 Услужливость 55 Примеры с придирками: что такое «хорошо» и что такое «плохо» 57 Десять макроуровневых рекомендаций 63 0. Имейте широкий взгляд на мир и планируйте на будущее . . 64 1. Стремитесь к балансу во всех решениях 72 2. Сделайте принцип KISS вашей религией 77 3. You(Now) != You(Later) 79 4. Уважайте ресурсы вашего пользователя 85 5. Используйте DLL осторожно 90 6. Придерживайтесь широкого взгляда на инструментарий ... 90 7. Не забывайте о производительности 92 8. Не забывайте о тестировании 97 9. Думайте о повторном использовании кода 100 Глава 3. Философия разработки программного обеспечения для Windows: микропроблемы 105 Шесть микроуровневых рекомендаций 106 10. Абстрагируйтесь, абстрагируйтесь и снова абстрагируйтесь (и не беспокойтесь о цене вызова функции, пока программа сама не заявит об этом) 108 11. Применяйте оборонительное программирование 109 Что такое «оборонительное программирование»? 109 Природа предположений 110 «Доверяй, но проверяй» 112 Оборонительное программирование и повторное использование кода 117
Солержание 12. Выделяйте код, отвечающий за пользовательский интерфейс вашей программы 136 13. Используйте умные файлы данных 138 14. Элегантно реагируйте на сюрпризы со стороны 140 15. Остерегайтесь противоестественных действий 142 Глава 4. Инструменты 147 Что такое «инструмент программирования» и почему он так важен. 148 Рекомендации по выбору и использованию инструментов 150 0. Исследуйте и эксплуатируйте 150 1. Добивайтесь симбиоза ваших инструментов 153 2. Не разбрасывайтесь и не переоценивайте свои силы 155 3. Не будьте слишком доверчивы к вашим поставщикам .... 156 4. Разумно эксплуатируйте публичные исходные коды 157 5. Не влюбляйтесь в ваш новый блестящий молоток 159 Типы инструментов \qq Справочные материалы 161 Компиляторы и языки программирования 164 C/C++: системный язык, используемый в качестве прикладного . 166 Миф о переносимости 169 C/C++ и определяемые пользователем типы 170 Аттестации, аттестации и еще раз аттестации 171 И чтобы не забыть 173 Если Pascal так прекрасен 175 Как жить с C++? 177 Каркасные библиотеки 178 Добавления и аксессуары 179 Третьесторонние библиотеки и компоненты 179 Третьесторонние отладчики 180 Системы управления версиями 180 Инсталляционные программы 181 Часть Ия Практика Алава 5. Идиома локальной функции 195
Локальные функции: Pascal реализует их так, как надо 196 Шаг 1: грубый перенос на С 199 Шаг 2: C++ и идиома локальной функции 201 Реальное применение идиомы 206 Альтернативы и надежды 208 Глава 6. Как жить с DLL 209 Отчаянный поиск DLL 210 Кто загружается первым? 213 Спасет ли нас Win32? 215 Этикет разумного применения динамических библиотек 217 Используйте DLL только при необходимости 218 Правильно располагайте DLL 219 Старые времена 220 Новые времена 223 Загружайте все DLL явно 227 Пример: Hello 230 Глава 7. Минимизированные Windows-программы: вселенная внутри значка 239 Разновидности минимизированных программ 240 Механика минимизированных программ 241 ...и как Windows 95 поменяла правила игры 247 DROP1 и DROP2 259 Не бойтесь pi минимизируйте 259 Глава 8. Защита программ от искажений 263 Добро пожаловать в наш кошмар 263 Что вы сможете сделать, а чего не сможете 265 Спасение в CRC 266 В поисках места для тайника 267 SELFCHCK и PATCHER 269 Реализация самопроверки в вашей программе 276 Вперед, вычисляйте CRC 279 Глава 9. Умные файлы данных 283 Для чего используются постоянные хранилища данных 284
Системные данные .9 284 Настройки приложения 285 Документы приложения 286 Основные формы постоянного хранения данных 286 INI-файлы 288 Реестр Windows 291 Специальные бинарные файлы 293 Умные файлы данных 295 Пример хранения конфигурационных данных 305 Глава 10. Абстрагирование в действии: создание своих виртуальных машин 317 Зачем это надо? 317 «Доверять, но проверять» или «асфальтировать»? 322 Три степени абстрагирования 326 Искусство Абстрагирования 334 Win32 API: бинарный кельвиибол? 341 Е Pluribus Unum 353 Глава 11, Сделайте вашу программу замкнутой 355 Зачем нужна минимальная защита? 356 Защищать или не защищать? 356 Базовая модель, дубль первый 357 Базовая модель, дубль второй 359 Как сделать замкнутой существующую программу 365 Проблемы безопасности 367 Что бы еще замкнуть сегодня? 370 Часть Ш. Великий перевал Глава 12. Из Win 16 в Win32 373 Что такое Windows 95 с точки зрения прикладного программиста. . 374 Wint61ock 379 Изменение типов данных 381 Что значит изменение размеров типов для программистов . . . 385 Перенос 16-битового кода на Windows 95 387
10 Солержание Выбор стратегии переноса 388 Великие неизвестные 402 «Ужастики» 407 * Проверка номера версии: не все так просто 408 GDI обрезает координаты до двух байт 409 Ограничения значений cbWndExtra и cbClsExtra 410 Формат результата GetOpenFileNameO 411 Где искать ассоциации? 413 Проблемы с перетаскиванием 414 Арр Paths срабатывает для 16-разрядных программ лишь наполовину 416 Функция GetFreeSystemResourcesO пропала в Win32 417 Ау, кто стянул мой значок? 419 Диалоги масштабируются по-разному 419 Доступ к lNl-файлам 420 Norton Navigator 1.0 и вы 421 Глава 13. Не слишком краткая лекция о длинных именах файлов 429 Обзор основных свойств длинных имен и псевдонимов 430 ...и что это означает 432 Хранение длинных имен 433 «Длинные имена файлов как параметры командной строки. ЕХЕ» 435 Становится только хуже и хуже 441 Что делать? 443 Крайние меры 444 Хорошие новости 446 Ради Бога, не забывайте о человеческом факторе! 446 Ждите ошибок, ждите ошибок 447 Ну что ж, умники, и как же, по-вашему, Microsoft должна была реализовать длинные имена файлов? 450 Глава 14. Разновидности Win32 453 Почему Win32 != Win32 != Win32 454 Документация, документация и снова документация 457
Даже Microsoft иногда пьет из этого колодца 460 Проверка версии и платформы 463 Никого нет дома 464 «Ужастики» 472 GDI обрезает значения координат до двух байт 473 Окна-списки обрезают значения строковых индексов 477 Окна-списки не могут обрабатывать длинные имена файлов в Windows 95 479 Функция GetLastErrorO возвращает непригодные для работы значения 482 Windows 95: проблема с SUBST и GetShortPathNameO .... 486 Странности функции GetShortPathNameO 487 Типы данных в реестре 492 Функция WNetGetUniversalNameO проваливается в Windows 95 494 Ограничения для cbWndExtra и cbClsExtra 497 Диалоги масштабируются по-разному 498 Ау, кто стянул мой значок? 499 Графические траектории не функционируют иод Windows 95 502 Функция GetSysColorO не может потерпеть неудачу (или все-таки может?) 505 Win32s может свихнуться, когда виртуальной памяти слишком много 505 Win32s + RICHEDIT = катастрофа 507 Окна RICHEDIT по умолчанию не посылают уведомление EN_CHANGE 509 MoveFileExO не работает под Windows 95 510 Рекомендации 511 Windows NT 4.0: свет в конце туннеля? 515 Часть IV. Ресурсы Глава 15. Хит-парад программистского инструментария 521 Рекомендации 524 ABC Flowcharter 4.0 от Micrografx 524 allCLEAR Version III от Clear Software, Inc 524
Bounds Checker Professional 3.0 от Nii-Mega Technologies, Inc. 525 Drag and File 1.0 for Windows 95 and Windows NT от Canyon Software 526 ISYS for Windows 4.0 от Odyssey Development 526 , Microsoft Development Network CD-ROM от Microsoft 527 Multimedia ToolBook 3.0 от Asymetrix 528 Orpheus 1.00 от TurboPower Software Company 528 Partition Magic 1.02.177 от PowerQuest Corporation 529 SourceSafe 3.1 от Microsoft 529 System Commander 2.11 от V Communications 530 Windows 95 Device Driver Kit 530 Windows NT 3.51 531 YAHU: еще одна утилита для работы с заголовочными файлами 531 Глава 16. Дополнительная литература 533 Призовой раздел: три неожиданных открытия 542 Приложения Приложение A. MegaZero — самая законченная в мире, ничего не делающая 545 Приложение В. Конспект программистских небылиц 585 Приложение С. Библиотека Win32u 593 Как вы можете принять в этом участие 594 Что включено в Win32u 594 Общая философия и поддержка версий Windows 595 Обертки для сообщений окон-списков 595 Обертки для GDI-функций 595 Вспомогательные обертки и функции 596 Проверка платформ и отладочный режим 596 Эпилог 621
кого предназначена эта книга? Эта книга предназначена для программистов, имеющих средний или значительный опыт разработки для Windows и совершающих переход от Windows 3.1 к 32-разрядным платформам Windows — Windows 95 и Windows NT. Если вы — новичок в области разработки 32-разрядных программ для Windows, то эта книга послужит вам отличным помощником в освоении технологий и приемов программирования в 32-разрядном мире и предложит массу полезных теоретических и практических советов по написанию переносимых Windows-приложений . Что мне необходимо знать и иметь? Вы должны иметь некоторый опыт в программировании для Windows на языке C/C++. Для запуска большей части программ-примеров необходим персональный компьютер с установленной на нем операционной системой Windows 95. Для компиляции и редактирования кода вам понадобится Microsoft Visual C++ или MSC. Что Ждет вас в WWW-библиотеке? По адресу http://www.symbol.ru/russian/library/prof_prog вы найдете многочисленные программы-примеры, а также другие ресурсы, которые помогут разработчикам создавать отличные 32-разрядные приложения для Windows. Программы были оттестированы при помощи Microsoft Visual C++ и MFC. Несколько примеров 16-разрядного кода могут быть скомпилированы при помощи Microsoft Visual C++ 1.52. На сервер также выложены все файлы, созданные компиляторами и соответствующими интегрированными оболочками.
Я хотел бы поблагодарить: Фила Юргенсона, моего далекого друга, с которым я пока не виделся лицом к лицу и который предложил мне огромное число советов, а также прочитал большую часть этой книги (из чего следует, что все ошибки ); Джима Кайла, известного также как Доктор CRC; Пита Дэвиса; Мэтта Питрека; Ричарда Олверсона; коллектив издательства Coriolis; Рона Шеннона и, конечно же, Кейта и Джеф- а; Марию Валлоне и Дэвида Уинклера; Пола и Эдель Эверетт; Вирга Мэредита, Веса Эрнсбергера, Гарри Хайна, Джека Хупера, Дэрил Кал и всех прочих обитателей покинутого, но не забытого Корпуса j9; весь клан Дэннин- гов, особенно Майкла и Марианну, моих студентов по программированию. И самая большая моя благодарность по праву принадлежит Лиз — жене, партнеру, лучшему Другу, деловому советнику, личному адвокату и неутомимому редактору. Без нее буквально не состоялась бы ни эта книга, ни, если уж на то пошло, я сам.
С любовью посвящается памяти Ирэн Мэй Кирби Гринзоу и Льюиса Энтони Гринзоу-старшего
ЕВшедиеюие You use your creative talents to transform a business environment. Предсказание, которое я получил в китайском ресторане в 4 Р день подписания контракта о создании этой книги Forward, forward let us range, Let the world spin for ever down the ringing grooves of change. Альфред, Лорд Тснписоп Я не верю в пирожки с предсказаниями (как, впрочем, и в остальную, очень модную сегодня метафизику), но вышеприведенный «прогноз» весьма удачно и перекликается с теми целями, которые я преследовал при написании этой книги: изменить у большинства из вас образ мышления о ваших пользователях и о программировании для Windows 95, усовершенствовать методы, которые вы используете в ремесле программирования. При этом я уже высказал одно размашистое предположение (на самом деле, их сделано несколько, но давайте для начала ограничимся одним, наиболее очевидным и существенным): программирование не является ни искусством, ни наукой, но оно явно несет в себе признаки и того, и другого; и вместе с остальными чертами это делает программирование именно ремеслом. Я думаю, что этот факт имеет важный, даже глубинный смысл для всех программистов, особенно сейчас — во время бурного развития мира Windows. Благодаря появлению Windows 95, взрывному расширению рынка домашних компьютеров, унылой картине качества коммерческого программного обеспечения в целом, а также возросшей популярности операционных систем Windows NT и OS/2 Warp, окружающий нас компьютерный мир буквально изменил свой облик в течение последнего года. Когда происходит нечто подобное, У вас есть выбор из двух путей: продолжать старую игру и смотреть на затухающее вращение рулетки, или впрячься в работу по адаптации себя к этому миру. Я предпочитаю последнюю стратегию, и, если вы уже смогли выжить в этой игре, я подозреваю, вам она также больше подходит. Что такое «программирование»? Как вы увидите в этой книге, я постоянно (и, надеюсь, убедительно) Утверждаю следующее: чтобы быть отличным программистом, вы должны всегда стараться окинуть как можно более широким и внимательным взглядом стоящие перед вами проблемы, а затем стремиться к сбалансированному Решению. Лишь немногие профессии подвержены опасности «не видеть леса за
18 В веление деревьями» в такой же степени, как программирование. Поэтому для вас жизненно важно научиться глядеть на вашу задачу «с высоты 30 000 футов». Ладно, хватит увиливать от основной темы разговора. Что же такое программирование? Программирование — это ремесло использования разнообразного инструментария (аппаратного обеспечения, программ, исследовательских материалов, вашего собственного опыта и т. д.) в создании уровней абстракции и применении их для решения логических задач. Эти уровни абстракции позволяют вам сконструировать послойную иерархию архитектур или виртуальных машин, которые в конечном счете и работают совместно, решая поставленные задачи. Хмм... Звучит так, как будто мы уже вышли в открытое море, в то время как на самом деле едва оттолкнулись от причала. В качестве примера этого наслоения виртуальных машин, давайте рассмотрим, как вы обращаетесь с математическими операциями в ваших программах. Когда вы пишете операторы C++, совершающие простые вычисления с вещественными числами, у вас уже есть некоторый набор предположений о том, как будут работать соответствующие подпрограммы стандартной библиотеки компилятора, как они будут обрабатывать переполнение, деление на ноль, другие общие ошибки. Но что если компьютер, на котором работает ваша программа, не имеет сопроцессора? Программисты уже так давно привыкли к идее надежной поддержки эмуляции о вещественных вычислении, что вряд ли уже задумываются над этим вопросом; такая поддержка — это просто еще одна данность; часть пейзажа, она работает так, как и должна работать. И точка. Пожалуйста, не проходите мимо этого простого и известного примера, как все мы обычно это делаем. Остановитесь и задумайтесь над ним на секунду. Если в компьютере нет математического сопроцессора, значит в нем буквально нет поддержки математики плавающей точки. Но наличие такого изобретения, как поддержка эмуляции математики плавающей точки, как раз pi обеспечивает вам тот самый уровень абстракции — виртуальную машину, — надстройку над существующим аппаратным обеспечением. И именно эта виртуальная машина дает вам более высокую, более мощную платформу для создания ваших программ. Чтобы развить этот пример, давайте представим, что вам понадобились комплексные числа в вашей программе. Тогда вы берете и используете коммерческую библиотеку, которая обеспечивает вам тип комплексных чисел и все необходимые операции над ними. Теперь вы используете не что иное, как второй уровень абстракции, другую виртуальную машину, построенную на базе первой. Все обстоит даже более интересно: те люди, которые писали библиотеку для работы с комплексными числами, тоже не знали и не заботились о том, есть ли математический сопроцессор на вашем компьютере. Они просто знали, как должна работать математика плавающей точки, и создавали свою библиотеку, основываясь на этом документированном интерфейсе. Если ваш
Введение 19 BO компьютер отвечает их ожиданиям (что, мягко говоря, очень вероятно), а их библиотека — вашим, то эти два уровня абстракции как бы сплавляются для вас воедино, и все работает так, как было задумано. Результат выглядит, как лшебство: ваш компьютер, который не может самостоятельно сложить 1.0 и 1 0 вдруг становится способен с совершенной легкостью манипулировать комплексными числами. Если вы хотите другой пример, задумайтесь о работе в Windows и составлении SQL-запроса к реляционной базе данных. Между строкой «SELECT FROM CUSTOMERS» и набором нулей pi единиц, закодированных на вращающейся тарелке диска, так много всего происходит! Опять же, все это уровни абстракции и виртуальные машины. (Я бы с удовольствием когда-нибудь взялся обучать студентов компьютерной архитектуре, и тогда вынес бы на о заключительный экзамен только один вопрос: перечислите детально все уровни архитектуры и интерфейса, задействованные в этом примере. Правда, я подозреваю, что студенты вряд ли получат столько же удовольствия от такого эксперимента, сколько получу я.) Такая работа с уровнями абстракции и конструирование своих собственных виртуальных машин означают, что вся ваша программистская деятельность насквозь пронизана многочисленными предположениями. Подкопайтесь достаточно глубоко под эти предположения, и вы достигнете сути, вашей философии программирования. Это влияет и накладывает отпечаток на все, что вы делаете как программист, независимо от того, понимаете вы это или нет. Ваша философия формируется вашим опытом и образованием, а зачастую — вещами, вовсе не имеющими непосредственного отношения к программированию, такими, как ваш возраст, например. (Если вам кажется, что ваша одежда и прическа на фотографиях 15- или 20-летней давности выглядят забавно, попробуйте посмотреть на ваш же собственный код, написанный не так уж давно. Это будет почти то же самое, что смотреть шестнадцатеричную версию «The Brady Bunch»1.) Многое из того, что вы делаете как программист — это выбор и использование инструментария (в широком смысле слова), а не только штамповка строк о кода, жадное поглощение колы и игнорирование указании этих чокнутых, умилительно бестолковых комиков из администрации. Последние изменения в мире настольных компьютеров (в немалой степени связанные с появлением Windows 95), сделали эту задачу намного более интересной и важной, а порой — рискованной. Выбор инструментария — это отдельная тема, о которой мы детально поговорим позже. The Brady Bunch — название телевизионного шоу на семейные темы, чрезвычайно популярного в 70-х годах в США (но мотивам этого шоу был спят целый ряд фильмов и сериалов). [Примечание переводчика ]
20 Ввеление Мой подход В интересах вашего доверия к моим рекомендациям, а также в стремлении не спугнуть вас (чего ни в коем случае не хотелось бы делать ни мне, ни моему издателю) я должен рассказать о своем подходе к написанию данной книги: Эта книга будет больше говорить о том, чего вам не следует делать, чем о том, что следует. Например, я буду часто упоминать многочисленные «программистские байки», которые все мы рассказываем друг другу для оправдания своих программерских трюков и стиля. Заметьте, что при этом я вовсе не претендую на собственную непорочность; я скажу сейчас и буду говорить потом, что я сам совершал те грехи, которые будут многократно упоминаться в этой книге, pi рубцы на моих программах — тому доказательство. Я был удачлив и не один раз по- KJ t-P О падал в программистскую аналогию той житейской ситуации, когда просыпаешься утром в жестоком похмелье, абсолютно не помнишь о том, как добрался домой прошлой ночью, и обнаруживаешь, что твой автомобиль припаркован на газоне с распахнутой дверью. (И если вы хоть на наносекунду подумали, что данная аналогия как-то оправдывает такое поведение, то вы не уловили моего едкого сарказма.) Мне часто пррщется полагаться на то, что вы будете самостоятельно су- дрпъ о том, как прргменять тот pi ли ршой мой совет. Как бы все мы ни желали обратного, современное программирование очень редко ставит «бинарные» проблемы; почти каждая задача, с которой вы столкнетесь (а всякая интересная задача — наверняка) будет иметь решение как раз в той самой безбрежной, пасмурной серой зоне между черным и белым. Когда я говорю «будьте рассудительны», — это не просто дежурная фраза, заменяющая более конкретные рекомендации; эти ело- *j ва означают, что правильньш ответ сильно зависит от вашей конкретной ситуации, и что с моей стороны было бы безответственно высказывать более определенное предложение. Многие решенрш вообще просто-напросто зависят от контекста. KaKPie ршетрументы вы используете? Какие ограничения накладываются временем, вашей администрацией, вашими пользователями? Какова ваша квалификация? И даже еще интереснее: то, что является правильным для вас сегодня, может оказаться совершенно неверным для другого рыи для вас самого, но спустя несколько месяцев. Трюк заключается в том, что на проблему нужно посмотреть через прргзму ваших собственных предубеждений (а в этой профессии их у каждого в изобилии!) и выбрать оптимальный курс ее решения в данном проекте. Всякий раз, когда я слышу из уст программиста фразы типа «Я был вынужден использовать инструментарий X, потому что... начальство приказало / только он был доступен / у меня не было времени изучить более подходя-
йвеление 21 щий», в моем мозге раздается тревожный сигнал. Иногда программист прав, и у него действительно не было другого выбора, кроме этого, очевидно, не самого оптимального. Но слишком часто программист просто оправдывает такими словами использование любимого старого инструмента или блестящей новой игрушки. И В некоторых местах я проведу вас по всей цепочке решения, включая обсуждение тех возможных альтернативных решений, которые на самом деле оказывались тупиковыми. Я думаю, что такой способ подачи программистского материала очень полезен (в особенности, когда дело касается программирования для Windows), поскольку он отражает реальный процесс борьбы за нахождение решений наших програм- мерских проблем, через который мы все проходим. В В книге есть несколько мест, где я показываю наилучшее решение той или иной проблемы — наилучшее из тех, которые я сам смог найти, — и затем прошу вас самих сказать мне, нет ли еще более удачного решения, которое я не увидел. Я не претендую на обладание совершенным знанием по тем проблемам, которые упомянуты в этой книге; почти ни один технический писатель не мог бы честно претендовать на такое (хотя многие все еще будут делать это, явно или неявно). Также не следует ожидать, что мои взгляды и мнения являются окаменел остями. Если спустя шесть месяцев или год после того, как это издание попадет на книжные полки, вы столкнетесь со мной на конференции, или мы обменяемся электронной почтой, вполне вероятно, что я скажу вам: «С тех пор я приобрел новый интересный опыт, который изменил мое мнение по тому или иному вопросу». Эта книга — на самом деле лишь мо- О U IJ ментальный снимок, сделанный на одном из отрезков моей персональной одиссеи в мире программирования. Мир меняется, вещи меняются, я — тоже. Мои предположения относительно вас Писатели-беллетристы часто говорят о мифическом «среднем», или «типичном», читателе, которого они себе представляют, когда пишут свои творения. Технические писатели также держат в голове некоторую модель мен- Поскольку гждое моими предположениями и дать вам заглянуть в него: В Вам нравится программировать, pi вы делаете это либо профессионально, либо как страстный любитель. Я считаю всякого, кто программирует за деньги, профессионалом, даже если его рабочая должность не называется «программист». Одной из волнующих сторон сегодняш-
22 Ввеление о него мира Windows является то, что все больше и больше непрограммистов пишут программы, на которые другие люди полагаются в реальной работе, — к проблемам, вызванным такой ситуцией, я обращаюсь на многих страницах этой книги. Вы — «фанатик качества». С одной стороны, я ненавижу все эти шут- ки и результаты заумных научных исследований, делящие все человечество на какие-нибудь два лагеря. Но в то же время я нахожу, что компьютерные пользователи, вообще говоря, распадаются на две категории по тому, как они относятся к ошибкам в программах. Представители одной группы просто пожимают плечами по поводу брака, произносят «Ну, что ж, ошибки случаются...» и продолжают жить с тем, что есть под рукой. Лично я неодобрительно отношусь к таким пользователям. В основном, потому, что сам принадлежу ко второй категории — к группе людей, которые могут буквально взрываться негодованием, когда обнаруживают ошибку или сбой в программе. Всем моим знакомым известно, что моя ругань порой бывала весьма многоэтажной, когда я спотыкался о то, что я называю «по-настоящему глупейшей ошибкой». Вы без сомнения заметили, что сегодня — совсем не счастливое время для «фанатиков качества» в компьютерной индустрии. Качество коммерческого программного обеспечения в последнее время катилось вниз по скользкому склону, порой с головокружительной быстротой. Еще более беспокоит то, что поставщики программного обеспечения переходят на другую модель поддержки заказчиков (в данном случае я использую термин «поддержка» с большой натяжкой) — такую мо- %j дель, при которой в лучшем случае делаются вялые усилия для выпуска заплат, а зачастую все вообще сводится к тому, что заказчик платит деньги за свою собственную работу по обнаружению ошибок и оповещению о них производителя. (Вероятно вы слышали такое определение, как chutzpah1 — мальчик, который сначала убил своих родителей, а потом сдался на милость правосудия и требовал снисходительности к себе как к сироте. Мне кажется, что современная программная индустрия рискованно приближается к тому, чтобы вот-вот заслужить такое определение. Но я отвлекаюсь.) Вы заинтересованы в серьезных, долгосрочных проектах, а не только в наспех сделанных халтурах для одноразового использования. Об этом критичном противопоставлении, о разнице между публичными и 1. chutzpah — это слово (на языке идиш) по значению близко к «наглый», «самонадеянный». [Примечание переводчика.]
Введение 23 приватными программами я говорю в Главе 1, «Добро пожаловать в KJ реальный мир». Q Вы часто вынуждены ломать голову над наиболее неприятной стороной программирования — «старым кодом». (Данная проблема сильно перекликается с предыдущей, однако эти проблемы —совсем не одно и то же.) Почти любая книга по программированию сегодня О KJ предполагает, что вы начинаете каждый новый проект с чистым экраном, что весь бинарный мир распростерт у ваших ног, что вы, как Колосс, возвышаетесь над вашим винчестером и так далее, и тому подобное. В реальном же мире программирования так просто не бывает (а если и бывает, то далеко не так часто, как нам хотелось бы). Во время моих консультационных и контрактных программистских работ я регулярно сталкивался с необходимостью улучшать или переделывать программы, написанные одним или несколькими программиста- болыпе не раб очевидно, не имели большого опыта на момент написания этих кодов. И KJ KJ вопросе далеко не уникален; программисты в компаниях самой разной величины сталкиваются с похожими проблемами чуть ли не ежедневно. В Вы заинтересованы в переносимости исходных текстов, а также в совместимости программ с разнообразными воплощениями Windows. Я скажу сразу: если вы думаете, что не заинтересованы в этом, то вы, вероятно, сами себя обманываете. По моим ожиданиям, еще долгое время после публикации этой книги наши пользователи будут запускать самые всевозможное версии Windows — от Windows 3.0 до Windows NT 4.0 включительно. Эпоха наивности закончилась; старое доброе время, когда мы могли игнорировать все эти сложности, уже тускнеет в памяти. В Вы обладаете достаточным опытом разработки программного обеспечения для Windows на одном или нескольких языках программирования, таких как C/C++, Pascal или Basic. Под определением «достаточный опыт» я имею в виду примерно 3000 строк кода, написанных в течение последнего года, или 20 000 строк — в течение последних пяти лет. Что вы найдете в этой книге Эта книга разделена на четыре основные ч Великий перевал» и «Ресурсы», «Основы», «Пр приложений. В части «Основы» мы поведем долгий разговор о ремесле программирова- о многих его сторонах, которые все мы порой воспринимаем неверно. Этот
24 Ввеление большое число проблем уже хорошо знакомы. Тем не менее, я призываю прочесть эту часть книги даже тех, кто уже миллиарды раз проходил цикл редактирование/компиляция/ тестирование — многие затронутые здесь вопросы являются важными как никогда, особенно в свете новых реалий нашего изменчивого мира. В части «Практика» я демонстрирую некоторые приемы, технологии и идиомы, которые я изобрел, открыл или позаимствовал (читайте: украл) у друзей. Почти весь код, встречающийся здесь, отнюдь не является примером передовых достижений. В то же время, ничто в этой части книги не содержит каких-либо таинственных, граничащих с недозволенным, трюков типа использования недокументированных интерфейсов или возможностей Windows. Скорее, эти главы задумывались как демонстрация того, как при помощи минимального планирования вы можете комбинировать простые и легко сопровождаемые технологии кодирования для создания удобных и полезных программ для Windows 95. Часть «Великий перевал», охватывает тему, важность которой постоянно растет, — переносимость кода между различными вариациями Windows, a также бинарная совместимость между тремя 32-разрядными платформами Windows (Win32s, Windows 95 и Windows NT). До того, как на свет появились Windows 95 и Windows NT 3.51, эти проблемы практически не существовали для подавляющего большинства Windows-программистов. Мы писали программы для 16-разрядной Windows и продолжали радостно шагать своим путем, беспечно игнорируя Windows NT и довольно ограниченный и специфичный (относительно говоря) круг ее пользователей. В заключение, в части «Ресурсы» я расскажу об инструментах и ресурсах, KJ которые, на мои взгляд, являются весьма и весьма полезными программирующим для Windows. Как я говорил выше, программирование это ремесло, в котором вопросы практики и используемого инструментария переплетены так тесно, как ни в одной из других профессиональных областей. Поэтому я считаю, что любой длинный книжный разговор о программировании бесплоден и незавершен, если он хотя бы краешком не касается проблем выбора инструментария и обсуждения конкретных инструментов. Другими словами, теория и философия программирования безусловно являются необходимыми компонентами вашего умственного багажа, но их одних совершенно недостаточно. На самом деле, я немного солгал, и в книге есть пятая часть, состоящая из и трех приложении: В Приложение A: MegaZero, самое законченное в мире, ничего не делающее 32-разрядное приложение (которое призвано просто продемонстрировать некоторые программистские приемы, обсуждаемые в части «Практика» ).
Введение 25 В Приложение В: Конспект программистских небылиц — в нем я каталогизирую и критикую многочисленные байки и отговорки, которые мы порой используем, вводя в заблуждение самих себя. В Приложение С: Библиотека Win32u — первая, зачаточная версия вспомогательной оберточной библиотеки, которую вы можете использовать для преодоления некоторых проблем совместимости между Win32s, Windows 95 и Windows NT. Приложенный hog i Очевидно, я был вынужден выбрать для примеров только один язык, компилятор и среду разработки, в противном случае я рисковал бы вообще никогда не закончить эту книгу. (Кстати говоря, этот выбор был похож на обычную проблему выбора инструментария для выполнения очередного проекта.) Я выбрал Microsoft Visual C++ 2.2 и MFC, самые последние версии этих продуктов, доступные во время написания этой книги. Кроме того, в книгу вошли несколько примеров 16-разрядного кода, компилируемого при помощи Visual C++ 1.52 и (о-ох!) один модуль на языке Pascal. На сервере находятся все файлы, созданные компиляторами и их интегрированными средами разработки. Это должно позволить вам инсталлировать примеры и использовать их, применяя все возможности Visual C++. В целях экономии времени, я не озадачивал себя такими проблемами, как удаление ненужных ресурсов из программ или создание специальных значков для каждой программы. Я надеюсь, вы не питаете ненависти к стандартному значку «AFX», который используется оболочкой Visual C++ по умолчанию, потому что вы не раз увидите его в моих примерах. Пожалуйста, обратите внимание на то, что решение об использовании компиляторов Microsoft было скорее отражением реалий рынка, но никак не одобрением или поддержкой Visual C++ или MFC. Когда я начинал писать эту книгу, будущее известной библиотеки OWL (как и будущее самой компании Borland) выглядело очень неопределенным, в то время как MFC становилась почти что «языком межнационального общения» в программировании для Windows. Чего вы не найдете в этой книге Опять пришло время поговорить о доверии к моим рекомендациям, изложенным в этой книге. Совершенно определенно, эта книга не является исчерпывающим обзором, охватывающим C++, или объектно-ориентированное йсс примеры кода, встречающиеся в этой книге, помещены на WWW-ссрвср издательства «Символ-Плюс» по адресу http://www.symbol.ru/russian/library/prof_prog
26 Ввеление программирование (OOP), или OLE 2.0, или Win95 API, или WinG, или любую другую специальную область. Предназначение этой книги — коснуться Windows-программирования на уровне фундамента и философии; в связи с этим, я буду говорить о концепциях и проблемах, общих для программирования в целом, но в то же время особенно уместных для кодирования под Windows 95. Нахальная вставка Одним из наиболее поучительных и развлекательных моих занятий в течение последних нескольких лет было создание и маркетинг условно-бесплатной Windows-программы под названием Stickiest. Эта программа предоставляет пользователю электронный аналог записной книжки, файловую базу данных, напоминающую шкаф с ящичками, будильник и другие многочисленные возможности. На протяжении повествования данной книги я не раз упоминаю те знания и навыки в области программирования и общения с незнакомыми пользователями, которые я приобрел в процессе маркетинга программы Stickiest. А также — целый ряд неожиданных проблем переносимости, с которыми я столкнулся при работе с Windows 95. Для патологически любопытных: программа Stickiest была впервые выпущена 2 октября 1992 года, а ее следующая версия (4.0) выйдет в свет вскоре после того, как я закончу эту книгу и недельку отосплюсь. Оценочная копия Stickies 3.1 находится по адресу: http://www.symbol.ru/russian/library/ prof_prog/tools/stickies. Не оставайтесь в стороне Карьера в программировании, как и сама жизнь, — путешествие. С одной стороны, эта книга представляет собой некую контрольную точку в моей личной программистской одиссее, сборник избранных моих приключений, включая и успехи, и неудачи, плюс некоторые наблюдения и рекомендации, взгляд на все это сквозь линзы, подкрашенные цветами Windows 95. Но с вашей помощью данная книга может оказаться чем-то большим, нежели только это. В начале этого проекта я разговаривал с Кейтом Уэйскампом, моим издателем, и Роном Пронком, моим редактором, и мы сошлись на том, что эта книга должна иметь расслабленный стиль изложения, напоминающий разговор на встрече друзей. Поэтому я надеюсь, вы извините меня, если я скажу, что при написании этой книги я часто представлял себе такую картину: дюжина приятелей собралась во дворе моего дома прохладной летней ночью после отличного ужина и бурного выходного дня на пляже, все развлекаются с летающими тарелками, болтают о программировании, делятся анекдотами, шутками и ужа-
Ввеление 27 С иками про руководителей и даже грешат случайными «религиозными» спорами. Что бы вы ни делали, не будьте посторонним. Скажите мне, в чем вы читаете меня правым, а в чем — нет, расскажите мне ваши программистские анекдоты (чем смешнее, тем лучше, но приветствуются любые), предложите что на ваш взгляд следует (или не следует) включить в следующее издание этой книги. Я выставлю закуски, а вы приносите прохладительное, и когда мне, о игрушечная летающая тарелка случайно залетит во двор моих соседей, мы по шлем Рона и Кейта за забор, чтобы притащить ее назад. Lou Crinzo р О. Box 8636 Endwell, NY 13762-8636 CompuServe: 71055,1240 Endwell, NY Jly Гринзоу P. O. Box 8636 Эндвелл, Нью-Йорк 13762-8636 CompuServe: 71055,1240 Эндвелл, Нью-Йорк
.&Cv
йювро The Universe is not hostile, nor yet is it friendly. It is simply indifferent. Преподобный Джон Г. Холмс All suffering is caused by ignorance of the nature of reality and the craving, attachment, and grasping that result from such ignorance. Вторая Великая Истина Будды Suffering can be ended by overcoming ignorance and at- tachment. Третья Великая Истина Будды От слов Джона Холмса веет холодом. Но я думаю, что для того сложного мира, в котором обитают наши программы, мира, состоящего из миллионов экранов пользовательских компьютеров, даже его характеристика все-таки была бы слишком оптимистичной. Это не праздный пессимизм; наоборот, если бы я пессимистично относился к программированию, я никогда не сел бы за написание этой книги и давно сменил бы профессию. Но я очень хорошо знаю по своему опыту, как ошеломляюще часто программы начинают давать сбои, будучи орошенными во взбаламученный котел широкой популяции пользователей. При этом чем меньше «энергичность» ваших программ, тем более рискованной и ненадежной может быть их работа — об этом я и собираюсь подробно рассказать в этой главе.
32 Лобро пожаловать в реальный мир Давайте прислушаемся к мудрым словам Будды и попробуем избавить себя от лишних страданий — разберемся, что представляет из себя операционная система Windows 95 в действительности, и чем она на самом деле не является/ Еще задолго до ее окончательного появления на свет масса людей критиковала Windows 95 за то, что ола не является стопроцентно 32-разрядной, multi-все-и-вся, TURBO (The Ultimate Really Better Overall) и т. д. Эти критики были правы в оценке фактов, pi я не хотел бы спорить с теми, кто говорит, что шумиха по поводу выхода Windows 95 была просто поразительной. Но теперь продукт уже вышел, и я считаю все эти споры античной историей. Значение имеет только следующее: что именно находится в этом черном ящике, как хорошо оно работает, и — главное для моей книги — во что все это выливается для нас, программистов. Была и другая, большая группа людей, поначалу разочарованных в Windows 95, и я был одним из них. Продукт имеет множество мелких странностей и откровенных ошибок, которые я нахожу очень раздражающими, особенно если учесть, что это было первое серьезное усовершенствование Windows за последние годы. К счастью, большинство самых досадных проблем (с точки зрения конечного пользователя) связаны с программой Проводник (Explorer), а это означает, что мы можем их избежать просто путем замены этой части системы. (При этом я не хотел бы дальше разжигать эту дискуссию, размахивая моим перечнем претензий к Проводнику. Я полагаю, всякий технически грамотный читатель этой книги смог бы и без моей помощи составить впечатляющий список пороков Проводника.) В то же время в Windows 95 явно есть много того, что должно понравиться пользователям. Например, наиболее примечательным для опытных пользователей является серьезно улучшенное управление ресурсами. Для тех, кто привык выжимать максимум из компьютера, одного этого уже почти достаточно для перехода на Windows 95. В тот день, когда я получил мою копию финальной версии Windows 95, я (в отличие от апгрейда к очередной бета-версии) сразу установил ее на тот компьютер, где храню мои деловые записи и прочие важные данные. И тотчас же получил значительный выигрыш: я имел возможность держать одновременно запущенными Stickies!, Lotus Organizer, Word for Windows, Access и Visual C++ pi при этом иметь 76% свободных ресурсов. Фактически, одной из самых больших моих проблем при работе с Windows 95 является теперь преодоление привычки выгружать pi перезапускать одну pi ту же программу по несколько раз за сеанс. Как и большинство из вас, я действовал таким образом в течение ряда лет, хотя логической необходимости в этом никогда не было. Теперь эта гимнастика с мышью больше не нужна — намного эффектршнее просто запускать программы тогда, когда они необходимы (или даже раньше), а затем оставлять их бездельничать на панели задач. же
п ^улшше_ и приватные программы 33 Все дело в том, что Windows 95, по сравнению с Windows 3.1, - ^еошенно другой зверь. Windows 95 поощряет попытки пользователей рабо- ио-другому Когда я пишу эти слова, никто не знает, как быстро смогут ые категории пользователей освоить Windows 95 и принять ее. Но уже сепия все мы можем быть уверены, что присутствие этой операционной системы рынке настольных компьютеров будет очень значительным, и что она с большой вероятностью будет доминантной ОС (в абсолютных показателях) на ближайшие годы. ПНщВАичные и приватные программы о *> Одной из ключевых концепции, изложенных в данной книге, является отличие публичных и приватных программ. Приватными программами я называю те, которые будут использоваться только вами (их разработчиками) или очень ограниченным кругом других людей, чьи навыки, требования и компьютеры хорошо известны. Под «очень ограниченным кругом» я имею в виду не пару дюжин ваших ближайших друзей, а максимум иять-шесть человек, да и то -- лишь в жестко контролируемых условиях. А определение «хорошо известны» следует понимать буквально: известны вам лично, причем ваши знания о них и о том, как они будут использовать вашу программу, относятся к каждому конкретному человеку, а не ко всем в совокупности. Например, для объявления программы приватной недостаточно сказать, что она будет использоваться инженерами по гидравлике, которые часто пользуются компьютерами и потому считаются грамотными пользователями. Такие размашистые обобщения слишком часто служат оправданием для срезания углов в программах. Никогда не забывайте: даже если вы твердо уверены, что вы полностью контролируете распространение вашей программы, на самом деле это не так. Ваша программа запросто получит широкое, неофициальное распространение без вашего ведома (конечно, если она не является настолько специализированной, что лишь несколько человек в мире смогут понять, что она делает, и без посторонней подсказки счесть ее полезной для себя). Я видел немало случаев • акого ненамеренного распространения в прошлом, а ныне - когда пользователи повсюду связаны локальными сетями и модемами, как никогда раньше - вероятность такого случая еще больше. Некоторые придерживаются той крайней точки зрения, что, как только вы лаегс попользоваться вашей программой другому человеку, программа пересе- ч<1ст Чсрту п становится публичной Я понимаю такую точку зрения, но все же пггаю се .экстремальной. Например, моя жена Лиз работала системным анали- ком п программистом в течение 16 лет, и она определенно разбирается в 'Шыотерах и программах. Я могу легко отдать ей (или некоторым моим зна- льгм консультантам в области компьютеров) программу «приватного
34 Аобро пожаловать в реальный мир качества», сказать, что эта программа еще не готова для «премьеры», и дальше ни о чем не беспокоиться. Однако такие ситуации являются скорее исключениями. Публичные программы — прямая противоположность приватным. Они распространяются среди большого числа людей и будут использоваться в неизвестных условиях и ситуациях. Это подразумевает раздачу программы более, чем шести пользователям, или людям, которых вы не знаете, или использова- KJ ние этой программы на компьютерах с характеристиками, сильно отличными от вашего или вообще заранее неизвестными. Публичной программой может быть что угодно — от простой утилиты для принтера, которую вы написали и поместили на общий сетевой диск в офисе, до таких широко распространенных названий, как WordPerfect или сама Windows 95. В первом случае вы могли не иметь намерения увидеть эту маленькую утилиту работающей за пределами вашей компании; и вы могли бы даже вставить в ее интерфейс соответствующее заявление. Но будьте уверены — если эта утилита представляет хоть какой-то интерес для широкого круга пользователей, она уйдет. Кто-нибудь из ваших коллег возьмет копию для выполнения работ, взятых на дом, его ребенок увидит программу и начнет раздавать ее копии своим друзьям, один из друзей выложит ее на несколько BBS (чтобы получить признание ровесников не просто за публикацию крутой программы, а за публикацию программы, которая явно U не предназначалась для широкого распространения; ведь запретный плод так сладок). В мгновенье ока утилита, которую вы сваяли за час небрежного U кодирования, оказывается в пользовании тысяч людей. (Я должен отметить, что Фредерик Брукс упоминает классификацию программ на приватные и публичные в своей классической работе «The Mythical Man-Month», на странице 164. Как мне кажется, он использует эти о термины в том же смысле, что и я в этой книге, хотя он касается этой концепции лишь мимоходом. Для протокола: я не заимствовал эти термины у Брукса; еще и это случайное совпадение -- лишь еще один из маленьких приятных жизненных сюрпризов.) Насколько публичное публично? Даже когда вы предназначаете программу для публичного использования вы можете столкнуться с сюрпризами. Проектируя Stickiest, мою условно бес платную программу, я предполагал, что она будет использоваться как простая удобная программа для напоминаний, с достаточными возможностями базь данных, чтобы пользователи могли отслеживать и запоминать прошедшие об суждения, идеи и тому подобное. Я как они используют Stickiest и. как i подьми о том, в значительной степени именно так, как я и задумывал (со случайным
публичные и приватные программы 35 включением в лице одного любителя генеалогии, который с помощью Stickiest отслеживал предков и потомков). Но в один прекрасный день я получил факс, приковавший мое внимание. На нем была отпечатана какая-то военная символика, и послан он был полковником американских вооруженных сил, который просил меня связаться с ним- по поводу программы Stickiest. Я позвонил и имел очень интересный разговор с этим джентльменом, желавшим приобрести копию моей программы. Он сообщил, что использует ее для хранения информации о месте службы 30 генералов и 600 полковников, а также их подчиненных. Это было намного более «серьезным» применением Stickiest, чем я когда-либо представлял. Как вы можете догадаться, мне было приятно, когда этот клиент сообщил мне, что перед таким важным использованием программы он пробовал всерьез «выкручивать ей руки», и что она прошла все тесты. Вы можете возразить, что, мол, всякая публичная программа должна быть достаточно надежна, чтобы не имело значения, кто и как ее использует; программы либо работают правильно, либо элегантно отказываются работать, не теряя при этом данных. Фактически, это точно совпадает с моим собственным мнением. Но все же такие звонки, как этот, удивительно способствуют тому, чтобы лишний раз задуматься о проблемах качества. Между приватными и публичными программами нет внутренне присущих им различий. Различия являются внешними по отношению к программам и диктуются тем, как они распространяются, как и кем они используются. Было бы здорово, если бы у компилятора или компоновщика существовал бы ключ, при помощи которого мы могли бы заставить программу оставаться приватной. Но я не предвижу появление подобной возможности в ближайшем будущем. Этика и метафоры Приватные программы не очень интересны в свете проблем, обсуждаемых в этой книге. Если вы пишете программу для своего личного пользования, делайте это так, как вам хочется. Пишите ее на каком-нибудь экзотическом языке (возможно, даже на том, для которого вы сами создали компилятор или интерпретатор), если вас увлекают вещи такого сорта. Не обременяйте вашу программу такой немыслимой ношей, как одна дополнительная строка, осуществляющая проверку ошибки. Даже не думайте беспокоить ее и заставлять выдавать предупреждение перед форматированием винчестера в ответ на опцию командной строки типа /TERMINATOR:YES. Лупите сами себя. Это ваше микрокоролевство, и вы в нем правитель, поэтому ваше дело, что там творить. Но только потом не приходите ко мне в слезах, когда ваша программа вдруг превратится в самонаводящуюся ракету с ужасающей точностью попадания. Публичные программы — совсем другое дело. Когда ваша программа «выходит на публику», она буквально становится инкапсулированным отражением
36 Лобро пожаловать в реальный мир вашей философии и сценария вашего личного поведения, которые будут рабо- С» тать на компьютерах других людей, имея доступ к жизненно важной информации - воплощениию великих дел ваших пользователей, их денег, времени и усилии. Уже одного этого отличия между приватными и публичными программами должно быть достаточно для того, чтобы мы пересмотрели подход к программированию (хотя, повторюсь, судя по качеству современного коммерческого программного обеспечения, этого явно не происходит). Есть другой момент, который полезно иметь в виду во время написания публичной программы, и который традиционно тяжел для запоминания программистами: когда вы закончили ее писать, программа только начинает свою жизнь. Вы можете быть довольны ей, когда выпустите в свет, но только лишь в этот момент в игру вступят мириады ранее сделанных вами предположений о Windows, о пользователях и их системах, о компромиссах между удобством и производительностью, и о том, как все эти факторы будут взаимодействовать. Неважно, сколько часов ушло па проектирование, кодирование и тестирование вашей программы, она — лишь безжизненная вереница нулей и единиц до тех пор, пока люди не запустят ее и не извлекут из этого пользу. Вот когда происходит волшебство. В этом смысле хорошее программное обеспечение очень напоминает хорошую беллетристику пли другое произведение искусства: среди всех человеческих изобретений они ближе всех находятся к истинному волшебству, потому что способны изменять жизнь далеких, неиз- с» вестных автору люден Если вы позволите мне провести еще одну аналогию, я часто представляю программу как скрипку. Вообще говоря, в скрипке нет ничего волнующего. Немного лакированного дерева с витиеватыми прорезями, кусочки металла и какие-то струны. Бездушный, скучный и далее немного забавный вид. Но вложите скрипку в руку Ицхака Перельмана, и она вдруг покажется почти живой, способной выжать эмоции у слушателей и рассказать о чем-то почти членораздельно. Неплохо для такого простецкого изобретения (или для строки нулей и единиц). Знакомьтесь: это ваши пользователи Но достаточно метафор. Вернемся к реальному миру и к реальным людям, которые будут использовать ваши программы. Как я отмечал выше и в других местах (например, в моей статье «Dialog Box» в июльском номере Windows Magazine за 1995 год), мир Windows навсегда изменился в 1994 году, задолго до выхода Windows 95 И произошло это благодаря неожиданному всплеску продаж персональных компьютеров домашним пользователям Различные аналитики п компании еще за несколько лет до того момента оптимистично предсказали гряду mini взлет этого рынка. В конечном итоге, причиной этого не добства Apple Macintosh пли Window л-
p. si личные и приватные программы 37 \6о ДРУгие намеренные усилия по заманиванию ^компьютеризованной дичи екТрониые силки. Причиной была простая хладнокровная экономика: пред- ожите приличные компьютеры достаточно дешево, и народ сбежится покупать v тая домашнего пользования, в большинстве своем — для детей. Именно это их произошло. Соотношение производительное! ъ/цена в конце концов достаточно выросло (при весьма низкой входной цене), компьютерные продажи взтетели, и база потенциальных заказчиков у большинства производителей ПО драматически изменилась. (Никогда не забывайте Первый Закон Л у о Компьютерах: хорошая экономическая инициатива кроет флеш-рояль.) В старые добрые дни компьютеры были вотчиной лабораторных экспертов, носивших белые халаты и посвящавших себя преодолению собственноручно созданных сложностей. Сегодня, вероятно в отместку за прошлое, главным направлением стали персональные компьютеры, и миллионы их владельцев теперь изучают DOS и Windows (в особенности Windows 95), компьютерную аппаратуру и прикладное программное обеспечение, игры и прочую чепуху. И все это происходит одним гигантским глотком. А это значит, что вы больше не *,» *-» можете надеяться на высокий уровень компьютерной грамотности у пользователей ваших публичных программ. Например, ваша публичная программа больше не может блаженно игнорировать некоторые ситуации, предполагая, что «пользователи будут знать, как это сделать». На мой взгляд, такие предположения всегда были ненадежны; а теперь они и вовсе неправильны. Игры, в которые играют люди Я не собираюсь затевать истерию «избиения тупых пользователей» (как это делают многие программисты) ни в этой книге, ни где-либо еще, потому что пользователи заслуживают уважения с нашей стороны. Ведь все как раз наоборот: без пользователей наши программы — не более, чем нули и единицы, а мы — кучка пижонов, занимающихся непонятно чем. Но я все же думаю, что имеет смысл привести несколько примеров того, что норой вытворяют компьютерные новички (да и не новички тоже). Я наблюдал подобные случаи в течение последних нескольких лет, и эти эпизоды на многое открыли мне глаза.. Н Дебаты, вызванные ошибкой процессора Intel при вычислениях с пла- \J KJ вающеи точкой, нужно рассматривать как огромную неоновую вывеску, объявляющую в адрес всей компьютерной индустрии, что Мир Изменился. Каждая основная версия каждого микропроцессора имеет по крайней мере некоторые ошибки. Это не новость. Учитывая жуткую сложность микропроцессоров, это также и не сюрприз. Тем не менее, когда ошибка процессора Pentium всплыла на поверхность, это стало воистину главной темой для обсуждения всей компьютерной (и околокомпьютерной) общественностью. Джим Лаудербек, тогда работавший
ш о пожаловать в реальный мир в PC Week, а теперь — главный редактор Windows Sources, давал интервью CNN, и весь мир, казалось, сидел и внимательно слушал Вначале Intel повела себя неумело и плохо, говоря, что производитель О процессора сам решит, так ли уж нужен исправленный чип для тех работ, которые пользователи делают на своих компьютерах. (Это был еще один из сымпровизированных жизнью IQ-тестов. Большинство людей согласится, что здесь Intel потерпела полный провал.) Такой ответ мог бы быть приемлемым (хотя по-прежнему высокомерным) в середине 80-х. Но в середине 90-х, после превращения индустрии персональных компьютеров в нечто сравнимое с рынком компонентов для стереоаппаратуры, такой ответ был заслуженно осмеянным грубым промахом. Intel, к своей чести, в конце концов поняла это и предложила бесплатную замену Pentium всем желающим. Я встречал пользователей, которые думают, что динамические библиотеки (DLL) — это некоторая разновидность чисто текстовых конфигурационных файлов, и изменяют их при помощи текстовых редакторов. И даже появление на экране разного бинарного мусора вовсе не настораживает и не разубеждает этих людей. Они просто игнорируют все нечитабельное, пролистывают файл туда-сюда, пока не увидят фрагмент текста, который, как им кажется, относится к необходимому изменению, редактируют этот фрагмент, сохраняют файл и снова запускают программу. Затем они звонят в службу технической поддержки. В офисе клиента, которого я консультировал, я однажды застал их директора по маркетингу за следующим занятием: он запускал Windows, программу-редактор, что-то вводил в документ, в течение нескольких секунд что-то выискивал, беспорядочно тыкая в разные кнопки на экране, после чего выключал компьютер из сети. Пока я наблюдал за ним, он повторил эту процедуру несколько раз. В конце концов я понял, что происходило: этот человек просто не знал, как удалить текст и начать новый документ. А включение/выключение компьютера просто оказалось ближайшим решением его проблемы. (Да, забыл упомянуть: клиентом являлась компьютерная компания.) Во время разговоров с пользователями Stickles! я почти каждую неделю слышал от них об одной и той же проблеме: многие новички с трудом догадываются, что поверхность стола (desktop) в Windows на самом деле трехмерна. Снова и снова я слышу, что «Stickles! сначала показывает мои заметки, а потом вдруг исчезает! Куда она девается?» Конечно же, ответ таков, что никуда программа не девается. Просто KJ XJ (_» пользователь ткнул мышкой в окно какой-то другой программы, оно получило фокус ввода, всплыло на передний план согласно z-order и
приватные программы накрыло собой другие окна, включая окно Stickles!. Некоторые пользователи даже спорили со мной и заявляли, что Windows, вероятно, не может так себя вести, потому что это бессмысленно. Стандартный диалог Save As — знакомый элемент Windows даже для новичков. Но он может привести к небольшой путанице, связанной с форматами файлов. Например, пользователям приходит в голову мысль, что, если вы хотите сохранить графику в PCX-файле вместо BMP-файла, то вы можете просто воспользоваться функцией Save As: достаточно выбрать *.РСХ в списке типов файлов, и графика будет сохранена так, как нужно. К сожалению, некоторые пользователи вообще не знают о том, что существуют различные форматы файлов, а не только различные соглашения о названиях файлов. Поэтому они полагают, что диалог Save As является только лишь удобным способом сохранить файл под другим именем. В зависимости от используемой программы, они запросто могут открыть этот диалог, заглянуть в список форматов и, не найдя там «PCX-файлы», просто набрать в поле для имени файла «FRED.PCX» и нажать ОК. Конечно же, они не получат при этом PCX-файла, а получат BMP, или TIFF, или что-то другое. Вот почему так важно, чтобы ваши собственные форматы файлов были самоидентифицирующимися. (Эта тема подробно рассматривается в главе 9.) Понаблюдайте за тем, как пользователи сохраняют файлы, и вы скоро заметите, что многие из них не обращают внимания на те части диалогов Save As и Save, которые предназначены для работы с каталогами. Такие пользователи просто набирают желаемое имя файла и жмут кнопку ОК, непреднамеренно позволяя программе поместить файл в том каталоге, на который в данный момент указывает диалог. И если вдруг этот каталог оказывается малопримечательным в их системе или *.» открытым случайно, он практически становится могилой сохраненного файла, потому что в следующий раз, когда пользователь попробует добраться до этого файла, ему покажется, что файл исчез. И даже если файл будет сохранен в нужном месте, достаточно того, чтобы диалог File|Open в следующий раз открылся в другом каталоге — снова будет казаться, что файл потерян. Этот пример показывает, как просто можно облегчить жизнь своим пользователям: сделайте так, чтобы все эти стандартные диалоги открывались в подходящем каталоге. Я видел работника, который совершал апгрейд от DOS 5.0 к DOS 6.0 путем копирования каталога С:DOS с одной машины на другую. (Он считал, что запуск программы-апгрейда был бы слишком хлопотным.) Результат получился варварским (в буквальном смысле этого слова подобным вторжению чужаков); поскольку работала по-прежнему
40 Лобро пожаловать в реальный мир DOS 5.0, а все внешние команды (например, FORMAT COM) находились в С:DOS и были от новой версии; естественно, они отказывались нормально работать с предшествующей версией DOS. Пользователи-новички не являются для нас ни добром, ни злом; просто их действия несколько отличаются от того, что вы ожидаете. И это особенно верно, если вы круглосуточно окружены программистами и опытными пользователями. Даже тогда, когда пользователи не занимаются исследованиями всяких закоулков, они совершают самые разнообразные действия, которые кажутся им совершенно обоснованными, но могут создать для вашей программы абсолютно катастрофические условия. Они постоянно будут совершать такие вещи, как установка вашей программы на один компьютер (в строгом соответствии вашим инструкциям), а потом попытка установки ее на другой, путем простого копирования файлов, принадлежащих, как им кажется, этой программе. Многие простые правила, воспринимаемые программистами как очевидные (вроде необходимости закрыть программу перед тем, как «снаружи» делать что-то с файлом, загруженным в нее), просто-напросто непостижимы для пользователей. В некоторых случаях Windows оградит вас от этих проблем, но чаще ответственность по-прежнему возлагается на программистов. Я хотел бы еще один, последний раз подчеркнуть: я привел все эти примеры (некоторые из них достаточно анекдотичны) не для того, чтобы смутить или тем более обидеть компьютерных пользователей-новичков; я рассказываю эти истории потому, что, взятые вместе, они составляют наглядную и поучительную повесть о программировании середины 90-х годов. Любовь приходит и уходит, а программы остаются Один из более коварных побочных эффектов этого наплыва новых пользователей связан с тем, что они терпеть не могут обновлять программное обеспечение Они покупают компьютер в каком-нибудь магазине и используют те же самые копии DOS, Windows и пучка других программ, полученных вместе с компьютером, до тех пор, пока не обновят всю систему в целом (лет этак через несколько). В конце концов, все работает нормально, так зачем тратить деньги на изменение чего-либо? Вопрос об апгрейдах напоминает мне об одной из самых интересных цитат, известных мне. Эти слова принадлежат сотруднику Microsoft и взята из октябрьского номера Windows Magazine за 1994 год: Стив Балмер сообщил, что от 5.5 до 7 процентов пользователей MS-DOS перешли на версию 6.0, и что лишь 24 процента зарегистрированных пользователей перешли с Windows 3.0 на
п.^^мные_и_ приватные программы 41 Windows 3.1. Что же касается прикладйого программного обес- печиния, то соответствующие показатели для Word и Excel в среднем колеблются между 20 и 30 процентами. Даже если предположить, что можно сделать некоторую поправку на rmiviTCTBO (например, компания, располагающая сотней компьютеров, поку и с ■хст только один апгрейд и затем использует его на всех системах), и что Mi- iosoft, вероятно, вычисляет показатели для Windows, деля количество зарегистрированных апгрейдов на общее количество проданных лицензий (включающее OEM-лицензии, часть которых реально не используется и никогда не будет обновлена), все же я нахожу оценку 24% неожиданно низкой. Вспомните, насколько отвратительной казалась Windows 3.0 при сравнении с Windows 3.1? Этот переход был прародителем всех последующих «само собой разумеющихся» апгрейдов (и состоялся он еще перед серьезным вступлением в игру рынка домашних компьютеров), тем не менее процентное отношение так и не превысило одну четверть. Я часто представляю себе новых компьютерных пользователей похожими па обычных домовладельцев. Они могут строить планы серьезного обновления своего дома в один прекрасный день (например, добавления ванной комнаты или перепланировки кухни), но до того момента они не станут делать никаких изменений. Для многих компьютерных пользователей смена важной программы пли (содрогнитесь!) операционной системы есть нечто столь же неприятное (каждому по-своему), как примирение со строительными работами внутри собственного жилища. Такое нежелание обновлять программное обеспечение имеет весьма серьезные последствия для программистов, поскольку операционная система явля- tj i_» «.» ется как раз топ частью «хозяйства» пользователей, которую они меняют особенно неохотно. Это значит, что любые ошибки в первой коммерческой версии Windows 95 или любой ее последующей версии станут перманентными дырками в киберпространстве, вокруг которых нам всем придется маневрировать В какой-то степени так было всегда, и программам всегда приходилось учитывать более ранние версии операционной системы (или, по крайней мере, ИхМ следовало это делать). Но мир изменился, и сейчас этот фактор намного более важен, чем несколько лет назад. Как не следует относиться к пользователям-новичкам Я уже слышу, как толпа разогревает свои текстовые редакторы возгласами «эго не моя впиа.../невозможно сделать все на свете.../пользователи должны сами нести ответственность за свои системы...» Если вы из этой категории, позвольте мне сэкономить ваши усилия' я не собираюсь предлагать вам стремиться делать своп программы полностью защищенными от дураков. Я
42 Аобро пожаловать в реальный мир U знаю, что сделать это невозможно, потому что всегда найдутся люди, которые будут совершать самые причудливые действия (например, выключать из сети компьютер в то время, как ваша программа записывает данные в дисковый файл, а потом ожидать, что она будет нормально работать при следующем запуске). Более того, я не думаю, что вам следует украшать ваши программы многочисленными подсказками и нянчиться с пользователем, пытаясь уберечь его от совершения малейшей ошибки. Даже решение о том, что подходит под определение «нянчиться», а что нет, может оказаться трудным. Определенно, я жду от текстовых процессоров и других документо-ориентированных программ выдачи предупреждения в том случае, когда я пытаюсь завершить работу, не сохранив предварительно сделанные мной изменения. Но даже та- KJ О кои простои уровень защиты может применяться неправильно и раздражать. Например, программа Resource Workshop от Borland будет ошибочно говорить, что RES-файл, который вы открыли для просмотра, изменен, хотя этого на самом деле не было — такое поведение сводит меня с ума. Еще более раздражающим был один инструмент разработки, который я однажды исполь- О зовал: в его состав входил сложный редактор для специальных структурированных файлов, который позволял перепрыгивать с экрана на экран, изменять поля, и много чего еще, но никак не мог делать такую О мудреную операцию, как предупреждающий писк при попытке выйти, не сохранив сделанную работу. Когда я спросил разработчика этого редактора, почему он так себя ведет, мне было сказано, что для осуществления моих пожеланий потребовалось бы слишком много труда. Даже для инструментария разработчика это совершенно неприемлемо. Пользователь и в самом деле должен нести какую-то ответственность за свой компьютер и за то, что на нем происходит. Но моя точка зрения и заключается в том, что пограничная линия между вашей ответственностью pi ответственностью пользователя сместилась одновременно с изменением уровня всеобщей компьютерной грамотности. Юристы часто упоминают термин «тест О здравомыслящего поведения», который означает, что при рассмотрении той или иной ситуации они спрашивают себя: «Что будет делать или чего будет ожидать в данной ситуации здравомыслящий человек?» (здесь можно вставить бесплатную улыбку юриста). В нашей области определение «здравомыслящего пользователя» в значительной степени изменилось за последние 12-18 месяцев (на момент написания этих строк) и будет продолжать меняться в быстром темпе по крайней мере ближайшие несколько лет. А это, в свою очередь, изменяет определение «здравомыслящей программы». В каком-то смысле я нахожу это довольно ироничным, поскольку я всегда считал, что в среднем программы должны быть намного более удобными и любезными, чем было до сих пор. Многие программисты отвергают такую идею, ссылаясь на миф о том, что все их пользователи знают (или должны знать), что они делают, а если они этого
p.fiличные и приватные программы 43 знают, что ж, тогда пользователи сами виноват^ы в своей безграмотности, оберегать пользователей от их собственного невежества — не дело ведь г тт актированных программистов. Ныне, под воздействием непреодолимых кономических стимулов, заставляющих программистов быть более услужли- рыми, как опытные пользователи, так и новички в конце концов начинают до: биваться больших удобств (что, вообще говоря, должно было происходить и раньше) Rah нуйкно относиться к пользователям новичкам и гуру По сути, ответ на этот вопрос растворен во всей книге. Нет той пресловутой единственной серебряной пули, которую я мог бы вам дать. Разве что в главах 2 и 3 я представлю набор конкретных рекомендаций по разработке программного обеспечения для Windows 95, которые, на мой взгляд, помогут справиться с этой серьезнейшей проблемой. *j *j Первый шаг, который все мы должны сделать, — скорректировать модель менталитета нашего среднего или типичного пользователя. Авторы бел- О летристики часто говорят о наличии у них ментальной модели своих типичных читателей. Имея свой собственный беллетристический опыт, я убежден, что все авторы действительно имеют в своей голове такую модель при работе, даже если они не говорят о ней так явно. Присутствие такого мифического тршичного пользователя как раз и привносит те самые разнообразные предположения о тем, что годится и что не годится в окончательном результате работы. Намного более сильное воздействие на нашу работу оказывает наша философия проектирования и реализации программ, что является темой следующих двух глав. Поразительное происшествие с CON.TXT и другие устройства-разрушители Одну из самых странных вещей в Windows 3.1 и Windows 95 я открыл совершенно случайно. Почти целый день я занимался копированием файлов под управлением Windows for Workgroups 3.11, используя File Manager от Central Point. Мне нравится этот заменитель стандартной программы File Manager, потому что он изящно оперирует с ZIP-архивами как с каталогами, позволяя мне увернуться от ужасно расточительной проблемы с размерами кластеров в FAT (см. главу 2, где эта проблема обсуждается более подробно). Я хотел скопировать простой текстовый файл в новый файл с именем CON.TXT в том же каталоге. Тогда я нажал F8, File Man-
о пожаловать в реальный мир сЩег открыл приглашение для ввода имени выходного файла, я ввел строку CON TXT и ударил по клавише Enter. Вместо того, чтобы скопировать один файл в другой, как я того ожидал, Windows скопировала исходный файл прямо на экран моего монитора — в текстовом виде, прямо поверх всей картинки Windows. Это не было ни DOS-окном, ни чем-либо подобным. Посмотрите на рисунок 1.1, чтобы увидеть пример аналогичного казуса в Windows 95 (такое вытворяет в Windows 95 программа НО- LYCOW.EXE, которую вы найдете в Web-библиотеке). Ответ, конечно же, в том, что CON.TXT (или просто CON, или CON mino- угодно) является в DOS названием устройства, и любая попытка открыть файл с таким именем и записать что-либо U в него приводит к открытию консольного устройства, а не дискового файла. Вопрос о том, почему Windows/DOS недостаточно умна для понимания разницы между CON.TXT и CON, имеет чисто академическое значение. Главное в этом случае (и во многих других примерах из этой книги) заключается в следующем: не важно, правильно или неправильно, умно или глупо, но это происходит именно так, и как раз нам, программистам, придется это расхлебывать и искать пути в обход таких ловушек. Как только я увидел проблему в действии, я обнаружил, что наши надежные друзья — стандартные диалоги — достаточно 4 лишь частичная защи поскольку не все приглашения для ввода имени записываемого файла осуществляются Н М Ъайла при помощи своего собс (как видно из поведения этой программы) делает типичную проверку на существование файла с таким именем, и, если все выглядит нормально, совершает копирование. В качестве другого теста, я связался с Сотри Serve при помо щи Procomm Plus for Windows 2.0 и выбрал файл для перекачки па свою машину. Затем я указал, что хочу скачать этот невинный файл себе под именем CON.ZIP. Вместо того, чтобы разукрасить мой экран всяческим бинарным мусором, как я того ожидал, Procomm и Windows зависли намертво, и мне пришлось выключить и включить питание компьютера, чтобы вернуть машину в работоспособное состояние. Не довольствуясь стопором на CON.TXT, следующим я провел аналогичный эксперимент с PRN.TXT, в результате которого появился системно-модальный диалог, сообщивший:
П)^личные_и приватные программы 45 SyGtem Error Cannot write to the device PRN Make sure the network is working If it ig a local drive, check the dick and the device driver 1 Попытка записать в COM 1.TXT привела к еще более понятно- 1-» <J му разъяснению: я получил другой системно-модальный диалог, с тем же самым текстом, что и выше (с точностью до замены PRN на СОМ1), но на этот раз система намертво повисла, и мне опять потребовалось перезагружать компьютер. Запись в NUL ТХТ, по- видимому, отправила текстовые строки куда-то к черту на кулички. Если вы желаете увидеть всю эту пиротехнику в действии, запустите программу HOLYCOW.EXE (она находится в каталоге HOLYCOW по адресу http://www.symbol.ru/russian/library/ prof_prog/source/chap01) под Windows 95 или Windows 3.1. Эта программа использует CON.ТХТ в качестве имени файла. Но перед тем, как делать что-либо с этой программой, пожалуйста, примите необходимые меры для защиты ваших данных и других программ. Нет смысла рисковать без необходимости. Мой друг Фил Юргенсон говорит, что в его Windows 3.1 HOLYCOW.EXE всего лишь поменяла цвет мышиного курсора. Если и вы увидите что-либо отличное от текста на экране, пожалуйста, дайте мне знать об этом. Непосредственный, основной урок, вытекающий из этой истории, состоит, конечно же, в том., что нужно быть осторожнее с именами файлов, которые вы используете. CON и некоторые из его «приятелей» должны отслеживаться либо при помощи стандартных диалогов, либо в вашем собственном коде. (Кстати, это применимо только к Windows 3.1 и Windows 95. Запустите HOLYCOW.EXE под управлением Windows NT 3.51, и ваш экран не будет разукрашен текстом. Правда, в этой операционной системе есть другой занятный глюк: попросите File Manager из Windows NT 3.51 поискать на вашем диске С: файл с именем «con.txt», и вам ответят, что в каждом каталоге найден искомый файл с именем «con».) В более широком смысле, мораль сей истории такова, что вы никогда не можете быть уверены, что охватили все платформы в вашей публичной Windows-программе. Сюрпризы будут возникать всегда, а вам придется лишь обеспечивать достаточную гибкость реакции на них и ваших программ. 1 ^чск'мная ошибка •"у °лппсь на ус i роист но PRN невозможна Ч)()нерыс, работает ли сегь Если же это локальное устройство, "роверые диск п драйвер устройства
й о пожаловать в реальный мир СОМ' cow!*! cow!!! глн! ** cow!!! го*»?'! соы!Н cowtt! cw!!! cowHt cow!!! cow!!t cow!!! сои!!! cowttt сои!!! cowttt COM?!! cowttt COWttt cowftt caw'?! cowttt cowttt cow*** it's Mhut's Uhat's mutt's Ifhat'» Uhat's what's «hat's Uhat's Uhat's U hut's Ubat's Uhat's Uhat's Uhat's What"s Uhat's What's Uhat's Uhat's Uhat's yhat's Uhat's Uttat * v this tins this thi4 this tMs this this this tbls this this ibis this tills this this this this this this this this this doing doing doing doing doing doiwj doing doing doing doing doing doing doing doing doing doing dot Tig doing doing doing doing doing doing doing doiw here. here?,1 hereTTI hprft'T here?"* here?'" here?4 here"! hepe?Tl hsrcTW fcere?*n here?*H hereT^ here?* here?r hereTH hereTfl ЬсгеТЯ here' ЬсгеТП here' herem here??1 ^du}*l Ш J ,,.Л№4 Рис. 1.1. Использование CON в качестве имени файла
(ЩШ Axioms in philosophy are not axioms until they are proved upon our pulses: we read fine things but never feel them to the full until we have gone the same steps as the author. Джои Ките Как и в более важных жизненных областях, все, что мы делаем как программисты, прямо или косвенно определяется нашей философией. Наши стили кодирования, наши предпочтения в выборе типов данных для переменных, буквально каждое из дюжин решений, которые мы делаем при написании даже маленьких программ — все это сводится к противопоставлению взглядов, которые в меньшей степени зависят от установленных фактов, а в большей — от мастерства. В этой и следующей главах я поведу долгий разговор об основных философских проблемах программирования для Windows и представлю целый ряд рекомендаций, которые должны помочь вам избежать многих ловушек. Эта глава фокусируется на макропроблемах, а глава 3 охватывает микропроблемы. £ВПШАЬ Преж де, чем вдаваться в детали того, что делает программу хорошей, и как создать хорошую программу, я хотел бы потратить немного времени на близкую к этим вопросам концепцию стиля.
48 Философия разработки ПО лля Windows: макропроблемы Стиль - это такая сторона программирования, о которой программисты, возможно, говорят даже больше, чем о философии (хотя порой они даже не знают, что говорят именно об этих вещах). Вы часто слышите, как люди разговаривают о стиле форматирования кода у конкретного программиста, об'ис- KJ пользовании или неиспользовании венгерской нотации, о том, насколько агрессивно какой-либо человек использует (или бранит) возможности C++ и т. д. В некоторых случаях стиль — это не более чем привычка, в плен которой мы однажды попали, и отказаться от которой у нас не было оснований. Делайте что угодно каким-нибудь способом достаточно долго, и этот способ покажется правильным; пока ваша практика не сталкивается с проблемами, вы привыкаете к нему, он становится частью вашего стиля, и влияет на остальную вашу работу. Но стиль программирования не является просто результатом вычитания инертности из случайности. Часто стиль является неожиданно возникшим свойством персональных философии и предположений. Например, Windows- программисты часто относятся к локальным переменным в функциях как к практически бесплатной памяти, по крайней мере когда речь идет о небольшом количестве переменных. Что следует использовать — четырехбайтное целое или двухбайтное целое — для маленького числа, которое легко уместится в любой из этих типов данных? Один лагерь говорит, что вам следует выбирать размер переменных максимально соответствующим их предназначению, чтобы сделать их «самодокументированными». Другой лагерь предлагает не беспокоиться об этой мелочи, а лучше учесть, что наиболее эффективным является тот тип, размер которого ближе всего к размеру машинного слова. Это один из тех споров, которые могут длиться вечно, включая в расмотрение все больше и больше тонкостей из области характеристик машин, компиляторов, и т. д. и т. п. Фактически, большинство из нас даже не задумывается над этой проблемой (за исключением случаев, когда нам необходимо передавать эту переменную API или другой функции, и когда на самом деле самым удобным оказывается выбрать тип так, чтобы избежать явного преобразования типа). Каков бы ни был индивидуальный подход, программист просто напишет данный фрагмент кода так или эдак и, особо не размышляя об этом, продолжит работу над проектом. А ведь то, к какому решению мы склонимся в данном маленьком вопросе, является философской проблемой в той же степени, в какой это является беспокойством о производительности. Интрсспым знамением нынешнего времени стал тот факт, что практически никто больше не обсуждает подобных проблем. Когда я начинал свою профессиональную карьеру программиста, во времена администрации Картера, мэйнфреймеры все еще дискутировали на такие темы. Что ж вы хотите, у нас не было ни мышек, ни графических интерфейсов, с которыми молено было бы поразвлечься, а нам нужно было хоть чем-то заполнять праздные минуты.
математики и ювелиры 49 Математики 3 течение многих лет я был знаком со многими программистами и обнаружил, что большинство из них (но определенно не все) распадается на две категории - математиков и ювелиров. (Несмотря на то, как я сформулировал это утверждение, я уверен, что получу несколько сердитых электронных писем oi представителей всех этих трех профессий. Я заранее приношу свои извинения на тот случай, если вы почувствуете, что я каким-то образом неуважительно отнесся к вам или вашей профессии.) Математики — это абстрактные ученые. Они делают сильный акцент (порой в ущерб другим факторам) на вопрос о доказуемой правильности (или, наоборот, неправильности) того или иного фрагмента кода. Их всецело поглощает беспокойство о том, чтобы код работал строго в соответствии с требованиями к нему, при подаче ему правильных входных данных. Если ответ положителен, то дискуссию, по их мнению, можно дальше не продолжать. %j и ! Следующий вопрос, пожалуйста! «_» о о С позиции чистой логики, суровой строгости, определяющей роли спецификаций (а это, конечно, самая лучшая отправная точка при оценке кода), совершенно невозможно спорить с этой группой программистов. Спецификации гласят, что функция должна давать на выходе А, когда на вход поступают X, Y и Z Делает она это или нет? Если да, то она очевидно правильна. Тогда какие проблемы? (Последнее предложение обычно добавляется только программистами из Ныо-Иорка.) Несмотря на трудность споров с математиками, как профессионал я часто должен с ними спорить, потому что они склонны игнорировать другие проблемы, которые могут быть важны не меньше, чем строгая корректность программы или функции. Предположим, функция Barney() явно работает корректно — делает в точности то, что от нее требуется, — но при этом она представляет собой трудносопровождаемую путаницу, имея 18 параметров взаимозаменяемых типов, что часто ведет к ошибкам в коде, вызывающем эту функцию. Очевидно, что такой дизайн плох, и что эта функция может и Должна быть улучшена. В данной ситуации (а она не так уж и надуманна) экстремистски настроенные математики могут быть весьма упрямы: они не будут «ковырять» работающий код и рисковать привнесением новых багов в ту часть системы, которая в текущий момент не имеет известных ошибок. (Я сам осуществлял сопровождение операционной системы, написанной на ассемблере, поэтому я испытываю немалую симпатию к такой точке зрения.) С другой стороны, ювелиры представляют собой иную проблему. Они из тех, кто часто говорит об элегантности алгоритмов и о поисках новых методов, которые будут работать хоть немного, хоть чуть-чуть лучше существующих. В нормальных условиях это хорошая позиция (говорим ли мы о программировали, садоводстве или каком-либо другом старательстве). Но лучшее — враг
50 Философия разработки ПО лля Windows: макропроблемы хорошего, и привычки более фанатичных ювелиров часто превращаются в бесконечное вылуживание, вызываяя серьезные проблемы по всему большому проекту в целом. Возвращаясь снова к гипотетическому примеру с функцией ВагпеуО, ювелиры обычно с охотой берутся исправить эту функцию (заменяя 18 параметров на один, представляющий собой структуру из 18 элементов, или еще каким-либо образом улучшая ее интерфейс) с целью уменьшить количество ошибок в вызывающем коде. Но это приводит к необходимости делать кучу из- ошибок И мы опять сталкиваемся с теми проблемами, ради избеж затевалось. На протяжении ряда лет я многократно спорил и с математиками, и с и ювелирами, и «религиозные воины» между этими двумя лагерями почти всегда сводились к следующему противопоставлению взглядов: что лучше — уживаться с дефектами pi постепенными, капля за каплей, затратами на усовершенствования, или бросить все свои скудные ресурсы по разработке и тестированию на то, чтобы исправить ситуацию раз и навсегда, даже рискуя создать большую проблему? Конечно, реальные жизненые ситуации почти никогда не сводятся к буквально такой постановке вопроса, а это означает, что мы опять возвращаемся прямиком к философии, к тому, как она направляем наши взгляды, решения и, в конечном итоге, нашу работу. Вероятно, вы уже заметили, что я склонен быть больше ювелиром, чем математиком. Это правда, но известно, что я являюсь ярым ненавистником программных интерфейсов, которые не ведут себя в точном соответствии с их описаниями. Например, обратите ваше внимание на дискуссию в главе 14 о некоторых приключениях, с которыми нам всем придется столкнуться при работе с Win32 API. Пускай это будет еще большим обобщением, но весьма интересно понаблюдать, как относятся друг к другу эти два лагеря. Ювелиры имеют тенденцию смотреть на математиков как на двумерных, слишком суровых простаков, которые не способны понять картину в целом и оценить далекие последствия, и для которых недоступны хитрость и изящество большинства решений. Математики же при общении с обеспокоенными жесткими требованиями, предъявленными к программе. Я обрисовываю все это не для того, чтобы выступить в качестве популярного психолога, а для того, чтобы подчеркнуть, что оба лагеря правы каждый по-своему. Их любимые заботы в самом деле очень важны, и грубое пренебрежение ими может привести к серьезным несчастьям. Истинная же проблема, как всегда, в нахождении правильного баланса в вашем конкретном проекте. У меня есть сильное желание — видеть как программисты используют та- себе эти два лагеря) расширяем наши взгляды (так, чтобы объеди
и три составные части качества программы 51 (так, чтобы многие из ситуаций, подобных гипотетическому примеру с функ- гией ВагпеуО, не возникали вообще). В конце концов, не можем же мы тратить воемя на споры о том, как починить то, что никогда не ломается! (Ладно-ладно, мы можем это делать, программеры есть программеры. Но я надеюсь, вы по ня ли мою основную мысль.) Позже мы вернемся к этому вопросу и поговорим о нем шире и подробнее. А пока я хотел бы поговорить о том, что же делает программное обеспечение хорошим ТГры части Ваша первейшая цель при написании любого программного обеспечения — серьезной ли прикладной программы, с которой будет работать множество пользователей, или функции в библиотеке, которую увидят лишь несколько других программистов — сделать его по-настоящему полезным. Если вам это удастся — то есть если выигрыш от использования вашего продукта будет намного превышать его стоимость — люди по достоинству оценят ваш труд и будут им пользоваться, потому что ваше программное обеспечение действительно расширит их возможности и сэкономит им время. В некоторых случаях (таких, как одна древняя DOS-программка, которую я до сих пор использую ежедневно), это определение сохраняет силу даже тогда, когда, казалось бы, весь мир ушел далеко вперед. Главными целями при написании широко распространяемой функции или законченной программы должны быть ее корректность, разумность и услужливость. Добейтесь этого, и определение «по-настоящему полезная» приложится само собой, потому что оно является результатом сложения этих трех качеств. Если сказанное выше звучит не очень понятно, вы можете применить термины стратегии и тактики: стратегия заключается в том, чтобы сделать вашу программу по-настоящему полезной, а тактика — в том, чтобы сделать ее корректной, разумной и услужливой. Одна поясняющая деталь: до конца этой главы я буду использовать термин «программа» в более широком смысле, чем обычно, и буду иметь в виду любое ПО, с которым будет осуществляться взаимодействие извне, — от законченной прикладной программы до отдельной функции в совместно используемой библиотеке. Аналогично, я буду говорить о «пользователях» более широко, не ограничивая это понятие только людьми, которые запускают прикладные программы. Корректность Корректные программы допускают только два варианта реакции на ввод пользователя:
52 Философия разработки ПО лая Windows: макропроблемы В Выполнить ожидаемые функции в точности согласно запросу, учитывая общий контекст программы или системы. Элегантно отказаться выполнить запрос из-за неправильно введенных данных, недостатка ресурсов и т. д. Не нужно объяснять, что термин «элегантно» не является эвфемизмом для «путем форматирования жесткого диска пользователя и затем вызова GPF» или чего-нибудь подобного. Вы должны рассматривать корректность как едва достаточный минимальный уровень приемлемой функциональности. Оценка корректности — это еще одна из необычайно запутанных проблем. На мой взгляд, любое определение API, документация, руководство пользователя и т. д. — это контракт между программой и окружающим ее миром. Если документация и программы не согласуются, провал может потерпеть весь программный пакет. Вопрос же о том, О что конкретно является причиной провала — документация или программа, это уже следующая, более глубинная проблема, которая может привести к своему собственному бесконечному циклу споров. Я знаю немало случаев, когда вначале функция предназначалась для какого-либо конкретного использования и была соответствующим образом документирована, а позже выяснялось, что при каких-то странных условиях это соответствие могло нарушаться, и тогда код оставался неизмененным, а корректировке подвергалась именно документация (так, чтобы оговорить возможную аномалию). Иногда это было просто запоздалое открытие того факта, что действительно существует патологическая ситуация, в которой функция никак не может работать согласно исходной документации (кстати, это означало не что иное, как ошибку в изначальном дизайне, так как он на самом деле обещал недостижимые результаты). А иногда это было просто проявлением лени со стороны программиста или компании- производителя, которые не желали чинить свои программы и разрешали противоречие самым дешевым способом — путем корректировки документации и приведения ее в соответствие с кодом. функц Один из великих грехов программирования — написание ных спецификаций с использованием скверного выражег ленное поведение» («undefined behaviour») Обычно оно встречается примерно в таком виде «если параметр lpFlapDoodle равен NULL, то по- «неопреде Фу Перевод: «программист был слишком близорук, чтобы предвидеть, что вы можете случайно передать NULL через параметр lpFlapDoodle (или слишком ленив для того, чтобы что-то сделать в связи с такой возможностью), поэтому сия 4 • • ^> Передайте NULL через этот параметр — и вся ваша программа (а возможно, и операционная система) рухнет на пол, как тонна кирпичей, сброшенных с околоземной орбиты»
части качества программы 53 Важно отличать вопиющую программерсЛсую лень от условии, которые действительно не могут быть проверены Возьмите стандартную С-функ- цию memsetX), которая заполняет указанный блок памяти заданным байтовым значением. Когда вы используете ее для обнуления структуры, как это часто делается в Windows-программировании, подпрограмма mem- set() должна полагаться на то, что при ее вызове вы передадите ей *_* корректное значение длины, просто потому что у нее нет никакой возможности самостоятельно узнать размер вашей структуры. Хотя реализация этой функции в Visual C++ 2.2 (и, возможно, другие ее реализации) воспримут NULL в качестве передаваемого указателя Еще более крайним примером ситуации, когда возникновение ошибки невозможно определить, является тривиальное выключение питания компьютера без предварительного завершения работы (shutdown) Windows, что, весьма вероятно, приводит к потере данных. Я не знаю никого, кому пришло бы в голову обвинять Microsoft за то, что она не предохраняет от этой обычной проблемы. Разумность Несмотря на важность корректности, просто корректная программа еще далеко не является «по-настоящему хорошей». Разумные программы не ограничиваются просто принятием правильного ввода и обеспечением правильного вывода, они ожидают появления ошибок пользователя и адекватно реагируют на них. При этом я использую термин «ожидают» не в том узком смысле, что программа лишь предвидит ошибочное действие пользователя программа совершает продиводействие еще до того, как это действие совершено. Противодействие может иметь самые различные формы, такие как дополнительное усердие программы при проверке входных параметров или просьба к пользователю подтвердить его намерение предварительного сохранения его на диск. выбросить документ без Если принять во внимание все места в Windows-программировании, где в качестве параметров передаются указатели, неудивительно, что простейшим способом нарушить работу программы является передача функциям нулевых указателей. А это неизбежно приведет к началу 1372-го витка философской X 9 воины на тему: чьей работой - вызывающего или вызываемого кода — должно быть обеспечение того, чтобы NULL-указатели не проникали в сердцевину программы. Разумеется, ответ на этот вопрос следует искать в спецификациях на функции, так как именно они являются контрактами между функциями и окружающим миром. К сожалению, почти невозможно всегда докопаться до явного описания Гого, как обрабатываются те или иные общие патологические случаи. папример, многие интерфейсы Win32 описаны так: в случае успешного выполнения возвращается значение TRUE, в случае провала — FALSE (или на-
54 Философия разработки ПО лля Windows: макропроблемы оборот), а дополнительную информацию о случившейся ошибке вызывающий может получить при помощи интерфейса GetLastErrorO. Но в подавляющем большинстве случаев это «определение» интерфейса (кавычки означают, что термин в данном случае применен из снисходительности) не только ничего явно не говорит о том, что будет делать Windows при неправильном вводе, но даже не дает и намека на то, какие коды ошибок вы можете ждать от GetLastErrorO в данном конкретном случае. (Смотрите главу 14, где вы найдете массу компрометирующих материалов на функцию GetLastErrorO.) Конечно же, разумное программное обеспечение должно делать много больше, чем просто «проверять и катапультироваться» (касательно входных параметров). В некоторых случаях, функция может не только обнаруживать условия, не позволяющие выполнить ее работу, но и различать ошибочные ситуации, которые она может самостоятельно и безопасно исправить. Здесь я хочу привести мой излюбленный пример такого не слишком опасного дефекта данных: нежелательные пробелы в началах и концах строковых данных. На протяжении многих лет я видел огромное число программ, которые теряли ориентацию (или данные пользователя), сталкиваясь с такой гнусной ошибкой, как нечаянный ввод пользователем пробела в начале или конце имени. Часто \J U \Л это является тривиальнейшей задачей для программы — скорректировать подобную ситуацию, однако многие программы этого не делают. К этой теме я более подробно обращусь вновь в главе 3, где буду говорить об оборонительном программировании. Пользователи не являются единственным источником «незначительных отклонений» (термин позаимствован из области пошива одежды). Программы часто получают сюрпризы от сторонних библиотек, от самой Windows, или даже от более пассивного (но далеко не статичного) объекта под названием «состояние системы пользователя» (например, когда стандартная DLL, которая «просто обязана присутствовать в системе», вдруг отсутствует; или присутствует, но не той версии; или это вообще посторонняя DLL, по случайности носящая то же самое имя). Я могу еще много чего порассказать о динамических библиотеках (см. главу 6), но уже здесь я хочу отметить, что при использовании несистемной DLL разумным подходом является ее явная загрузка (при помощи функции LoadLibraryO), а в случае ее отсутствия или неподходящей версии — предоставление пользователю осмысленной информации об этой типичной проблеме (желательно, с возможностью получения еще какой-либо дополнительной помощи, если это уместно). С этой проблемой также перекликается вопрос о том, что делать программе, когда отсутствует DLL, отнюдь не являющаяся жизненно важной для работы этой программы. Многие программы в такой ситуации откажутся работать (тогда как на самом деле они просто неявно загружают свои DLL и тем самым позволяют Windows самой запрещать запуск этих программ, вообще не отдавая им управление).
Той источника и три составные части качества программы 55 Как и в других спорных ситуациях, упомянутых в данной книге, здесь рекомендуется искать сбалансированное, компромиссное решение. И если уж 1уЧилось так, что вы пишете одну из тех ужасно редко встречающихся в рИроде функций, которые буквально не могут вынести бремя дополнительной пары строчек типа if(lpSomeData — NULL) return ERROR_BAD_DATA, тогда хотя бы ясно и недвусмысленно напишите в документацрш, что эта функция взбесится, если ей передать нулевой указатель (а затем ясно и откровенно поясните в самом коде, почему такая проверка не была сделана, чтобы потом, когда вы будете в отпуске в следуюшем году, какой-нибудь ювелир, в хорошем смысле этого слова, не оказался в полной беспомощности и вставил в нужное место вышеприведенный лоскуток кода). Услужливость Услужливые программы делают дополнительные шаги для того, чтобы облегчить жизнь пользователям (в особенности новичкам). Интересовало ли вас, когда я закончу ходить вокруг да около человеческого фактора? Если да, то можете перестать волноваться — это время пришло. Для примера, каждый диалог в вашей публичной программе должен содержать кнопку Help. Причем это касается не только «рабочих» диалогов, которые реально осуществляют ввод/вывод данных пользователя, но pi диагностических диалогов, которые «только лишь» показывают кусочек текста (не важно, является ли он кратким сообщением типа «операция закончена», или представляет собой что-то более серьезное, — например, весть об отсутствии нужного файла или ресурса). Я поражаюсь тому, как часто сама Windows или другие программы выбрасывают на экран диалоги, которые без сомнения тут же породят кучу вопросов в голове пользователя (и первым среди них наверняка будет: «Почему???»), но затем не предоставляют никаких дальнейших пояснений или помощи. Одним из самых дурацких примеров подобного рода является реакция Windows 3.51 на попытку запустить программу, у которой «ожидаемая версия Windows» указана как 4.0 («ожидаемая версия Windows» — это набор значительных pi незначительных кодов версии, который хранится в заголовке исполняемого файла и указывает Windows, какая версия операционной сис- 1емы требуется программе). Вместо того, чтобы посоветовать пользователю приобрести более новую версию Windows (как это делает Windows 95, хотя и в не слишком оптимальной формулировке), Windows NT открывает дртлог, у которого и заголовок, и текст выглядят одршаково — «Cannot run program» v«Невозможно запустить программу»). (Этот пример может и не показаться такой уж важной проблемой сейчас, но как только начнут появляться 32-разряд-
56 Философия разработки ПО лля Windows: макропроблемы ные программы с «ожидаемой версией Windows», равной 4.0, — а мы все знаем, что они в конце концов появятся — пользователи будут часто наблюдать это явление и спрашивать авторов незапускаемых программ, что за чертовщина происходит. Таким образом, загадочное диагностическое сообщение в Wind6\v$ NT становится проблемой прикладных программистов.) Даже когда программы дают.возможность обратиться за помощью, иногда это делается решительно не самым удачным способом. По моему мнению, PageMaker 5.0 заслуживает «Премии Секретного Рукопожатия» за его подход в предоставлении помощи Вместо того, чтобы сделать такую земную и сподручную вещь как кнопка Help, он требует от пользователя одновременного нажатия клавиши Shift и щелчка правой кнопкой мыши по фону диалога. Эта проблема касается не только пользовательского интерфейса. При проектировании библиотечных функций следует всегда держать в уме самые насущные вопросы и цели того, кто потом будет эти функции вызывать. Например, многие интерфейсы или функции берут набор параметров и возвращают значение, указывающее, был ли вызов успешно завершен. Если все было в порядке, то возвращаемые данные (если таковые имеются) помещаются в буфер, на который указывал один из параметров. Это — — простая, ясная и весьма полезная модель, которая позволяет вызывающему коду делать примерно такие вещи: if(SoineApiFunction(/* параметры */) { /* Вызов завершился успешно, обрабатываем выходные данные */ i else > /* Вызов потерпел провал, жалуемся пользователю, закрываемся и т д / Но встречаются исключения. Одно из них поистине сводит меня с ума: UINT GetDlgltedmlnt HWND hDlg, i \ mt nIDDlgltein, BOOL* lpTranslated, BOOL bSigned ), // дескриптор диалога // идентификатор элемента управления // указатель на переменную, которая должна получить // значение, индицирующее успех/неуспех // указывает, является ли значение знаковым/беззнаковым Этот интерфейс извлекает значение из контроля (элемента управления) диалога, такого как поле редактирования, и конвертирует его в беззнаковое целое. На мой взгляд, функция GetDlgltemlntO спроектирована «вверх тормашками», поскольку возвращает она сконвертированное значение, которое запросто может оказаться нулем и в случае успешного завершения, тем самым не позволяя вызывающему коду делать проверку в операторе if — в той форме, которую как правило используют Windows-программисты. Я бы предпочел видеть такой прототип этого API:
источника и три составные части качества программы 57 B00L GetDlgItedmInt( HWND hDlg, mt nIDDlgltem, UINT* lpValue, BOOL bSigned ) // дескриптор диалога // идентификатор элемента управления // указатель на переменную, которая должна получить // сконвертированное значение // указывает, является ли значение знаковым/беззнаковым г те в случае провала возвращалось бы значение FALSE, в случае успеха — TRUE, а буфер, указанный параметром lpValue, заполнялся бы целым числом, порученным от элемента диалога. Примеры с придирками: что такое «хороша», и что такое «плохо» Почти каждый компьютер с установленной Windows напичкан воплощениями как хорошего, так и плохого дизайна. Я думаю, что нижеприведенные примеры хорошо показывают, насколько тонка грань между разумным, услужливым программным обеспечением и программами, которые попросту бьют мимо цел pi. S Windows 95 умеет автоматически загружать программы с CD-ROM. Когда я впервые увидел это в действии, меня сразу осенила мысль: что это •.> — одна из тех идеи, про которые говорят «почему же никто до сих пор не догадался сделать это?». Если вы еще не знакомы с этим трюком и не понимаете, о чем я говорю, то знайте, что работает это так: вставьте CD-ROM в ваш компьютер, и Windows 95 автоматически U проверит корневой каталог этого диска на предмет наличия там чисто текстового файла с именем AUTORUN.INF. Если такой файл обнаружится, Windows 95 прочитает его и выполнит указанные в нем команды. Например, дистрибутивный CD-ROM с Windows95 содержит файл AUTORUN.INF, в котором имеются следующие строчки: [autorun] 0PEN=AUT0RUN\AUT0RUN EXE IC0N=AUT0RUN\WIN95CD ICO Нетрудно догадаться, что они приводят к запуску программы AUTORUN. EXE с использованием соответствующего значка. Этот трюк может показаться совершенно банальным и исполненным на достаточно примитивном техническом уровне, но именно это я и хочу подчеркнуть: разумные, услужливые функциональные возможности вовсе не обязаны быть достижениями передовой науки и требовать десятков тысяч строк сложнейшего кода. Если бы окно Проводника не дергалось, как эпилептик, когда вы вставляете диск, я бы поставил Microsoft пять с плюсом за то, как Windows 95 работает с CD-ROM.
58 Философия разработки ПО лля Windows: макропроблемы В Одна из наиболее примечательных вещей, которые вы обнаруживаете после перехода на Windows 95, — это две заставки, появляющиеся при закрытии (кстати, их растровые картинки хранятся в файлах LOG- OW.SYS и LOGOS.SYS в Windows-каталоге). А одна из самых распространенных проблем при работе с Windows 95 — выключение пользователями компьютера без предварительного закрытия Windows (или до того, как Windows успеет закончить процедуру закрытия). Конечно, эти две заставки не решают магическим способом данную проблему, но по крайней мере они элегантно и в то же время достаточно навязчиво (в хорошем смысле этого слова) поддерживают у KJ пользователей мысль о том, что перед щелканием по выключателю питания следует закрывать систему. Помня, сколько я видел компьютеров с файловыми системами, искалеченными преждевременными выключениями питания, я рад видеть хотя бы минимальную помощь в этой области. Попробуйте взять файл из America Online (или поместить файл туда), и понаблюдайте за тем, какую интересную вещь делает программа передачи файлов. Она показывает вам традиционную (читайте: обязательную) графическую «бензиновую шкалу», поясняя, сколько времени осталось до конца перекачки. В этом ничего удивительного пока нет. Но текст, который при этом используется, не представляет собой типичные супер-точные минуты и секунды, к которым мы привыкли в других коммуникационных пакетах. Вместо них программа округляет время. Например, она сообщает «осталось примерно 5 минут», затем надпись меняется на «осталось почти 5 минут», «осталось примерно 4 минуты» и т. д. Я предпочитаю именно такой подход, потому что, искренне говоря, мне не нужно точно знать, сколько минут и секунд будет длиться перекачка. Если она будет занимать больше одной-двух минут, я, вероятно, отвлекусь и займусь чем-нибудь другим, отличным от наблюдения за дурацкой «бензиновой шкалой». (Блиц-вопрос на засыпку: а меняются ли реально числа на шкале, если никто на нее не смотрит?) А если даже я и взгляну на нее по какой-то причине, то информации об округленном значении будет вполне достаточно. Путем показа приблизительных значений America Online придает интерфейсу *-» %j чуть-чуть менее искусственный, менее навязчивый оттенок, если угодно — создает ощущение человечности. В Недавно о *_t ф вершить неизбежный перезапуск системы для того, чтобы изменения конфигурации вошли в силу. В этот момент программы проявляли слабый проблеск разума: они проверяли, не осталась ли до сих пор дискета в дисководе, и если она там присутствовала, программы выдавали
источника и три составные части качества программы 59 9 сообщение, предлагавшее вынуть дискету. Это еще один пример того, как «совсем не передовая наука» оказывается превосходным дополнением к программе: она деликатно предупреждает типичную ошибку бходитс о В Панель задач (Taskbar) Windows 95 явилась новаторским решением проблемы, с которой, согласно предположениям Microsoft, сталкивались многие новые пользователи Windows — с потерей открытых окон. (А я знаю наверняка, что некоторые пользователи-новички действительно имеют проблемы в этой области.) Это была еще одна возможность Windows 95, которая поначалу казалась мне диковинной, но вскоре начала нравиться. Следует заметить, что одно из преимуществ Windows 95 — ее способность намного лучше манипулировать системными ресурсами — могло бы еще более усугубить проблему «потери окон»: если пользователям становится легче держать запущенными несколько программ одновременно, то даже новички будут делать это, что только увеличивает вероятность заблудиться в многочисленных открытых окнах. В то же время, на мой взгляд, Microsoft очень близко подошла к тому, чтобы панель задач оказалась неправильным изобретением: если бы они не предоставили опцию «Автоматически убирать с экрана» (Auto- hide), которая прячет панель задач, как только другая программа получает фокус, то присутствие панели задач на экране было бы слишком навязчивым. (Я не знаю, на каком этапе проектирования Windows 95 возникла идея опции «Автоматически убирать с экрана», но эта и идея служит ярким примером того, как можно найти хорошее, но еще недостаточно правильное решение, и как совсем небольшое изменение может-таки привести точно к цели.) Если бы они дали нам еще и такую опцию, при которой панель задач умела бы автоматически менять свои размеры (чтобы не менялись размеры кнопочек на ней)... Что ж, может быть, нас порадуют этим в следующей версии? в Заметили ли вы, что происходит в Windows 95, если вы пытаетесь что- нибудь напечатать, а ваш принтер (типа моего доисторического HP LaserJet Series II) еще не прогрелся? Windows 95 показывает диалог, сообщающий о том, что принтер не откликается. Изящная же деталь состоит в том, что если вы просто игнорируете этот диалог, или если вас нет в комнате (надеюсь, эти две ситуации неразличимы для компьютера, но никогда нельзя быть уверенным), Windows 95 продолжает пытаться послать данные принтеру. Когда принтер, наконец, откликается, Windows закрывает диалог и печатает без дальнейших хлопот. В Этот пример я обнаружил совершенно случайно: загрузите файл в Visual SlickEdit for Windows, затем загрузите этот же файл в другой
60 Философия разработки ПО лля Windows: макропроблемы редактор, измените файл и сохраните его на диск. А теперь переключите фокус на Visual SlickEdit — он сообщит вам, что файл изменился, и предложит перезагрузить его. Не знаю, как вы, но я имею склонность держать открытыми множество окон при работе в Windows, и вышеописанная возможность Visual SlickEdit исключает по крайней мере один, из путей, который мог бы привести к путанице и потере данных. В этом примере есть один нюанс, связанный с тем, что пользователь может забыть об изменениях, сделанных им в самом Visual SlickEdit Когда файл перезагрузится с диска, эти изменения будут потеряны (если конечно они не были повторены в другом редакторе). У Visual SlickEdit (как, впрочем, и у любой другой программы) нет хорошего способа обойти эту проблему. Вероятно, единственным жизнеспособным решением было бы проверять, изменял ли пользователь файл когда-нибудь в течение текущего сеанса редактирования (а не только со времени последней операции сохранения), и напоминать, что эта работа может быть потеряна при перезагрузке файла, если перед этим не сохранить этот файл на диске под другим именем. Проверьте правописание в документе при помощи Word for Windows 6.0, и он запомнит исправления, сделанные вами, на весь сеанс проверки (даже если вы не добавляете их во вспомогательный словарь). Например, если ваш документ содержит слово «WinWord», оно будет помечено как ошибочное. Замените его на «Word for Windows», и это значение будет предлагаться вам в качестве правильного при всех последующих появлениях «WinWord» на протяжении текущего сеанса проверки. (К сожалению, Word 6.0 уже не помнит эти исправления во время других сеансов проверки в текущем или других сеансах редактирования.) Проводник использует тот же самый подход для разных данных, что и в упомянутом выше примере с перекачкой файлов в America Online. Но, в отличие от первого примера, этот подход в исполнении Проводника только лишь расстраивает меня до невозможности. В данном случае округляются размеры файлов. Я не могу представить, что заставило Microsoft округлять размеры файлов до ближайшего килобайта, и при этом не давать пользователю даже опциональной возможности видеть точные размеры файлов. Каким бы ни было обоснование, я уверен, что категорически не согласен с таким решением. Когда я был бета-тестером Windows 95 pi увидел это впервые, я подумал, что опция просто погребена где-то в дебрях меню, и не смог ее найти. Потом я думал, что она появится позже. Но она так и не появилась, и это одна из причин, по которой я использую Проводник только для тестирования. (Я пытался еще исследовать эту проблему, но пока так и не нашел
и три составные части качества программы 61 решения. Если вы знаете о супер/секретном ключе в реестре Windows 95 или о другом механизме, управляющем этим поведением Проводника, пожалуйста, сообщите мне.) Q В версии 4.0 справочной системы Windows появилось несколько возможностей и функции, которые я всячески приветствую. Но без одной из них (по крайней мере, в ее нынешнем виде) я определенно мог бы прожить — без этого диалога с надписью «Загрузка перечня слов...» («Creating word list...») и авторучкой, заполняющей страницу за страницей в маленькой книжке. Обратите внимание на то, что эта функция нарушает сразу несколько правил, связанных с человеческим фактором: иногда это (очень) долгая операция; не предоставлена возможность ее отменить (ее л pi она есть, то я ее точно не смог найти; она должна была бы быть реализована в виде кнопки Cancel прямо под книжкой); и, наконец, не показывается ни индикатор прогресса, ни какая-либо другая информация, позволяющая оценить, сколь долго будет продолжаться этот процесс. Каким же раздражительным это показалось, когда я создавал перечень слов для 39 750 389-байтного файла AP132.HLP, пришедшего с пакетом Visual C++ 2.2, и наблюдал бестолковые каракули этой авторучки в течение 15 минут. (Когда я повторно проводил этот тест для книги, я велел программе WinHelp сделать полнофункциональный вариант перечня слов для API32.HLP на компьютере с Pentium/75 МГц и 24 Мб оперативной памяти. Процесс продолжался ровно 12 минут, после чего появился диалог с таким сообщением: «Please free memory by closing applications or by removin unnecessary files.» («Пожалуйста, высвободите память путем закрытия приложений или удаления ненужных файлов.»). В моей системе были 24 Мб ОЗУ и два винчестера, на которых было по 50 и 700 Мб свободного пространства, соответственно. И это двусмысленное сообщение заставило меня гадать, какого же именно ресурса мне не хватило (оперативной памяти или места на жестком диске), а также подумать, что кто-то в Microsoft обязательно должен потратить время pi сделать этот диалог более ясным.) н Моя самая «любимая» причуда Windows 95 (точнее, программы «Проводник») — это та глупость, с которой обрабатываются перетаскивания файлов левой кнопкой мыши. Перетащите левой кнопкой непрограммный файл, скажем FRED.TXT, из одного каталога в другой, и он будет перемещен, как и ожидалось Но попробуйте перетащить левой кнопкой исполняемый файл FRED.EXE, и Windows 95 возьмет на себя ответственность оставить исходный файл на месте, а в целевом каталоге создать ярлык (shortcut) для него. Совершенно ясно, что Microsoft сделала это для того, чтобы предохранить нас - бедных, глупых пользователей — от случайных
62 Философия разработки ПО лля Windows: макропроблемы перемещений туда-сюда исполняемых файлов и от нарушения файло вых ассоциаций. Я нахожу это поразительным, потому что был намно го лучший способ добиться той же цели и не раздражать при этом всех нас, старых работников Windows. Все, что им нужно было сделать,tэто перенести текущую функцию перетаскивания правой кнопкой (всплывающее меню с опциями Переместить, Копировать, Создать ярлык(и) (Move Here, Copy Here'и Create Shortcut(s) Here) на левую кнопку, на ту, с которой мы привыкли работать. А перетаскивание правой кнопкой вообще убрать из продукта. Затем им следовало бы дать нам «ключ гуру» для левокнопочной операции, чтобы можно было заставить ее работать так, как она работала в File Manager и в тысяче-и- одном его замещениях. И тогда Крутые Пользователи конфигурировали бы все и вся под себя, новички и нерегулярные пользователи были бы под защитой новой схемы работы, и никто из нас не замусоривал бы свой диск нежелательными ссылками. Мы продолжаем слышать о том, как объектно-ориентированные технологии и даже скромные динамические библиотеки в скором времени перенесут нас в волшебные сферы, где мы сможем настраивать для себя наши системы, использовать только те компоненты, которые нам действительно нужны, и т. д. и т. п. Не знаю как вы, а я не вижу достаточных оснований для этой утопии. Во многих случаях решение напрашивается само собой: хотите сохранить некоторое количество места на винчестере вашего ноутбука и в то же время никогда не используете в вашем текстовом процессоре компоненты для рисования и составления уравнений? Просто удалите внешние программы, которые реализуют эти возможности. Такие неуклюжие оптимизации почти никогда не документируются, даже когда они безопасны и, главное, дают желаемый результат. Именно поэтому я давал пользователям Stickiest возможность отключать некоторые вспомогательные функции («подсказку дня», историческую справку про сегодняшнее число, «цитату дня»), так чтобы базы данных, необходимые для этих функций, можно было удалить. И разумеется, все это было ясно отражено в справочном файле. (Некоторые более солидные прикладные программы при установке дают возможность при желании не инсталлировать какие-то свои компоненты или даже деинсталлировать их потом. К сожалению, немногие сегодняшние программы способны так же гибко помогать пользователю в распоряжении ресурсами компьютера.) / Пожалуйста, постарайтесь запомнить, что в программном обеспечение волшебство кроется за кулисами, а совсем не в пользовательском интерфейсе. В этом не так-то просто убедить, особенно в то время, когДа разработка для Windows буквально утопает в интерфейсных наворотах от Microsoft и бесчисленных поставщиков VBX (а скоро и OCX) He пой'
сять макроуровневых рекоменлаиий 63 мите меня превратно, я приветствую все эТи усовершенствования пользовательского интерфейса с раскрытыми объятиями и наслаждаюсь видом этого цветущего конкурентного рынка производителей, борющихся не на жизнь, а на смерть за наши доллары Не имеет значения, насколько KJ улучшают практическую ценность конкретной программы все эти древовидные списки, форматированные поля редактирования, мультипликационные заставки и украшения (хотя в некоторых случаях они и в самом деле весьма помогают). Все-таки именно стержень функционирования программы (или, если угодно, «реальное программирование») решает, станет ли она тем, с чем мы хотим жить и работать изо дня в день. Один из моих любимых авторов, Джойс Кэрол Оутс, однажды сказал: «Техника изложения приковывает внимание читателя и ведет его от одного предложения к другому, но только содержание останется в его мыслях». Я не смог бы выразиться лучше. й(ВШЮЯ1Ь Вв&Ибввмендащий Давите перейдем к конкретике. В этом разделе я представлю первые десять рекомендаций по написанию хороших Windows-программ. Мне могут достаточно резонно возразить, что все они на самом деле являются общими советами из области разработки программного обеспечения, и что они не имеют ничего специфичного для работы с Windows. В каком-то смысле это правда, но так же верно и то, что зыбучие пески мира Windows делают многие из этих рекомендаций чрезвычайно важными именно для Windows-разработчиков, порой по не таким уж очевидным причинам. Все рекомендации этой главы касаются того, что я называю макроуровнем; в главе 3 я представлю микроуровневые рекомендации. Это раздвоение не означает, что я считаю макропроблемы более важными. Скорее эта разбивка призвана отразить то, как эти. рекомендации затрагивают вашу работу. Макроуровневые вопросы больше касаются дизайна, проектирования и ориентированы на перспективу, а микроуровневые обычно вступают в игру, когда вы сидите в окопах, то есть когда кончики ваших пальцев касаются клавиш, 11 вы плывете вперед в час ночи с музыкой Томаса Долби в наушниках (и не всегда думаете о чем-то ином, кроме как о достижении следующей успешной компиляции).
64 Философия разработки ПО лля Windows: макропроблемы О. Имейте широкий взгляд на мир и планируйте на будущее Не [the poet] must write as the interpreter of the legislator of mankind, and consider himself \ being superior to time and place. if future Сэмюэль Джонсон Я искренне желаю, чтобы все программисты подобно поэту, персонажу строк Джонсона, как можно глубже осознавали свое место во времени и свои обязательства перед будущим. На самом деле, если бы у меня была волшебная палочка и возможность исполнить одно и только одно желание, я без колебаний переделал бы программистские головы так, чтобы исполнилось именно это. Сегодняшние программисты слишком интересуются тем, «как заставить это работать», и намного меньше, чем следовало бы, заботятся о создании того, что не будет кодом одноразового пользования, и что не придется выбрасывать с появлением новой версии операционной системы, инструментария разработки или tj самой программы. Я не упрекаю программистов в такой ситуации. Сегодня практически все профессиональные программисты находятся под сильнейшим давлением временных графиков, а это означает, что значительная: часть кода в публичных программах спроектирована и продумана не так хорошо, как следовало бы, просто потому что не хватает времени. Тот, кто изобрел поговорку «никогда нет времени сделать правильно, но всегда есть время переделать», наверняка был программистом сопровождения (maintenance programmer), в очередной раз исправлявшим чужие ошибки. Для Windows-программистов дела обстоят намного хуже. Мы тратим уйму времени лишь на попытки понять, как работают последние версии наших сред разработки, компиляторов, компоновщиков, ресурсных редакторов, динамических библиотек, сторонних библиотек и, наконец, самого Windows API. Поэтому чудом является уже то, что мы хоть что- нибудь написали, так что оставьте в покое заботы о будущей адаптивности нашего кода. И все же существует несколько приемов и правил, при помощи которых мы можем делать наш код более «дружественным к будущему», причем большинство из них требует очень малых временных затрат: В Всегда, всегда, всегда пытайтесь продумать все возможные ответвле- и иия ваших проектировочных решении pi удостоверьтесь, что вы понимаете (настолько полно, насколько позволяют ваши способности) все потери и выигрыши этих вариантов. Наилучший пример (который я могу найти в Windows 95) такого подхода — решение реализовать компоненты GDI и USER 16-разрядным кодом, а затем обрезать
рекоменлаиий (читайте: кромсать) значения графических координат и индексов в окнах-списках. (См. главу 14, в которой этот вопрос освещен более I ю дробно.) Очевидно, что вы не можете заглядывать далеко в будущее, а даже если бы могли, то программное обеспечение, спроектированное с учетом этого знания, зачастую оказывалось бы очень плохо согласованным с современными ему аппаратным обеспечением и другими программами. (См. врезки «Патологический случай: размер кластера в файловой системе FAT» иа стр. 69 и «Конец (компьютерного) света близок» иа стр. 75, в которых вы найдете размышления на тему прогнозирования.) Но существует целый ряд легко предсказуемых событий, которые вы дол лены (должны были) соответствующим образом оценивать и учитывать в своих планах: переход Windows на 32-разрядную платформу, появление новых стандартных возможностей C++ (которые так широко обсуждались в разнообразных журналах еще задолго до того, как была завершена работа над стандартом ANSI/ISO) и т п. На как можно более ранней стадии цикла разработки вашей публичной \Ут32-программы решите, является ли она самостоятельной независимой версией, результатом переноса из Win 16 или просто незначительно пересмотренной версией уже существующей 32-разрядной программы, какие из трех 32-разрядных платформ (Win32s, Windows 95 или Windows NT) вы будете официально поддерживать. Эту проблему ни в коем случае не следует решать с налету простым огульным заявлением типа «давайте будем работать везде», потому что существует большое число мелких (а также несколько крупных) различий, которые %j легко могут вынудить вас остерегаться одной или двух из этих платформ. Например, я настоятельно рекомендую не позволять вашей программе запускаться под Win32s. (См. главу 14, в которой вы найдете больше информации о проблемах с Win32s и о том, как с ними справляться.) Побеспо (Под локализации сразу, а не перед самым вы вашей программы, использующих при общении с пользователем различные языки.) Конечно, не все программы нуждаются в локализации, п даже не все публичные программы. По некоторые нуждаются. Поэтому вы окажетесь в намного лучшем положении, если направите серьезные усилия на то, чтобы как можно раньше обнаружить, что ваш проект потребует локализации. Это одна из тех проблем, которые могут превращаться в бесконечные споры между управляющими и технарями. Поэтому технари часто пугаются ее и избегают лишний раз обращать на нее внимание управляющих. Управляющие же решают
66 Философия разработки ПО лля Windows: макропроблемы эту проблему просто, без особых раздумий — планировать на будущее, заложить данную возможность сейчас, несмотря на то, что ближайшие планы не предусматривают ничего, кроме одного языка, и т. п. Тех- нари рассматривают такой вариант как потенциальную растрату усилий впустую, поэтому пытаются игнорировать вопрос. Иногда это правильно, а иногда нет. Все зависит от нескольких факторов — главным образом, от вероятности того, что нужно будет поддерживать другие языки, и от наличия достаточных ресурсов для выполнения необходимой работы б! Я часто вижу публичные программы, написанные вообще без каких- либо заготовок для очевидных будущих усовершенствований. Это означает, что, когда клиент, администрация или заказчики просят сделать неминуемое изменение (либо когда вы сами вдруг поймете, что это является хорошей идеей), вы обнаруживаете, что реализация этого изменения потребует огромной работы, включая порой выбрасывание и изменение значительной части старого кода. А это ведет к большему количеству ошибок и значительному увеличению накладных расходов на тестирование (ведь вы уже однажды тестировали старый код, чтобы убедиться в его правильности, а теперь вам приходится делать это снова после его изменения или замены). Иногда вы можете серьезно сэкономить ваши усилия по тестированию и кодированию путем применения одного из моих излюбленных 6у бесцеремонно называю «подвешивание d вы идентифицируете какую-либо новую функциональную возможность, которую вы добавите (или можете захотеть добавить) в будущей версии вашей программы. Быть может, вы хотите предоставить пользователю выбор между «длинными» и «короткими» меню, как делают некоторые Windows-программы; или вы захотите сделать опциональным создание страховочных файлов для сохраненных документов. Как только вы определили для себя вероятное будущее усовершенствование такого типа, сделайте следующее: 1. Заведите глобальную переменную, которая будет отражать параметры этой запланированной функции. В моей практике, эта переменная обычно оказывалась булевской, — вот откуда взялось мое название данного приема. Присвойте этой переменной то значение, которое потом (когда следующая версия позволит его менять) будет ее значением по умолчанию. 2. Убедитесь, что вы понятно задокументировали при помощи комментариев то, что сделали! Это один из тех редких случаев, Я t когда вы пишете код, который явно не является само
я х рекоменлаиий документированным, поэтому вы должны потратить минутку на пояснение ситуации. Вам не нужно писать «Войну и мир», просто вставьте строчку, гласящую что-то типа «Флажок UseShortMenus Эт будут о KJ О настраиваемой опцией». 3. «Пишите ваш код так, чтобы он вел себя по-разному в 4J *-» соответствии со значением этой переменной, пусть даже оно pi не О меняется в текущей версии программы. 4. Удостоверьтесь, что ваша программа правильно сохраняет и t f tj востанавливает значение этой переменной из того постоянного хранилища (например, INI-файла или файла специального формата), которое ваша программа использует для хранения пользовательских настроек между запусками программы. 5. Тестируйте вашу программу, поочередно задавая этой переменной все возможные допустимые значения, которые будут реально присваиваться ей потом. Как только вы убедитесь, что программа работает корректно, можно считать, что заготовка для будущей функции сделана. (Очевидно, при очень большом числе допустимых значений этой переменной не обязательно тестировать каждое из них. Вместо этого протестируйте несколько «краевых» значений, которые наиболее вероятно могли бы привести к возникновению проблем.) 6. Когда подойдет время новой версии, и вы решите сделать данную функцию настраиваемой, вся тяжелая работа окажется уже сделанной и проверенной. Вам останется лишь добавить \-9 О подходящий элемент управления в диалог с настройками и связать его с глобальной переменной. Старый код остается без изменения, значит ваши накладные расходы на тестирование относительно малы и состоят из проверки таких вещей, как правильная работа диалога и корректная реакция программы на изменение значения переменной во время сеанса. Вот вы и получили новую функциональную возможность ценой небольших усилий и очень незначительного риска ввести новые ошибки. использовал этот подход в программах, которые написал для заказчиков, а также несколько раз применял его в ранних версиях Stickiest . В одних случаях я сам хотел, чтобы программа делала что-то различными способами, но просто не был уверен, следует ли давать пользователю возможность настраивать это поведение. В других я предвидел будущие запросы клиента и обеспечивал для себя отсутствие проблем в тот момент, когда клиент позвонит мне и потребует еде-
68 Философия разработки ПО лля Windows: макропроблемы лать изменение как молено скорее. Во всех этих ситуациях я «подвешивал функцию на булевскую переменную», что давало мне (и моему клиенту) замечательную гибкость в решении вопроса о том, будет ли %} эта переменная поднята на поверхность в виде настраиваемой опцци В большинстве случаев я в конце концов делал такое изменение, и при этом необходимая работа была минимальной — в среднем такая «дополнительная разработка» занимала буквально не более 10 минут, поскольку касалась она фактически только диалога настроек. Этот прием может также снасти вас в случае опасности, когда вы вдруг обнаружите, что свежеиспеченная новая функция как-то неуловимо взаимодействует с системой пли порождает какой-нибудь таинственный сбой, который вы не заметили при тестировании. Вы можете % t К.Р просто вставить соответствующий переключатель в исходный текст ц перекомпилировать программу, заблокировав новую опцию. Или можно даже позволить пользователю заблокировать ее путем непосредственной правки файла, хранящего эту настройку. Никто из нас не любит делать исправления наспех, но в кризисной ситуации это может \j сэкономить вам денек-другои. В самом ли деле я призываю вас догадываться о том, какие функции вам придется реализовыватъ в будущем, и уже сейчас тратить на них дефицитные рабочие ресурсы? Не сошел ли я с ума? Да, я призываю вас делать это, pi нет, я не сошел с ума. Но я вовсе не предлагаю вам сидеть над листингом вашей программы и искать все умопостижимые будущие изменения, которые ваш босс или вы сами могли бы захотеть сделать, и тут же разукрашивать вашу программу поддержкой каждого из них до того, как оно понадобится. Это было бы абсурдным, безответственным растранжириванием вашего времени. По я в самом деле утверждаю, что случаются такие моменты, когда, работая над программой, разговаривая со своим клиентом, боссом или пользователем, вы вдруг безошибочно, буквально собственным • • • 9 нутром чувствуете, какой следующий запрос от них поступит Такое случается даже тогда, когда вы не ищете этих деталей специально Происходит это нечасто, но, по моим наблюдениям, когда такое чувство появляется, оно чаще всего оказывается правильным В Никогда не забывайте, что очень легко увлечься повышением гибкости ваших программ, а в результате можно лишь обременить пользователей. Например, у функции RegOueryValueExO из Win32 API третий параметр определен так: lpdwReserved Зарезервирован, должен быть равен NULL
макроуровневых рекоменлаиий 69 Зарезервированный, обязанный быть равным NULL указатель на DWORD? Это — абсурд, а людям, ответственным за такое, следовало бы надавать по рукам. Пожалуйста, не смешивайте этот пример с многочисленными местами в Windows API, где при описании сообщений говорится о ненужности параметра wParam (или lParam) и необходимости задавать его равным нулю. В этих случаях у Microsoft нет возможности убрать эти необязательные параметры, так как они вмурованы в базовую архитектуру Windows. И кроме того, как раз в этих случаях совершенно разумно требовать задавать их равными нулю: тем самым Microsoft получает максимальную свободу для будущего усиления API без риска нарушить работу существующих приложений. В этом примере мы видим, как очень незначительное требование интерфейса дает выигрыш обеим сторонам. Патологический сличай: размер кластера в файловой системе FAT Все файловые системы (или по крайней мере все те, о которых я знаю) предоставляют место на диске в виде «кусочков» (chunks), а это означает, что количество свободного пространства на диске, расходуемого на добавление нового файла, всегда кратно размеру такого кусочка. Создайте однобайтовый файл, и система предоставит намного больше одного байта (и даже еще больше, %j К.* если учесть дополнительный административный расход на поддержку еще одного элемента в каталоге). В файловой системе, использующей FAT (таблицу размещения файлов), «размер кусочка» также называют «размером кластера» Размер раздела Тип FAT Количество секторов в кластере Размер кластера О Мб - 15 Мб 16 Мб- 127 Мб 128 Мб - 255 Мб 256 Мб- 511 Мб 512 Мб - 1023 Мб 1024 Мб - 2048 Мб 12-битовая 16-битовая 16-битовая 16-битовая 16-битовая 16-битовая 4 2 4 8 16 32
70 Философия разработки ПО лля Windows: макропроблемы v^^^mm Иными словами, в файловой системе FAT на каждом файле теряется в среднем 16 Кб (половина от 32 Кб) свободного пространства, если общий размер раздела превышает 1 Гб, что соответствует самому обычному на сегодня размеру жесткого диска. Очень маленькие файлы, такие как заголовочные файлы (*.Н, *.НРР) или объектные модули (*.OBJ), с которыми постоянно имеют дело разработчики, приводят к потере почти всех 32 Кб. Давным-давно, на заре каменного века, когда эти ограничения были только что изваяны из... хмм... камня, сама мысль о наличии в персональном компьютере жесткого диска размером свыше одного гигабайта должна была казаться устремленной в такое далекое и туманное будущее, что о ней не стоило и задумываться. Ну а теперь уже поздновато начинать об этом думать. Первая серьезная проблема, с которой я столкнулся и которая была следствием этой ситуации с размером кластера, затронула моего бухгалтера, Чарли. Я только что помог ему улучшить парк его рабочих персональных компьютеров до более мощных, быстрых систем с жесткими дисками большей емкости. Мы детально проанализировали его потребности и совместно решили, какие скорости и емкости нужно обеспечить сразу, чтобы потом в течение года не нужно было делать дополнительных апгрейдов. Через несколько месяцев после того, как мы инсталлировали его сверкающее новенькое аппаратное обеспечение, он позвонил мне и пожаловался, что ему перестало хватать свободного места на дисках. Я был уверен, что причина должна быть очень простой (например, заполнение временных каталогов всяким «мусором», который можно стереть и высвободить сотню-другую мегабайт). Когда я исследовал систему, я не обнаружил никакого подобного тайника с ненужными файлами. Зато я обнаружил каталог, в котором находилось 3434 файла, — все они содержали какие-то данные по клиентам Чарли и были созданы программным пакетом, который он использовал. Средний размер этих файлов был равен почти ровно 500 байт. У Чарли был 540-мегабайтный жесткий диск, следовательно, на каждом из этих маленьких файлов терялось по 16 Кб - 500 байт =15 884 байт. В сумме же потери составляли 54 545 656 байт, или 96.9% от всего пространства, занимаемого ими. Это был бы неплохой бензин, как заметил кто-то и t^ «-> из свидетелей данной потрясающей картины. Пока я писал эту книгу, я добавил новый компьютер к своим владениям. Он прибыл с 850-мегабайтным винчестером и с обычным изобилием предварительно установленного программного обеспечения. В частности, на диске был каталог C:\MSIN'
сЯТЬ макроуровневых рекоменлаиий 71 PUT\MOUSE\EFFECT, содержавший 457 файлов с курсорами, каждый размером ровно 326 байт. Это значило, что я использовал 7 487 488 байт (457 х 16 Кб) для хранения 148 982 байт (457 х 326 байт) реальных данных, имея взамен великолепный 98- процентный бензин. (Я дам вам 457 попыток угадать, какие именно файлы я первыми упаковал в ZIP-архив на этой системе.) Суть здесь не в том, чтобы побранить первоначальный дизайн файловой системы FAT за отсутствие в нем перспективного планирования (хотя эта ситуация служит интересной поучительной повестью о планировании). Но обратите внимание, что Windows 95 не дает нам радикального решения этой проблемы. Я ожидал, что она все-таки обеспечит (хотя бы в виде дополнительно оплачиваемой опции) какой-нибудь инсталлируемый компонент, который позволил бы использовать файловую систему Windows NT (NTFS), гораздо лучше управляющую свободным пространством на диске. К сожалению, такой опции нет, хотя я подозреваю, что \j t_> тот или иной предприимчивый производитель в конце концов возьмется сделать это. В любом случае, Windows 95 выходит на рынок во времена удивительно дешевых жестких дисков (о чем Microsoft, конечно же, была осведомлена), pi свободное место на них все еще является системным ресурсом, с которым Windows 95 не может управляться сколько-нибудь разумным способом. Кстати, менее чем за две недели до официального выхода Windows 95, я приобрел жесткий диск размером 1.2 Гб в моем местном компьютерном магазине за какие-то 298 долларов (что было — невероятно! — дешевле 25 центов за мегабайт, pi что без сомненрш будет казаться немыслимо дорогим вскоре после выхода этой книги). Вышеупомянутые мной файлы с курсорами заняли бы на этом дрюке ужасающе много — 14 974 976 байт (рыи почти в сто раз больше своего реального содержания). Если пример с маленькими курсорными файлами кажется вам слишком искусственным, позвольте мне рассказать, что «-> и случилось, когда я установил на этот новьш др1ск реальный продукт. Я инсталлировал компилятор C++, но не весь пакет целиком, а только инструменты, заголовочные файлы и библиотеки. Я не инсталлировал примеры (которые, как известно, очень велики по объему и включают сотни очень маленьких файлов) и исходные тексты библиотек. Результатом были 1767 файлов с суммарным объемом данных 89 759 861 байт, занимающих 146 538 496 байт дискового пространства, из которых терялись 56 778 635 байт, или около 39 процентов. Это на самом деле весьма милосердный пример, потому что компиляторы C++, инсталлируемые
72 Философия разработки ПО лля Windows: макропроблемы таким образом, включают некоторые файлы очень большого размера, которые уменьшают средний эффект потерь свободного места на диске из-за размеров кластера. Тем не менее, если экстраполировать эту оптимистичную процентовку на весь диск, я потерял бы 468 Мб дискового пространства (или 116 долларов). Как же нам обойти эту проблему? Одно из решений - использовать компрессию диска, которая размещает дисковое пространство по своей собственной схеме и тем самым позволяет избежать катастрофы с кластерами. Но это решение совершенно не годится для людей типа меня, которые вынуждены делить дисковое пространство одной машины между Windows 3.1, Windows 95, Windows NT и OS/2. He существует общего способа компрессии диска для 16- и 32-разрядных Windows; не говоря уж о чужеродной диковине иод названием OS/2. Историческая справка: у меня сохранилась копия журнала PC (не путать с PC Magazine) — выпуск 1 Номер 1, Февраль-Март 1982 года, - на странице 22 которого есть реклама от компании под названием VR Data. Реклама предлагает несколько жестких дисков и в том числе экземпляр емкостью 6.3 Мб, который поставляется покупателю в корпусе размером с системный блок современного настольного компьютера. Цена указана пустяковая — какие-то символические 2895 долларов (459.52 за мегабайт, кхе... кхе...), не считая еще 249 долларов за адаптер и программу-драйвер. Представьте, что сказали бы бравые парни из VR Data, если бы они в 1982 году увидели мой винч емкостью 1.2 Гб и габаритами 3.5 дюйма на 1 дюйм. 7. Стремитесь h балансу во всех решениях Then at the balance let's be mute, We never can adjust it; What's done we partly may compute, But know not what's resisted. Роберт Борис В этой книге я неоднократно буду возвращаться к одной и той же мысли, которую все мы забываем время от времени: в программировании почти не бывает абсолютов. Практически каждое решение, которое вы принимаете — будь то выбор языка программирования и среды разработки для нового проекта или типа локальной переменной в функции — имеет свои компромиссы. Иногда эти компромиссы могут быть очень незначительными, и почти всегда у них длинные руки - в том смысле, что они могут начать преследовать вас не сразу, а
лесять макроуровневых рекоменлаиий 73 9 * lUIb спустя удивительно долгий промежуток времени, (см. врезку «Конец (компьютерного) света близок» на стр. 75). Один способ избежать проблем — это просто помнить, что компромиссы встречаются повсюду, и что почти все ваши решения имеют две стороны. Никогда не следует ставить себе задачу-максимум — заставить что-то работать настолько хорошо, насколько это возможно; фанатичная упертость в этом приво- •пп к бедствию так же верно, как пренебрежение качеством. Вероятно, вы счышали старый афоризм «лучшее — враг хорошего». Он чрезвычайно актуа- ieH с нашей области, потому что программисты очень часто стремятся к совершенству, не останавливаются на достаточном, и дело заканчивается - выпуском никому не нужного кода. Но этот афоризм — палка о гах, и программисты так же часто будут использовать его в качестве печально оправдания и заявлять, что, раз уж совершенство недостижимо, то нет смысла делать те или иные предложенные изменения для улучшения качества программы. Вы должны быть экономистом. (Бьюсь об заклад, вы никогда не собирались им стать.) Чтобы не попасть в ловушку экстремизма, вы должны всегда думать о соотношении «затраты/выгода». Например, программисты часто говорят, что нет смысла добавлять в программу минимальные защитные функции, поскольку они могут быть легко взломаны тем, кто понимает, как такая защита работает. Но эти же самые люди наверняка закрывают на замок двери своих автомобилей, паркуясь около магазина, даже если дело происходит днем, и отходят они лишь на пару минут. И делают они это (как и я), потому что видят пользу даже в таком примитивном способе защиты: она О К.9 предохраняет по крайней мере от части воров, хотя для опытного угонщика взлом такой защиты - дело нескольких секунд. Иными словами, запирание автомобиля в таких случаях осмысленно, так как это обеспечивает хотя бы какую-то безопасность при практически нулевых затратах. Соотношение здесь достаточно хорошее, оттого мы это и делаем. Когда я говорю об оценке «затрат» па то или иное проектировочное решение, я имею в виду не только время, необходимое для его выполнения, или влияние его па размер и скорость работы программы. Я использую этот термин в максимально широком смысле и включаю в рассмотрение такие вещи, как перспективное сопровождение кода, практичность результирующей программы, общая «дружественность программы но отношению к системе» vнапример, насколько просто инсталлировать пли деинсталлировать ее, требует ли это изменений в пользовательском файле SYSTEM.INI п тому подобное), конечно, вопрос о сбалансированности затрат пользователя и ваших собственных затрат - то лее немаловажная проблема, обсуждение которой может Ыстро превратиться в самостоятельную бесконечную цепочку философских еноров. Выступая как пользователь многих коммерческих программ, я твердо
74 Философия разработки ПО лля Windows: макропроблемы уверен, что программисты, делавшие эти программы, уделяют своим пользова- телям гораздо меньше внимания, чем следовало бы. В своей классической работе The Mythical Man-Month, Фредерик Брукс говорит: «Поскольку целью является легкость использования, окончательной оценкой системного дизайна является отношение функциональности к концептуальной сложности. Ни функциональность, ни простота сами по себе не обеспечивают хорошего дизайна». Он не называет это «соотношением затраты/ выгода», но, уверен, имеет в виду ту же самую коцепцию. И, я думаю, он абсолютно нрав. Однако также я полагаю, что программистам следует иметь намного более широкий и содержательный взгляд на обе части уравнения, и в особенности — на «затратную» сторону. В частности, когда вы оцениваете стоимость рассматриваемого замысла или проекта, не забудьте включить в эту оценку следующее: В Полная стоимость разработки, включая документацию. В нашем деле никто не любит задумываться о документации (по меньшей мере, таково мое безошибочное впечатление от того, насколько отвратительной и до ужаса неточной является большая часть виденной мною документации). Но она является важной частью целого, даже если вы создаете «всего лишь» библиотеку функций для домашнего пользования парой-тройкой других людей, не говоря уж о публичной программе, которая будет продана миллионам пользователей. В Стоимость сопровождения и поддержки. Опять же, мыслите широко и помните, что исходный код, который вы пишете для публичной программы, может иметь очень долгое время жизни. Если вы создаете вашу программу при помощи какого-либо необычного языка или экзотической технологии, в то время как другие люди (включая вас самого по прошествии нескольких месяцев) должны эту программу сопровождать, вы, вероятно, нарветесь на серьезнейшие перспективные затраты, которые сильно перевесят весь выигрыш. Будьте осторожны с применением возможностей операционной системы. Не перегружайте вашу программу такими сложными деталями, как поддержка OLE, если они на самом деле не нужны. (В наши дни существует тенденция, которую я называю «инфляцией галочки», и которой подвержено множество коммерческих программ. С целью зарабатывания как можно большего числа «галочек» в итоговых обзорных таблицах журнальных статей, производители программного обеспечения загромождают свои творения такими функциональными возможностями, которые пользователям в действительности не нужны В результате получаются огромные программы, наполненные почти никем не используемыми функциями, да еще потребляющие массу ресурсов при разработке и поддержке. Если вам кажется, что я шучу
ь /мзкроуровневых рекоменлаиий 75 или преувеличиваю, поговорите с пользователями многочисленных KJ О текстовых процессоров и узнайте, как много мудреных опции они отключают или игнорируют.) Ц Стоимость потенциальных усовершенствований. Если вы собираетесь сэкономить время разработки путем использования какого-либо ' третьестороннего продукта, такого как библиотека функций или классов, задумайтесь над тем, каковы шансы на получение хорошей под- 1_J U KJ держки от этой третьей стороны, и с какой вероятностью эта третья сторона все еще будет заниматься этим продуктом через пару-тройку лет. Что, если вы решите в следующей версии перенести вашу программу на платформу OS/2, а у этого третьестороннего продукта нет соответствующей версии? Третьесторонний пакет может значительно по- *j мочь вам с выпуском в срок текущей версии, но создать вам огромные дорогостоящие проблемы в будущем. Конечно, во многих проектах такая проблема не стоит, но вам следует как можно раньше выяснить, является ли ваша конкретная задача одним из них. В Стоимость для О о работы, объем потреб ребг В сущности, проблема сбалансированности сводится к вопросу: «Выгодна ли сделка X для всех участников?» К сожалению, многие публичные программы изначально проектируются абсолютно без учета этого вопроса. Например, менеджеры часто проталкивают свои любимые проекты или технологии скорее в угоду своим «имперским интересам» (увеличению размера своего штата и усилению своего влияния в компании), чем в стремлении выпустить наилучший конечный продукт. У нас, программистов, в этом смысле тоже рыльце в пушку, поскольку мы часто позволяем себе использовать инструменты и приемы, которые применять вовсе не следовало бы, и в результате наносим серьезный ущерб конечному продукту. И на этом фронте я могу дать вам лишь один совет: защищайтесь от этих вредных тенденций внутри себя самого, внутри вашей ко- анды и будьте готовы к тому, что борьба не будет легкой. м Конец (компьютерного) света близок Я уверен, все вы уже слышали, что огромное число деловых прикладных программ в мире сойдут с ума в 2000 году. Проблема, как известно, в том, что эти программы хранят информацию о календарном годе в виде только лишь двух цифр. И когда они начнут делать бухгалтерские подсчеты или что-нибудь еще для января 2000 года, нулевое значение календарного года вызовет жут- Я t кип хаос.
76 Философия разработки ПО лля Windows: макропроблемы Насколько я могу судить, это как раз тот единственный случай, когда многочисленные пророки Страшного Суда могут оказаться правы. Общеизвестно, что сегодня многие компании используют довольно старое программное обеспечение, причем во многих случаях они даже не имеют его исходных кодов, а следовательно они не смогли бы устранить проблему, даже если были бы осведомлены о ней. Кроме того, во многих случаях базы данных и другие программы настолько пропитаны этими двух- цпфровымп значениями года, что починка окажется страшно дорогой и наверняка приведет к появлению новых ошибок. Так что в течение первых месяцев нового тысячелетня вы смело можете ожидать целую волну репортажей CNN о том, как кто-то оплачивает (а кто-то получает) чеки на миллионы до л ларов. Помимо предстоящего развлечения для CNN, я предвижу как минимум два-три серьезнейших скандала, порожденных этой проблемой (например, чересчур агрессивно настроенное программное обеспечение по ведению счетов в каком-либо банке начнет самостоятельно закрывать счета, или еще хуже, вплоть до появления человеческих жертв). Я искренне надеюсь ошибиться в своих предсказаниях, но пока оптимизма мало. Как профессионал, я нахожу эту ситуацию весьма позорной, она очень ярко демонстрирует насколько близорукими могут быть программисты. Справедливости ради следует отметить, что у многих авторов таких программ просто не было выбора — например, они могли писать новую программу, которая должна работать с существующей уже базой данных заказчика, и им просто пришлось работать с имеющимся форматом независимо от того, насколько очевидным был для них тот факт, что они лишь усугубляют этим грядущую катстрофу. Эта история является настолько поучительной для программистов, настолько ярко доказывает важность предусмотрительности, что я не хочу больше смаковать этот пример. Только, пожалуйста, будьте осторожны сами.
\есять макроуровневых рекоменлаиий 77 9 2- Сделайте принцип KISS вашей религией Give те a look, give me a face, That makes simplicity a grace; Robes loosely (lowing, hair as free: Such sweet neglect more taketh me, Titan all the adulteries of art; They strike mine eyes, but not my heart. Бен Джонсон Our life is jrittered away by detail... Simplify, simplify. Ген pi i Д^впд Торо Ax этот принцип KISS1! Если и есть в мире что-то такое, что почти все программисты постоянно повторяют наизусть, как мантры, но при этом откровенно игнорируют, то это - принцип KISS. Иногда я даже думаю, что следовало бы к каждому программистскому компьютеру прицепить бирку с лозунгом: «KISS - это не просто хорошая идея, это закон». (Правда, практический эффект от этого вероятно был бы нулевым. Ведь программисты, эти закопченные комики, обязательно стали бы использовать эти бирки в качестве скребков для льда, дверных стопоров или подкладок для выравнивания стола). Позвольте пояснить: в данном контексте под словом «проще» следует понимать то, насколько понятным и годным для сопровождения является код. По какой-то странной причине (которую я хотел бы в конце-концов найти), многие программисты неправильно интерпретируют этот принцип как «используй как можно меньше строк при написании кода». А потом растрачивают драгоценные программистские силы па дурацкие конкурсы (типа «Эй, я могу реализовать эту функцию 47-ю ударами по клавишам! Слабо побить этот рекорд?»). Я не ноблю различных обобщений по половому признаку, но мой опыт показывает, что данное явление почти без исключений свойственно мужчинам. Еще более юразптелен тот факт, что большинство мужчин отрицает свое пристрастие к этому, а большинство женщин немало удивляется, зачем же это отрицать. Нигде эта проблема не стоит так остро, как в С-программах. Некоторые фрагменты рабочего С-кода, по которым я пробегал (или они пробегали по мне?), выглядят так, как будто бы их писали под влиянием недавно ирочитан- i ноп книжки по развлекательной химии. Я не спорю со всей идиоматикой С; х^рактерная минималистская грация хорошо написанного С-кода может не KISS — ла аббревиатура, широко распространенная в профессиональном жаргоне аш- 1с)язычных программистов, расшифровываемся как «Keep II Simple, Stupid» («Будь проще, дурачок») И одновременно участвует в лплоя.шчпоп игре слов («kiss» - «поцелуй»). Выдули ь руеекоя шчпын .-эквивалент этого «принципа» кажется невозможным (и ненужным). IПримечание переводчика ]
78 Философия разработки ПО лля Windows: макропроблемы только быть эстетически приятной, но и способствовать его пониманию. Однако я категорически протестую против таких бессмысленных конструкций, как условный оператор. Загляните в ассемблер, сгенерированный следующим оператором: х = а > 100 ? у z, и скорее всего вы обнаружите, что точно такой же ассемблер был бы сгенерирован, если бы вы написали if(a > 100) х = у, else х = z, н кими компиляторами и обнаружил, что условный оператор действительно порождает тот же самый код, что и более земная конструкция if-then-else, а в некоторых случаях — даже немного больший по размеру код. Я честно пытался найти пример, в котором условный оператор оказался бы эффективнее, но найти его так и не смог. Другими словами, мы имеем явные издержки при использовании этого оператора — более большую длину строки, (Я даже был вставками условного оператора где >. только можно - в интересах производительности, результатом чего явилось еще меньшее быстродействие программы. Это замечательный пример так называемой «ручной слоптимизации»1. ) Кстати, многие программисты попадают в плен иллюзии того, что меньшее количество знаков или строк кода в итоге превращается в меньшее количество процессорных команд. Это опасное заблу- кдение, наоборот, очень часто приводит к проблемам с производительностью. Далее когда вы не используете «сишные навороты» и пишете по-настоящему хороший код, вы все равно можете непроизвольно нарушить принцип KISS. Например, в такой ситуации, когда задача лучше всего решается каким- нибудь действительно сложным, замысловатым алгоритмом. Что ж, выход здесь безумно прост — используйте этот алгоритм, но обязательно добавьте несколько комментариев о том, что данный код делает. Я знаю, как банален и очевиден этот совет, но я все-равно упоминаю его, потому что я до сих пор продолжаю встречать код, где автор либо не думал над этим вообще, либо был слишком занят, «заставляя этот код работать» (ох, опять эта фраза!). Конечно же, стремление к упрощению I. Например, вспомните мои трюк с « функции на б\ 1 Слоппшмпзацпя — от английского, чисто профаммерского жаргонизма «sloptimization» («slop» — помои, слякоть), означающего процесс, обратный оптимизации [Примечание переводчика ]
ять макроуровневых рекоменлаиий 79 9 тю переменную», описанный ранее. С точки зрения ближайшей задачи, этот нем явно нарушает принцип KISS, поскольку реализация текущей версии называется более сложной, а ее тестирование — более долгим, чем это необ- олимо. Но с точки зрения перспективы его использование весьма осмысленно, оскольку получаемый в конечном итоге выигрыш значительно перевешивает небольшие дополнительные затраты. 3. You(Now) != You(Later) Time present and time past Are both perhaps present in time future, And time future contained in time past. Г С Элиот Заголовок данной рекомендации — это в действительности только квинтес- сенция (записанная в стиле языка С) наблюдения о том, что всем профессиональным программистам приходится работать над множеством проектов, и что во время этой работы мы меняемся. Наша программистская философия и стиль меняются, наши взгляды на многие вещи меняются, и мы медленно превращаемся буквально в других людей. Этот очевидный факт имеет некоторые нетривиальные последствия для программистов. Наиболее важное из них заключается в том, что даже когда вы являетесь единственным человеком, который когда-либо увидит исходный код данной программы (а зачастую даже это суждение ошибочно, если говорить о перспективе), через достаточный У промежуток времени данный проект станет коллективным. Обычно програми- стский коллектив (команда) составляет от нескольких до нескольких сотен людей, параллельно работающих над проектом, то есть команда прежде всего разбросана в пространстве. Команда также может быть разбросана и во времени — например, вы сегодня (you(now)) плюс кто-то другой (или you(later)), работающий над тем же проектом, спустя месяцы или годы. Обратите внимание, что распределение во времени может быть более серьезной проблемой, чем распределение в пространстве — просто потому, что не может быть преодолено таким простым способом: посетить офис коллеги, швырнуть на столь листинг pi спросить, что, черт побери, этот код делает. (Кстати, это бьгл бы неплохой сюжет для фантастического ужастика — путешествия сопровождающего программиста во времени, но я бы не хотел особо развивать эту тему, если вы не возражаете.) И даже на мгновение не смейте подумать, что речь тут идет о годах. Даже пара месяцев работы над другим проектом могут сделать вас аутсайдером вашего собственного кода. Программисты особенно подвержены этой проблеме, поскольку мы склонны хорошо выполнять лишь следующий простой алгоритм: загрузить весь замысел в кратковременную память, замариноваться в нем, реализовать его, а затем выгрузить его и приступить к следующей задаче. В
80 Философия разработки ПО лля Windows: макропроблемы течение девяти лет моей работы в IBM, я принимал участие в разнообразных ц многочисленных проектах (в качестве программиста, дизайнера, тестера) и несколько раз попадал в такую ситуацию. Когда кому-нибудь из моих коллег приходилось искать ошибку или просто делать изменения в коде, написанном *J мной за несколько месяцев пли лет до этого, и когда этот человек приходил ко мне с вопросами, я точно помню, как я порой едва мог понять свой собственный код и мучительно вспоминал, зачем я делал ту или иную вещь таким способом В действительности, мы сейчас говорим об одном из самых опасных мифов программирования: «Я наверняка пойму, что делает этот код, когда увижу его в следующий раз». Если «следующий раз» случится лишь несколько недель спустя, то да, программист вероятно поймет свой код без особых трудностей Но если пройдет шесть месяцев или больше, в течение которых голова программиста будет занята другим проектом со всеми его деталями и проблемами, то я уже не так уверен. Тем не менее, этот миф часто используется программистами tj в качестве оправдания своего нежелания тщательно документировать свои код Явление «you(now) != you (later)» - очень опасная, скользкая тропинка, прямиком ведущая к проблемам с качеством и эффективностью. Когда кому- нибудь (не исключая вас самого) придется заглядывать в ваш «старый» код, часто это будет делаться для исправления какой-либо ошибки или для внесения каких-либо авральных изменений. И происходить это будет, скорее всего, под сильнейшим давлением плановых сроков. Вы прекрасно знаете, во что это обычно выливается: беглый просмотр листинга, мучительная попытка в течении пяти минут расшифровать, что же этот код «делает иа самом деле», несколько строк нового кода, написанные впопыхах и без учета недокументированных особенностей проекта, и, если программа недостаточно протестирована, ошибка благополучно становится головной болью ваших пользователей. И даже если финальный тест обнаружит ошибку раньше, чем программа [[опадет к пользователям, все начнется сначала - код снова окажется у вас в руках, и вам придется исправлять ошибку, вероятно, под еще более жестоким прессингом временных рамок. Решением всех этих проблем, разумеется, является документирование кода. Без сомнения, вы воочию знакомы с такой практикой, как ведение подробных «мемуаров» обо всех изменениях, сделанных в модуле (в виде специально оформленных комментариев, помещаемых в начале исходных текстов). Необходимо ко всем программистам предъявлять требование следовать t » t * этой полезной практике, поскольку она является косвенным признанием простого и важного факта: всякий код имеет свое «время жизни» и в течение этого времени может кардинально меняться. Это особенно справедливо в тех случаях, когда над одним и тем же кодом работают несколько программистов, или когда значительные изменения касаются интерфейса, обеспечиваемого этим кодом.
сять макроуровневых рекоменлаиий 81 9 Очень валено документировать не только то, что в коде есть, но и то, чего > нем пет. Почти никто не удаляет из «истории изменений» те записи, которые • ) временем становятся неактуальными (и удалять их, кстати, категорически не следует ) }юрм оторые были сделаны и почти сразу л<:е были отменены но каким-либо суще- ■ ценным причинам. А ведь именно эта сторона использования комментариев ,&пт ОСОбс и W нам очень часто приходится перебирать массу вариантов и приемов до тех пор, пока не олшебпое заклинание ( ), которое сделает то, что мы хотели. Еще бо сов и их параметре» гакие комментарии могут принести в тот момент, когда вы спустя какое-то время вновь обратитесь к коду с целью «вычистить» его. Когда вы будете искать лазейки для хотя бы минимальной оптимпзаци «старого кода», вы запросто можете сделать такую вещь, как преждевременное освобождение памяти ияи ресурса, если рядом не окажется предусмотрительного комментария, предохраняющего от этого поступка. Оптимизация такого сорта чрезвычайно коварная вещь: бегло посмотрев па код опытным взглядом, заметив места, потенциально улучшающие производительность пли алгоритм, но не заметив никаких предостерегающих комментариев, программист смело приступает к изменениям (читайте: к неизбежному внесению новых ошибок). Например, в своем коде я постоянно расставляю краткие комментарии такого вида: «Следующая строка должна находиться именно здесь, а не в конструкторе, гак как нам нужно иметь правильное значение параметра hWnd для вызова данной функции» или: «Нельзя использовать функцию FredExO, поскольку в данной ситуации она не работает правильно. Поотохму мы идем в обход и добиваемся тон лее цели при помощи функции FredO». При непосредственном написании процедур, вы должны взять на себя труд по документированию всех неявных и нспараметрических зависимостей и связей (например, «главное окно программы не свернуто» или «передаваемая строка имеет формат, в котором функция GetOpenFileNameO возвращает список имен файлов, выбранных пользователем»). Еще более важным является документирование проверки ошибок функцией (а также - должное внимание к У}ке имеющейся документации такого тина). Бросив взгляд на функцию, легко сказать: «Ерунда! Зачем напрягаться и проверять указатель на равенство '\'ULL? Этого не должно произойти». По если документация гласит, что функ- 111151 отказывается работать при вызове с нулевым указателем, то крайне риско- Иапдо .менять такое ее поведение (особенно в большой программе или в том е*1Учае, когда функция входит в состав библиотеки, используемой большим \ ** g Отчеством людей - то есть когда с большой вероятностью работа другого КоАа зависит от ее реакции па переданный нулевой указатель). Например, в главе 3 на стр. 130 я упоминаю функцию FileExistsO, которая '(М1>ращает булевское значение, указывающее, существует ли файл с заданным
82 Философия разработки ПО лля Windows: макропроблвМк именем. Эта функция проверяет переданный ей указатель имени файла ц. равенство NULL и, кроме того, она проверяет, не передано ли ей имя нулевой длины; в обоих случаях она возвращает FALSE. Далее, эта функция используется в моей «оберточной библиотеке» Win32u — вызывается функциями названными GetShortPathNameO и uGetShortPathNameO, которые, в свок, очередь, уже не заботятся о правильности указателя, передаваемого функции FileExistsO, полагаясь на то, что все необходимые проверки она сделает сама Удалите проверку параметра из FileExistsO, и это изменение тут же пагубно отразится на работе uGetShortPathNameO. Именно поэтому я считаю доку- ментирование макро-, а не микровопросом: правильное или неправильное ис- может решать или создавать пробл ходящие и Позвольте мне преподнести вам еще Н " ; ^которое время назад мы с компаньоном сделали заказной программный продукт для одного клиента. По прошествии одного года с того момента, как мы трогали тот код в последний раз, клиент позвонил нам и попросил сделать небольшое изменение в программе. Это изменение было несложным, но после бы особенностей Но в главном ф примерно 100 строчек с подробными комментариями об истории изменений кода, наших пробах и ошибках, конечной версии, а также о паре «функций, подвешенных на булевск) В результате нам потребов гбочее в памяти, а затем еще пара часов на реализацию и проверку затребованной заказчиком новой функциональной возможности. Нам удалось решить задачу без введения новых ошибок в программу, а сэкономленное при этом время, определенно, значительно превышало то время, которое я год назад потратил на документирование. Еще один из интересных программистских мифов, который я постоянно слышу, это утверждение о том, что тот или иной фрагмент кода является «самодокументированным». Существует ли в природе самодокументированный код? Конечно, да! Я был бы последним из тех, кто посоветовал бы вам комментировать каждую строчку кода. Проблема не в этом. (На самом деле, в написанном мной коде полно подпрограмм, в которых нет ни строчки комментариев, за исключением простого общего описания назначения функции п ее параметров. В этих случаях я не вставлял комментарии потому, что они не были нужны. Разумеется, в большинстве своем это были несложные функции — типа функций поиска по таблице, например - содержащие дюжину-другую строк кода.) Проблема заключается в том, что этот миф (как и многие другие программистские мифы, независимо от степени их правдоподобности) слишком уж часто используется программистами во вред — оправ-
десять. макроуровневых рекоменлаиии 83 9 и с »<>рт увиливание от выполнения необходимой работы. (Я знаю, знаю — рерное, я уже начинаю звучать так же, как Уилфорд Бримли в своей рекламе 'сяных хлопьев!1 Но я говорю истинную правду.) Зачастую дело вообще доли ДО абсурда' «Почему я должен комментировать это? Разве вы не можете опять, что этот код делает? Лично я прекрасно понимаю». И даже когда Формулировка не так вульгарна (а я слышал много вариаций высказывания )ТОЙ позиции — от прямых и резких до сильно завуалированных), программист тем самым дает понять, что отсутствие достаточного числа комментариев \J сВоем коде он считает чуть ли не величайшим своим достоинством, показателем своих умственных способностей и энергичности. Другая проблема, являющаяся по сути перевертышем предыдущей, KJ заключается в том, что некоторые программисты находятся во власти такой причудливой идеи: если по какой-либо причине (читайте: но капризу программиста) приходится использовать ужасно плохой стиль и приемы кодирования, то такой код будет приемлемым, если снабдить его подробнейшими комментариями. Конечно же, такая идея неправильна и вредна. Голова идет кругом от того количества бессмыслиц, которые я наблюдал в рабочем коде многих программ. И я могу дать вам лишь один совет — не поддавайтесь соблазну следовать всем этим дурным примерам, и старайтесь предостерегать от этого ваших коллег и сотрудников. К сожалению, пока еще не изобрели ни инструментов, ни технологий, которые могли бы помочь вам в этом вопросе. Рассчитывайте только на свои собственные силы — на свою старательность и свои убеждения. Самое важное, всегда помните о том, что тем человеком, будущей работе которого вы на самом деле помогаете, может ока- ДутСс Великий Том и таинственный цикл Однажды, во время моей работы с операционной системой VM в IBM, мне пришлось адаптировать и встраивать в систему ассемблерный код, ранее написанный кем-то из исследовательской лаборатории. Код был очень хорошо написан, но весьма скупо задокументирован. В целом, его задачей было создание совместно используемого сегмента памяти (грубо говоря, аналога комбинации DLL и проецируемого в память файла в Windows). В какой-то момент я наткнулся на небольшой, буквально в несколько строчек, фрагмент кода, который показался гюдозрнтель- 1 11 'форд Брим.ш - американским актер, просдавпвппшея на вею страну благодаря ноючпеленпым рекламным роликам, в которых он с добродушным видом, по достаточно 1 ,(>ил11во, увещевал и убеждал .зрителей, как полезно сечь овсяные хлопья па завтрак Но, в •11'мпс, скажем, oi Jlc]\\\ Голубкова, Брпмлп рекламировал (п довольно успешно) действи- ihiio полезную вещь [Примечание переводчика ]
84 Философия разработки ПО лля Windows: макропроблемь i но бессмысленным. Он циклически сканировал область памяти предназначенную для сегмента, п загружал в регистр первые четыре байта из каждой 4-килобайтной страницы памяти. \\у скольку с прочитанными значениями ничего не делалось, все выглядело так, как будто этот код просто «трогал» все страницы причем явно по какой-то осмысленной причине. Поскольку эт\ причину я сам попять не мог, я решил воздержаться от удаления этого фрагмента кода. Я обратился к нескольким местным знатокам системы VM с просьбой помочь мне разгадать, чем же все-таки занимается этот таинственный код, но никто не смог мне ничего подсказать Наконец, я встретился с великим гуру этой части операционной системы, Томом, который взглянул на код и мгновенно прояснил ситуацию. Этот магический цикл просто-напросто обеспечивал синхронизацию страниц в сегменте и в нуле. Как ока- I 9 О залось, в операционной системе имелся патологический недоста- *J ток, приводивший к десинхропизацип страниц памяти, что в свою очередь не позволяло корректно создавать требуемый сегмент; а KJ KJ '-> и этот презренный ничтожный цикл как раз и служил затычкой этой дыры. (На самом деле, технических деталей было много больше, но, я надеюсь, вы и так уже представили себе суть ситуации.) Как только я понял, что происходит, я написал внушительных размеров комментарий, объяснявший как саму природу данной патологии, так п предназначение этого загадочного фрагмента кода (Кстати, именно тогда я, должно быть, поставил свой личный рекорд отношения объема комментария к объему комментируемого кода -- что-то вроде 10:1, если не больше.) Несколько моих коллег, увидевших это, пытались пошутить над моим излишним, по их мнению, писательским усилием. Но я категорически не согласился с ними, поскольку я как никто знал, сколько работы совершили мои ноги для того, чтобы разобраться в предназначении этого кода И я с гордостью получил доказательство своей правоты спустя несколько месяцев. Другой программист, работая с этим кодом и не зная о том, что этот комментарии сделал я, случайно обмолвился при мне, что он чрезвычайно благодарен автору этого фрагмента за время, потраченное па такой *» «_» *-> полезный п ясный комментарии.
макроуровневых рекоменлаиий 85 9 4. УваЖайте ресурсы вашего пользователя ? Is there по respect of place, persons, nor time, in you: Уильям Шекспир, «Двенадцатая ночь» зап> Как насчет такой очевидной мысли: вы пишете программы, которые будут ткаться на компьютерах, по пользователями которых будут люди Звучит '.ша'1ыю? Но тогда почему так много современных программ игнорируют это относятся к компьютеру как к дешевому мотелю, населенному студенческими общинами на весенних каникулах? Пишете ли вы коммерческий продукт, который собираетесь продавать мил- шонами копий, или небольшую программу для домашнего использования или чдя ваших коллег по работе - вы всегда обязаны с уважением относиться к вашим пользователям и ко всем ресурсам, которыми они располагают. В Не калечьте систему пользователя при инсталляции вашей программы. Эта заповедь чрезвычайно важна. Лично я отказался от использования бессчетного числа программ только потому, что они нарушали это простое правило: заменяли системные DLL п VBX без моего разрешения (пет, простого сообщения об этом в README.TXT недостаточно!), изменяли файлы AUTOEXEC.BAT, CONFIG.SYS, WIN INI it SYSTEM.INI, сообщая об этом только после того, как дело уже сделано, портили настроенные мной файловые ассоциации и, наконец, просто требовали установки каких-то своих файлов в строго определенные каталоги. И этот краткий перечень — лишь малая часть тех выкрутасов, из-за которых ненависть к инсталлятору в два счета переносится на саму программу. Кстати, проблемам, связанным с местоположением DLL-файлов, подробно посвящена часть 6. Тесную связь с этой проблемой имеет вопрос о том, насколько легко и удобно будет пользователю запускать вашу программу под управлением разных инсталляций Windows па одной и той же машине. Если внимательно присмотреться к большинству современного программного обеспечения, то нетрудно заметить, что забота об этом является концепцией, просто-таки чуждой разработчикам. Я не могу припомнить, как часто мне приходилось инсталлировать один и тс же программы по нескольку раз - для последующего запуска их под Windows 3.1, Windows 95 тт Windows NT. Конечно же, трудность таких операций объективно возрастает: программы все больше и больше зарывают своп настройки в систему, и запуск их под разными инсталляциями Windows становится намного более сложной задачей, чем просто запуск главного исполняемого файла. Появление Windows 95 еще больше уеугубля- ет ситуацию: с одной стороны, все больше стимулируется использование программами специфичного для этой версии Windows реестра, а с
86 Философия разработки ПО лля Windows: макропроблем^ I другой стороны, большинство пользователей (особенно те, для ког( работа с компьютером является критичной в их бизнесе) наверняка це будут спешить с окончательным переходом на новую платформу, ; значит еще как минимум в течение многих месяцев Windows 95 буде сосуществовать с Windows 3.1 или Windows 3.51 на их машинах. В Позволяйте пользователям работать так, как предпочитают они Важно всегда помнить, что ресурсы пользователя отнюдь це ограничиваются самим компьютером и программным обеспечением Они включают в себя такие вещи, как знания и квалификация пользователя, а также его привычки и персональный опыт. Например, я считаю весьма приличными мои навыки работы с клавиатурой ц мышью — я могу довольно быстро манипулировать как Windows- программами, с которыми работал раньше, так и с самой Windows. Но когда я гляжу на свою приятельницу Марию, которая около восьми лет проработала графическим дизайнером, и вижу, как виртуозно она работает с PageMaker на своем Маке, у меня возникает навязчивое желание срочно поступить на Лечебно-Исправительные Курсы По Управлению Мышью. А ее стремительный стиль выполнения верстки и других DTP-работ служит для меня ярким напоминанием о том, насколько отработанными и закрепленными в привычку могут быть те или иные процедуры, выполняемые пользователями, и насколько важно помнить об этом нам, программистам при выпуске очередных версий наших программ. Другим, пусть менее драматичным, напоминанием об этом были мои личные проблемы при первом знакомстве с программой «Проводник» в Windows 95: например, привыкнув копировать файлы в File Manager при помощи горячей клавиши F8, я долго и безуспешно пытался пользоваться ею в Проводнике. в Не заставляйте пользователя тратить свое время понапрасну. Я имею в виду не только программы, работающие слишком медленно (об этом грехе будет отдельный разговор), но и те программы, которые делают такие глупые вещи, как принуждение пользователя любоваться красочной заставкой при запуске (без документированного способа отключения). Вообще говоря, всякий раз, когда ваша программа собирается «устроить шоу» — продемонстрировать мультипликацию, сыграть м)7' зыку и т. д. и т. п. — следует всегда делать такое развлечение пастраП' ваемым. При этом недостаточно просто давать возможность отменит11 начавшееся шоу, необходимо сделать это шоу отключаемым раз и Ш1' всегда. Довольно традиционной является рекомендация делать все звУ' ковые эффекты в программе отключаемыми (поскольку в рабочей обстановке они могут серьезно раздражать не только непосредственно пользователя, но и окружающих). Это превосходный совет, но я обЯ'
рекоменлаиий зательно распространил бы его и на всяческие не относящиеся к делу вндеодемонстрации. Пользователи часто дорожат каждой секундой своего времени, и заставляя их просматривать глупый (с их точки зрения) мультфильм в 7325-й раз, вы серьезно рискуете навсегда отва- I-* *_* днть пользователей от своей программы. Говоря о «развлекательных шоу» в программах, нельзя не вспомнить о так называемых «Пасхальных яйцах», которые программисты так любят прятать в своих продуктах. Я имею в виду небольшие красочно оформленные анимационные представления (чаще всего -- подробные списки разработчиков программы), которые пользователь может наблюдать после выполнения каких-нибудь «секретных» нажатий клавиш или манипуляций мышью. Довольно экстравагантный пример такого представления описан во врезке «Пасхальное яйцо Windows 95» на стр 89. Признаюсь, у меня двойственное отношение к этим штукам. Да, они могут быть по-настоящему забавны, но, как правило, только однажды или дважды. Потом они становятся абсолютно неинтересны. Или даже начинают раздражать. Например, наличие Пасхального яйца в программе, которая имеет ряд действительно ужасных ошибок или не обеспечивает каких-нибудь очевидно необходимых функций, вызывает у меня чувства, весьма далекие от радостного трепета. (Воз- и <-» можно, я становлюсь скрягой и занудой в свои тридцать с хвостиком, но когда я смотрю на Пасхальное яйцо в Windows 95, меня мучает один вопрос: почему они не использовали драгоценные программистские часы, потраченные на это шоу, для более полезных дел - например, для обеспечения настраиваемого показа файловых атрибу- Проводиика или для поддержки горячей клавиши F8 копировании файлов, как в Дис Пасхальное яйцо в Windows 95 покойство по /тугой, более сеш г бес меня настораживает тот факт, что такая техническая реализация этого шоу очевидно в объектов (задумайтесь: при потребовала изменений в самой глубине процедурах переименования и открытия каждом переименовании пли открытии папки системе приходится сравнивать введенные пользователем символы с «секретными ключами» Пасхального яйца!) Неужели стоило трогать ядро оболочки операционной системы, пусть даже незначительно, для таких целей, как одноразовое развлечение?! Я уж не говорю о том, что ребята явно перестарались с «засекречиванием» этого шоу, - почему бы не поступить проще и не привязать его куда-нибудь поближе к диалогу «About». Одним словом, если бы я имел право голоса при обсуждении реализации этого Пасхального яйца, я несомненно проголосовал бы категорически против нынешнего технического решения.
/ lie заставляйте пользователя зря тратить пространство на жесщ~ ком диске, оперативную память или процессорное время. Отчасти это просто рекомендация писать такие инсталляционные программы которые позволяют пользователю выбрать необходимые ему функциональные возможности (и которые, разумеется, потом подчиняются требованиям пользователя и действительно копируют на его жесткий диск только те компоненты, которые необходимы) Увы, я много раз видел ситуацию, когда на винчестере оказывались как раз те компоненты, которые пользователь специально старался исключить. В более широком смысле, это мольба об эффективности и экономичности ваших программ. Признаюсь, такой призыв наверняка звучит для вас комично (или патетично - все зависит от степени вашего цинизма) в эпоху 30-мегабайтных текстовых процессоров и 100-мегабайтных сред разработки на C++, с легкостью генерирующих 200-килобайтные показательные программы весовой категории «Hello, World!». Будьте элементарно вежливы. Я уверен, многие удивленно поднимут брови, но я слишком часто поражался тому, наскольго грубыми могут быть сообщения в программах. Например, попробуйте запустить программу с «ожидаемой версией Windows», большей 4.0, и Windows 95 скажет вам следующее: «The file expects a newer version of Windows Upgrade your Windows version.» («Программа рассчитана на более новую версию Windows. Обновите вашу версию Windows».) Когда я увидел это в первый раз, мне тут же захотелось обратиться в Microsoft с просьбой заменить мою версию Windows 95 на более вежливую. Я даже готов был заплатить за это еще несколько долларов. В самом деле, неужели им было так трудно (или дорого?) сказать то же самое, но с иной интонацией: «The file expects a newer version of Windows than your current version. Sorry, but you'll have to upgrade to a newer version of Windows to run this program.» («Программа рассчитана на версию Windows, более новую, чем ваша нынешняя. Извините, пожалуйста, но вам придется обновить вашу текущую версию Windows для того, чтобы запустить эту программу».) Я не намереваюсь бранить именно Microsoft (как раз она работает в этом плане немного лучше других); просто вы- 1 » * * ч t шепрпведенньш конкретный пример оказался иод рукой и является весьма показательным в качестве иллюстрации для этой книги. Поделюсь с вамп маленьким секретом, который я открыл при тесном знакомстве с рынком условно-бесплатных программ* если ваша 4 * программа окажется полезной людям и поправится им, то у нее гораздо больше шансов на успех, чем если она всего лишь окажется полезной. А если ваша программа будет вежливой и доброжелательной (без перебарщпванпя, разумеется), то людям она поправится больше. Делайте язык ваших программ более вежливым, потому что именно так
яТь макроуровневых рекоменлаиий 89 1 S \ мы, программисты, должны разговаривать с пользователями (с теми, без кого мы были бы просто толпой безработных). В конце концов, делайте ваши программы учтивыми хотя бы из жадности (это один из тех *-» 4J немногих случаев, когда хороший поступок, сделанный из плохих по бужденин, в конечном итоге окажется праведным делом - вряд ли пользователей будет волновать ваша мотивация, но программа им понравится и они отблагодарят вас). Пасхальное яйцо Windows 95 Как только в свет выходит новая версия известной Windows- программы (или самой Windows), немедленно начинается массовый бум - поиски знаменитого Пасхального яйца, «скрытого» развлекательного представления, помещенного программистами в продукт. Ниже приведены подробные инструкции для просмотра Пасхального яйца Windows 95 (я благодарен нескольким моим респондентам, подсказавшим мне эти «заклинания»). Набирайте текст внимательно - названия папок должны в точности совпадать с тем, что описано ниже1. 1. Щелкните правой кнопкой мыши по поверхности стола и затем выберите в контекстном меню команду New | Folder. 2. Назовите только что созданную папку «and now, the moment you've all been waiting for» (без кавычек). 2 3. Тут же переименуйте эту папку так: «we proudly present for your viewing pleasure» (снова без кавычек). .} 4. Снова переименуйте папку, на этот раз так: «The Microsoft Windows 95 Product Team!». 4 5. Откройте папку и посмотрите (и послушайте) представление. (Кстати, на одном из моих компьютеров закрытие этой папки посредине представления иногда оставляет мою карту Sound Blaster в возбужденном состоянии — она монотонно издггет низкий, противный гул до тех пор, пока я не завершу работу Windows. Интересно, принимает ли Microsoft сообщения об ошибках и сбоях в Пасхальном яйце?) 'фиводимыс сведения предназначены для пользователей англоязычной версии Windows 95 «and поте, the moment you've all been тсс/it in g foi» — «а теперь — момент, которою вы все '•<да щ» <■<<■('(' pmudly present foi your viezcing pleasure» — «мы с юрдосгыо представляем вашему ВПП- ^ИНПо» *I he Miciosoft Windozcs 95 Pioduct Team!» — «коллектив разработчиков Microsoft Windows 95'
90 Философия разработки ПО лля Windows: макропроблемь 5. Используйте DLL осторожно Динамические библиотеки лучший пример «тепличного создания» в Windows 95. В контексте данной книги, под «тепличными созданиями» я п0. нимцю такие функции, возможности и свойства, которые прекрасно работают в жестко контролируемых, «тепличных» условиях (в руках экспертов или даже в руках своих создателей), но надежность которых резко падает, когда они брошены на произвол судьбы во враждебных, непредсказуемых условиях реального мира. Более подробный и интересный разговор о DLL я отложу для главы 6, а пока я хотел бы подчеркнуть одну деталь: в Windows-программировании динамические библиотеки зачастую чрезмерно широко и интенсивно используются (читайте: неправильно используются), а также неверно располагаются на диске. Единственным результатом этого является масса проблем, возникающих у всех участников (в особенности, у пользователей). . Придерживайтесь широкого взгляда на инстрдментарий Man is a tool-using animal... Without tools he is nothing with tools he is all. Томас Карлл'п Почти все программисты ужасно любят забавляться со своим инструментарием разработки; но при этом порой проявляют выдающуюся неспособность правильно сделать его выбор. Это приводит к значительным и негативным последствиям для их программ. Этой проблеме (а также другим проблемам, связанным с инструментарием разработки программного обеспечения) подробно посвящена глава 4. Но один из аспектов данного вопроса кажется мне настолько важным, что я уже здесь обращаюсь к нему, как к одной из макропроблем программирования: программисты слишком узко определяют понятие «инструментарий разработки», сводя его к набору компиляторов, компоновщиков, отладчиков, текстовых редакторов, третьесторонних библиотек и т. п. Подобный подход недальновиден, потому что его результатом чаще всего является такое поведение: програМ' мист готов тратить кучу денег на то, чтобы установить на свой жесткий диск самую последнюю и самую крутую версию излюбленного компилятора или редактора, но при этом пренебрегает инструментом, который в действительности так же важен, как и все инструменты-программы вместе взятые — своим собственным образованием и обучением. В наши дни Windows-программирование представляет собой такое высоко- и многослойное нагромождение инструментов, интерфейсов и архитектур, что сегодня, как никогда, нам необходимо изо всех сил пытаться быть студентами»
сСПТимакроУРОвневых рекоменлаиий 91 - оянно изучающими программирование, и оставаться открытыми для всех возможных источников информации. Вспомните старые добрые времена программирования для DOS: вы поку- компилятор (например, Turbo С или Turbo Pascal), пару книг П с) Л М- вп дачУ — и все, вы уже были достаточно вооружены для продолжительной " 3V тьтативной работы. Многочисленные SDK? Подписка на Development Net- оЛО Третьесторонние библиотеки? Огромные архивы исходных кодов на CD- ROM? В то время о таких вещах либо вообще не слышали (не слышали в буквальном смысле слова, потому что многих из них еще не существовало в природе), либо они были реально необходимы лишь горстке программистов, работавших на передовом крае индустрии. Другой побочный эффект любви программистов к своим инструментам вы- 1 - [ядит довольно парадоксально (и с течением времени только усиливается): о многие из нас так и не находят времени для действительно детального изучения наших инструментов. На первый взгляд, достаточно приобрести ту или иную программу — и ничто не мешает изучить ее возможности досконально. Но сильнейшее давление па нас со стороны рабочих проектов, планов и сроков зачастую ведет к следующему сценарию: мы инсталлируем новую версию компилятора, игнорируем 95 процентов новых возможностей и свойств и довольствуемся лишь теми улучшениями и исправлениями, которые достаются нам автоматически. (Последите за дискуссиями в телеконференциях, и вы будете часто встречать реплики настоящих асов программирования, которые до сих пор ведут все свои разработки, управляя своими инструментами из командной строки. Эти люди вовсе не являются нео-луддитами, которые страшатся выхода в новый прекрасный мир визуального программирования. Они просто четко знают, что для них годится, а что нет, и поэтому продолжают работать с тем, что годится.) Проблема заключается в том, что, недостаточно изучив свои инструменты (а я грешу этим так же, как и большинство других программистов), мы порой просто не знаем, что они могут делать, а что нет. Насколько привычными для слуха стали обсуждения, подобные такому диалогу двух программистов: «Как жаль, что Visual C++ 2.2 до сих пор не позволяет создать или отредактировать ресурс с описанием версии. — Вы неправы, 32-разрядная версия VC++ давно позволяет делать это!». Как я упоминал выше, эта проблема все больше и больше обостряется по Мере развития Windows-программирования. Недавно мой близкий друг очень своеобразным способом проиллюстрировал этот факт. Имея серьезнейший °пыт в программировании (но не для Windows), он спросил меня, что (и в ка- 011 последовательности) я посоветовал бы изучить и освоить среднему DOS/ "Программисту для того, чтобы он смог начать разрабатывать Windows- Рограммы профессионального качества. Я догадался, что он намекал на себя \Мого и ждал от меня честного ответа. И когда я начал перечислять все те об- сСти, которые я рекомендовал бы для исследования этому гипотетическому
92 Философия разработки ПО лля Windows: макропроблемь новичку основы архитектуры Windows, событийно-управляемс^ программирование, C++, какую-нибудь каркасную библиотеку1 (скорее всег(, MFC) и прочее мы оба удивились и одновременно ужаснулись количеств t f основных пунктов в этом списке и грандиозности соответствующеп [[S{ суммарной кривой обучения. Примерно то же настроение вызывает наблюдение другого моего друга однажды он справедливо заметил, что сегодня стало практически невозможно написать более-менее сложную прикладную Windows-программу в одиночку. Ц это - исключительно благодаря размеру и сложности тех инструментов и архитектур, которыми необходимо овладеть. Я думаю, что наша профессия уже давно п неуклонно двигалась в этом направлении. Но в то же время последние достижения, такие как пакеты RAD (программы для быстрой разработки приложений), в какой-то степени противодействуют этой тенденции Подчеркну: я вовсе не утверждаю, что Delphi (или Visual Basic, или другие подобные пакеты программ) могут заменить знания программиста о системе. Эти инструменты очень хорошо помогают повторно использовать старый код, быстро писать новый, и даже защищают от многих ловушек Windows API Но они не могут защитить вас от всего. И как только вы продвинетесь достаточно далеко вперед от рубежа «hello world», вы снова окажетесь в холодном « • * > %J * • *-» важный и самый основной инструмент .щеждой и опорой буд :обетвеииые знания. 7. Не забывайте о производительности You can't be too rich, too thin or have a too fast computer. Ciapbiii программистский афоризм Dost thou love life? Then do not squander time, for that's the stuff life is made of Бенджамин Франклин Настало время правды в рекламе: я - человек, издавна привыкший мыслить на уровне битов. Я всегда был одним из тех людей, которых обвиняют в желании иметь самый большой, самый быстрый, еамый-самый компьютер на свете, независимо от того, нужен он или нет. В каком-то смысле это правда. Н° и я, в свою очередь, убежден, что некоторые люди недооценивают важность производительности Например, мне нравится слушать старую байку о том, что для работы с текстами и документами не нужно больших компьютерных мощностей. Если под «работой с текстами» подразумевать просто ввод текста в компьютер, то да, я согласен. Но если вы собираетесь делать такие вещи, как 1 «Каркасная бпблпоюка» — именно такой карлам г перенода ашлппекот термина «tramcwoik* был выбран редакцией [Примечание переводчика ]
ийкроуровневых рекоменлаиий 93 tj 0 IIU , iq индексов и оглавлении, проверка грамматики и правописания, за- I (_'|lCptHU1''1 •"• г макрокоманд и т. п., вас охватит страстное желание немедленно поставить 'V,0p стол самый последний BelchFire 5000 Wonderbox1. (Я говорю все это с ' с ил и пояснить одну деталь: когда я рекомендую не изменять код для улучшена*. IbК' ' 1 " сГо производительности, я всегда делаю это после тщательного обдумыва- и колебании, потому что на самом деле я с самого рождения хочу, чтобы абогало максимально быстро и в доли секунды откликалось на действия ,зователя ) Смысл этой моей макрорекомеидацпп совсем не в том, чтобы лишь в чередноп раз отметить важность такой характеристики программного обеспечения, как производительность Большинство программистов (но не все) прекрасно это понимают, и им не нужно напоминать, что чем оно быстрее, тем 1учше Я гораздо больше обеспокоен тем, как некоторые превращают заботу о (И С Р *v производительности в свою религию, и к каким последствиям это приводит. Вот несколько примеров разнообразных проявлений этой проблемы: в Меньше строк кода == меньше инструкций процессора == лучше дптельность. Этот миф очень коварен, шно бессмысленным спорам. Даже опы сбывают, что. когда они пишут на ЯВУ лбстр виртуальную машину, и тем самым заведомо отказываются от непосредственного управления генерацией машинного кода. Всякий, кто хотя бы раз заглядывал в высокооптимизированный выходной код %J компилятора, может удостоверить, что нет никакой гарантии соответ t f ствия между этим кодом и структурой исходного кода на таких языках, как Pascal или С. Оптимизаторы только тем и занимаются, что перемещают куски кода с места на место, изменяют их до неузнаваемости или даже вообщ Все зависит только от степени а агрессивности оптимизатора и от того, насколько хорошо был написан исходный текст Как я уже упоминал выше, этому мифу наиболее часто поддаются именно программисты, использующие C/C++, часто стремящиеся втиснуть ту или иную функцию в как можно меньшее количество строк. Результат таких действии, как правило, совершенно противоположен желаемому и приводит к «слоптимизпрованному» коду. Еще более страшной ошибкой, очень часто наносящей вред качеству публичного программного обеспечения, является то, что я называю 1 1 niriic 5000 Wonderbox - выдуманная авюром марка воображаемо! о суиермощнот 1'чыогера, которое можно перевесш примерно гак «Чудесный Ящик, И.жер! aiomnii Оюпь» фпмечлпие переводчика ]
94 Философия разработки ПО лля Windows: макропроблемк со «синдромом невзвешенного усреднения». Попробую пояснить :гго ц. простом примере- если вы принесли из магазина шесть покупок стой* мостыо 2, 2, 2, 2, 2 и 10 долларов соответственно, то какова средняя стоимость покупки? Равна ли она 3.33 доллара (20/6) или 6 долларов (12/2)? Разумеется, ответ зависит от того, как вы усредняете — взвешиванием или без. (Ну, и еще от того, какой результат вы хотите получить - больший или меньший. Ладно, ладно, статистики действительно не врут, но статистики — это совсем другое дело.) В нашей же профессии данный синдром заключается в том, что многие программисты (особенно новички), сильно озабоченные вопросом производи- тельности, имеют тенденцию смотреть на все детали без взвешивания и поэтому тратят массу времени на оптимизацию того, что нет смысла оптимизировать. Да, быстрее -- всегда лучше, чем медленнее. Но это не значит, что имеет смысл проходить буквально по каждой тропке, позволяющей сэкономить хотя бы пару-тройку шагов процессора Баланс - вот в чем ключ к решению этого вопроса. Если вы обнаруживаете, что можно на 2 процента ускорить печать документа вашей программой, но при этом придется выскрести и переписать 200 000 строк кода - стоит ли это делать? Я подозреваю, что правильный ответ - пет (если, конечно, вы не выполняете ваш проект в каких-нибудь уж очень необычных условиях). Концентрируясь на подобных изначально неверно понимаемых проблемах, команды разработчиков часто проматывают самый драгоценный свой ресурс — рабочие часы программистов. И при этом пренебрегают гораздо более важными (но менее сексапильными) вещами -- такими, как тщательное комментирование исходного кода и более скрупулезное тестирование — а ведь именно они, несмотря на свою кажущуюся невзрачность, являются реальным залогом качества продукта (особенно, если речь идет о перспективе — о будущих новых версиях). б У «синдрома невзвешенного усреднения» есть еще один побочный эффект, еще худший, чем растрата программистского времени — он может приводить к таким компромиссам, результатом которых становится пустой и слабый код. Я имею в виду непосредственное уменьшение надежности кода в погоне за производительностью. Дело в том. что любой фрагмент вашего кода должен очень скептически относиться ко всем данным, которые в нем используются и которые не были созданы в нем самом (это касается не только таких потенциально опасны* данных, как ввод пользователя, но и данных, приходящих из другп* частей кода). Лучшим способом борьбы в такой ситуации является создание простых заградительных барьеров, которые будут обнаруживать ошибочные данные и отбрасывать пли (если возможно) корре#'
\лакроУРовневых рекоменлаиий Л^ _-— 95 9 тировать их. (Такой подход я называю «оборонительным программированием», и более подробный разговор о нем состоится в следующей главе.) Как раз эти барьеры — небольшие участки кода, проверяющие ошибки, — и оказываются за бортом первыми, когда программист «вычищает» код, стараясь выгадать пару лишних микросекунд. Даже такие маленькие вещи, как проверка указателя на равенство NULL \J m проверка попадания целого значения в допустимый интервал, часто брасываются, когда программисты начинают срезать углы. Это так •обтазнительно _ вмиг сократить размер кода с 23 строк до 17. Программист так доволен и горд этой экономией — целых 25 процентов! А тем временем реальный выигрыш в производительности даже близко не стоит к этой цифре. Действительно, если удаляются строки вида ifdpStuff == NULL) г eturn ERROR_BAD_PARAMETER. которые требуют лишь нескольких инструкции для выполнения, и если сравнить их с остальными внутренностями функции (например, с циклом, который и выполняет основную работу), то станет ясно, что реальный выигрыш в производительности будет много меньше одного процента. Мне не хотелось бы, чтобы мои слова прозвучали как обвинение программистов в идиотизме. Я сам был там и знаю, как это происходит: вам необходимо срочно ускорить работу какого-то фрагмента кода, и вы начинаете срезать углы. Идея убрать «лишние» 25 XJ «_> « » ОШ процентов кода с экрана кажется такой заманчивой правой половине вашего мозга. Левая половина настойчиво твердит, что реальной экономии в этом — всего 1 процент; но вы торопитесь, вам нужно ускорить работу кода немедленно, и вы удаляете эти строчки, несмотря ни на что, получаете короткую иллюзию удовлетворения и приступаете к оптимизации следующей функции. А что мы имеем, если рассматриваем эту тенденцию в терминах соотношения «затраты/выгода»? В большинстве случаев мы получаем незначительный выигрыш в производительности и порядочного размера, но не такие очевидные, Утраты — вполне вероятно, что удаленные строки кода незаметно исправляли icuyio проблему с данными, возникающую в неизвестной стрессовой ситуа- 111111 > и, благодаря этому, никто в вашей команде даже не подозревал, что это фопеходило. Возможно, эта обнажившаяся проблема не будет обнаружена при I ei рессивном тестировании, и тогда дело кончится отгрузкой ошибки пользовано То есть, пользуясь сугубо технической терминологией, вы просто- фосто остреливаете себе (и своему продукту) палец, добившись такого не- аЧ1П'елы1ого улучшения производительности, что его никто и не заметит. Подобно другим общим проблемам программирования, обсуждаемый здесь рос серьезно усугубляется реалиями Windows. Ваш код просто обязан быть Ровеццо параноидальным, чтобы правильно и надежно работать в условиях
96 Философия разработки ПО лля Windows: макропроблелл Illf бесконечных вывертов со стороны драйверов, и постоянной толкотни в то одновременно запущенных программ, многие из которых недостаточно обрц3( ванны п учтивы, чтобы оставить в покое своих соседей (в главе 12 на стр. /^, * J г J вы найдете замечательный пример такого поведения - рассказ о том, как Noi ton Navigator от Symantec крушит другие программы). Что же я могу порекомендовать в этом случае, помимо тщательной оцещ(! того выигрыша, который вы ожидаете получить при «сжатии» кода? Преж; {< О всего, сконцентрируйте ваше внимание на алгоритмических вопросах: icaicoi механизм сортировки лучше использовать в .тгом месте? Так ли нужно использовать здесь связанный список, или нам следует разместить большие блоки на мятп фиксированного размера п избежать лишних перипетий с указателями'/ Трудность заключается в том, что наличие «проблем с производительностью,) зачастую долго «не обнаруживается» и становится «сюрпризом» лишь на поздних стадиях цикла разработки. (Кавычки в данном случае расставлены для того, чтобы намекнуть па обычное реальное положение дел: как правило, команда программистов на протяжении всего проекта прекрасно знает, что продукт - куча... неэффективного кода, но лишь по прошествии достаточно ]> лег приказ срочно что-нибудь предпринять в связи с этим.) Обычно к тому мо менту, когда улучшение производительное™ становится чуть ли не самым на сущпым вопросом, уже поздно заниматься алгоритмическим анализом такого рода н делать соответствующие изменения в коде. Когда к виску программиста приставляют двустволку - план + производительность — он приходит в бс шепство и начинает вспарывать все, что можно. И тогда проверки ошибок которые «па самом деле не нужны, поскольку ничего не делают», становятся первыми жертвами. Что касается выявления в программе мест, действительно критичных дтя производительности, то надо признаться честно: во многих случаях (если нес большинстве их) такие места молено увидеть еще до того, как начнет писаться код. Мой коллега (программист и писатель) Пит Дэвис однажды дела! компрессор растровых картинок для своего клиента. Главной задачей .угон программы было масштабное уменьшение графического изображения, но itf простое, а очень интеллектуальное — такое, чтобы уменьшенная растровая картинка нормально смотрелась. Известно, что такая задача весьма требовательна к такому ресурсу, как рабочее время процессора. Поэтому с самого начала Пит спроектировал (а потом п реализовал) программу должны*' образом: пользовательский интерфейс был выполнен на C++ и MFC, а ядр° занимающееся собственно масштабированием графики на ассемблер11 1 • «ручной выделки». Среди многочисленных примеров проявления обсуждаемых здесь пробл^1 встречаются и исключения (которые, правда, все равно в конечном счете сво дя'1ся к оплошностям со стороны программистов). Б моей практике был oii{l]] проект, который можно упомянуть в качестве хорошей иллюстрации — э'11
макроуровневых рекоменлаиий 97 пЫ-'га очень большая программа, имевшая свою „собственную внутреннюю ' ' m<6v размещения и управления памятью. Уже в ранней альфа-версии стала vriia ужасающе слабая производительность программы. Поначалу это пока- ось неожиданным для всех, но оперативное исследование быстро выявило iqiniv - в самой глубине кода системы размещения памяти был обнаружен. ! написанный на высокоуровневом языке, который при каждом запросе па | i \ I ■* * >е jeirne памяти совершал' побайтную чистку размещаемого блока памяти. )[01 фрагмент кода был немедленно заменен на ассемблерную вставку, выпол- hihvio эквивалентную работу при помощи подходящей инструкции процессора 80x86. В результате мы получили впечатляющее увеличение нроиз- Ч 9 ,();ппельности всей системы в целом. В заключение, еще одна идея, которую следует постоянно держать в го борьб наших программ Ино i [и случается так: вы прекрасно знаете, что некоторая подзадача в вашем проекте имеет более эффективное решение, вы даже четко видите, как его реа- шзовать, по у вас катастрофически не хватает времени на это. Например, вместо того, чтобы хранить частичные описания объектов в отдельных связанных списках и использовать бинарный поиск, вы просто сваливаете все в один ли- О • • псиный список, а затем порождаете кучу простых циклов, гоняющих указатели гуда-сюда по всей цепочке. Часто в подобной ситуации есть смысл взглянуть ira задачу немного шире и заметить, что она является достаточно общей и может встречаться не только в других частях этого же проекта, по и во многих других проектах. А тогда вполне резонным будет потратить немного больше времени на ее эффективное решение прямо сейчас - написать правильный нроизводи- 1ельный код п превратить его в отдельный, достаточно самостоятельный компонент, который йотом легко можно будет использовать в других местах. Переведя это на язык вашего руководства -- указав па возможность разбросать •ГП! небольшие дополнительные затраты по нескольким проектам, - вы, наверное, без особого труда сможете добиться согласия на то, чтобы пойти правильным путем. В результате от этого выиграют все. в- Не забывайте о тестировании Поразительно, как сильна иллюзия того, что красота есть добродетель. Лев Толе* roii Computers don't make mistakes. They do, however, execute yours very carefully. Джек Xyuep Мы терпеть не можете тестировать? Лично я, определенно, ненавижу это clIb Для меня в цикле разработки програМхЧпого обеспечения пет задачи, ме-
98 Философия разработки ПО лля Windows: макропроблел* —————— ——/ч(1| нее интересной п менее приятной, чем тестирование. Даже собрания протру, мистов и инспекции кода, как правило, приносят больше удовольствц, поскольку есть возможность пообщаться на темы проекта с другими людь^, кто-то из них обязательно принесет с собой пончики пли еще что-нибудь вк\< пенькое... Но, прошу прощения, я отвлекся. Ус Парадокс заключается в том, что никто не любит заниматься тестеров- пнем (и поэтому разработчики часто смотрят па это занятие свысока, как и, грязную черновую работу), в то время как оно является невероятно важно, задачей. Моя жена Лиза шестнадцать лет проработала в качестве программист « f с и системного аналитика, и мы вместе с пен очень часто замечали, как легко на- удается находить ошибки в коммерческих Windows-программах. Обычно дЛ; этого не требовалось п десяти минут небрежного использования. При этом я не говорю о настоящих «шоу-стоиперах», таких как разрушение операционнор системы пли потеря данных (хотя и их я встречал больше, чем положено н мою долю по статистике) Прежде всего, я имею в виду мелкие, не очень суще ственные, но тем не менее неприятные проблемы типа полей редактирования вылезающих за края диалогового окна, горячих клавиш, конфликтующих друг с другом, пли (мой излюбленный тип) опечаток, забавных и не очень, в диало гах, сообщениях и справочных файлах. Я хотел бы выделить два интерсных аспекта этой проблемы, показы вающне, как происходит (и не происходит) тестирование при разработке программного обеспечения для Windows: в Тестирование — это защитная сеть вокруг вашей программы, послед % • няя и порой единственная надежда на серьезное повышение ее качества до того, как она выйдет в свет. Это та самая красная черта, разде ляющая приватный («что хотим, то творим») и публичный («Мы затрагиваем интересы реальных людей!») периоды жизни вашел программы. А еще - это как раз та часть цикла разработки, чью долю в плановом расписании проекта очень часто «съедают» проектпрова иие и собственно реализация программы. Я нахожу такое положение дел одновременно ужасающим и удиви О тельным, потому что тестирование - действительно одна из важпеп шнх сторон разработки (если рассматривать готовую программу ДО1, результат сделки между разработчиком и пользователем, то я не нобо ялся бы определить тестирование как одно из неотъемлемых услоши этот! сделки). Многие программисты видели в той или иной вариаШ11 известную диаграмму -- зависимость стоимости исправления ошибь'1 от момента времени, когда эта ошибка была обнаружена. Во врсМ* проектирования и дизайна стоимость ошибки минимальна; в пери0'' реализации она возрастает, по все еще остается небольшой; во вре^ тестирования ошибки становятся уже чувствительно дорогими; и, № конец, после выпуска продукта в свет мы наблюдаем жуткий изгиб
макроуровневых рекоменлаиий из почти горизонтальной линии кривая Превращается в почти вертикальную, а починка уже требует не только проектирования и тестирования, но и доставки исправленной версии всем клиентам. (Конечно, широко распространенная ныне практика сопровождения каждой новой версии потоком «заплат» к ней (patches, bugfixes) позволяет современным поставщикам программного обеспечения радикально сократить своп расходы на исправление ошибок. Более того, даже найден оригинальнейший способ превращать свои собственные ошибки в О дополнительный источник доходов: хотите получать самые иоследные версии и заплаты к ним? - тогда покупайте годовую подписку на нашу службу обновления продукта! Хмм. И куда же, черт возьми, девается этот докучливый Департамент юстиции США, когда возникает реальная необходимость в нем?) Разумеется, тщательное тестирование Windows-программ может быть ужасно трудным делом, и я готов побиться об заклад, что почти каждое коммерческое Windows-прпложепие можно положить на обе лопатки путем создания достаточного дефицита какого-либо компьютерного ресурса. (При этом я хотел бы сразу провести различие между теми программами, которые вовремя обнаруживают такой дефицит и элегантно завершают свою работу, и теми, которые продолжают пахать, не обращая внимание на проблему, и в результате работают неправильно.) В Некоторые разработчики часто озабочены лишь тем, чтобы порадовать начальство вовремя написанным и выпущенным кодом, и тогда они используют небрежное тестирование в качестве костылей. Они выполняют минимальное тестирование какой-либо функции и говорят: «Видите? Работает!» Это — основной грех программирования по-ковбойски: использовать мимолетную демонстрацию в качестве доказательства (неважно, себе или кому-то другому) того, что проверяемый код тботает правильно и с ним бу ничего не нужно больше делать. И это не вопрос из области противопоставления математиков и ювелиров, это вопрос выполнения тестирования в осмысленном объеме. Такие короткие демонстрации почти никогда этого не обеспечивают — программист обычно подает на вход функции лишь относительно банальные, «ручные» наборы данных, которые, как правило, не включают в себя общие патологические варианты. Как же можно после этого заявлять, что функция окончательно готова? Решение первой проблемы - очень простое с концептуальной точки 1*я и неимоверно трудное с точки зрения реализации, поскольку это, на са- Деле, вопрос культуры управления проектом (особенно большим).
1 00 Философия разработки ПО лля Windows: макропроблемь, Простые административные указания и проскрипции — например, требование полного тестирования продукта на всех поддерживаемых платформах (огромное спасибо всем этим странным различиям между тремя вариациями Win32') или настаивание па тщательном повторном тестировании последних изменений делу помогают слабо, поскольку они чаще всего вообще игнорируются из-за постоянной нехватки времени или других ресурсов. Нахо- либо дясь перед сложнейшим выбором — либо выпустить продукт н несмотря на неопределенность риска в связи с его качеством, его в заведомо лучшем сосгояшги, но с таким опозданием, что он все равно проиграет своим конкурентам - большинство компаний просто бросают кости и выбирают первую альтернативу. Когда разговор касается денег, давление мгновенно превращается в ясно различимый пинок: «Отгружайте продукт немедленно!» И нам ничего не остается, кроме как выдерживать в этих условиях тяжелейшую битву за то, чтобы отгружаемое имело как можно более высокое качество. А. Лдмайте о повторном использовании кода If men could learn from history, what lessons it might teach us! But passion and parly blind our eyes, and the light which experience gives is a lantern on the stern, which shines only on the waves behind us! Сэмюэль Тэилор Кольридж буду ком- объекты. шаблон 1будь я уверен, что программирование в конце концов вышло на уровеш i6ho Мы уже сегодня получаем в руки такие инструменты, которые убедительно подтверждают, что это И i6 б\ Суть, разумеется, вовсе не в вышеупомянутых инструментах (таких, как Delphi, к примеру). Сводить проблему повторного использования кода к бы Особенно DOS- и Windows-программисты уже давно, целые ° десятилетия применяли это самое повторное использование кода в той пли иноп форме Вставка директивы «#include» в вашу С-программу н последующая компоновка с соответствующим LIB-файлом не так проста и сексапильна, как. с Объ Интегрированной Среды Разработки. Но об * * сят практически эквивалентный выигрыш го б
макроуровневых рекоменлаиий 101 0 вы используете хорошо проверенный и (как правило) хорошо понятый созданный ранее. Вот в этом и заключается суть вопроса. Во время основной работы где-нибудь в уголке вашего мозга должен посто- I 9 ико идти еще один процесс - процесс, отслеживающий, какие куски кода, ,даваемого нами, могут и должны быть трансформированы в повторно-ис- )1Ьз\емыс компоненты. В подавляющем большинстве случаев код, который >ъ\ пишете, настолько специален для конкретного проекта или программы, что ')Ы'Ю бы глупо тратить ваше время на превращение его в такой компонент. (В • к j ( конце концов, какой смысл в повторно используемом компоненте, который и рнмепяется только в одной программе?) Но в этом потоке кода обязательно встретятся фрагменты (функции, модули или даже целые библиотеки), которые являются хорошими кандидатами на повторное использование. Вот их обычные характерные признаки: В Они либо имеют общее назначение, либо могут быть обобщены достаточно легко. Такие вещи, как контейнерные классы, п даже 1\елые каркасные библиотеки для Windows-программирования (например, MFC или OWL) имеют достаточно обобщенный дизайн и обеспечивают достоточно входов, выходов, лазеек для тонких настроек и адаптации — с их помощью вы можете сделать почти все, что захотите. в Они имеют очень специальное назначение. У меня есть целый чемодан кода, который я таскаю за собой из проекта в проект, и в котором мож- t f но найти множество узкоспециализированных подпрограмм, выполняющих такие мелкие поденные работы как сброс атрибута «read-only» у файла, добавление обратной косой черты (backslash) в конец строки, если этого символа там пет, подсчет количества файлов, указанных в строке, возвращенной функцией GetOpenFileNameO и т. д. Н Они делают некоторую противную работу, которую вы не хотели бы кодировать и тестировать больше одного раза. (А если вы найдете какой-нибудь хороший третьесторониий компонент, делающий это, то считайте, что вам очень повезло — и одного раза не понадобится.) Отличным примером в данном случае может служить каркасная Windows-библиотека. Конечно, и в наши дни можно найти причины не использовать в Windows-проекте ни MFC, ни OWL, нп какую-либо другую подобную библиотеку, но тогда эти причины должны быть весьма и весьма основательными. И Они сами напрашиваются на надежную реализацию - то есть вы чувствуете, что можете сделать их почти стопроцентно пуленепробиваемыми. Это очень важная характеристика для будущего иовторпо-пеполь- зуемого компонента, потому что как только вы опубликуете его, другие люди будут смотреть на него так же, как на функции стандартной библиотеки С-компилятора — не утруждая себя доскональным изучением
1 02 Философия разработки ПО лля Windows: макропроблем */ сопроводительной документации, они будут предполагать, что ващ Ко просто не может работать неправильно. (Всегда помните, что ваще!- главной целью является повышение вашей производительности качества ваших программ, а вовсе не выигрывание в кафетерии спор0ь о том, кто чей код неправильно использует.) I! В главе 10 я расскажу значительно больше об абстрагировании и созданы ваших собственных библиотек, которые смогут повысить эффективность ващщ разработок для Windows. К сожалению, Win32 ЛР1 имеет вполне достаточное количество вывертов и различий между тремя своими платформами - доста точное для того, чтобы вы захотели (или далее были вынуждены) написать свою собственную «обертку» для этого API. Подробнее об этом я также расска жу в главе 10 Довольно чувствительной может оказаться проблема тестирования по вторно-используемого кода, разработанного вашей командой. Одним из пред 1 ? полагаемых выигрышей от применения ранее изготовленных компонентов яв ляется уменьшение продолжительности циклов тестирования всего проекта в целом. В самом деле, если вы используете компонент, проверенный ранее, то зачем нужно тратить время и бить по нему молотком тестовых примеров снова и снова? Я знаю как минимум пару серьезных оснований для того, чтобы это делать. Во-первых, границы между новым и повторно-используемым кодом очень редко совпадают с границами тестовых задач. Например, вы пишете класс для работы со списком заказчиков, старательно тестируете его специальными t 9 программами-примерами и в какой-то момент решаете, что этот класс достаточно проверен и достоин помещения его в ваш арсенал повторно используемых компонентов. Позже, когда вы приступаете к разработке реальной программы — например, программы для работы со счетами и накладными - вы используете в ней этот класс (совместно с другими классами). И при тестировании этой программы вы вряд ли получите заметную экономию времени, потому что вам придется писать множество новых тестовых примеров, проверяющих именно совокупную работу всех частей и компонентов. Пожалуй. единственным моментом, когда вы сможете реально сэкономить время за счет повторного использования кода, является поиск причин обнаруженных ошибок и сбоев — в этом случае вы можете смело концентрироваться прежде всего HJ новом коде. Во-вторых, после того, как компонент изготовлен и протестирован в изо* ляции, часто обнаруживается, что последующее его использование в работе?1 проекте несколько отличается от того, что предполагалось изначально. 4aflif всего это отличие бывает чисто количественным, а не качественным. НаприМеР написанный ранее класс для работы со списком заказчиков планировался Д^ манипулирования всего лишь парой сотен элементов (и, следовательно, бы1 строго протестирован только для соответствующих объемов данных), а реаЛ&
мзкроуровневых рекоменлаиий юз программа должна иметь дело с десятками тысяч заказчиков. Это значит, вы должны снова потратить время на тестирование ваи/его компонента -- этот раз в новых, изначально не предусмотренных условиях. В противном учае вы сильно рискуете неожиданно попасть в засаду — какая-нибудь с О шибка в дизайне или реализации вашего класса может «всплыть» именно то- •х когда вы попытаетесь занести в список больше, чем 32К элементов. В условиях этой дилеммы с тестированием есть, пожалуй, только один ГД *_» »-» пр авильныи путь: делать главный акцент все-таки на тщательном тестировании компонентов в изоляции и добиваться от них максимально возможной надежности до того, как они будут применяться в реальном проекте. К сожалению, как раз этот тип тестирования наиболее часто оказывается выброшенным за борт, когда планы прижимают программистов к стенке: пытаясь рациона- j шзировать ситуацию, разработчики заявляют, что, если они потом достаточно хорошо протестируют законченный продукт, то все ошибки будут найдены и исправлены, а ведь только это и имеет значение в конечном итоге. На мой t I взгляд, этот агрумент очаровательно противоречив, поскольку в действительности он эквивалентен такой формулировке: мы можем позволить себе решительно ослабить часть тестирования (и даже вообще исключить ее) за счет того, что оставшаяся часть тестирования будет проведена супер-экстра-тщательно. Повторное использование кода является важным приемом в программировании любого типа, но в разработке для Windows его ценность велика, как никогда. Как я уже упоминал ранее, сегодня Windows-программи- <_» * • О стам приходится ценой тяжелейших усилии поддерживать свои знания на том уровне, который необходим. Поэтому очень серьезной подмогой для вас может стать именно разбивка работы па части и последующее использование полученных компонентов, независимо от того, состоит ли ваша команда из нескольких человек (разброс в пространстве) или только из вас одного, работающего над несколькими частями проекта по очереди (разброс во времени). Кстати, в последнем случае особенно велик соблазн отказаться от дополнительных усилий по превращению того или иного фрагмента кода в настоящий компонент или хотя бы в тривиальный LIB-файл. Но вы должны преодолевать этот соблазн и по возможности находить время для создания и документирования компонентов так, как если бы они заведомо предназначались для использования другим человеком, которого вы никогда не встретите и которому не сможете потом помочь; если вам это удастся, то позже - когда вы посмотрите на этот компонент уже как пользователь, а не как автор, и когда настанет ваша очередь рас- Ш11Фровывать его документацию и интерфейс — вы скажете себе огромное спасибо
'Yes, I have a pair of eyes,' replied Sam, 'and that's just it. If they wos a pair o' patent double million magnify in' gas microscopes of hextra power, p'raps I might be able to see through a flight о' stairs and a deal door; but bein' only eyes, you see my wision's limited.' Чарльз Диккенс «Посмертные записки Ппквнкского клуба» ОНО у всех программистов дела со зрением обстоят точно так же, как у Сэма — Ограничено. Но иногда и этого бывает более чем достаточно. Если, ПеРефразируя другое известное высказывание, мы делаем решительное усилие «мыслить глобально, а действовать локально», то наши старания окупаются СТоРицей. В главе 2 я представил некоторые макроуровневые рекомендации, говоря Юм, что делает Windows-программу хорошей, и какую роль в нашей работе Рает философия программирования. Теперь пришло время заглянуть внутрь Рассмотреть некоторые микроуровневые проблемы.
1 06 Философия разработки ПО лля Windows: микропробле щ i Шесть Как я уже упоминал в главе 2, различие между макро- и микропроблемами состоит не в их важности, а в том, как и когда они возникают. Макропроблемь, больше касаются проектирования и планирования, в то время щ микропроблемы возникают в «боевых условиях» рутинного кодирования, когда программы конструируются строчка за строчкой. И как бы ни было nopojj трудно правильно решать макропроблемы в большом программном проекте в некоторых случаях микропроблсмы могут оказаться невероятно более сложными. Случается это потому, что на микроуровне решение следовать (или не следовать) тем или иным рекомендациям, как правило, находится в компетенции самого программиста. Строгий контроль за индивидуальными действиями на О этом уровне осуществляется крайне редко, и даже тесная программистская ко- и манда зачастую создает код, состояищи из удивительно непохожих друг на друга но стилю и философии написания фрагментов. И дело тут далеко не только в эстетике (например, идея написания кода в одинаковом стиле — так, как если бы его писал один и тот же человек — вполне реализуема, хотя и очень сомнительна), поскольку даже одним и тем же рекомендациям разные люди склонны следовать по-разному. Например, все члены команды могут договориться придерживаться принципов оборонительного программирования (см. рекомендацию 11 на стр. 109), но каждый отдельный программист будет применять этот принципы лишь в той степени, в какой может (или хочет); и, кстати говоря, при этом последствия для всего проекта могут быть весьма неприятными. Некоторые микрорекомендации, представленные в этой главе, могут показаться неплохими кандидатами па включение в макросписок. В конце концов, разве не относятся к области проектирования такие вопросы, как способ хранения постоянных данных или четкое отделение друг от друга фрагментов кода занимающихся пользовательским интерфейсом и собственно реализации алгоритмов задачи? Да, относятся. Или, по крайней мере, должны относиться Но все дело в том, что те методы, которыми сегодня часто осуществляется Win clows-программирование, просто сталкивают их с макро- на микроуровень Проблема — в постоянной нехватке времени, ведущей к значительное сокращению (а иногда — даже к полной ликвидации) этапа проектирования1 цикле разработки проекта. Эта тенденция серьезно повышает отношение «кла внатурного времени» к «бумажному времени», то есть важные дизайнеров решения все чаще и чаще принимаются не хладнокровно, не на ясную голов}' не в специально предназначенное для этого время, а в пылу сумасшедШе1 борьбы с проблемами и ошибками (своими и чужими) при непосредственно1' написании кода. U
клИКроуровневых рекоменлаиий 107 {OB- а надеюсь, что, вне зависимости от специфики ваших конкретных проек- постараетесь не забывать (а, быть может, и применять на практике) мной рекомендации и советы, включая представленные ниже п <«. оедложеняые лпчьные руководящие линии» микроуровня. Быстрая разработка приложений (БРП1) — сегодня на нее делается все больший и больший акцент в нашей с вами профессии. Но, несмотря на * * *_» то, что эта тенденция калсется мне полезной и захватывающей, я вынужден добавить, что она лишь усугубляет проблему попадания проектов под в часть рефлекторных и поспешных решений. Программисты (а во многих случаях и непрограммисты) куда-то тыкают курсором, щелкают мышью, что-то перетаскивают и бросают, как сумасшедшие, и, имея зачастую лишь самые туманные представления об архитектуре Windows, создают бросовые программы, иногда выходящие в свет даже без минимального тестирования Что особенно примечательно, эти программы действительно оказываются вполне работоспособными (если выразиться точнее, продолжительность их нормальной работы оказывается вполне дрисмле- мой), в то время как их создатели практически ничего не смыслят в том, как и почему функционируют специфичные для Windows детали их •J творении. Чтобы меня не поняли превратно, позволю себе отметить, я так же, как и любой другой киберпопулист, скептически отношусь к идее привнесения компьютерной мощи в массы Ведь проблема здесь не в том, что средства БРП могут вывести эту мощь из-под контроля экспертов, а в том, что они вкладывают ее в руки неспециалистов, которые тут же начинают создавать публичные программы. Если неофиты желают писать программы для своего яичного пользования, это замечательно, и я более чем охотно готов помогать им ориентироваться п искать все входы и выходы Но когда их программы пересекают черту и становятся публичными (пускай даже в пределах одной компании), я начинаю нервничать, потому что под угрозой моментально оказываются компьютеры п данные других людей. Иными словами, инструментарий для БРП одновременно делает две совершенно противоположные вещи: 1 Экспертам и специалистам он дает возможность создавать программы более продуктивно, затрачивая намного меньше сил на 2 «проникновение в тайны» Windows-программирования. Как человек, «слонявшийся по закоулкам Windows» достаточно много, я без лишнего стеснения могу заявить, это очень хорошее дело Неспециалистам он позволяет создавать программы и «заставлять их работать». Но в зависимости от того, какова природа этих программ, ЬРЦ ()| апмоязычпом аббревиатуры RAD (rapid application development) [Примечание Рядчика J
1 08 Философия разработки ПО лля Windows: микропро6лем кула они потом попадают, как и кем они используются, и кем он сопровождаются, это может быть очень и очень плохим дело,\ Например, я слышал уже много историй о том, как сотрудцИк обращались в отдел информатики и компьютерной автоматизации своей компании с просьбой о каком-нибудь новом программном обеспечении, а в ответ, получали лшпь подробнейшее объяснение почему этот отдел имеет такое огромное количество недоделанные программных заказов, а впридачу — копию Visual Basic или Delph, Да, именно так - в некоторых случаях компании прямо-такц заставляют своих бухгалтеров, инженеров и прочих далеких от программирования сотрудников помаленьку заниматься этим делом самостоятельно Г 0. Абстрагируйтесь, абстрагируйтесь и снова абстрагируйтесь (и не беспокойтесь о цене вызова функции, пока программа сама не заявит об зтом) На мой взгляд, суть программирования как ремесла заключается в создании и использовании уровней абстракции с целью навести мосты через пропасть, разделяющую физические возможности системы и желаемы! логический результат. Эти зверски быстрые, но тупые как пин, компьютеры постоянно нуждаются в подсказке, что и как нужно делать на следующем шаге, и поэтому становятся тяжетым бременем для тех, кто их использует. В особенности - для программистов. Абстракция как раз и является тем самым золотым ключиком, который помогает справиться со всеми этими головолом пыми сложностями. Одной из ваших основных целей в программировании должна быть работа на как можно более высоком логическом уровне. Сегодня очень немногие программисты пишут на ассемблере. И в основном потому, что это уже не так часто необходимо. Такие языки высокого уровня, как C/C++, Pascal и BASIC являются гораздо более продуктивными инструментами для большинства программистских задач. Работая иа более высоком логическом уровне любого из этих компилируемых языков, программист фактически имеет дело с не' которой виртуальной машиной, установленной поверх других виртуальных М'1' шин (включая стандартную библиотеку языка, третьестороииие библиотеки Windows, DOS и г. д.). Основная проблема, которой посвящен данный раздел, состоит в том, irr° программисты привычно воспринимают эту готовую кучу абстракции и IlC пользуют ее, но при этом часто проявляют неспособность думать о дальнейШс}| создании своих собственных виртуальных машин. Рассматривая существую^11 абстракции как данность, как часть окружающего программиста пейзажа, °l{l не видят в абстракции как таковой реального инструмента для своей работе
^икроуровневых рекоменлаиий 109 п дробнее об абстрагировании и, в частности, об оберточных библиотеках для г 32 я буду говорить в главе 10.) I f. Притеняйте оборонительное программирование Things without all remedy Should be without regard; what's done is done. Уильям Шекспир Never check for an error condition you don't know how to handle. Программистский афоризм This error should never happen. Огромное число диалоговых окоп Оборонительное программирование похоже на оборонительное вождение автомобиля: если по всеобщему предположению каждый осознает правильность этой идеи (а вес мы заявляем, что именно так обстоит дело), то почему до сих пор так много водителей таранят телефонные столбы, ездят по улицам с односторонним движением в противоположном направлении или, еще хуже, садятся за баранку под мухой, и почему так много на свете коммерческих программ, надежность и устойчивость которых едва допустимы даже для бета-версий в ранней стадии тестирования? По моей теории, в обоих случаях все зависит в основном от культуры. Ни оборонительное программирование, ни оборонительное автовождение не выглядят достаточно круто или сексапильно. Особенно для американцев, которые предпочитают ходить по краю, рисковать и жить по- ковбойски, оставляя заботы и беспокойства для скучных людей, находящихся за чертой игрового поля. Что ж, мне не хотелось бы испортить вам вечеринку, но есть голый и не- оспоримый факт: оборонительное программирование — это наиболее важная Мцкропроблема, с которой вы столкнетесь. И несмотря на то, что применяется °но в микрокосме, оно легко может повлиять на надежность всей вашей пРограммы в целом. Ко Что такое «оборонительное программирование»? По своей сути, оборонительное программирование — это минимизация ;Шчества опасных предположений, которые делает ваш код. В данном случае РеДеление «опасное» обозначает такое предположение, которое, оказавшись работ Т
110 Философия разработки ПО лля Windows: микропроблем образом, это определение является той красной чертой, которая разделяет еу щественные и несущественные неверные предположения. Например, моя позиция такова, что серьезным браком является даже ^ ошибка, после которой программа продолжает нормально работать (то есть работать без потери функциональности или данных), по нарушается лцщь внешний вид пользовательского . интерфейса. Многие программисты, с которыми я разговаривал, не соглашались со мной в этом примере. И я встречал даже работников из отделов поддержки пользователей, которые рас. сматривали такой пример как чисто косметическую проблему, ясно давая понять, что до тех пор, пока программа не потеряла мои данные, мне не о чем беспокоиться. (Кстати, было так же ясно и то, что такие проблемы их тоже не беспокоят.) Когда я сажусь в один из двух своих автомобилей (оба — марки «Хонда»), я не перестаю дивиться тому, насколько надежно и удобно спроектированы эти машины, и тут же меня охватывает страх: что произошло бы, если бы японцы когда-нибудь вдруг решили сделать индустрию разработки программного обеспечения областью своих стратегических интересов. Предположения плохи по одной простой (но не бросающейся в глаза) причине: каждое предположение, сделанное в вашем коде, является зависимо- «> *J K.t стью, а всякая зависимость - потенциальной ахиллесовой пятой в случае каких-либо внешних изменений. А изменение может произойти не только в действиях пользователя, но и в вашем инструментарии, в Windows или в другой «.» части вашей программы. Природа предположений Разумеется, даже при написании самой тривиальной программы не избежать десятков предположений. Каждая строка вашего кода предполагает, что и о компилятор правильно переведет ее на машинный язык, что задействованные в этой строке переменные не могли быть испорчены системой или еще чем-нибудь с момента выполнения предыдущей строки и т. д. Ваш код буквально находится на плаву в безбрежном океане предположений. С ними ничего нельзя (да и не нужно) поделать. Остается лишь принять их как неизбежную данность и сконцентрироваться лишь па тех предположениях, о которых реально следует беспокоиться. Опасные предположения, о которых я говорил выше, можно разделить на две широкие категории: 1. Предположения о данных. Данные -- это кровь вашей программы- Если не будет данных, то всей логике в мире будет не с чем работать. Возможно, это тривиальная мысль, но она прямиком ведет к довольно тревожному факту: точно так же, как и настоящая кровь, кровь вашей программы может оказаться зараженной. Я имею в виду не коМ' пьютерпые вирусы, а именно плохие данные — данные, которые
ix рекоменлаиий W неправильны, недопустимы или не соответствуют ожидаемому формату Плохие данные могут появиться откуда угодно — от опечатки поль- % t .зоаателя при вводе, от другого кода вашей программы, от используемой вами третьестороиней библиотеки и даже от самой Windows. Помните, что при программировании для Windows не все источники плохих данных очевидны. Так, например, Windows/DOS Developer's Journal в своей серии аннотаций к Windows SDK указал несколько случаев, в которых вызов функции может приводить к изменению переданных ей данных, несмотря на то, что документация этой функции явно утверждает обратное. Такие скрытые ловушки могут приводить к скверным сюрпризам и длительным отладочным сеансам. Предположения о процедурах. Вторая половина сущности вашей программы — алгоритмы. Когда вы отдаете управление другому куску кода (такая формулировка всегда звучит более интересно и более правильно, чем традиционный оборот «вызываете подпрограмму»), вы неизбежно делаете целый ряд предположений о том, каких результатов можно ожидать от этого действия. Эти предположения основываются на двух источниках — документации и наблюдениях (я не беру в расчет такие вещи, как праздную болтовню программистов в кафетерии пли так называемые «закулисные сделки» о небольших изменениях в поведении кода, которые программеры заключают друг с другом для взаимного удобства). Почему вам следует заботиться о чем-то еще, кроме документации? Да t ! потому, что, к сожалению, в самой документации очень часто встречаются откровенные ошибки или недоговоренности, и у вас не остается другого выбора. Самый классический пример такого брака — документация по Wind32 API: существуют многочисленные функции, реагирующие на одни и те же входные данные по-разному на разных платформах (Win32s, Windows 95 и Windows NT), а документация по таким вещам, как расширенные коды ошибок, возвращаемые функцией GetLastErrorO, поистине ужасна. (Если вы желаете увидеть яркий пример вариаций поведения одного и того же API на разных платформах Win32, обратите внимание на обсуждение функции GetShorl- PathNameO в главе 14 на стр. 486.) А это означает, что во многих случаях вы вынуждены гадать и проводить эксперименты для того, чтобы понять, как на самом деле работают процедуры. И поскольку ни у кого из пас нет времени на тестирование буквально каждого API, нам приходится делать еще больше опасных предположений.
112 Философия разработки ПО лая Windows: микропроблещ «йоверяй, но проверяй» обоснованна: иногда вам действ^ о нет. то случайная ошибка мож*. бо Оборонительное программ[1рование осуществляется по трем основным явлениям, па следующих трех фронтах: 1. Отношение вашего кода к данным, получаемым из ненадежных источников. Именно этим традиционным аспектом часто и ограничивается рассмотрение дайной проблемы. Тем не менее, этот сторона оборонительного программирования, действительно, очень важна Точно так же, как вы делите программы на две взаимоисключающие категории — приватные и публичные, вопрос о целостности данных можно ставить лишь радикально — или все, или ничего. Утверждение о том, что из того или иного источника поступают «в большинстве случаев правильные данные» имеет столько же смысла, сколько заявление о том, что «женщина чуть-чуть беременна». Либо данные являются достаточно надежными для того, чтобы использовать их без проверки, либо нет. И точка. Иногда программисты говорят о неких «доверительных интерфейсах» пли нолуфантастически надежных источниках данных, на которые можно всецело полагаться. Это еще один миф и еще один скользкий аргумент. Да, в некоторых случаях вам действительно не нужно проверять данные, но ведь это так просто — пойти на поводу собственной лени и использовать этот довод для оправдания своего бездействия там, где такие проверки все-таки необходимы. И уж если случилось так, что вы пишете подпрограмхму, которая не проверяет переданные ей параметры, обязательно нужно отметить этот факт в заголовочном комментарии. При этом вовсе не следует превращать этот комментарий в кричащее предупреждение, обрамленное неоновыми звездочками, - вполне достаточно простого однострочного предложения типа // Проверка параметров отсутствует Л если ваш код, помимо комментариев, задокументирован в HLP-фаи- ле или еще где-нибудь, то замечание об отсутствии проверки параметров необходимо включить и туда. Когда вы решаете вопрос о том, как и в каком объеме следуй проверять данные в вашей подпрограмме, используйте следуюшу10 тактику: сначала ищите способы исправить плохие данные, а если это О i * * I невозможно - то, по крайней мере, не допускайте проникновения плохих данных в сердце вашего алгоритма путем отказа от выполнения вызова. Кроме того, вы должны предотвращать передачу пепровереЯ'
сть микроуровневых рекоменлаиий 113 иых данных другим подпрограммам, если только вы не уверены, что они смогут сами справиться с такой ситуацией должным образом. t » KJ На процесс проверки данных вашей программой можно смотреть как на прохождение ею нескольких этапов, на каждом из которых повышается увсреиносгь программы в надежности данных, происхождение которых вначале абсолютно неизвестно. Например, в функции void DoStuff ( int x, char* у ) if((x > max_x) || (x < inm_x) || (y == NULL) || (strlen(y) == 0) ) return BAD_PARAMETER, /* А теперь выполняем необходимую работу */ оператор if, выполняющий проверку параметров, переводит программу из состояния, в котором определенность в отношении данных была I 9 практически нулевой, в такое состояние, когда на правильность данных молено полностью положиться и использовать их дальше без каких-либо проверок. Такой «интервал неопределенности» данных существует в начале каждой функции, а также в любой точке вашей программы, где данные появляются из сторонних источников. До тех пор, пока ваш код или какая-либо вызываемая им функция не ликвидирует этот «интервал неопределенности», он сможет распространять- %j 11 ся по всему коду, оставаясь лазейкой для «заражения крови» вашей программы. Последствия этого могут быть не просто опасными, но и трудными для обнаружения — передаваемые туда-сюда данные в конце концов могут быть сохранены в постоянном хранилище и привести к явным сбоям лишь через несколько дней. (Если вы когда-либо сталкивались с проблемами, порожденными плохими сохраненными данными, и если в конце концов вам удавалось отыскать те места в коде, в которых на самом деле происходила «порча» этих данных, то вы наверняка знаете, какими невероятно тяжелыми, мучительными и долгими бывают порой подобные «расследования».) Конечно же, самым развлекательным источником синтаксически и семантически сложных данных всегда были (и всегда будут) пользователи. Спросите у пользователя его имя, и он обязательно введет номер своего телефона, спросите про номер телефона - он введет свой почтовый адрес, а когда вы спросите про адрес — он вообще случайно нажмет клавишу Enter и выдаст вам пустую строку. Он также может ввести текст, в котором буквы «О» перепутаны с цифрой «О», а в самых неподходящих местах вставлены пробелы. И это всегда будет заботой вашей программы - попытаться обнаружить подобные ошибки и затем либо сконвертировать данные в правильный формат
114 Философия разработки ПО лая Windows: микропроблемы 2 (например, отбросить предваряющие и завершающие пробелы в строке), либо отказаться работать с ними (путем возврата соответст^ вующего кода ошибки или выдачи пользователю ясного диагностического сообщения). Разумеется, есть ситуации, в которых даже простейшие двухшаговые защитные барьеры типа «проверь и катапультируйся» не имеют особого смысла - например, когда ваш код слишком глубоко закопан в логику программы, или когда пренебрежимо малы шансы на то, что возвращаемые вашей функцией коды ошибок будут кем-то использоваться. В таких случаях вы должны с удвоенной тщательностью исследовать код и выяснить, имеет ли все-таки смысл проверять параметры или другие данные, чтобы просто ничего не делать, когда они окажутся неверными. Я знаю, какой безумной может показаться эта рекомендация, но я наяву видел случаи, когда такой подход был оправдай. Тем не менее, такое решение должно приниматься с особой осторожностью, потому что речь идет о таких экзотических ситуациях, когда, с одной стороны, программа наверняка даст сбой, если функция продолжит работу с плохими данными, а с другой стороны, программа сработает корректно, если функция просто не будет ничего делать (код, полностью удовлетворяющий обоим эти условиям, встречается крайне редко, по все же встречается). Кроме того, в этом случае вы должны четко определить, как будет осуществляться возврат управления вашей функцией, и использовать «максимально безопасный» набор % * возв ращасмых значенш [. Если вам все-таки случится писать код такого «безопасного бездействия», обязательно задокументируйте этот факт подробно и ясно, причем одновременно в двух местах -- в точке возврата из функции (вероятно, где-нибудь рядом с вызовом OutputDebugStringO в отладочной версии вашей программы) и в ее заголовочном комментарии. (Кстати говоря, мне кажется, что именно в таких ситуациях чрезмерно усердные программисты и вставляют печально известные диагно- бщеппя типа «Эта ошиб должна Если вдруг вы поймаете себя на том, что собираетесь сделать то ) боты кода б особ Win32 API -- настоящего фонтана сюрпризов. О ность заключается в том, что вы не можете до конца протестировать каждый отдельный API. Практически все, что вы можете делать - это изучать документацию (включая все сообщения Microsoft об об' иаружепных ошибках, публикуемые на их CD Developer Netzvork) Jl тщательно тестировать поведение тех функций, которые наибоЛее
рекоменлаиий критичны в вашем конкретном проекте. В их число, как правило, попадают те функции, которым вы часто передаете непроверенные данные, или результат работы которых вы проверяете при помощи Get- LastErrorO — самого «многоликого» представителя Win32 API. Каждый вам скажет, что не следует рассчитывать на недокументированные побочные эффекты различных API, а тем более не следует использовать их. Это замечательный совет, хотя иногда ему очень трудно следовать — так часто эти досадные побочные эффекты как раз и делают то, что вам нужно (а в некоторых случаях вообще являются единственным способом добиться желаемого). Всеми силами сопротивляйтесь соблазну использовать что-либо недокументированное, если только у вас нет действительно непреодолимой причины делать это. А KJ если такая причина возникла, задокументируйте то, что вы сделали, и объясните, почему вы это сделали. Объяснение должно быть настолько ясным, чтобы всякий мог безошибочно понять, что произошло. Придерживайтесь такой философии, что всякий код за пределами той подпрограммы, которую вы в данный момент пишете, вероятно изобилует коварными ошибками и, наверняка, был написан каким-нибудь лунатиком. Под таким углом вы должны смотреть и на свой собственный код, и на Windows API, и на все остальное окружение. Увы, эта философия поразительно контрастирует с тем предположением, которое обычно делают (как правило, неявно) все программисты — с уверенностью в том, что их программы находятся в центре доброй и любящей вселенной, которая всегда оградит их от сумасшедших пользователей, гамма-лучей и несвежего пива. Но так никогда не было и никогда не будет. Глядя на каждую подпрограмму, которую пишете, вы должны пытаться увидеть все возможные изобретательные (читайте: извращенные) пути ее неправильного использования, а затем постараться перекрыть эти лазейки (если не все, то хотя бы столько, сколько получится при разумных затратах). Ключевым моментом здесь опять является термин «разумно». Вы не должны пытаться достигнуть какого-то фантастического совершенства; вы всего лишь должны пытаться предугадать (то есть предвидеть и принять соответствующие меры предосторожности), каким образом могут возникнуть неприятности. Я припоминаю один коллективный проект, в котором я принимал участие, и в котором мы однажды столкнулись с довольно простой на первый взгляд проблемой: программа упорно и регулярно «ложилась» при попытке совершить некоторые действия над несуществующим файлом. Нас тогда очень заинтриговала причина такого поведения в, казалось бы, элементарной ситуации. Вместе с коллегой я исследовал
116 Философия разработки ПО лля Windows: лликропроблец код, и мы обнаружили фрагмент, написанный другим программист^, и выглядевший приблизительно так (в псевдокоде): file_handle = OpenFile(file_name), гс = ReadFile(file_handle,buffer), while(rc == OK) { ProcessBuffer(buffer), re = ReadFile(file_handle,buffer), CloGeFile(file_handle), Разумеется, проблема была в авторском предположении о том, что файл всегда существует. Первый вызов ReadFileO «проваливался» и возвращал код ошибки, который расценивался как признак конца файла. После этого просто делалась попытка закрыть этот несуществующий файл, и дальнейший код оказывался в опаснейшем заблуждении, предполагая, что файл был успешно прочитан и обработан. 3. Проектирование вашего собственного кода и вытекающие из этого возможные способы его использования другими. Многие программисты возразят мне, что этот вопрос, мол, вовсе не имеет отношения к оборонительному программированию. Правильный дизайн кода, пред- KJ назначенного для вызова его из других частей программы, очень важен, и вы должны стремиться предугадывать возможные ошибки вызывающей стороны точно так же, как предвидеть типичные ошибки в данных, которые ваш код получает. (В главе 10, посвященной оберточным функциям для Win32 API, будет приведено множество моих рассуждений на тему проектирования публичного кода, но именно сейчас я хотел бы сконцентрироваться на тех аспектах проектирования, которые связаны с принципом оборонительного программирования.) Первой линией обороны против неправильного использования вашего кода является документация. Сделайте ее полной и точной, и другим программистам (возможно, включая вас самих) останется очень мало места для ее «творческой интерпретации». Я уже не раз высказывал мое негативное отношение к пресловутым «спорам программистов в кафетерии», тем не менее я не удержусь и замечу, что чем лучше заДО' кументирован ваш код, тем больше у вас шансов выиграть в таком споре, когда другой программист попытается упрекнуть ваш код в #е' корректности. Мне хотелось бы надеяться, что большинство програМ' мистов стоят выше подобных разборок, но, к сожалению, офисная по' литика всегда находит способ проникнуть и в технические области.
микроуровневых рекоменлаиий 117 Вы дол лены всегда задумываться над тем, что может произойти, когда вызывающая сторона игнорирует или неправильно интерпретирует тот индикатор успеха/провала, который возвращает ваша функция. Такое иногда случается, поэтому во многих случаях есть смысл раскошелиться на несколько дополнительных строк кода, которые обеспечат осмысленные (или по крайней мере максимально безопасные) значения для всех выходных данных вашей функции даже тогда, когда она не сможет успешно выполнить свою работу до конца. Небрежное отношение к выходным данным функций может привести к серьезным неприятностям, которые вы можете и не заметить Никогда не делайте бездумно «то, что делают все», и не полагайтесь наивно на «то, что всякий знает» Например, я несметное количество раз наблюдал такое нарушение этого правила: функция, которая, согласно ее описанию, должна возвращать индикатор успеха/провала типа BOOL, на поверку возвращает результат вызова другой аналогичной «булевской» функции Такой прием в особенности чреват при работе с Win32 API, в котором некоторые функции вместо TRUE (определенного как 1) могут возвращать совершенно непредсказуемые ненулевые значения. На первый взгляд, это незначительная проблема, но я так не считаю* если описание вашей функции гласит, что она должна возвращать BOOL, по она возвращает что-либо отличное от TRUE или FALSE, это ошибка. И все тут. (Тот факт, что вы имеете полное право обвинить Win32 API по этой статье, значения не имеет — грехи Microsoft не могут быть оправданием подобного поведения с вашей стороны.) Оборонительное программирование и повторное использование Иода юворя о преимуществах, которые дает повторное использование кода, наиболее часто упоминают тот факт, что вы не только не тратите свои силы и сред- СТва на изобретение колеса, по и приобретаете исключительно хорошо Проектированное и надежно изготовленное колесо. Предполагается, что это го- г°вое колесо представляет собой закаленный в боях код, написанный, по-видимому, с применением тактики оборонительного программирования. Но между оборонительным программированием и повторным использованием кода есть п другая, менее заметная связь. Среди всех причин, по которым Программисты не используют принцип оборонительного программирования, °Рвейшеп является банальная лень (о чем говорит п мой собственный опыт <-ам грешу этим ire меньше других.) Л ведь есть простой и очевидный способ 1а<-Штельно уменьшить своп затраты па правильный подход к делу: составить
118 Философия разработки ПО для Windows: микропроблем!, набор небольших подпрограмм, которые можно будет повторно использовать для проверки и коррекции данных. Как я уже упоминал в самом начале, большая часть представленного в этом книге кода не содержит ни примеров крутого стиля написания, ни сложных за. умных алгоритмов. Но и такой код может успешно предохранить вас и пользователей вашей программы от будущих неприятностей. Перефразируя известное высказывание из совершенно другой области, важно не то, что вы имеете, а то как вы этим пользуетесь. Самыми хорошими кандидатами для набора проверочных функций могут быть маленькие и простые подпрограммы, которые исследуют переданные им данные на предмет соответствия каким-либо форматам или состояниям, а затем возвращают значение тина BOOL, показывающее результат. Неплохие примеры таких функций есть и в самом Win32 API - IsIconicO, IsChildO, IsWindowsO и др. Обратите внимание на принятый в данном случае способ наименования — он не только четко описывает предназначение функции, но и приближает внешний вид кода к естественному языку: if (IsIcomc(f red_hWnd)) SendMeGGage(fred_hWnd.WM_SYSCOMMAND.SC_RESTORE,0), He увлекайтесь составлением вашего набора проверочных функций слишком страстно. Если вы начнете искать все возможные и невозможные подходя- U О t 9 щие случаи для создания очередной маленькой защитной подпрограммы, вы вскоре обнаружите, что занимаетесь уже написанием программистского инструментария, а не самих программ. (Это, кстати, тесно перекликается с одной из хронических проблем C++: программисты порой могут тратить уйму времени на проектирование, реализацию и совершенствование классов, а не на их использование.) Не волнуйтесь, если ваш набор будет более угловатым и менее элегантным, чем хотелось бы. Вы должны позволить ему расти и развиваться органично: как только у вас появится твердая уверенность, что вы в самом деле будете часто использовать ту или иную новую функцию, тогда уделите немного времени на ее написание, тестирование п документацию, и затем включите ее в ваш набор. Зацикленность на поиске новых добавлений не сулит ничего хорошего — вы просто угробите слишком много сил на возню с мелкими деталями и нюансами, которые йотом могут никогда вам не понадобиться. Также не следует беспокоиться, если у вас получаются подпрограммы, не дающие 100-процентной гарантии правильности проверенных данных. Если до- кументировать и применять такие процедуры должным образом, они могут быть весьма и весьма полезны. Хорошим примером такого инструмента может послужить функция LooksLikeCISIDO, проверяющая строку на соответствПе формату идентификатора сети CompuServe (ее код полностью приведен ]l подробно прокомментирован в этой книге, во врезке «LooksLikeCISID: путеШ^' ствле но уровням неопределенности» на стр. 134). Дело в том, что никакое изолированный код не в состоянии абсолютно достоверно определить, являете*1
микроуровневых рекоменлаиий 119 in р0Ка правильным идентификатором CompuServe (единственный способ 111 ггать это -- позвонить в CompuServe и послать электронную почту по соот- ,тВуЮ1дему адресу). Максимум того, что программа реально может чать, — это посмотреть, кажется ли строка соответствующей требуемому iaTy. Такая неточность проверки вполне осмысленна: когда ваша огромна запрашивает у пользователя идентификатор CompuServe, вы просто пываете эту функцию и в том случае, когда она возвращает FALSE, выдаете ' ъзователю диалог с сообщением примерно следующего вида: «Введенный •imii идентификатор CompuServe, возможно, не соответствует правильному формату. Хотите ли вы использовать его, несмотря на это?» Тем самым вы, с одной стороны, предохраняете пользователя от большинства случайных оши- t t бок а с ДРУГ0И ~ оставляете возможность преодолеть параноидальное не- юверпе вашей программы к вводимым данным. (Последнее всегда является . • it отличной идеей в условиях сегодняшнего постоянно меняющегося мира, то и дело порождающего на свет «ложные отрицания».) Кроме того, в диалог следует включить кнопку Help, которая выдаст справочную информацию, пока- о зывающую, как, по мнению вашей программы, должен выглядеть «нормальный» идентификатор CompuServe, и подробно объясняющую, почему введенная пользователем строка вызывает подозрения. Всякий раз, когда я пишу функцию, осуществляющую неточную проверку, я использую для ее имени шаблон LooksLike, поскольку он лишний раз подчеркивает заведомую нестрогость результата (в отличие от имен точных проверочных функций, которые традиционно строятся по шаблону Is)1. Кстати, пример* с функцией LooksLikeCISIDO иллюстрирует еще одну ценную идею: зная особенности потенциальной области1 применения вашей программы и учитывая их, вы повышаете не только надежность своего продукта, но и его интеллектульный уровень в глазах пользователя. В заключение этого раздела, я позволю себе привести примеры тех проверочных подпрограмм, которые я часто использую в своей повседневной работе. Все они включены в программу-иллюстрацию TEST_TB, прилагающуюся к данной главе — эта программа состоит из собственно набора 1[роверочных функций и простого консольного \Ут32-приложения, демонстрирующего эти функции в действии. Построение примера: TEST ТВ Местоположение: http://www.symboLru/russian/library/prof_prog/source/chap03/ toolbox Платформа: Win32 1 * -°oks like » -- «Похож па >> «Is » - «Является » [Примечание переводчика ]
1 20 Философия разработки ПО лля Windows: микропроблеМк Инструкции по сборке: откройте при помощи Visual C++ приложенный МАК-файл и скомпилируйте полученный проект обычным способом. Функция AppendSlashO. При работе с именами каталогов и файлов очень часто встречаются ситуации, когда у вас нет стопроцентной уверенности в наличии завершающего символа обратной косой черты в строке. Это как раз те случаи, когда вам может пригодиться функция AppendSlashO. Алгоритм ее работы прост: она проверяет последний символ в заданной строке и, если он не является обратной косой чертой, дописывает ее в конец строки (предварительно проверяя, достаточно ли в строке места для этого). /////////////////////////////////////////////////////////////////////////// // AppendSlash" Добавляет обратную косую черту ('\\') в конец заданной // строки только в том случае, если для этого есть свободное место, и // если эта строка уже не заканчивается таким символом. // // dest строка, которую нужно проверить и изменить // max_len максимально допустимая длина строки dest, включая завершающий NULL II I/ Проверка параметров // NULL или пустая строка в качестве dest отвергаются // значения max_len, меньшие 3, отвергаются /////////////////////////////////////////////////////////////////////////// void AppendSlach(char *dest, Gize_t max_len) { #ifdef .DEBUG if((dest == NULL) || (*dest == '\0') || (max.len < 3)) OutputDebugStnng('AppendSlach обнаружен недопустимый параметр1\п ), #endif if((dest == NULL) || (*dect == '\0')|| (max_len < 3)) return, if((strlen(dest) + 1 < max_len) && (dest[Gtrlen(dest) - 1] '= '\\')) strlcat(dest, '\\ ,max_len), V Это хороший пример маленькой, производительной подпрограмме которая может уберечь вашу шею (пли другие части тела) даже без ъ% шего ведома. (Не обращайте пока внимания на таинственную функДО$ strlcatO, которая вызывается в конце — о ней я отдельно рассказе гуть позднее.)
рекоменлаиий фупщии stripLeadingi), stripTrailing(), stripLT(). Как я уже упоминал ранее, пробелы в началах и концах строк могут стать источником ужасных головных болей. К счастью, это один из тех случаев, когда вы почти наверняка можете исправить данные самостоятельно — привести их к нужному виду без риска потерять существенную информацию или вызвать какие-либо другие проблемы. Я использую эти три функции в своей повседневной работе на каждом шагу, порой даже тогда, когда они почти наверняка не нужны. /////////////////////////////////////////////////////////////////////////// // stripLeading удаляет лидирующие пробелы и символы табуляции из строки // // Проверка параметров // NULL или пустая строка отвергаются /////////////////////////////////////////////////////////////////////////// void stnpLeading(char *s) { #ifdef _DEBUG if((G == NULL) || (*s == '\0')) OutputDebugStringC'GtnpLeading обнаружен недопустимый параметр1 \п"), #endif if((G == NULL) || (*s == '\0' )) return, char *z = — о while(*z && ((*z == ' ') || (*z == '\t'))) z++, if(G '= z) meminove(G,z.Gtrlen(z) +1), // Безопасно - переполнения не будет } /////////////////////////////////////////////////////////////////////////// // GtnpLT удаляет лидирующие и завершающие пробелы и символы табуляции из строки // // Проверка параметров // NULL или пустая строка отвергаются /////////////////////////////////////////////////////////////////////////// void stripLT(char *g) { #ifdef _DEBUG if((s == NULL) || (*g == '\0')) OutputDebugStnng("GtnpLT обнаружен недопустимый параметр1 \п '), #endif if((G == NULL) || (*g == '\0')) return, GtripLeadmg(G),
1 22 Философия разработки ПО лля Windows: микропробЛе, stripTrailing(c), /////////////////////////////////////////////////////////////////////////// // GtnpTrailing удаляет завершающие пробелы и символы табуляции из строки // // Проверка параметров // NULL или пустая строка отвергаются /////I/////!I//////////////II////I//,'//////////I////I//I///////////I/////// void stripTrailing(char *s) { #ifdef .DEBUG if((5 == NULL) || (*g == АО1)) OutputDebugStnng( GtnpTrailing обнаружен недопустимый параметр1 \п '), #endif if((s == NULL) || (*s == '\0')) return, int z = strlen(s), while((z >= 0) && ((s[z] == ' ') || (s[z] == '\f) II (g[z] == '\0'))) z--, g[z + 1] = 0, В Фупщии-зажимы. Случаются ситуации, в которых вы можете безо пасно скорректировать параметр, насильно приведя его значение i требуемому интервалу (очевидно, подобные трюки необходимо тща тельно обдумывать и совершать осторожно). В особенности такой прием годится для тех случаев, когда «X абсолютно никогда не может быть меньше Y или больше Z» — ведь рано или поздно Z обязательно найдет способ нарушить эту иллюзию и привести вашу программу' хаотическое состояние. /////////////////////////////////////////////////////////////////////////// // Cliplnt принудительно переводит целое число внутрь заданного интервала // // х значение, которое необходимо зажать в интервал // limit"!, limit2 границы интервала для х // ЗАМЕЧАНИЕ параметры lunitl и limit2 не обязательно должны иметь значения // в возрастающем порядке // // Проверка параметров НЕТ (допустимы любые значения) I/III11II/1/1/11//I//III////I/I/I//////I/IIIII111111/1/III/III///III/IIIIII long int Cliplnt(int x, int limitl, int limit2) { long int min_x, max_x, // Вычисляем истинные границы интервала Вызывающий мог передать их в
рекоменлаиий ■9 II обратном порядке, но мы застрахуемся от этого if(limit1 < lnnit2) i inin_x = limitl, max x = lnnit2, else К inm_x = lnnit2, max x = limit"!, \ \ if(x <= min_x) return min_x, else if(x >= inax_x) return max_x, else return x, i Обратите ваше внимание на то, как функция обходится ей границами интервала - вызывающий код может предоставлять их как в возрастающем, так и в убывающем порядке. И это вовсе не праздная мишура. Подпрограмма такого типа может часто использоваться в таких условиях, когда пограничные значения не являются предопределенными константами, а вычисляются по ходу дела (а значит, могут являться ошибочными). Поэтому, прикладывая лишь небольшое дополнительное усилие — делая функцию достаточно умной для работы с границами произвольного вида, мы надежно предохраняемся от возможных мерзких, трудновоспроизводимых проблем в будущем. Подпрограммы для безопасной работы со строками (строками фиксированной длины, завершаемыми символом NULL). Стандартные библиотечные функции языка С, предназначенные для различных манипуляций со строками (strcatO, strcpyO и др.), предоставляют просто-таки безграничные возможности для возникновения проблем, поскольку они либо вообще не заботятся о безопасности своих действий, либо делают это неудобными способами. Я выкручиваюсь из этого при помощи своего набора функций, которые знают о длине той строки, в которую производится копирование, добавление и тому подобные действия. Эти функции никогда не переполнят выходной буфер и никогда не оставят его в незавершенном состоянии (как это делает в некоторых случаях strncpyO). Конечно же, для использования этих функций требуется определенная сила воли Я не-
124 Философия разработки ПО лля Windows: микропро6лем * t однократно выслушивал от других людей возражения против щ функций, которые сводились к следующему тезису: их нельзя испо-, зовать все время, потому что иногда максимальная допустимая дд11н строки неизвестна. И в ответ на подобные заявления я каждый раз 3 О давал людям простои вопрос: если вы не знаете, насколько длинц0, может быть выходная строка -без риска перейти границу отведенног для нее участка памяти и испортить другие данные, то какого дьявоч вы копируете в нее данные или добавляете к Heir другую строк} почему вы считаете, что делать это безопасно? (Обычно за этцу вопросом следует классическая немая сцена.) Кроме шуток, я повсюд\ нахожу подобные приемчики в рабочем коде — подпрограмма по лучает указатель па строку (но не получает ее максимально допустимую длину) и начинает производить над этой строкой те или иные действия, которые запросто могут привести к ее удлинению. Такая прак тика, очевидно, является отвратительной, и ее следует объявлять вне закона. Я хотел бы подчеркнуть: я вовсе не говорю, что вы должны использо вать эти функции всегда. Бывают ситуации, в которых можно обойтись стандартными библиотечными функциями С без какого-либо риска, - когда у вас есть какие-либо гарантии того, что выходная строка не мо жет быть переполнена. Тем не менее, в нормальных условиях я все равно использую мои безопасные строковые функции везде, поскольку любые сегодняшние гарантии пе вечны — кто-нибудь когда-нибудь внесет в программу такие изменения, которые сделают мое нынешнее предположение ошибочным, и в тот же миг в коде образуется трудноуловимая лазейка для чрезвычайно опасных сбоев. Когда речь идет о подобных подпрограммах «безопасной работы со «.» \j строками», возникает один типичный и немаловажный вопрос: а что следует делать, когда выходной буфер и вправду окажется недоста точно большим? Должна ли функция заполнить буфер до конца, или ей лучше вообще отказаться от работы? Ответ и прост, и сложен од t новременно: все зависит от конкретной ситуации в вызывающем коде Легко представить себе реальные примеры, в которых и то, и друг° поведение будет правильным. Я решаю эту проблему путем предостав ления отдельных функций, работающих по каждому из возможны^ сценариев. Те функции, имена которых имеют суффикс «Atomic», 6} дут работать только в том случае, если им удастся полностью за кончить необходимые операции. (Выбор такого суффикса призва1 подчеркнуть, что эти функции работают в манере «все или ничего»»' не то, насколько мощно они взрываются при получении неправильна параметров.)
рекоменлаиии ц/l/l I ll/l////////I//I//I//////1////////I//III////////11/11//ПИ l/l III/If // ctrlcat Прицепляет строку зге к концу строки dest с учетом максимально // допустимой для dest длины (max_len) и всегда обеспечивает должное завершение. // Эта функция прицепит к dest столько символов, сколько в нее поместится // // шах_1еп включает завершающий NULL // // Возвращает TRUE, если прицепление произошло, и FALSE - в противном случае // // Проверка параметров // NULL или пустая строка в качестве src или dest отвергаются // Значения шах_1еп, равные 0 или меньшие начальной длины dest, отвергаются И///////!///!/////!///////////////////1/1/I///I//////I//I/I/II1/1II/III III BOOL strlcat(char *dest, const char *src, size_t max_len) { #ifdef _DEBUG if((dest == NULL) || (src == NULL) || (max_len ==0) j| (strlen(dest) >= max_len - 1)) OutputDebugString("strlcat обнаружен недопустимый параметр1\п*'), #endif if((dest == NULL) || (src == NULL) || (max_len == 0)) return FALSE, UINT d_len = strlen(dest), if(d_len >= max_len - 1) return FALSE; strncat(dest,src.max_len - d_len - 1), return TRUE, I//II/IIIIII////IIIIIIII//III/I/I//II/I//I/////I/II/II/II/I/II/I//IIIII/I/I I/ GtrlcatAtomic. Прицепляет строку src к концу строки dest с учетом максимально // допустимой для dest длины (max_len) и всегда обеспечивает должное завершение // Эта функция сработает только тогда, когда все символы из src поместятся в dest // // max_len включает завершающий NULL // // Возвращает TRUE, если прицепление произошло, и FALSE - в противном случае // // Проверка параметров // NULL или пустая строка в качестве src или dest отвергаются // Значения max_len, равные 0 или меньшие начальной длины dest, отвергаются /////////////////////////////////////////////////////////////////////////// BOOL strlcatAtomic(char *dest, const char *src, size_t max_len) { #ifdef _DEBUG if((dest == NULL) || (src == NULL) || (max_len ==0) || (strlen(dest) >= max_len - 1))
Философия разработки ПО лля Windows: лликроп OutputDebugStnng( strlcatAtomic обнаружен недопустимый параметр1\п'), #endif if((deot == NULL) || (src == NULL) || (max_len == 0)) return FALSE. UINT d_len = Gtrlen(deot) if(d_ien + Gtrlen(Grc) >= max_len - 1) // Проверяем, все ли символы влезут return FALSE Gtrncat(deGt,src max_len - d_len - 1), return TRUE, \ /////////////////////////////////////////////////////////////////////////// // otrlcpy Копирует символы из строки ore в строку dect с учетом максимально // допустимой для dest длины (шах_1еп) и всегда обеспечивает должное завершение // Эта функция поместит в deGt столько символов, сколько в нее поместится // // max_len включает завершающий NULL // // Возвращает TRUE, если копирование произошло, и FALSE - в противном случае // // Проверка параметров // NULL или пустая строка в качестве ore или deGt отвергаются // Значение тах_1еп, равное 0, отвергается /////////////////////////////////////////////////////////////////////////// BOOL Gtrlcpy(char *dest, const char *src, size_t max_len) \ ffifdef _DEBUG lfUdest == NULL) || (src == NULL) || (maxJLen == 0)) OutputDebugString( strlcpy обнаружен недопустимый параметр1\п ), #endif if((dest == NULL) || (зге == NULL) || (max_len == 0)) return FALSE, *dest = '\0', return Gtrlcat(dest,зге,max_len), /////////////////////////////////////////////////////////////////////////// // GtrlcpyAtomic Копирует символы из строки зге в строку dest с учетом максимально // допустимой для dest длины (max_len) и всегда обеспечивает должное завершение // Эта функция сработает только тогда когда все символы из зге поместятся в dest // // max_len включает завершающий NULL // // Возвращает TRUE, если копирование произошло, и FALSE - в противном случае //
ых рекоменлаиий II Проверка параметров // NULL или пустая строка в качестве ore или dest отвергаются // Значение max_len, равное 0, отвергается BOOL oTrlcpvAtomic(char *dest const char *src, size_t max_len) #ifaef _DEBUG if((dest == MULL) || (sre == NULL) || (max_len == 0)) OutputDebugString( "strlcpyAtoimc обнаружен недопустимый параметр'\п ), #endif if((deot == NULL) || (crc == NULL) || (max_len == 0)) return FALSE. *dest = \0' return GtrlcaTAtomic(dest src,max_len), /////////////////////////////////////////////////////////////////////////// // ctrlncat Прицепляет не более п первых символов из строки crc к концу // строки dest с учетом максимально допустимой для dest длины (шах_1еп) // и всегда обеспечивает должное завершение Эта функция прицепит к dest // столько символов, сколько в нее поместится (но не больше п) // // шах_1еп включает завершающий NULL // II Возвращает TRUE если прицепление произошло, и FALSE - в противном случае // / / / Проверка параметров // NULL или пустая строка в качестве crc или deot отвергаются // Значения шах_1еп равные 0 или меньшие начальной длины deot отвергаются /////////////////////////////////////////////////////////////////////////// BOOL otrlncat(char *dest, const char *src, size_t max_len, size_t n) { ftifdef _DEBUG if((dest == NULL) || (sre == NULL) || (max_len ==0) || (strlen(dest) >= max_len - 1)) OutputDebugString( "ctrlncat обнаружен недопустимый параметр1\п ), tfendif if((dest == NULL; l| (sre == NULL) || (maxjen == 0)) return FALSE, UINT d_len = strlen(dest), if(d_len >= max_len - 1) return FALSE, // Достаточно ли в dest места для п символов7 if(max_len - d_len - 1 >= n) strncat(dest,stс,n), // Да копируем все п символов else
Философия разработки ПО лля Windows: микропробле otrncat(de3t,ore max_len - d_len - 1). // Нет копируем только то, что влезет return TRUE, \ l/ll/l/ll/l/UIIII/I/III/I////II//I////I//II//I/I//I//I/I/I////I////////III II GtrlncatAtoinic Прицепляет не более п первых символов из строки сгс к концу // строки deot с учетом максимально допустимой для dect длины (шах_1еп) // и всегда обеспечивает должнде завершение Эта функция сработает только тогда, // когда все символы из сгс (или хотя бы первые п из них) поместятся в dect // // max_len включает завершающий NULL // // Возвращает TRUE, если прицепление произошло, и FALSE - в противном случае // // Проверка параметров // NULL или пустая строка в качестве зге или deot отвергаются // Значения шах_1еп, равные 0 или меньшие начальной длины deot, отвергаются I/II/I/I/IIIIIIII/III//I/I/II IIII/II//II/IIIll/Ill III/IIII/I/I//IIII//II//I BOOL GtrlncatAtomic(char *dect, conct char *src, cize_t max_len, cize_t n) ttifdef .DEBUG if(<dest == NULL) || (ore == NULL) || (max_len ==0) || (strlen(dect) >= inax_len - 1)) OutputDebugStringC'GtrlncatAtomic обнаружен недопустимый параметр1\п ), #endif if((dest == NULL) || (crc == NULL) || (max_len == 0)) return FALSE, if(max_len - Gtrlen(dect) -1 < n) // Проверяем, все ли поместится return FALSE, ctrncat(deGt,crc,n), // Да копируем все п символов return TRUE; i /////////////////////////////////////////////////////////////////////////// // Gtrlncpy Копирует не более п первых символов из строки зге в // строку dect с учетом максимально допустимой для dect длины (шах_1еп) // и всегда обеспечивает должное завершение Эта функция скопирует в dect // столько символов, сколько в нее поместится (но не больше п) // // max_len включает завершающий NULL // // Возвращает TRUE, если копирование произошло, и FALSE - в противном случае // // Проверка параметров // NULL или пустая строка в качестве сгс или dest отвергаются // Значение max_len, равное 0, отвергается
микроуровневых рекоменлаиии 129 а BOOL Gtrlncpy(char *dectt const char *orc, cize_t max_len, Gize_t n) ) rflfdef _DEBUG if((dest == NULL) || (src == NULL) || (maxJLen == 0)) OutputDebugStnng( otrlncpy обнаружен недопустимый параметр'\п ); #endif if((dest == NULL) || (crc == NULL) || (max_len == 0)) return FALSE. *dest = '\0', return ctrlncat(deot,src,max_len,n), l/lllll/l/////////////!//Ill/Ill I///////III//////1//////////1////////////// // GtrlncpyAtomic Копирует не более п первых символов из строки зге в // строку dect с учетом максимально допустимой для dect длины (тах_1еп) // и всегда обеспечивает должное завершение Эта функция сработает только // тогда, когда все символы из зге (или хотя бы первые п из них) поместятся // в dest // // max_len включает завершающий NULL // // Возвращает TRUE, если копирование произошло, и FALSE - в противном случае // // Проверка параметров // NULL или пустая строка в качестве зге или dest отвергаются // Значение max_len, равное 0, отвергается /П1///1///////1//II//////////III//////////II/////1///1/////////II/Hi/III/ BOOL strlncpyAtomic(char *de3t, const char *згс, size_t max_len, cize_t n) t #ifdef _DEBUG if((dest == NULL) || (зге == NULL) || (max_len == 0)) OutputDebugStnng( 3trlncpyAtomic обнаружен недопустимый параметр1\п ), #endif if((dest == NULL) || (зге == NULL) || (max.len == 0)) return FALSE, *dest - '\0' , return 3trlncatAtomic(dest, 3t'C,inax_len n)1 Функция CopyFileForccRW(). Несмотря на предельную простоту этой функции, пользователи наверняка скажут вам спасибо за ее применение. По сути, она является слегка усовершенствованным вариантом функции CopyFileO из Win32 API: она просто вызывает CopyFileO, передавая ей свои параметры, а затем делает выходной файл доступным для чтения/записи. Без этого дополнительного действия файлы, скопированные с CD, сохраняли бы своп атрибуты,
1 30 Философия разработки ПО лля Windows: микропроблем. включая бит read-only. И если вы забудете про этот факт, недостуц ность выходного файла для модификации может раздражать пользова телей (да и ваши собственные программы тоже). IIIIIIIII III IIIП1/1/III/!Ill/llIII/IIII/II III///III II III III/l/lIII IIII/III II CopyFileForceRW Использует CopyF-ile API для копирования файла, а затем // делает выходной файл доступным для чтения/записи // // Возвращает TRUE, если копирование произошло и атрибут результирующего // был успешно установлен в R/W В противном случае, возвращает FALSE // // Проверка параметров // NULL или пустая строка в качестве сгс или dect отвергаются IIII/IIII/IIIIIIIIIIIII/IIII/IIII/IIIIIIIIII/IIIIIIIIIIIIIIIIIIIII/IIII/III BOOL CopyFileForceRW(char *src, char *deot, BOOL copy_flag) { #ifdef .DEBUG if((dest == NULL) || (etc == NULL) || (*dest == '\0') || (*src == '\0')) OutputDebugString('CopyFileForceRW обнаружен недопустимый параметр1\п '), #endif if((dest == NULL) || (зге == NULL) || (*dect == '\0') I I (*src == '\0')) { SetLaGtError(ERROR_INVALID_PARAMETER); return FALSE; > II Все остальные вызываемые функции используют SetLactError(), // поэтому мы отдаем код ошибки им на откуп if('CopyFile(3rc,dest,copy_flag)) return FALSE, DWORD fa = GetFileAttnbutec(deGt), if(fa ' = Oxffffffff) { fa &= ~FILE_ATTRIBUTE_READONLY. return (SetFileAttributeG(deGt.fa) '= 0), return FALSE, v В Функции FileExistsi) и DirExists(). Эти две еще более просты1 подпрограммы могут спасти шею вашей программы (кстати, на т°' случай, если вы вдруг этого не знаете: шея программы — это незаД0 кументироваиная часть заголовка исполняемого файла в формате РЬ' Все, что они делают — это возвращают значение типа BOOL, котор0' указывает, существует ли на самом деле файл или каталог, имя ^° торого было передано этим функциям.
рекоменлаиий Несмотря на простоту этих функций, между ними есть небольшие, но существенные различия. Функция FileExistsO вернет FALSE, если объект существует, но является каталогом. Аналогично, DirExistsO вернет FALSE при «встрече» с существующим файлом. Это различие отражает предполагаемые условия использования этих подпрограмм: вам следует использовать FileExistsO только для проверки строк, которые далее будут использоваться для передачи имен файлов, a DirExistsO аботы Обратите внимание на финальный оператор return в DirExistsO — на то, как эта функция прилагает специальные усилия, чтобы в случае ус- <-» *.» пеха не просто вернуть ненулевой результат логической операции AND, а выполнить строгое правило: «булевская функция» обязана возвращать лишь одно из двух возможных значений - либо TRUE, либо FALSE. /////////////////////////////////////////////////////////////////////////// // FileExists Проверяет предположение о том, что заданный файл // существует и НЕ является каталогом // // Возвращает TRUE, если файл существует, или FALSE - если файл // не существует или является каталогом. // // Проверка параметров // NULL или пустая строка отвергается /////////////////////////////////////////////////////////////////////////// BOOL FileExiGts(const char *fn) { #ifdef _DEBUG if(fn == NULL || (Gtrlen(fn)) == 0) OutputDebugString( 'FileExicts обнаружен недопустимый параметр1\п"), #endif if(fn == NULL || (strlen(fn)) == 0) return FALSE, DWORD dwFA = GetFileAttributes(fn), if(dwFA == OxFFFFFFFF) return FALSE, elce return ((dwFA & FILE_ATTRIBUTE_DIRECTORY) ■= FILE_ATTRIBUTE_DIRECTORY), } // DirExistG Проверяет предположение о том, что заданный файл // существует и является каталогом // // Возвращает TRUE, если файл существует и является каталогом, // в противном случае возвращает FALSE
1 32 Философия разработки ПО лля Windows: микропрорЛ(, / 7 // Проверка параметров // NULL или пустая строка отвергается /////////////////////////////////////////////////////////////////////////// BOOL DirExict3(const char *dn) i #ifdef _DEBUG if((dn == NULL) || (*dn == '\0')) OutputDebugString( DirExictc обнаружен недопустимый параметр1\п ), ffendif if((dn == NULL) || (*dn == '\0*)) return FALSE, DWORD dwFA = GetFileAtttlbutes(dn), if(dwFA == OxFFFFFFFF) return FALSE, else return ((dwFA & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY), в Функции AliasToLFN() и LFNToAlias(). Признаться, эти две фущ цип не имеют отношения к оборонительному программированию. 0} нако они, как минимум, являются ближайшей родней всем остальньг подпрограммам, описанным в этом разделе, поскольку помогают еде лать вашу программу немного более интеллигентной. Эти функции за файлов н обратно. Обе файла, отбрасывая им; ф Наибольшую пользу эти функции могут принести в такой ситуации когда вы имеете имя файла неопределенного вида и желает преобразовать его к требуемому формату (например, для помещения постоянное хранилище данных или для показа пользователю). Если файл с указанным именем не существует, функции возвращаю FALSE. Это отражает тот факт, что они, по определению, служат и для абстрактной алгоритмической трансформации предложенной $ строки, а для поиска другого, уже существующего имени файла. А# операция не имеет смысла, когда сам файл не существует. I III/III//IIIIIll/Ill 11/11II///IIIIII/1111IIIIIIII/I/III//IIIII/II/IIII//I/ I/ AliasToLFN Конвертирует имя файла из формата 8 3 в его длинное имя // Возвращаемое имя содержит только локальную часть полного имени файла, //те НЕ включает путь // Эта функция сработает ТОЛЬКО в том случае, если файл существует // // lfn_length включает завершающий NULL
рекоменлаиий II /I В случае успеха возвращает длину возвращаемого длинного имени файла, // в противном случае возвращает -1 // // Проверка параметров // значения NULL в качестве указателей на строки отвергаются // lfn_length, меньшая МАХ_РАТН, отвергается int AliasToLFN(const char *alias, char *lfn, UINT ifn_length) #ifdef .DEBUG if((lfn == NULL) || (alias == NULL) || (lfn_length < MAX_PATH)) OutputDebugStringC AliasToLFN обнаружен недопустимый параметр1\п'), #endif if((lfn == NULL) || (alias == NULL) || (lfn_length < MAX_PATH)) return -1, WIN32_FIND_DATA temp, HANDLE search_handle = FindFirstFile(alias,&temp); if(search_handle == INVALID_HANDLE_VALUE) { lfn[0] = 0x0, return -1; } else { о trlcpy(lfn,temp cFileName,lfn_length - 1); FmdClose(search_handle), return strlen(lfn), } I /////////////////////////////////////////////////////////////////////////// // LFNToAlias Конвертирует длинное имя файла в формат 8.3 // Возвращаемое имя содержит только-локальную часть полного имени файла, //те НЕ включает путь. // Эта функция сработает ТОЛЬКО в том случае, если файл существует // // alias_length включает завершающий NULL // // В случае успеха возвращает длину возвращаемого псевдонима файла, // в противном случае возвращает -1 // // Проверка параметров // значения NULL в качестве указателей на строки отвергаются // alias_length, меньшая МАХ_РАТН, отвергается int LFNToAlias(const char *lfn, char *alias, UINT alias_length)
134 Философия разработки ПО лля Windows: микропробл ем { #ifdef _DEBUG if((lfn == NULL) (alias == NULL) (aliac_length < 13)) OutputDebugStnngC LFNToAliac обнаружен недопустимый параметр'\п* ), #endif i (alia о = NULL) if((lfn == NULL) return -1; WIN32_FIND_DATA temp, HANDLE Gearch_handle = FindFirstFile(lfn.&temp), if(search_nandle == INVALID_HANDLE_VALUE) (aliac_length < 13)) i alias[0] = 0x0, return -1, else { О trlcpy(alias,temp cAlternateFileNaine,aliac_length - 1), FindCloce(search_hanclle),, return Gtrlen(alia3) } Возможно, это только моя личная проблема, но я нахожу любопытным! одновременно досадным тот факт, что в таком богатом и сложном API как Win32, не нашлось места для функций, выполняющих перечислений выше простые операции. Конечно, там имеются некоторые экземпляры которые кажутся близкими к тому, что хочется. Например, функция го имени GetShortFileNameO, которая, к сожалению, сама путается в собст венных причудах. Или другая вице-мисс — GetFullPathNameO, котора* всего лишь берет заданное имя файла и использует его для построен^ полного пути, используя при этом имена текущего диска и текущего ката лога (я вообще назвал бы эту функцию иначе например PrependCiuTenlDirO — чтобы более точно описать ее деятельность) LooksLikeCISID: путешествие по уровне неопределенное^ Ситуации, в которых вы можете и должны проверять данн$ (и даже корректировать их), встречаются очень часто. А еще ^ вают случаи, когда, для удобства ваших пользователей, вы ДоЛ* ны делать лишь «трезвую оценку» правильности данных и, еС'1 они оказываются подозрительными, задавать пользователю вопр0 типа «Вы действительно хотите использовать их?». Хоро$1[ примером такой ситуации является исследование строки на пре' мет соответствия ее формату идентификатора CompuServe. Эт°
иКроуровневых рекоменлаиий 135 пример интересен еще по двум причинам: во-первых, он наглядно демонстрирует, как программа может заработать себе дополнительные очки на знании специфики области ее применения; во- %j вторых, конкретная реализация этой проверки, представленная L> <J ниже, служит неплохой иллюстрацией к методу «постепенного сужения интервала неопределенности» данных, о котором я недавно упоминал. Что мы знаем об идентификаторах CompuServe? Все они Kf имеют следущии вид: <набор цифрхточкахнабор цифр> В них не встречаются ни буквы, ни пробелы, ни другие знаки пунктуации, кроме точки (которая, кстати, может находиться только где-то посредине строки). Какой длины бывают эти идентификаторы? Самый длинный идентификатор CompuServe, который я видел до сих пор, состоял из одиннадцати символов (шесть цифр, точка и еще четыре цифры), а самый короткий — из семи (пять цифр, точка и еще одна цифра). Если мы будем считать подозрительными все строки, длина которых больше двенадцати или меньше пяти, это будет безопасно. (Помните, постановка задачи не требует 100-процентной уверенности, поскольку мы всего Kf лишь делаем синтаксический анализ строки, а не проверяем ее на точное совпадение с действительным идентификатором CompuServe.) i Обратите ваше внимание на то, что код функции LooksLike- CISIDO построен по очень простой схеме, удобной для многих других подобных проверок: отталкиваясь от предположения, что строка правильна, подпрограмма последовательно проверяет несколько условий и немедленно отвергает строку (возвращает FALSE), как только любое из этих условий не выполняется. И если управление доходит до конца процедуры, значит строка успешно прошла все тесты, и функция может вернуть TRUE. // LooksLikeCISID Синтаксически проверяет заданную строку и выясняет, ' м°жет ли она представлять собой правильный идентификатор CompuServe / / // Тесты // /' 1 / Длина должна находиться в интервале от 5 до 12, включительно * * Должна присутствовать ровно одна точка, но ие в начале и не в конце ^ Все остальные символы могут быть только цифрами (0-9) 0звращает TRUE, если строка успешно проходит все вышеописанные тесты ' пРотивном случае возвращает FALSE
ия разработки ПО лля windows: микропробл // Проверка параметров // NULL или пустая строка отвергаются /////////////////////////////////////////////////////////////////////////// BOOL LookGLikeCISID(conGt char Aid) { #ifdef .DEBUG if((id == NULL) || (*id == '\0')) OutputDebugStnng('LooksLikeCISID 'обнаружен недопустимый параметр'\п ), #endif // Первая проверка if((id == NULL) || (*id == '\0')) return FALSE, char local_id[MAX_PATH], // Делаем локальную копию и 'очищаем ее strlcpy(local_id,id,sizeof(local_id)), 3tripLT(local_id). // Грубая проверка длины if((3trlen(local_id) < 5) || (strlen(local_id) > 12)) return FALSE, mt commas = 0, // Сканируем (в один проход) и ищем повторную точку или // какой-нибудь нецифровой символ for(UINT i=0, 1 < strlen(local_id), i++) \ if(local_id[i] == '.') // Точка не может находиться ни в начале, ни в конце if((i ==0) || (l == strlen(local_id) - 1)) return FALSE, commas++, // He слишком ли много точек9 if(commas '= 1) return FALSE, i / else if('isdigit(local_id[i])) return FALSE, I /I Все тесты пройдены, значит она похожа на CIS ID return TRUE, 12. Выделяйте код, отвечающий за пользовательский интерфейс вашей программы Настало время обсудить еще одну проблему, которая, казалось бы, доЛ*г быть причислена к разряду достаточно общих. В самом деле, разве в наш5' 4* объектно-ориентированных технологий и повторного использования не Я01 ется совершенно естественным требование отделять друг от друга ^ отвечающий за пользовательский интерфейс, и код, который выпоЛ^'
микроуровневых рекоменлаиий Л э/ нуЮ работу» вашей программы? Конечно, да. Но иногда следование ^вС г пр^в11ЛУ тРе^Ует очень больших объемов рутинной работы, что нередко j!° лит к «срезанию углов», помещению кода туда, куда не следует, и *!Р 1едствгш — к серьезным проблемам при сопровождении и поддержке. Я замечал, что на практике подобные провалы чаще всего происходят 0 в Windows-программировании и, что интересно, на противоположных V 1 1 они** спектра технологии: g В программах, написанных на С (не на C++), которые работают старомодными способами, не используют каркасных библиотек и ведут рукопашный бой с API за каждую его услугу. С точки зрения организации, эти программы часто представляют собой кошмарные зрелища, а их исходные файлы — чудовищные нагромождения самых разнообразных функций и данных, не имеющих никакой логической связи друг с другом. Когда приходит время сопровождать такой код, программист обычно вынужден тратить кучу времени на выяснение ис- KJ тинных взаимосвязей между всеми этими многочисленными элементами (функциями, константами и переменными). И даже простейшие задачи по сопровождению — исправление небольшой ошибки порой или добавление незначительной новой функциональности — приходится делать варварскими способами: переносом (или, еще хуже, копированием) кода из одной функции в другую, добавлением параметров к старым функциям и т. п. Любой из этих способов — прямая дорога к появлению одной или нескольких новых ошибок, опасность которых может быть просто несоизмерима с тем, ради чего все затевалось. В В программах, написанных на C++, вовсю использующих многочисленные вспомогательные средства интегрированых сред разработки — мастеров (wizards), экспертов (experts) и пр. - для выполнения той работы, которую я называю ассенизацией Windows-программ. Даже такая простая вещь, как установление связей между событиями Windows и соответствующими фрагментами вашего кода, может отнимать много сил п приводить к ошибкам (спасибо за это возвратным вызовам (callbacks), разнообразию форматов сообщений, многочисленным соглашениям и исключениям из них и т. д.). Одно из сильнейших разочаровании в Windows-программировании мы испытываем, когда часто добавляем в программу стопроцентно корректный код, а при последующем тестировании обнаруживаем, что этот код втихомолку игнорируется только лишь потому, что мы неправильно сделали соответствующий участок канализации. Конечно, каркасные библиотеки и нитрированные среды разработки оказываются существенной подмогой в этой области — вы просто бросаете на диалоговое окно кнопку, а затем просите среду создать для вас функцию, которая будет получать Управление всякий раз, когда пользователь эту кнопку нажмет; более с
1 38 Философия разработки ПО лля Windows: микропробл^ того, эта функция будет автоматически показана в окне редактор^ курсор установится прямо на ее тело — пиши не хочу. Полный р01(' ролл. И вот тут-то обрадованного программиста поджидает главц- опасность — соблазнившись простотой и автоматизмом предлагаем/ средой схемы, он начинает писать код прямо здесь, не отходя от касс (естественно, кому охота после такого праздничного фейервет мучиться с созданием новых файлов или функций в других файлах')) В обоих случаях, обсуждаемая проблема явно оказывается смещенной и микроуровень. Благодаря недостатку предварительной проектировочной рабс ты во многих Windows-проектах, программист нередко принимает решение г реализации той или иной детали программы прямо за клавиатурой, когда паль цы так и норовят поскорее отстучать на клавишах код — то есть в самом опас ном для программиста состоянии. Всякий раз, когда это возможно, вы должны строго придерживать» следующего правила: все участки кода, необходимые для выполнения «реаль пой работы» вашей программы, следует помещать в отдельные объекты комщ ляции, отличные от тех, в которых реализуется пользовательский интерфейс И когда интерфейсному коду потребуется запустить в дело какой-нибудь меха иизм или обратиться к каким-то данным, это должно происходить через как можно меньшую щель в стене, разделяющей интерфейс и выполнение. Например, некоторые программисты встанут на дыбы при виде такого кода // Обработка нажатия кнопки Reset в нашем диалоге void CLoadDlg -OnResetQ { ResetAllDataO, // Выполняем все действия EndDlg(ID_OK); // Выходим, закрывая диалог \ потому, что они считают глупостью иметь целую функцию лишь для того, что бы вызвать из нее другую функцию и затем закрыть диалог. Я никак не мог) согласиться с их точкой зрения. Более того, я бываю просто счастлив видет* именно такой подход в том коде, который мне предстоит модифицировать, по тому что эта «глупость» не только облегчает мою работу, но и вносит сущ#т венный положительный перспективный вклад в качество программы в целом IJ. Используйте умные файлы данных Если вы захотите начать религиозную войну между программистами, Tl сможете добиться гораздо худшего, если поднимете эту тему. Некоторые про^1 не видят никакой ценности в усилиях по изобретению и использованию спеШ1а лизироваиных, автоматически распознаваемых форматов для хранения данй^ в файлах. Если их программа должна хранить настроечные параметры, то о*11 просто помещаются в INI-фанл или реестр, а данные в традиционном, ДоК-
микроуровневых рекоменлаиий 139 ориентированном смысле небрежно сваливаются в бинарные файлы. >1С тпе программисты, включая меня самого, наоборот, считают что такие уси- < uMfci°T смысл. И именно такие автоматически распознаваемые файлы я на- аК) «умными файлами данных». Каждому программисту наиболее знаком такой пример умного файла дан- как исполняемый файл, хотя многие даже никогда не смотрели на него Таким углом зрения. В частности, новый РЕ-формат исполняемых файлов ПОД i \Viii32-nporPaMM помимо кода и сегментов данных содержит массу дополни- тель ной информации. Чтобы не обвинять меня в чрезмерном сгущении красок при т аком отождествлении, вспомните, пожалуйста, что в самом начале своей по кизни в системе ваша программа действительно представляет собой не более чем обычный файл данных. Пользователь совершает двойной щелчок мышью вашей программе (или по файлу, ассоциированному с ней), заставляя Windows загрузить и запустить ваше приложение. Но для того, чтобы выполнить ваш приказ, Windows должна сделать кучу дел: сначала убедиться, что указанный файл в самом деле является исполняемым файлом; затем проверить, соответствует ли указанная в этом файле «ожидаемая версия Windows» данной системе (если, конечно, вы не имеете дела с Win32s, которой наплевать на такие юнкости); после этого Windows должна разобраться со всеми неявными ссылками на динамические библиотеки, которые нужны вашей программе, и % 9 совершить еще уйму других мелких операции, прежде чем ваше творение типа «Hello, World» сможет открыть рот и вообще начать жить. Умные файлы данных точно так же необходимы и в более традиционной области - для хранения данных большинством программ. Именно они позволяют различным текстовым редакторам, базам данных, графическим программам успешно использовать и создавать файлы в самых разнообразных специальных форматах (в том числе в форматах, применяемых конкурирующими программами) и не полагаться на какие-либо другие, ненадежные средства опознавания (такие, как соглашения о наименованиях фай- л°в, например). Важно отметить, что способ хранения данных программой оказывает нема- юважиое влияние на ее качество, и в первую очередь — на надежность. Если ПРограмма хранит свои настроечные параметры в lNI-файле (то есть в чисто 1екстовом файле), то ей очень легко сделать подножку — достаточно какому- Ш1оУдь авантюристу-пользователю начать развлекаться со своим любимым тек- 1°вым редактором, открывая им все, что попало. (Кстати, хранение данных в i CTPe Windows - в какой-то степени еще более рискованное дело. Ведь Win- XXs поставляется со специальной программой для редактирования реестра. v HbI Думаете, какую мысль первой спровоцирует этот факт в голове пользо- едя?) Все это является еще одной причиной, по которой я до сих пор 'Иось фанатом бинарных конфигурационных файлов, несмотря на их ка- - чмоея старомодность. При должном обращении с ними, они могут быть
1 40 Философия разработки ПО лля Windows: микропроблелл от эффективнее и немного безопаснее INI-файлов или реестра, хотя бы гютоь, что у пользователей не будет достаточно удобного способа их покалечить, (о правда, видал и таких людей, которые занимались редактированием DLL-ф^ лов при помощи текстовых редакторов, даже не обращая внимания на то, ilT( их экраны были заполнены россыпью бинарной абракадабры вокруг несколь ких байт членораздельного текста*. К сожалению, в природе нет способов говорить человека от таких проделок, однако большинство пользователей загрузив в Notepad бинарный файл, все-таки сумеет быстро понять, что ощ вторглись в запретную зону, и что лучше будет ретироваться.) Короче говоря, все ваши файлы данных — будь то хранилища документов или настроечных параметров - должны быть автоматически опознаваемыми (или, как минимум, надежно проверяемыми на корректность формата и целостность данных), а ваши программы должны умело и грамотно этим пользоваться. Кроме того, все ваши бинарные файлы данных должны проектироваться так, чтобы быть готовыми к новым (необязательно предсказуемым заранее) изменениям. (Последнее требование, на самом деле, гораздо проще, чем кажется на слух, но подробнее об этом и о самой проблеме «умных файлов данных» я расскажу в главе 9.) 14L Элегантна реагируйте на сюрпризы со стороны Все мы привыкли совершать в нашем коде целый ряд простых ритуальных проверок — таких, как проверка успешного выполнения запроса на размещение памяти (перед тем, как использовать возвращенный нам указатель), выяснение, действительно ли открылся файл, который мы собираемся читать или изменять, и так далее. При программировании для Windows в число таких ритуалов также входят проверка дескрипторов (handles) окон и других объектов перед их использованием и тому подобные вещи. (По крайней мере, я надеюсь, что все мы действительно привыкли к этим приемам!) На самом деле, эти маленькие программистские пустячки, которые все мы М_ _ . TTlJ* пишем на автопилоте, находятся лишь на краю широчайшего спектра поведи о пня программы в критических ситуациях. Этот спектр простирается от простеП' ших, очевидных проверок до более серьезных действий, за которыми моЖ# стоять гораздо больше раздумий и решений (например, как обращаться с 0' намическими библиотеками, или на каких платформах Win32 должна работать программа). То, что стоит на более простом краю, — это не более чем азь! оборонительного программирования. Но как только вы попытаетесь распространить эту общую концепцию на более сложные проблемы, она выДе' ляется в отдельную тему (со своими правилами и рекомендациями) и немног0 по-другому влияет на процесс разработки программы. В частности, в круг раС смотрения попадает вопрос о настороженном отношении программы к т^1
^икроуровневых рекоменлаиий 141 9 /1емам, которые не созданы ей самой. А это, в свою очередь, приводит нас ворили ранее. лужливос ю орых Вот неполный перечень наиболее важных моментов из этой облает х вы должны знать, и которые вы должны делать: g Всякий раз, когда для этого есть возможность, старайтесь проверять наличие всех ресурсов, находящихся вне контроля вашей программы, до того, как они понадобятся. Обратите внимание на выделенное уточнение: я ни в коем случае не хотел бы советовать вам проверять KJ все внутренние ресурсы вашей программы сразу после ее запуска - это было бы бесполезной тратой времени и превратило бы сопровождение программы в постоянный кошмар. (Хотя я допускаю, что и у такого экстремистского подхода найдутся свои защитники.) в Ограничивайте возможность запуска вашей Win32-rrporpaMMori только теми \\^н32-платформами, которые вы собираетесь поддерживать полностью. Как я уже говорил ранее (и еще подробнее расскажу в главе 14), в Win32 API каверзных сюрпризов больше, чем в спелом арбузе семечек. В результате вам придется выбирать один из двух путей — либо узкую тропку в обход многочисленных ловушек и поддержку всех трех платформ, либо отказ от работы программы на одной или двух из них. Следует признать, что, на самом деле, мы сейчас заговорили о макропроблеме внутри микропроблемы. Ведь при принятии решения о поддержке (или неподдержке) той или иной платформы Win32 необходимо учитывать и перспективное планирование, включая такие вопросы, как маркетинг и техническое обеспечение. Я не думаю, что многие коллективы разработчиков могут принимать «на лету» такие решения, как, например, отказ от поддержки Win32s (даже находясь под сегодняшним аномальным воздействием временных ограничений). По крайней мере, мне хотелось бы надеяться, что они так не делают. в Разумно осуществляйте загрузку динамических библиотек. Пользователи заслуживают хотя бы минимальной помощи с вашей стороны в том случае, когда программа не может найти и загрузить одну из необходимых ей DLL, - не молчаливого отказа от работы и даже не скупого сообщения, которое иногда выдает в таких ситуациях Windows 95, а подробного объяснения и подсказки именно со стороны вашей программы. Еще более важным является умение вашей программы, если это возможно, элегантно сократить свою функциональность, когда соответствующая DLL не найдена или не может быть загружена. (Классическим примером такого интеллигентного поведения может
142 Философия разработки ПО лая Windows: микропроб а быть отключение всех функций и свойств программы, относящихс элсмронной почте, если не удается подгрузить MAPI.DLL.) Разумно обрабатывайте потерю или порчу постоянных данных, к, уже было упомянуто в рекомендации 13 на стр. 138, при хранении д<, ных в INI-файле или реестре эта проблема может оказаться далеко ь такой пренебрежимо невероятной, как думают многие программисты 15. Остерегайтесь противоестественных действии Программирование для Windows — это совсем не то, что было в стары» добрые времена, когда вы вместе с вашим компилятором самоотверженш противостояли всем пришельцам. Теперь вы сидите на вершине высокого иуз кого здания, построенного из виртуальных машин и абстракций, и имеете по„ рукой так много разнообразных инструментов, что одно лишь изучение их сам по себе является сложной задачей. Должно быть, теперь, при наличии этого здания и помощи Microsoft (i других третьих фирм), вы наконец стали обладателем понятных, эффективны^ и надежных средств достижения практически любых интересующих вас целен правильно? (Здесь я сделаю паузу на несколько секунд и подожду, пока более опытные Windows-программисты закончат хохотать и поднимутся с пола Конечно же, мир не так прост, и рано или поздно все мы приходим к тому, чте начинаем (или, как минимум, испытываем сильнейшее искушение) время oi времени использовать недокументированные возможности Windows Например, если ваша Win32-nporpaMMa хочет вызвать функцию GetFreeSys temResourcesO, вы не можете сделать это напрямую (по крайней мере, со гласно правилам Microsoft). Вам придется обратиться к переходника)' (thunks) — очень уродливому методу. Но это лишь официальный способ peine ния. На самом же деле, вы можете вызывать 16-разрядную DLL из 32-разря^ ной программы, минуя всякие переходники, но при условии, что вы готовь пойти на использование недокументированного API. (Отвратительные детал' этого процесса вы найдете в главе 12.) 11 \ айс А теперь вы, наверное, ждете того момента, когда я вскочу на трибун) б}'ду говорить вам (словами Джорджа Карлина1), что использование недок! ментированного API «заразит вашу душу, согнет вашу спину и помешает стр выиграть войну», правильно? Нет, пожалуй, я пощажу уши (и драгоцеНН0 время) почтенной аудитории, поскольку подозреваю, что наверное для кажД°Г1 из всех собравшихся здесь очевидно, почему недокументированные возмо^ 1 Джордж Карлии — популярный (особенно в 70-х годах) американский комик, «ciic11^ лпзпрующпйся» на пародиях и сатире в адрес администрации США [ПримсЧ'1 переводчика ]
иКроуровневых рекоменлаиий 143 i I "ствительно опасны, особенно для публичных программ. В конце концов, 1,1' -и та же се^е Microsoft проявить непростительную жестокость по отно- 1)3 л к программистам даже в документированной части Win32 API! (См. 1К г 12 п 14.) Почему же вы думаете, что она будет хоть сколько-нибудь бо- 1 U> мшосердной к вам в случае с недокументированными возможностями? (Я известную шуточку о том, что, мол, недокументированные функции — это U ЧТО 0 >[е функции, которые действительно необходимы продуктам самой Mi- -0ft и поэтому как раз эти функции, вероятно, являются самыми надеж- ми Если кто и готов поставить на кон свою программу и держать пари за жую трактовку ситуации, то это не я.) Поистине отвратительной чертой недокументированных API является то, они заставляют вас принимать очень тяжелые решения. Как в вышеописанном примере с функцией GelFreeSystemResourcesO: вам приходится выбирать между простым, прямым путем (который, кстати, и был бы по-настоящему прямым, если бы был задокументирован) сделать что-то и официально одобренным маршрутом, при путешествии по которому сильно чешутся руки (естественно, в фигуральном, а не в анатомическом смысле). К сожалению, подобный выбор может оказаться еще более экстремальным: либо использовать недокументированную функцию или деталь, либо вообще отказаться от реализации каких-то возможностей своей программы. Мне пришлось столкнуться с таким выбором при написании Stickle si, когда я захотел показывать в About- диалоге количество свободных ресурсов в модулях USER и GDI и вынужден был обратиться к использованию недокументированного API. Мне не нравилось делать это таким способом, однако в той конкретной ситуации у меня было одно веское оправдание: если бы вдруг используемые мной API перестали правильно работать, это не нанесло бы программе серьезного ущерба, а я смог бы очень быстро изменить диалог About и ликвидировать проблему. (Кстати, с выходом Windows 95 эта деталь Stickiest работать не перестала, и я все еще Держу скрещенные пальцы за спиной.) Увы, использование недокументированных API и структур является 10ЛЬКо лишь самым очевидным (и, кто-то правильно добавит, самым °'<стремальным) примером противоестественных действий. Это лишь вершина <Шсберга. К этому «жанру» можно причислить любое действие, пррг котором Р°граммист сознательно неправильно использует возможности какого-либо Исгрумента или интерфейса, рискуя создать себе и пользователям* проблемы ^П1бо немедленные, либо перспективные). Особая щекотливость данной Р°олемы заключается в том, что идущий на подобные действия программист 61 Да имеет веские (по его мнению) причины на это. Поэтому, как всегда, . °чом црИ решении таких вопросов является поиск разумного баланса. 0граммнст может определить и объяснить вам сиюминутный выигрыш от ° Или иного противоестественного действия, он может даже привести вам ми- ,1альные «доказательства» безопасности этого действия (см. обсуждение
1 44 Философия разработки ПО лля Windows: микропробле мифа под названием «Видите? Работает!» в рекомендации 8 на стр. 99). Но д.,, решения спора этого, безусловно, мало. Вопрос должен ставиться только в г кой форме: так ли выгодно данное действие с точки зрения перспективы? Некоторое время назад мы с моим деловым партнером консультировали ол пого клиента, и та ситуация, с которой мы тогда столкнулись, может служцт KJ превосходным примером противоестественного действия, которое, скорее всего казалось его автору совершенно естественным. Клиент просил нас существенна переделать довольно большой объем специализированного кода на С. Этот ко- был написан на протяжении нескольких лет разными людьми, большинство ц< которых не были профессиональными программистами. Результат их труда представлял собой то, что я обычно называю техническим термином «ночной кошмар». Однако среди множества грубо отесанных прелестей этого кода одна жемчужина явно выделялась на общем фоне: почти в каждый исходный файл был включен заголовочный файл по имени PASCAL.H, и (вы наверняка уже догадались) в этом заголовочном файле мы нашли строки такого сорта: «define begin { «define end } #defme integer mt #defme then которые позволяли программистам писать затем бесконечные вандализмы типа integer fred = 0, if(fred > 0) then begin printf( "fred waG > 0\n\n') end elce begin printf( fred was <= 0\n\n') end, // Обратите внимание на беспричинную точку с запятой' И они детали это от души. Почти весь код был написан в этом стиле. № знаю, как у вас, а у меня от подобных выходок — мурашки по всему телу. Поясню: на мой взгляд, хорошо написанный Pascal-код значительно читабельнее хорошо написанного С-кода, но ничего нет хуже подобного смешанного си Мы « лже Пас каль» — ^поспешили прямо заявить клиенту, что замена этого стиля 6уДеТ первым делом, которое нам придется сделать. ел Если приведенный выше пример - ваша первая встреча со «лже-ПасК< лем», и если вы вдруг подумали, что это - «изящный трюк», пожалу11' 13Ы ИЗ ста, одумайтесь и никогда не используйте его в рабочем коде. А если не способны одуматься сами, то, пожалуйста, не говорите никому, что впервые познакомились с этим стилем здесь. «Лже-Паскаль» - одна самых худших вещей, которые можно сделать на языке С (эти cJ№$1
микроуровневых рекоменлаиии 145 говорят о многом, если учесть мое общее мнение о тех многочисленных экземплярах С-кода, которые я повидал), и мне не хотелось бы пребывать в страхе оттого, что спустя годы кому-нибудь вдруг придется биться над кодом, наполненным «лже-Паскалем» и хвалебными ссылками на мою книгу как на первоисточник Другим, менее шокирующим примером, с которым иногда сталкиваются оГраммисты, является проблема модификации кода каркасной библиотеки. г1Яда на то, какими огромными и сложными стали сегодня такие библиотеки, «ы можете подумать, что вряд ли существуют какие-либо резонные причины ло делать. Однако такое действительно случается. Мне пришлось делать это с OWL при работе над Stickiest, когда мне потребовалось изменить способ соз- \J ij дания миогострочного поля редактирования, изменить по всей классовой иерархии - вглубь до самого уровня CreateWindow. Поскольку я хотел сохранить все остальные удобства, предоставляемые OWL для работы с полями редактирования, я был вынужден модифицировать исходный код OWL па нескольких уровнях, и в итоге я получил в точности тот эффект, которого добивался. Вообще говоря, такой подход рискован, так как вы можете запросто ввести в библиотеку неуловимую ошибку, которая потом отнимет у вас до нескольких рабочих дней. Еще более вероятна другая проблема: если вы сделаете корректное исправление пли изменение, то вам потом придется переносить его в новые версии библиотеки по мере их появления. Например, однажды сломавшись и «подкрутив» что-нибудь в MFC, вы будете заниматься такими переносами гораздо чаще, чем вам хотелось бы. Кроме, того, всегда есть непулевая вероятность того, что авторы каркасной библиотеки в какой-то момент сами добавят в свое детище ту же (или почти ту же) возможность, которая была вам нужна и которую вы уже реализовали сами. И тогда вы окажетесь перед непростым выбором — то ли адаптировать свою программу под использование «официальной версии», то ли в очередной раз переносить свое «доморощенное» решение в новую версию библиотеки. В основе многих противоестественных действий (но крайней мере тех, которые относятся к программированию) часто можно разглядеть чрезвычайно 0гвратительный побудительный мотив: «Я был вынужден так поступить». От tUvHx заявлений меня сильно коробит, потому что чаще всего они являются Шщь неубедительной попыткой увернуться от ответственности. Программист ,Ь1л Доставлен перед трудным выбором, но вместо того, чтобы грамотно найти Рояльный выход пх создавшейся ситуации, просто пошел по наиболее лег- МУ пути, прикрываясь вышеупомянутой дежурной фразой. (Кстати, бывают ^ Унац, когда под прикрытием этой присказки выбирается, как раз наоборот, самый легкий путь. Но тогда нужно непременно искать другой, более силь- 11» Чем лень, стимул, определивший такой выбор программиста. Например, Мо>киость поразвлечься со своим любимым инструментом.) к Не
1 46 Философия разработки ПО лля Windows: микропроблелл а Конечно, бывают случаи, когда у вас действительно нет другого выбор кроме как совершить грех против всего программистского сообщества. Д0 таточно часто это диктуется причинами вовсе не технического происхождения и программисты просто идут в том направлении, которое было указано начцдь ством, и по которому сами они скорее всего не пошли бы. Тут может быть мног вариантов: использование «неправильного» языка или инструмент программирования, третьесторонней библиотеки или каркасной библиотеки или даже такая экзотика, как использования одного внутреннего стратегического продукта компании вместо другого продукта той же компании В подобных случаях программисты буквально принуждаются к противоестест- с KJ венным действиям. Также бывают такие ситуации, когда программист совершает ошибк\ просто в спешке, не имея времени на тщательное изучение альтернатив и выбирая противоестественный путь просто по незнанию. И я не осуждаю программистов, попавших в такие ситуации (хотя менеджерам, ответственным за создание подобных авралов, лучше не попадаться мне па пути). Как и во многих других вопросах программирования, предотвращение * • KJ противоестественных действии сводится к вашему здравомыслию и прилежанию в борьбе со злыми силами Для опытных программистов самое правильное решение часто заключается в том, чтобы просто прислушаться к своим чувствам. Когда вы или кто-то другой из вашей команды соберется пойти на противоестественные действия, вы это обязательно почувствуете, и наступит тот самый момент, когда нужно вдарить по тормозам и поискать решение получше.
Гизмонавт — сущ. м. 1. личность, склонная воспользоваться любым новшеством просто потому, что эта вещь новая, не обращая внимания ни на ее потребительские качества, ни на ее уместность в конкретных условиях работы, ни на стоимость ее адаптации. 2. (прогр.) любой участник вашего XJ XJ проекта, который хочет использовать новый инструмент вместо старого, который вы использовали годами потому, что он *_■ явно превосходит новый Нео-луддит — сущ. м. 1. личность, отказывающаяся перейти от старых, неэффективных инструментов и технологий к но- I вым, более эффективным, и пытающаяся оправдать свое поведение благовидными аргументами, уводящими в сторону от темы. 2. (прогр.) любой участник вашего проекта, который не хочет расстаться со старым инструментом в обмен на но- О *-> О выи, который вы только что нашли и который очевидно t_f превосходит старый. Компиляторы и редакторы, отладчики и библиотеки. Игрушки, примочки, прибамбасы, штучки-дрючки. Всякая чепуха. Инстру ченты. Немного есть предметов обсуждения, более близких и дорогих Р°граммерскому сердцу, чем инструменты. А почему собственно им не быть Новыми? Первая струйка воздуха с запахом поликарбоната из свеже- гкРЫтого ларчика с CD-ROM... Нетерпеливые метания мышиного курсора по 1алогам программы-инсталлятора... Волнение при перелистывании послед- ^о номера каталога «Программистская Феерия» и трепетное предвкушение, 1 т- 1 ^ислитсрация английского термина «gizmonaut», по-видимому, придуманного самим 1ором, происходит от английского жаргонизма «gizmo», по смыслу близкого к таким i.^ciciiM жартннзмам, как «наворот», «прпбамбас» и т. и [Примечание переводчика ]
148 Инструмент Ы что вот именно в этот раз, наконец-то, вы сможете найти тот золотой ключир который повысит вашу производительность, сведет частоту ваших ошибок мизерному уровню и улучшит цвет вашего лица... Ну кто может желать чегп. к го большего? Если инструменты так важны для нас (а они точно важны, поскольку щ вместе образуют главное связующее звено, своего рода «церебральный интерфейс» между нашими умами и нашими проектами), если мы так любим их и так часто говорим о них, то почему же так много программистов так потрясающе глупо и неумело их себе подбирают? Опыт моих проектов по разработке программного обеспечения (среди которых были pi новые версии операционной системы для высокопроизводительных ЭВМ, и незначительные изменения маленьких условнобесплатных программ) показывает, что большинство программистов являются либо гизмонавтами, либо нео-луддитами. Иными словами, огромное множество людей выбирает себе инструменты, основываясь на неверных доводах и мотивах. А нейтральная полоса, разделяющая два упомянутых выше полюса, мягко говоря, очень мала и довольно сильно разбросана. В этой главе я обращаюсь к теме инструментов: что они из себя представляют, как их отбирать и, самое главное, как их использовать. Что «инструмент Инструмент программирования состоит не только (и не столько) из - процессов и существительных предметов и продуктов, но и из глаголов методов. Поэтому и выбор инструмента происходит не только на макроуровне («Используйте Delphi/32 вместо VC++»), но и на микроуровне («Отключите оптимизацию у этого компилятора»). К инструментам следует причислить аппаратное обеспечение, программное обеспечение и информацию — все, что вы прямо или косвенно используете в своей работе. Даже сервер вашей локальной сети, расположенный в зале ниже этажом, является вашим инструментом, несмотря на то, что он не стоит на вашем столе. Даже полузабытый обрывок КЗ' О кой-то давней дискуссии с коллегами вносит свой вклад в ваш инструментарии На самом деле, все ваши знания, включая опыт проектирования, кодировЗ' ния и тестирования, являются отдельным, самым важным вашим инструмеН' том, хотя многие программисты не думают о них в таком ключе (если вообШе что-то думают). Стоит вам только перешагнуть ступеньку «Hello, World», и Bbl окажетесь в глубоком, холодном океане, где вашим единственным оружие против акул будет лишь ваша собственная сообразительность.
такое «инструмент программирования» и почему он так важен 149 9 Тем временем, все без исключения производители программистского ументария ие устают навязывать свои IDE-матики1, и с любовью со- 11 1ЯЮТ статьи, рекламные проспекты и выставочные демонстрации: «Взгля- С с j Rrero шесть щелчков мышью, и вы тоже сможете создать программу о зчварительпым просмотром печати, диалогом About, и общими диалогами - р это без единой набранной вручную строки кода!» Теперь я с нетерпением ,.IV когда они начнут прилагать к каждой копии своей IDE набор столовых V'жей, салатницу и волшебную щетку для чистки автомашины. Почему же так важно, чтобы программисты правильно отбирали инструментарий для своей работы? Q Ваш набор инструментов непосредственно определяет, что сможет делать ваша программа, а что нет. Например, попробуйте отреагировать иа некоторые Windows-сообщения в Visual Basic 3.0 -- и вы обнаружите, что это невозможно (по крайней мере, без помощи хитроумных третьесторониих примочек к VB). Для многих программистов это не является проблемой, и они успешно создают при помощи Visual Basic замечательные программы. Но для других это — шоу-стоппер. б В больших программистских фирмах руководство нередко берется издавать декреты о том, какие языки программирования и другие инструменты должны использоваться в том или ином проекте. Эти официальные декларации очень часто базируются на пустом месте или на некорректной информации, собранной у «технических» работников, которые также бестолковы, как менеджеры. Это ваша забота — бороться за право использовать те инструменты, которые лучше всего годятся для заданной работы. И единственный способ выигрывать эти в сражения - предъявлять факты. В Вы бог гели кодирования и В ныи и очевидный пример анализа соотношения / /го он вытерпит очередной повтор, поскольку слишком многие, судя по всему, игнорируют его суть: если инструмент стоимостью 400 долларов обеспечит вам экономию времени (конечно, с учетом временных затрат на его освоение) в размере хотя бы нескольких часов иа каждый цикл разработки, то, вероятно, имеет смысл этот инструмент приобрести. (Существуют исключения из этой упрощенной схемы анализа, о которых я вскоре расскажу подробнее.) Windows в значительной степени усиливает остроту проблем, связанных с инструментами. Программисты балансируют па очень высокой, -~магика - or комбинации английских «IDE» (интегрированная среда разрабомсн) и «idio- 1е>> (идиоматический, характерный для чего-либо) [Примечание переводчика J
150 очень узкой и не слишком твердой площадке внутри башни, составлю ной из многочисленных построений, интерфейсов и инструментов, э башня насквозь пропитана ошибками, сбоями, недокументированно\ побочными эффектами и паршивой документацией. Временами \Iei,, посещает одна дикая мысль: это просто чудо — когда что-то боле, сложное, чем «Hello, World», вообще как-то работает. енотендации по выбору и использованию инструментов Теория — чудесная вещь, но ею не прокормить и собаку. Когда вы листаете каталог программистского инструментария и когда вы сталкиваетесь с, казалось бы, бесконечным ассортиментом предлагаемых вам продуктов, вам определенно необходим какой-нибудь надежный метод принятия конкретных (и правильных!) решений о покупке. Сожалею, но я не смогу помочь вам в принятии всех ваших решений. Увы, я даже не могу уберечь самого себя от совершения редких, но поразительных промахов при выборе инструментов. Но у меня все же есть несколько рекомендаций, которые, я надеюсь, должны помочь вам провести ваше судно через подстерегающие за каждым поворотом пороги. О. Исследуйте и эксплуатируйте Исследование означает, что ваш кругозор в области новых инструментов и технологии должен постоянно расширяться, и что ваш ум должен все время оставаться восприимчивым к информации о новинках в области ваших профессиональных интересов. Присматривайтесь ко всему, что проходит мимо вас - к каждой библиотеке и к каждому программному приспособлению, для которых у вас найдется время. Заставляйте ваше руководство покупать оценочные версии (или сами доставайте бесплатные копии у поставщиков; некоторые обязательно предложат такие копии при соответствующих условиях) просто внимательно читайте все найденные обзоры и пресс-релизы ^ инструментарии для разработчиков. Исследование должно быть бурной, свободной , беспорядочной (в разумных пределах) частью процесса отбор'1 каи инструментов, которьш никогда не закончится, потому что новые п новые дидаты будут вечно выходить на рынок. В противоположность исследованию, эксплуатация является консерва'Ш15 ной, строгой, дотошной стороной процесса. На этом этапе вы должны оче^1' пристально присматриваться к тем инструментам, которые отобрали, и мак^1 мально интенсивно и разносторонне эксплуатировать их. Ни один 1Р
иенлзиии по выбору и использованию инструментов 151 9 о Оументов не должен считаться годным до тех пор, пока ваш тщательный И\ -щз этого не подтвердит. в Обращайте внимание на то, как отобранные вами инструменты работают сте Ваша главная цель — подобрать как можно лучший по общей оценке пор инструментов, а не просто насобирать симпатичные п изящные прибам- г-сы Работает ли приглянувшийся вам отладчик с исполняемыми файлами, бранными вашим компилятором? Справится ли компилятор с трансляцией годного кода библиотеки без чрезмерных его модификаций? Помимо сложных многофункциональных продуктов, всегда ищите р0Шие узкоспециализированные инструменты, предназначенные для реше- шстных, относительно обособленных задач. Например, если вы хотите нпя tJ tJ вк почить в вашу программу приличный текстовый редактор, не надо писать его самостоятельно. Если вас не интересует совместимость с Windows 3.1, используйте новый элемент управления Windows 95 — форматированное поле редактирования. Если же такая совместимость вам необходима, познакомьтесь с одним из нескольких существующих третьесторонних компонентов, предоставляющих примерно те же возможности. В любом случае, именно такие узкоспециализированные инструменты становятся примерами наиболее удачного t^ i выбора: вы получаете инструмент, который делает только одну вещь, но делает ее настолько хорошо, что одна из частей вашего проекта вмиг перестает быть проблемой. Изучая возможности отбираемых инструментов, не забывайте оценивать и их стоимость, которая отнюдь не сводится только к цифре, указанной на ценнике продукта. Например, если производитель компилятора заявляет, что для использования его продукта вам необходимо иметь 16 Мб памяти, то хорошенько подумайте: в самом ли деле вы будете чувствовать себя комфортно, работая на 16 Мб, или есть вероятность того, что уже на следующий день после компиляции чего-нибудь более серьезного, чем «Hello, World», вы начнете откладывать деньги на покупку еще 8 или 16 Мб? До этого момента вы все еще работали на макроуровне и исследовали Целую вселенную доступного вам ршструментария. И все ваши решения на этом Уровне касаются вопроса, какие инструменты выбрать. Как только вы делаете свой выбор — вот этот компилятор, вот те библио- 1еки, вот тот третьестороннпй отладчик — процессы исследования и эксплуа- а1Шн переходят в новую фазу, на микроуровень. Теперь вы интенсивно эксплуатируете возможности и свойства конкретных, отдельных продуктов и уже много иначе ставите вопрос: как использовать каждый из выбранных СТРУментов. Точно так же, как и на макроуровне, вы должны выяснить, Р°оовать и оценить абсолютно все, что может делать конкретный продукт И u { только наступит момент принятия окончательного решения о том, как ' енно будет использоваться тот пли иной инструмент, вы должны нацепить 11 ВиРтуальные темные очки и превратиться в ужасно несговорчивого поку-
152 Инструмент пателяг заставить каждую функцию, каждое свойство инструмет, неопровержимо доказать вам свою ценность, корректность и надежность. Помните, что ваше решение использовать определенный компилятор ^ библиотеку само по себе вовсе не означает, что вы тем самым косвенно подщ, сываетесь на применение всех его возможностей. Многие из этих возможносте!', вполне могут быть неинтересны для вас или даже вообще не годиться для вашей работы. При совершении выбора ваш процесс селектирования отнюдь ^ кончается, он лишь пересекает границы конкретного продукта и продолжается дальше па другом уровне. Плакат программиста: панацея». который следует поставить рядом с компьютером каждого ООП1 — это хорошо, ООП — наш друг, но ООП - Не Однажды Ларри Константин заметил, что ООП - это, прежде всего хорошие упаковка и оформление. Да, это так. Но голая правда состоит в том, что дополнительные усилия, необходимые для этой упаковки и оформления, просто не окупаются. Что бы ни говорили производители программ и инструментов, у ООП есть оборотная сторона — излишняя сложность. Иногда оптимальное решение вовсе не требует никаких объек тов: нужно просто весь код и все данные, необходимые для выполнения некоторой логической задачи, поместить в отдельную единицу компиляции, а затем выставить напоказ одну единственную функцию, которую и смогут вызывать другие части программы. Именно такой подход использован в программе-примере из главы 8 на стр. 271: весь код и данные модуля CHECKER можно было бы запросто превратить в объект, но что мы имели бы с этого? По-моему, абсолютно ничего. Все, что может когда-нибудь понадобиться (или захотеться) вызывающей программе от этого кода, -- это вызвать функцию GoodCRCO и исследовать возвращенное ею значение. Поэтому нет никакой нужды вертеть в руках классы, конструкторы, деструкторы и прочее Подобное изолирование кода и данных внутри отдельной единицы компИ' ляции, на самом деле, эквивалентно выполнению одного из принципов ООП, но без использования всех его дорогостоящих наворотов Возможно, следует дать этому подходу название «ОБК», означающее «ООП № классов». Не поймите меня неправильно, я — убежденный сторонник коннегШ1111 объектно-ориентированного дизайна, но в то же время я прекрасно знаю как их использование может вылиться в настоящую стрельбу из пушки f'° воробьям Слишком многие программисты смотрят на ООП, как на бл0' стящий новенький молоток, а на распростертый перед ними мир — как * необъятный лес из гвоздей. 1 ООП - объектпо-орпеигпрованное программирование. [Примечание переводчика.]
менлаиии по выбору и использованию инструментов Г** ———""- а 153 Много раз я наблюдал, как людьми совершается одна типичная ошибка, о езность которой трудно переоценить: руководство долго мечется и аго- L тует, пытаясь произвести правильный выбор инструментария, а потом но -тостью отдает решение этого вопроса на откуп отдельным программистам. ' •■мультате старательно отобранные инструменты довольно часто вообще не В Ре - 1ЬЗуются, прозябая на полке в кладовой, пылясь в оригинальной упаковке с гом с коробками для исписанных красных фломастеров. Учитывайте все кратковременные и перспективные проблемы. Не забы- > liire оценивать стоимость возможного в будущем отказа от использования того ш иного инструмента. Такой отказ может оказаться весьма трудным и дорого- оящим. Да, какой-то новый стандартный интерфейс в Windows (MAPI, TAPI и т. п.) кажется вам замечательно полезным. Но что, если вы примете его, пустите в дело, привяжетесь к нему, а потом выясните, что он не позволяет вам сделать очередной шаг вперед? Удостоверьтесь, что продукт и в самом деле делает то, что обещает, и что он обеспечивает хорошее отношение затраты/выгода. Я не говорю, что существуют такие производители програмного обеспечения, которые лгут, но вам cl «_> «_» лучше быть настороже, на всякий пожарный. Если это возможно, обязательно рассмотрите документацию, прилагающуюся к продукту. Помните, что во многих случаях документация бывает серьезным слагаемым в оценке инструментария (если не множителем). Если продукт кажется вам идеально подходящим для ваших целей, но у него нет документации (или есть, но плохая), поищите какую-нибудь другую, гретьестороннюю доступную документацию. Конечно, это выглядит несколько глуповато — покупать книжку цяя того, чтобы можно было эффективно использовать крутой новомодный программный продукт, но кого это волнует? Смысл всего процесса выбора инструментария — в его результатах, а не в логике Помните и еще одну важную вещь: тот, в чью могилу рядом с гробом торжественно положат больше всего инструментов, отнюдь не будет победители Знание и использование лишь 10-ти процентов возможностей ваших 50-ти "нструментов — это неэффективно и неэкономично. Какова ваша истинная &дь> Стать обладателем самого быстрого, самого большого винчестера, битком 'Литого программными продуктами? Или все-таки стать как можно более ' Ффективным, более умелым программистом? ' * Добивайтесь симбиоза ваших инструментов оьгбирая себе инструменты, следите за тем, как они работают совместно. ' иоз инструментов — это нечто большее, чем просто совместимость исходили объектных файлов, о которой я уже упоминал выше. С
154 Инструмен к BL Например, вам следует всегда держать под рукой печатную копию вацц, исходных текстов (уже стабилизированных), даже если на это тратится мНо бумаги и печатного порошка. Почему? Не только потому, что распечат/ зачастую служит сподручным справочником, но и потому, что она может ц0 служить и страховочной копией в критической ситуации. В век недорог^ сканеров и хороших OCR-программ (OCR — оптическое распознавание сщ1Во лов), такая распечатка может спасти вас, когда ваш винчестер сломается и вдруг обнаружите, что магнитная лента со страховочной копией больше ^ читается (или что кто-то забывал делать страховочную копию вашего раздел в течение последних четырех месяцев). Другой пример — использование индексирующего и поискового программ ного обеспечения (например, такого, как Isys от Odissey Development Corporation) для работы с CD, набитыми разнообразными примерами исходных текстов. Вы можете по дешевке (всего 50 долларов и меньше за диск) накупить много таких дисков, но поиск информации в них без каких-либо дополнительных средств может стать настолько трудным и долгим делом, что сведет на нет смысл их покупки. (Иногда к подобным дискам прилагается какое-нибудь простенькое информационно-поисковое программное обеспечение, но, к сожа лению, эти программы, как правило, несовместимы ни друг с другом, ни с форматами «чужих» каталогов на CD-ROM.) Вот тут-то и может весьма приго диться более универсальный инструмент, подобный упомянутому выше Isys построив индексы ваших многочисленных каталогов, вы сможете в считанные секунды осуществить поиск нужных ключевых слов, а затем быстро достать соответствующие файлы. Кстати говоря, в один ряд с такими «каталогами исходников» можно поставить и CD с дистрибутивами компиляторов — я, разумеется, имею в виду огромные залежи примеров с исходными текстами, изобилием которых славится почти каждый современный компилятор. Вы скажете, что в подобных ситуациях можно с тем же примерно успехом использовать утилиту grep или функцию многофайлового поиска в любимом редакторе Да, можно, но тогда вам придется выбирать между установкой всех примеров на ваш жесткий диск и осуществлением поиска прямо на CD-ROM. Хмм, в общем-то, это тоже вариант... Еще одна область применения индексирующих программ — работа с вашими собственными накоплениями исходного кода. Признаюсь, я личн0 являюсь настоящей архивной крысой (разумеется, специализирующейся я3 архивах исходных текстов) и подозреваю, что в этом я похож на многих cboi^ собратьев по профессии: скачиваю исходники, найденные на BBS, в Интерне или в конференциях, копирую их с гибких дисков, приложенных к книгам журналам, и т. д и т. п. Индексирование этой «копилки» — очевидно, #е° ходимая вещь. Кстати, ее индексирование может оказаться особенно эффекте' ным, если индексирующая программа умеет должным образом работать с Z™ архивами — тогда вы сможете сэкономить место на вашем диске, не теряя в°3 и
енлаиии по выбору и использованию инструментов 155 г с^_—■—" )сти быстрого доступа к отдельным файлам (а заодно обойти известную чи'\ еМу с потерей дискового пространства из-за фиксированного размера кла- ,1'"> в файловой системы, о которой я подробно рассказывал в главе 2 на L'Г69} ' О пшм из залогов желаемого симбиоза является использование таких тх-менчов, которые могут принимать и выдавать данные в чисто текстовом ' ' (помимо своих «родных» форматов ввода/вывода/хранения данных). И пример, предназначенный для рисования различных диаграмм и блок-схем 01раммный пакет allCLEAR без проблем примет от вас чисто текстовое опи- . шце блок-схемы и по нему построит ее графическое изображение. Это позво- ,ш>т вам без труда расширять возможности ваших собственных программ за чег aNCLEAR. Например, вам может понадобиться написать программу, 010рая читает внутренний телефонный справочник вашей компании и строит по сто данным организационную схему. Было бы слишком долго и дорого пи- сагь программу, самостоятельно выполняющую все рисунки в графике. Но при аличпп такого «уживчивого» инструмента, как allCLEAR, этого и не требу- i н eiai. вашей программе достаточно выдать результат в текстовом виде, а всю остальную работу за нее сделает allCLEAR. Говоря обобщенно, вы можете рассматривать текстовые файлы как промежуточный язык, наиболее подходящий для общения между разнообразными специализированными инструментами. 2. Не разбрасывайтесь и не переоценивайте сваи силы Независимо от размера вашей программистской команды - состоит она из в^с одного или из 1000 человек, старайтесь честно и непредвзято оценить тот вровень мастерства, который необходим для выполнения ваших проектов pi использования ваших инструментов. Например, если вы собираетесь применить Д1я написания программы Visual Basic 3.0 и, в частности, изготовить для этой программы несколько компонентов VBX, это у вас не получится (при наличии опыта работы только с Visual Basic); поскольку VBX-компоненты нельзя напить при помощи самого Visual Basic, вам еще понадобится знание C/C++ или dscal, а также необходимый для применения этих языков инструментарий. То СТь> вам понадобится целый набор инструментов и, что более важно, целый 'Р^нал навыков работы с ними. Ьолее-мепее точная оценка реального уровня мастерства собранной ко- сИДЬ1 может быть весьма трудным мероприятием. Печальную славу имеет v °нность многих программистов заявлять, что они «знают» тот или иной 1к црограммированпя (или инструмент разработки) в то время, как на самом ' е они потратили на него лишь несколько праздных часов, да и то около года 1'Ч Кстати, если при этом программист неосторожно добавляет что-то типа я хотел бы изучить его (язык, интсрумент и т. д.) еще поглубл^е!», в боль-
156 Инструме^^ шинстве случаев можно быть уверенными, что вас хотят провести, а заявле ные программистом «знания» — лишь предлог и попытка программу увернуться от текущей, возможно неприятной для него работы. Когда под0/ нь[е уловки удаются, серьезные несчастья в проекте почти гарантированы- К( манда формируется, планы и расписания фиксируются, и только спустя щ сколько месяцев вдруг обнаруживается, что ведущий программист на С+> ответственный за самую критичную часть проекта, на самом деле никогдс всерьез не использовал этот язык, хотя C++ и казался ему таким интересны по книжным и журнальным статьям. 3. Не будьте слишком доверчивы к вашим поставщикам Некоторые полагают, что нужно рассматривать только технические характеристики инструмента, а на такие вещи, как финансовое здоровье щ производителя или его политика поддержки пользователей (то бишь вас), можно не обращать внимания. Это — огромная ошибка, особенно при разговоре о публичном программном обеспечении. Если у компании — производи теля вашего инструмента — имеется длинный список заброшенных продуктов или если у вас есть основания полагать, что они могут так поступить при каких то условиях, значит у вас уже как минимум на одну проблему больше. Если вы посмотрите врезку «Что будет с OWL?» на стр. 185, то вы найдете там не которые подробности о моем личном опыте в этой области. Как же разобраться, каким поставщикам можно доверять? Вам придется начать с доверия к вашей собственной интуиции и, конечно же, к вашим кол легам по профессии. Однако учтите, что в данном случае недостаточно просто прислушиваться к публичным дискуссиям в Интернете. Потратьте некоторое время на них, и вы услышите как пылкие славословия, так и язвительные об винения в адрес практически каждого производителя, более-менее известного в этом бизнесе. Максимум того, что можно сделать в этом гаме — приложить Я0 к земле и попробовать разобраться, какие жалобы действительно обоснованнь' и имеют значение для вашей работы, а какие являются лишь сотрясением воз духа. Так же, как в рекомендации 0 на стр. 150 этой главы, я советую вам i{f данном вопросе не забывать про его микроуровневые проявления. Несмотря я* то, что вашей основной задачей должна быть минимизация количества стор011 них производителей, на чьи продукты полагаются ваши проекты, не слеДУс слишком зацикливаться на отдельном продукте. Например, иногда 6ывае' правильнее использовать компилятор одного производителя, а библиот^ классов -- другого. Разумеется, основной причиной подобного выбора дол^ быть только максимальное соответствие нуждам вашего проекта именно та**0 комбинации ипструментов.
ценлвиии по выбору и использованию инструментов Y^is —^— 157 \ плм ) Как можно более скептически относитесь к особо кричащим pi выставляе- напоказ возможностям продукта. Однажды Лили Томлин сказала, что от to трудно удержаться далее закоренелым циникам. Судя по ее словам, она, <>п\\ое, занималась программированием лишь в свободное от работы время. jUlHCp1 Всякий раз, когда вы изучаете возможности очередного инструмента, в ва- ni мозгу обязательно должен сидеть маленький червячок и постоянно юверять, не является ли то, что вы видите, подозрительно похожим на бес- татный сыр. Как говорится, «если что-то выглядит слишком хорошим, чтобы ')ыть правдой, то, вероятно, так оно и есть». Практически у каждого компи- п0ра C++ есть такая подозрительная возможность — оптимизация (подробный разговор о ней, а также об ошибочности многих общепринятых суждений ней, вы найдете в разделе «Компиляторы» на стр. 164 этой главы). «,» о В заключение, последний совет: не слишком доверяйте проектировочным решениям вашего поставщика. Как раз во время написания этой книги я изучал один продукт, который превосходно решал одну из проблем в моем текущем проекте. Это был отличный пример узкоспециализированного инструмента. Но, к сожалению, в итоге я был вынужден отказаться от его использования: проблема, которую он так успешно снимал, была довольно незначительной частью всего проекта, а инструмент требовал включения в финальную версию моей программы нескольких внешних файлов с данными (не давая возможности включить их в ресурсы). Такой дизайн неприемлем для публичной программы — выигрыш от небольшого усовершенствования сводится на нет стоимостью его реализации. В моем случае это была, в основном, стоимость дополнительной нагрузки на инсталлятор и саму программу в связи с наличием внешних файлов с данными: инсталлятору пришлось бы приложить дополни- 'ельные усилия по корректной установке этих внешних файлов, а самой программе — должным образом заботиться о проверке их доступности и целостности Всегда есть ненулевая вероятность того, что внешний файл может быть Удален, случайно подменен или модифицирован — вы никогда не можете де- 1ать никаких определенных предположений о состоянии и судьбе «выставлен- 1Ibfx напоказ» данных, если ваша программа не следит за ними специально. Если попробовать обобщить мораль, заключенную в последнем примере, то 0 можно сформулировать так: не позволяйте вашим инструментам навязывать ^правильные проектировочные решения вашему продукту. 4- Разумна эксплуатируйте публичные нехоуные коуы ^годня нас захлестывает настоящая приливная волна бесплатных, обще- ц *Упных исходных кодов — книги, журналы, диски CD-ROM, конференции ерпета, ftp-серверы и т. д. Мир качается на волнах океана С, сверкает в
158 Инструмен bl огнях Pascal и кренится под тяжестью LISP. Ей Богу, если хорошенько поц кать, то, вероятно, молено найти версию Pong1 на COBOL! Тут же встает вопрос следует ли все это использовать? Ответом является «да», но с серьезной поправкой. Поправка же состоитI том, что вы должны делать это «разумно». Прежде всего, поправка «разумно» означает, что вы должны судить о коде только по его качеству (или по отсутствию такового). Вы вообще не должны ни принимать, ни отвергать что-либо только лишь потому, что оно причислено к какой-то категории (такой, как бесплатное программное обеспечение). Такой подход неуместен в отношениях между людьми, он не годится и для программ Кроме того, «разумно» означает, что вы должны выработать для себя хорошо продуманную, четкую процедуру для заимствования какого-либо кода, пришедшего извне, в вашем проекте. В большинстве случаев вам абсолютно неизвестно, кто писал код — гуру программирования или какой-нибудь неандерталец (не говоря уже о персональной идентификации автора). Очевидно, вы должны проявлять чрезвычайную осторожность. Например, даже не думайте использовать материал такого сорта в публичной программе, если у вас нет всех исходных текстов. Тщательно проверьте и протестируйте все исходники прежде, чем начнете использовать их в производственных условиях; нив и коем случае не считайте код корректным только потому, что он успешно скомпилировал ся без особых усилий. Некоторые программисты серьезно сопротивляются использованию чужих исходных кодов, отгораживаясь от них Великой Китайской Стеной. А вы^ Знаете ли вы о программировании все, что надо знать о нем? Можете вы написать промышленного качества функции, которые будут кодировать методом Хаффмана, выполнять быстрое преобразование Фурье, совершать цифровое разложение BMP-файла, вычислять точную дату рождественского воскресенья для заданного года? Лично я не могу. По крайней мере, без изрядных усилий по исследованию предмета или без общедоступного кода. Разумеется, нет и быть не может такого правила, которое гласило бы, что вы должны непосредственно использовать публичный код в вашем продукте Вы можете просто чему-то научиться, глядя на чужой код, позаимствовать из него какой-нибудь алгоритм или прием, а затем применить все это в вашем собственном коде, который вы пишете с нуля. Но в любом случае, вы должны рас' сматривать публичные исходные коды как очень ценный программистски11 инструмент, требующий, правда, особой осторожности при обращении с ним 1 Pong — одна из первых компьютерных игр, завоевавшая поистине всемирную популярно1'г1) 70-х годах По сущ, является простейшим имитатором игры в теннис [Примсча'1' переводчика ]
енлзиии по выбору и использованию инструментов 159 5. ВВе влюбляйтесь в ваш новый блестящий гл @А&ток \\о ведь это так естественно, в самом деле! Вы принимаете решение пустить .jpvMeiiT в дело, ступаете на одну ступеньку вниз, на более низкий уровень ' ха"исследования и эксплуатации, и, наконец, находите многочисленные По- ! пне Крутые Штуковины всевозможных сортов, заложенные в этот продукт. и епи-' этого 11е знаете, но вы уже влюбились в него без памяти: вы рассылаете згзьям обширные электронные письма, полные бурных излияний ваших Чувств к этому новому, открытому вами Чудо-Прибамбасу. Своими рассказами нем у кофейного автомата, и во время обеденного перерыва вы до слез надое- тете вашим коллегам. Пока это все нормально, хотя и немного стесняет окружающих. Опасно то, что происходит чуть позже: сладостное головокружение вызы- uaei некоторое брожение в мозгах и трансформируется в фанатизм, серьезно деформирующий ваше мироощущение. С какого-то момента каждая проблема кажется вам гвоздем, который только и жаждет, чтобы вы вогнали его по самую шляпку ударом вашего нового блестящего молотка. Лучшая защита от такой «любви» — тот самый маленький червячок в вашей голове, о котором я упоминал выше. Вы должны все время быть настороже t.f *^ и сопротивляться каждому малейшему проявлению такой привязанности к инструменту, причем не только у самого себя, но п у других членов вашей f J программистской команды. \ Я должен сознатьсягчто сам достаточно склонен к такой влюбчивости, так что обо всех связанных с этим опасностях я знаю из первых рук Иногда О я с некоторым смущением вспоминаю тот эпизод из раннего периода моей !,* К* программистекоп карьеры, когда я впервые познакомился с теорией конечных автоматов и с основанным на ней табличным методом обработки данных, который использовался для реализации очень эффективных синтаксических анализаторов. Я так влюбился в эту теорию, что пытался использовать конечные автоматы для решения практически каждой встречной проблемы. Моему менеджеру и моей жене порой приходилось буквально держать меня на привязи, пока мое лихорадочное возбуждение не прошло окончательно. Особенно скверной является такая вариация этой темы, при которой основ- ' акцент ставится на слово «новый» в метафоре с блестящим молотком. пиком часто свежеиспеченные новые инструменты и/или технологии во- ^нотся в программные проекты по абсолютно неверным причинам: из-за ч<1се}тшого увлечения программиста, из-за благонамеренного, но основан- j. ^няцпку пли из-за какой-нибудь комбинации перечисленных выше ошибок. ип неверной информации приказа руководства, из-за слепого доверия к ^,l только возможно, вы должны сопротивляться любому навязыванию но-
160 Инструмеч k « t 3i вых инструментов и технологии до тех пор, пока эти новинки не докажут Cg готовность. В конце концов, мы работаем в той самой индустрии, в которой протяжении многих лет со сцены не сходила шутка о том, как безрассудно купать версию 1.0 любого продукта. В течение последних нескольких лет шутка (а если быть более точным - народная мудрость) слегка видоизмен, лась, и теперь она гласит: «ни в коем случае не покупайте версию х.О чег либо» И надо признать, что эта новая формулировка служит весьма метк^. хотя и грустным, комментарием о сегодняшнем состоянии дел в производи в I соммерческого программного обеспечения. К сожалению, эта мудрость в Toii/F, степени применима и к программистскому инструментарию в широком смыс слова. ЯП Например, вспомните дебют технологии OLE. Журналы наперебой пуб ковали минимальные демонстрационные OLE-программы, содержавшие тысячи строк кода предназначенных только для поддержки OLE. Теперь же когда у нас есть библиотеки, практически полностью инкапсулирующие рутин иые элементы поддержки OLE, только какие-нибудь экстремальные условия (или врожденный авантюризм) могут заставить программиста взяться за «ручное кодирование» поддержки OLE и пойти тем самым па огромный риск введения в продукт серьезных и трудноуловимых ошибок. На мой взляд, в обычных проектах не стоит торопиться с внедрением таких внушительных архитектурных решении, как OLE (и даже менее устрашающш новинок типа новых элементов управления Windows 95) до тех пор, пока не появится хорошая инфраструктура (читайте: каркасная библиотека) и\ поддержки О даться такой быть готовы к тому, что «официальная » «_> версия вспомогательной поддержки рано или поздно И придется решать, останется ли ваша оригинальная реализация жить в виде дои буд инструментария. инстриментов \6о\ всем инструментам сразу, особенно когда понятие «инструмент» использует^ П проблемах, которые спецпф t 9 для этих категории.
ы инстех^ентов Справочные материалы 161 Тоудио иметь под рукой слишком много справочных материалов. Иначе я когда про этот особый тип инструмента спрашивают «Сколько еще г° н0?», на это почти всегда отвечают: «Больше!» ^' Поэтому главная проблема в данном случае — как использовать •точный материал. Согласно моей теории об исследовании и эксплуатации цнстрУ ментария, изложенной выше, это - второй уровень, и ваш правильный 0д к нему может сохранить вам значительное количество времени. Например, всякий раз, когда вы вторгаетесть в какую-то область Windows- пограммирования впервые или погружаетесь в нее глубже, чем это делали ньШе, вы должны потратить несколько минут на поиск тех API, которые вы будете использовать в первый раз пли которые вы хотите применять более основательно. Внимательно прочитайте всю информацию о них, доступную в сопроводительной документации и на Microsoft Developer Network CD. Звучит [ак, как будто я только что предложил вам провести время за странным занятием ~ за чтением словаря, не правда ли? Что ж, в каком-то смысле именно это я и предложил В справочниках, в этих своеобразных «словарях др 1» есть масса интересной чепухи, которая окажется весьма полезной для вас. (Кстати, если вы никогда не читали настоящий словарь, то я советую попробовать и это. А тем любопытным, кому ваше поведение покажется странным, вы всегда можете сказать, что занимаетесь сравнительным частотным анализом латинских и греческих словообразующих корней в рамках большущего правительственного заказа.) Как только вы начинаете думать, что ваши новые знания об API достаточно скреплены, нередко есть смысл потратить еще немного времени и усилий на написание простых тестовых программ, на которых вы должны поупражняться в использовании этих API и заодно убедиться, что они и в самом деле работают Так, как вы представляете. Вы спросите, зачем же вы должны тратить свое Драгоценное время иа это занятие? Ответ прост: практически вся программистская документация, с которой вы имеете дело (а относящаяся к Windows — в Особенности), крайне ужасна. Она неточна, в ней опущены критические де- 1али, в ней жутко не хватает ссылок иа другп<- темы, и она никогда не дает вам 1°лной картины. (Кстати, именно тут хорошим подспорьем может оказаться uidows/DOS Developer's Journal — ежемесячно они публикуют несколько 1 1н°тащ[й к справочному файлу но SDK, которые латают дыры и исправляют 111°кц. И хотя эти усилия достойного журнала выглядят в какой-то степени УН1,Женными самой проблемой, я настоятельно рекомендую вам пользоваться 0X0,1 помощью.) На тот случай, если вы решите, что я дурачусь или чересчур осторожничаю н°Шенпи документации к API, вот вам примеры для размышлений:
Инст ^ Один из самых больших сюрпризов в Windows 95 (но не в Wi^o заключается в том, что все координаты, переданные при вызовах Gbi функций, ужимаются с 32 бит до 16. То, что получится при таком с^ тии (а оно может привести не только к изменению абсолют^ значения, но и к смене знака), и будет использовано модулем GDl вызывающая программа останется без какого-либо уведомления of этой проблеме. В главе 14 на стр. 473 вы сможете найти более подр0< ную информацию об этой аномалии, включая описание некоторых спо о вот собов обойти ее. Некоторые «булевские» функции в Win32 в действительносп возвращают не TRUE, а непредсказуемые ненулевые значения. Эт может приводить к некоторым странным проблемам. Например, такая проверка: if(GetClientRect(hWnd,lpRect) == TRUE) будет всегда оканчиваться неудачей, даже в том случае, когда вызов функции GetClientRectO абсолютно успешен. А вы потратите несколько часов на выяснение того, почему ваш код работает неправиль но, хотя проверка возвращаемого функцией значения производится в точном соответствии с документацией. Несмотря на то, что многие программисты на C/C++ обычно следуют общеизвестному соглашению о трактовке любого ненулевого целого значения как TRUE (кстати, в заголовочных файлах для Win32 константа TRUE строго определена равной 1), вы никак не можете критиковать этот код за некорректность, поскольку он следует букве закона (то есть верховному критерию правильности программы). Тем не менее, вне зависимости от того, кто тут прав, а кто нет, возникшая ошибка может быть очень дорогостоящей, особенно если пользователи наткнутся на нее раньше вас. Кроме того, это небрежное нарушение документации (или недоку мен- тированная небрежность?) может повлечь за собой целую всрениН) других, еще более неуловимых проблем. Что, если вы пишете фу11К цию, которая по определению должна возвращать значение Ttffl;1 BOOL, н код которой после выполнения каких-то своих действ^ вернет результат работы одного из тех самых причудливых «бул^ ских» Win32 API, которые так коварно пользуются вольной *I]1 терпретацией TRUE? Это хороший, пусть и не самый опасный, приМеР того, как плохие данные могут попасть в вашу программ)' ll роспространиться по ней. В данной книге я говорил и еще неодноКраГ но повторю, что вы должны весьма скептически относиться к правй^ тюстп данных, которые не создаются и не проверяются вашим соос
ТипЧ инструментов 163 зенным кодом. Поэтому наиболее надежным ( Eia вид) решением в данном случае была бы s ,-oturn SomeWIN32API(/* */); на return (SomeWIN32API(/* */) ' = FALSE), которая позволит вашей функции строго соответствовать своему описанию, несмотря на причуды Win32 API и его документации. функция FindFirslFileO (которая может использоваться для трансля- Is ции псевдонимов файлов в их длинные имена и обратно, а также для поиска файлов) не принимает имена файлов с символом «обратная косая черта» на конце, но зато прекрасно понимает такие имена, в которых на местах символов «обратная косая черта» стоят символы «прямая косая черта». Ни один из этих двух интересных фактов не задокументирован. К тому же в этой функции есть документированная ошибка при обработке метасимволов: символ «?» трактуется как «любой символ», а не как «любой символ или ничего». (Подробности вы можете найти в PSS-статъе Q130860 на Microsoft Development Network CD.) Это пример еще одной проблемы, которая, подобно случаю с «ложным TRUE», может вылиться в долгие отладочные сеансы. Функция GetLongPathNameO (новый сервис INT 21h/7160h, появившийся в Windows 95 pi не имеющий эквивалента в Win32 API) примет завершающий символ «обратная косая черта» в указанной строке с путем. Если бы при рабоде с файловой системой именно эта функция встретилась вам первой, вы, наверное, сделали бы резонное предположение, что и другие новые функции этого «семейства» принимают и игнорируют завершающие черточки. И вы оказались бы неправы. Наш старый добрый друг, функция _1ореп() примет и корректно обработает длинное имя файла, даже если вызвать ее из 16-разрядной программы, в штампике «ожидаемая версия Windows» которой проставлено 3.1. Flo этот весьма удобный (для некоторых) факт, насколько мне известно, никак не отражен ни в справочном HLP-файле, ни где-либо еще. (Я сделав это открытие совершенно случайно, когда экспериментировал с программой-примером LFNCD для главы 12 на стр 425. Подробные детали об этом вы можете найти во врезке «Использование длинных имен файлов в 16-разрядных программах: да Здравствует алхимия!» на стр. 395.) Иногда функция GetShortPathNameO работает по-разному в Windows 95 и Windows NT. В главе 14 на стр. 486 я подробнее расскажу о ней и о том, как можно «обернуть» этот API для того, чтобы уни- с Лидировать его поведение на различных платформах Windows.
164 Ин стру^9н X д Но это все частности. А обобщенную мораль я сформулировал бы так: вас является жизненно необходимым всегда оставаться студентом прогп', мирования. И для Windows-программистов это важно более чем для кого- другого. Под жестоким натиском новых API и возможностей, мы все чувству Щ себя, как новичок на велотренаже.ре — неистово крутим педали, спидометр Пг казывает 100 миль в час, а на самом деле мы остаемся на месте (я мог бы пора( суждать насчет того, кто в данном случае выступает в роли тренера - сцд,, рядышком с секундомером в руках и наслаждается зрелищем, но благоразу., нее будет промолчать). Все это чертовски изнурительно, но на самом деле у щ нет выбора. Стоит вам, профессиональному программисту, один раз отстать потом будет очень трудно найти силы, чтобы догнать уходящий поезд. Невзирая на ограниченность времени, которое отводится вам на образова ние, вы должны знакомиться с другими областями программирования отличными от той, в которой вы обычно работаете. И совсем не обязателш программировать в них, достаточно хотя бы читать какую-нибудь литературу Например, я давно убежден, что всем кодировщикам следует читать f программировании игр - как о проблемах искусственного интеллекта (в такт играх, как шахматы и шашки), так и о вопросах реализации интенсивной ани мационной графики (вы угадали: отличным примером тут является знаменита Doom). Почему? Дело в том, что программирование игр — чертовски слолш область, и вы можете почерпнуть оттуда массу полезных знаний и технологий пригодных для использования в вашей собственной работе (самый проста пример в этом случае - различные приемы оптимизации). Иногда все программистское сообщество представляется мне группа спортсменов на соревнованиях по бегу с препятствиями Вот появилао очердная крошечная помеха, через которую нужно перепрыгнуть (нова? г о версия DOS); затем показался довольно высокий забор, через которы нужно перелезть (переход от DOS к событийно-управляемой Windows) а теперь перед нами — огромная стена (OLE, Win32, сетевое программирование и все прочие важные новинки и изменения середин11 90-х). Почти на каждом препятствии часть бегунов сходит с дистанции истощения (иногда скорее от ментального, чем от физического) или на*0 дят себе какую-нибудь нишу, в которой продолжают бег (но уже тр}1 пой). Некоторые постоянно рвутся вперед, чтобы любой ценой занят' место во главе всей своры А тем временем большинство из продолжают бежать круг за кругом, не переставая удивляться тому» ^ на каждом кругу встречаются новые препятствия, порой так похожие предыдущие. ЦП и Компиляторы и языки программирования О'кей, вот н пришло время для хорошенькой, старомодной религиоз^0 войны. Честно говоря, начиная эту книгу, я не был уверен, стоит ли каса^1'
ТиПЬ1 инструментов 165 3 С утой темы «C/C++ против Pascal»; у меня было сильнейшее искушение 11 Рс" игнорировать ее. При том плачевном положении Pascal на рынке В°° тв разработки для Windows, которое он занимал в начале 1995 года, вряд С^ меЮ смысл даже упоминать этот язык. К тому времени война языков почти П г нчилась, было совершенно ясно, что C/C++ и Visual Basic победили, a Pas- Vнеслышно скользил за линию горизонта, как раз в направлении побережья с- tts Valley1. Трудное финансовое положение Borland было широкоизвестно, также играло определенную роль во всем процессе. А. затем, уже по дороге на похороны, случилась забавная вещь: Borland выпустила Delphi. Возможно это и не показалось бы вам столь забавным, окажись вы тогда на месте Microsoft, успешно продававшей Visual Basic, или на месте любого поставщика C/C++, потому что после этого события в течение первого же квартала было продано 125 000 копий Delphi. Эти продажи внесли значительный вклад в показатели Borland на конец квартала, и 30-го июня компания преподнесла аналитикам (да и почти всем на планете Земля) огромный сюрприз - показала наличие прибыли. Что же произошло? Конечно, это была классическая встреча спроса и предложения. Я думаю, что Delphi очень вовремя (для Borland) удовлетворили отчаянную нужду значительной части программистского сообщества, но нужду не в Pascal как таковом, а в чем-нибудь, отличном от C/C++. На основании моих лргчных бесед со многими программистами, я пришел к убеждению, что многих из них просто тошнит от причудливых лабиринтов и путаниц C/C++ (а также сопутствующих ему библиотек и интегрированных сред разработки), и что они всего лишь хотят видеть рациональную альтернативу. В апрельском выпуске Software Development Jornal за 1995 год я опубликовал статью о возможностях повторного использования кода в Delphi — об °Дной из сильнейших характеристик этого продукта. Эта статья вызвала целый поток писем в мой адрес, поразительно похожих друг на друга. Почти все со- 00Щения были от корпоративных разработчиков, собиравшихся начинать свои н°вые, в некоторых случаях довольно крупные, проекты, и просивших моего овета - не лучше ли им оставить C/C++ и перейти на Delphi. Основываясь информации об их проектах, которую они мне предоставили, я дал им ,ветствующие рекомендации. Но один мой совет был одинаковым для х ~~" приобрести копию Delphi и оценить продукт самостоятельно. (На мой ЛяД, глубоко ироничным был тот факт, что еще до выхода финальной Ini Delphi этому продукту был присвоен титул «Visual Basic killer»2, тогда 1 у g v > alley — город и Калифорнии (запад США), где расположена штаб-квартира компании incl» которая, и свою очередь, сегодня является clc facto монополистом на рынке компи- 1 °в ялыка Pascal. [Примечание переводчика.]
166 Инст Не ПС «ак ни в одном из вышеупомянутых писем Visual Basic даже не упоминался качестве возможного варианта для более-менее серьезного проекта. Вез> выбор делался между C/C++ и Delphi, pi лишь один респондент спрашивал цг PowerBuilder. Я полагаю, Delphi кладут Visual Basic на обе лопатки. Но щ кажется, что какая-то еще большая игра лишь только затевается.) Бывалые ветераны «языковых войн» часто отмечают, что следует четкг отличать языки от компиляторов. Это правильная точка зрения, но только ъ тех пор, пока вы ведете лишь теоретические споры об инструментах. Реально положение дел на этом рынке таково, что на самом деле вы выбираете не язык а прежде всего продукт, который, в свою очередь, уже включает в себя какой то язык. Кстати, в отношении Pascal этот нюанс практически незгшетен сегодняшний день Delphi от Borland, включая каркасную библиотеку VCL, сути безраздельно господствует на всем рынке «паскалевских» инструментов разработки для Windows. Л с языком C++ ситуация намного сложнее, так как существует несколько приблизительно равномощных компиляторов, раз нообразные каркасные библиотеки и т. д. — все это предоставляет достаточно много степеней свободы для выбора и смешения. Ну, ладно, достаточно я увиливал от прямого ответа. Теперь я готов выйти вперед и заявить, что я отдаю решительное предпочтение Pascal перед C++для практически любого программного проекта. И именно Pascal я использую в своих контрактных работах когда только возможно. Используя Pascal, я могу создавать надежные, высококачественные, легко сопровождаемые программы гораздо быстрее, чем при помощи любого инструментария, ориентированного на C++. И в этой книге я использовал для примеров Visual C++ только потому, что сегодня Visual C++ является доминирующим инструментом разработки для Windows, и, следовательно, именно код на C++ является наиболее доступным С/С++: системный C/C++ появился на свет в начале 70-х и предназначался для системное программирования. Поэтому вовсе не удивительно, что язык С (а в еще боль шей степени — сам факт построения C++ на основе С) демонстрирует филос°' фию, подходившую для этой роли и для того времени. Сегодня мы используй эти языки для прикладного программирования и тем самым эксплуатируемIIN такими способами, котоэых создатели и не пэедетавляли. 2 «Visual Basic killer» — (буквально — убийца Visual Basic) гот, кто должен окон чате подавить своим превосходством Visual Basic [Примечание переводчика.] wibl*
системный язык, используемый в качестве приклалного ■ 67 Классическим примером является схема работы с заголовочными файлами. " ie добрые времена заголовочные файлы были маленькими, и никаких ' СД\ проблем ие возникало. По крайней мере, с точки зрения производитель- Сегодня это не так. Например, Visual C++ предоставляет вам следующие !,п ювочные файлы: ]VlSVC20\INCLUDE: 247 заголовочных файлов, 4.4 Мб MSVC20\INCLUDE\SYS: 5 заголовочных файлов, 6 Кб |V[SVC20\INCLUDE\GL: 3 заголовочных файла, 84 Кб \MSVC20\MFC\1NCLUDE: 44 заголовочных файла, 604 Кб \ \ > \ы Конечно, в отдельно взятом проекте вам вряд ли понадобятся все эти фай- Но те заголовочные файлы, которые необходимы для Windows-програм- мнрования, в сумме представляют собой сотни килобайт, что при компиляции вызывает серьезные проблемы с производительностью. Совершив героические усилия для ухода от этой проблемы, производители обратились к методу прекомпиляции (предварительной компиляции) заголовочных файлов. Этот метод работает удивительно хорошо, но весьма далек от совершенства. Даже небольшие проекты порождают мегабайты преком- пнлированных файлов, присутствие которых вы вынуждены терпеть в течение всего времени разработки. В то же время, чуть ли ие каждое изменение в настройках вашего проекта вызывает перегенерацию прекомпилированного файла (зачастую без реальной необходимости) и добавляет еще одну минуту к очередной компиляции. Блиц-вопрос: в чем отличие между «const char *x» и «char const *x>>? Первое — это указатель на постоянный символ, а второе — постоянный указатель на символ. Многие программисты, пишущие на C++, легко ошибутся при ответе на этот вопрос, если только не задать его в контексте конкретного исходного кода. Либо они будут вынуждены достаточно долго думать над ответом, что в большинстве случаев все равно приведет к ошибке. Компиляторы C/C++ не обеспеч 110м°Щп. Например, Visual C++ 2.2 (даже включенш< I предупреждений), встретив такую lnt fred[100] = { 10, 10 }; Некоторые апологеты C/C++ заявляют, что это хорошо («Это не баг, это Нс1>> ); поведение компилятора в такой ситуации четко определено и дает фаммисту излитый, быстрый способ проинициализировать первые не- 1 1 « 110 не 6а/, это фича!» — (ог английского «it's not a bug, it's a feature'») любимая в«рка многих программистов; если очистить приведенный перевод от жаргонизмов, то она " 1а бы примерно так «Это не ошибка, гак и было задумано» [Примечание переводчика ]
168 Инструъъ сколько элементов массива, а остальные оставить компилятору, который авт матически присвоит им всем известное и полезное значение — ноль. А теце задумайтесь о том, в какой кошмар это может вылиться при сопровождении г кода. Например, пусть в вашем большом проекте есть несколько массивов од, какового размера (заданного одной константой), полностью инициализировав ных строками следующего вида: int fred[fred_size] = { 10, 10 ); Пусть в какой-то момент вам потребовалось изменить размеры всех эти», массивов с двух до четырех. После того, как вы измените значение коистанть fred_size, вам придется проверить каждое место, где она использовалась дл? инициализации массивов. И делать это вам придется «врукопашную», посколь ку компилятор не предоставит вам никакой помощи в этой ситуации. Для срав нения: в Delphi, в аналогичном случае несоответствие количества инициали зированных элементов размеру массива считается ошибкой, поэтому места для ваших ошибок тут уже не остается. Продолжая разговор о массивах, я просто поражаюсь, почему C/C++ до cm пор не поддерживает массивы с ненулевым началом отсчета индексов? Вам нужен массив с индексами в диапазоне от -37 до 104? В Delphi вы можете просто объя вить его именно таким, и нет проблем. А C/C++ вынудит вас заниматься арифме тикой индексов самостоятельно. Еще более поразительным является наличие (оп циопальное!) в Delphi такой возможности, как автоматическая проверка указателя на элемент массива на предмет выхода его за допустимые границы (хотя Pascal нуждается в таком контроле значительно меньше, чем C/C++). Все же в C++ есть один способ преодолеть многие проблемы, связанные с массивами: написать свой собственный класс, эквивалентный встроенной реализации массивов, и добавить в него все недостающие возможности — произвольные диапазоны индексов, пограничные проверки и так далее. Однако это довольно смехотворный аргумент — он лишь подтверждает мое давнее убежд?' ние, что в контексте прикладного программирования C++ скорее является очень полным языковым снаряжением, а не полнофункциональным языком прикладного программирования. В чем отличие между языком, который по сути является лишь «языков^1 снаряжением», от истинного прикладного языка? Ответ прост: в обШе11 направленности реального применения базовых возможностей по созданию повторно используемых компонентов В первом случае они очень часто используются для «программирования вниз» — добавления такИ* вещей, как пограничных проверок индексов массивов, строк перемени01 длины и т п — то есть для создания таких возможностей, котор^1 должны быть встроены в сам базовый язык. Во втором же случае язь1ь используется практически только для «программирования вверх» — ^ создания компонентов прикладного уровня (например, для реализаН*1
системный язык, используемый в качестве приклалного 16" специального вида диалога About, который затем может быть легко И ят встроен во многие программы и проекты) это были всего лишь несколько мелких примеров... (Правда, в главе 3 , же касался одной из подобных проблем — причудливого мира печально П естных «строк, завершаемых нулями».) К огорчению всех пользователей + язык действительно перегружен подобными недостатками, и из-за этого известные мне реализации этого языка являются поистине враждебными по тНОшению к программисту. В качестве последнего примера, позвольте мне за- b один вопрос: вы замечали, как порой сходят с ума компиляторы C/C++ пропущенного символа «точка с запятой»? Если вы уберете этот символ у последнего прототипа функции в заголовочном файле, то получите целое по- гнище сообщений об ошибках, ссылающихся на строки файла реализации (в все ю который был включен «искалеченный» заголовочный файл) и сбивающих вас с пути. Выбросите этот символ в Delphi — и компилятор наведет курсор точно на цель и скажет вам, что в этом месте он ожидал увидеть именно точку с за- к* пятой. Некоторые могут возразить мне, что поведение компилятора C/C++ сильно *j о варьируется от одной реализации к другой, и что производители этих компиляторов делают буквально все, что могут, пытаясь сделать свои инструменты удобнее и эффективнее. Вот в этом-то и состоит проблема: базовый язык настолько устарел, что для использования его в разработке приложений для Windows даже таких титанических усилий оказывается недостаточно. Мне меньше всего хотелось бы осыпать ту или другую сторону обвинениями по поводу этих проблем. И вам не следует этого делать. Значение имеют лишь объективные, текущие и перспективные характеристики тех инструментов, которые мы используем. 1Аиф о переносимости кодами переносимость кода преподносилась как одна из сильных сторон языка С. Позже эту характерную черту стали приписывать и его славному потомку — языку C++. Наверное, это сделали просто по инерции, потому что ПеРеносимость кода, написанного на C++ — смехотворный миф, особенно если сети речь о Windows-программировании. Для примера, возьмите исходный код программы Word Pad, написанный *я Visual C++ 2.2 и соответствующей версии MFC, и попробуйте перенести в Borland C++, который не поддерживает MFC. Думаете это нечестный iMep? Тогда попробуйте различия в реализации шаблонов, обработки чЛ1°чнтельных ситуаций, других мелких возможностей языка. Благодаря гообразшо каркасных библиотек и тому факту, что стандарта языка C++ до пор нет, все ныне существующие пакеты для Windows-разработки на C/C++
170 Инст на самом деле являются разными диалектами одного и того же языка (а в которых случаях — вообще разными языками). С другой стороны, Object Pascal от Borland был безумно нестандарту изначально и будет таковым всегда. Но он имеет при этом одно подавляюц' преимущество — он один на свете. Поэтому перенос кода, написанного на о ject Pascal, с одного компьютера на другой никогда не является проблемой Предназначение такой вещи, как стандарт языка программирован^ заключается в том, чтобы все отвечающие ему реализации могли быть таточно схожими, и чтобы перенос кода с одной реализации на другую мог ос ществляться эффективно и приводил к предсказуемым, успешным результата* Как же иронично выглядит тот факт, что именно нестандартный язык Delpf являет собой окончательное решение проблемы переносимости: поскольку шествует только одна его реализация, все ее инсталляции по определен^ идентичны (а не только «достаточно схожи»). Дос о С/С++ и определяемые пользователем типы На мой взгляд, наиболее странной чертой C++ является его одержимосп следующей идеей: дать возможность вашим собственным классам выглядеть по хожими на встроенные типы данных. Один только этот замысел потребова массу других возможностей и сделал язык более сложным, более медленным в смысле компиляции, и, в конечном итоге, — более чреватым ошибками. Не явные вызовы конструкторов и деструкторов, конструкторы копирования переопределение операторов, ссылки — вот неполный перечень самых неисся каемых источников трудноуловимых ошибок. Object Pascal, для сравнения, имеет настолько упрощенные возможноеи* ООП, что программисты бывают шокированы при знакомстве с ними после C++. Нет множественного наследования? Нет переопределяемых функций' операторов? Нет функций, возвращающих ссылки? Как же все это может бьДО серьезным языком для ООП? Это случайно не ООП для «чайников»? Наса мом же деле оказывается, что эти вещи не нужны в качестве внутренних язы ковых возможностей, даже если потом приходится строить их логические экви валеиты другими средствами. Например, программисты на C++ очень часто реализуют канонически' конструктор копироваия, который дает возможность делать «глубинные» к° пии объектов. Если такого конструктора в классе нет, то C++ будет соверши побитовое копированрге объекта. И если среди членов класса есть хотя бы 00 указатель, такое «поверхностное» копирование может привести к проблем самого разного сорта. (Наиболее типичный пример: если значение такого Ук' зателя получено путем динамического размещения памяти, то после побитой01 копирования исходного объекта у вас появится еще один объект, содержав1 а' указатель на тот же самый блок памяти; и если теперь какой-нибудь из этпх
• системный язык, используемый в качестве приклалного Л7\ 9 о3 попытается освободить эту память после того, как это уже сделал и объект — серьезные неприятности вам гарантированы.) В Object Pascal ДР- эовно такая же проблема. И функция «глубинного» копирования может eCl добиться в обоих языках. В Object Pascal использование такой функции П° ,/рт выглядеть примерно так: DeepCopyFred(deGtFred, GourceFred); а на C++ так: ое Г* tFred = sourceFred; по Надо отдать должное языку C++: его дополнительные возможности позволяют не только делать одиостроковые выражения более элегантными с виду. Нельзя отрицать, что поддерлска объектов в C++ глубже и полнее, чем в Object Pascal. Это очевидно является выигрышем, поскольку дает возможность ис- льзовать созданные объекты более обобщенно и естественно. Но какой ценой это достигается? Действительно ли повышенная сложность и запутанность языка является разумной ценой за более полные возможности эксплуатации объектов? Или все-таки эти дополнительные возможности являются лишь «тепличными» и непригодными для использования в реальном мире? Мой опыт, основанный на многих тысячах строк Windows-кода, подсказывает, что верным является последнее. Аттестации, аттестации и еще раз аттестации Модули языка Pascal являются логическими эквивалентами комбинации ^компилированных заголовков и OBJ-файлов языка C++: они имеют примерно такие же размеры, но их построение ие сопровождается такими неприятными накладными расходами на начальную компиляцию. Это одна из основных причин, по которым Delphi компилируют значительно быстрее компиляторов C/C++. С какой скоростью компилируют Delphi? В качестве ответа на эт°т вопрос я предлагаю вам взглянуть на результаты небольшого сравнительного эксперимента, который я провел на своей системе с процессором 486- Х4-100 и 32 Мб оперативной памяти, разумеется, под управлением Windows ' • Для эксперимента я выбрал компиляторы Delphi и Visual C++ 2.2 и исход- дЬ1е тексты Stickles! и Word Pad (последние входят в поставку Visual C++ 2.2). изменил настройки проекта Word Pad таким образом, чтобы сделать компи- Ции в Delphi и Visual C++ как можно более близкими друг другу в смысле с Рцт: я отключил все оптимизации (поскольку Delphi не являются оптими- РУющим компилятором), использовал статическую версию MFC (поскольку кия OWL для Pascal может быть скомпонована только статически), а также г °1101Шл генерацию МАР-файла и базы данных для броузера. Вот сводка ито- *1оего эксперимента:
172 Инстр Stickies! 16-разрядный Borland Pascal/OWL 43 696 строк исходного кода 48 единиц компиляции 32 ресурсных файла, суммарный размер — 146 Кб Суммарное время компиляции и компоновки: 16 секунд WordPad заго 32-разрядный C++/MFC 14 392 строки исходного кода (11 421 строка C++ и 2971 строка ловочного файла) 36 единиц компиляции 24 ресурсных файла, суммарный размер результирующего RES файла — 54 Кб Суммарное время компиляции и компоновки: 5 минут 7 секунд Компиляция ресурсов — 10 секунд Компиляция STDAFX.H — 53 секунды Компиляция исходников — 3 минуты 28 секунд Компоновка — 36 секунд Пури 32-разоядным, смешения и вообще почти всего в одном тесте. Такой протест был бы вполне обоснованным, если бы я, скажем, пытался сравнивать влияние какого-то отдельного компонента (языка или каркасной библиотеки) на общую результирующую Но меня волнует как раз не это. Моя ном эксперименте б! двух сред разработки на реальных проектах. Как показывают приведенные выше цифры, Delphi и Visual C++ даже близко не стоят друг к другу. По количеству строк кода WordPad втрое меньше Stickies!, в то время как полное его построение происходит в 19 раз дольше. Если вы возразите, что полное построение приложения - это еще не ъсе< и что вы гораздо чаще выполняете лишь частичные сборки проекта, я не буД) спорить. Я просто сообщу вам результаты еще одного дополнительно!"0 эксперимента с теми же инструментами: выбрав в исходниках Stickies! и Word' Pad примерно одинаковые, маленькие единицы компиляции (около 250 стр°ь кода в каждой) и сделав в них тривиальные изменения, я пустил секундомер1! «make». Результаты: Delphi/Stickies! - 5 секунд, Visual C++/Word Pad — ^ секунд. Что ж, по крайней мере порядок превосходства уменьшился на еД*1
системный язык, используемый в качестве приклалного ■ 73 правда, при этом можно заметить другой интересный факт: частичная Hf ■ ,0 „ Visual C++ 2.2 (35 секунд) все же происходила в два раза дольше, С°0})полная сборка в Delphi (16 секунд). Че> Кто-то скажет вам, что разница в производительности операций такого не имеет никакого значения в реальном мире, потому что в больших, 1 ^1 0\> ' езных проектах основная часть вашего времени тратится на проектирова- и тестирование, а не на наблюдения за мерцающим курсором во время ком- ядпй и компоновок. В какой-то степени, это очень ценное замечание. Но, в е время, всякий, кто использовал C++ в большом проекте, также может ..ссказать вам, насколько тягостными могут стать процедуры компиляции, и 1 к ЭТ0 деформирует рабочие привычки. Например, вы быстро заметите, что •гараетесь накапливать разнообразные мелкие изменения, которые нужно едешь, а потом совершать их все за один раз, чтобы избежать нескольких лишних компиляций. Я сам все время так делаю, работая на C++. Для сравнения, при работе на Delphi я компилирую гораздо чаще просто потому, что там эта процедура такая скоростная и дешевая. По сути, я использую компиляцию в качестве средства быстрой проверки синтаксиса. Чтобы зря не тратить силы на эксперименты с реальными проектами, попробуйте экстраполировать результаты моего маленького теста. Даже если великодушно предположить, что в большом проекте этапы компиляции и ресурсов, компиляции заголовков и компоновки займут столько же времени, а время компиляции главного исходного кода будет линейно зависеть от его размера, то для полной сборки проекта из 50,000 строк понадобится около 14 минут, а для 100,000 строк -- около 26 минут. Я видел сборки намного более длительные, происходившие на быстрых компьютерах и имевшие в распоряжении больше ресурсов (оперативной памяти, скорости жесткого диска и свободного места на нем) Еще один пример того, как скорострельность инструмента может влиять на разработку: вы когда-нибудь пробовали изменить имя константы или неремен- н°и, широко используемой в проекте? Какая морока — выискивать все эти ссЬ1лкн редактором или шерстить исходники утилитой grep. В Delphi я просто изменяю имя в месте его главного определения, а затем делаю повторные компиляции, по несколько секунд каждая. При встрече с каждой ссылкой, ставшей 1еперь неизвестным именем, Delphi останавливаются и помещают курсор прямо нУжное место -- простой трюк, который все еще невозможен в Visual C++. # чтобы не завыть... До сих пор я упомянул лишь некоторые из тех палок, которые C/C++ и чУЩие его реализации вставляют в колеса программных проектов. Вот что ще Дают нам эти же самые инструменты:
В Чувствительность к регистру символов, которая порождает класс Дурацких Синтаксических Ошибок, а также бессчетно личество семантически бессмысленных объявлений типа HWND hwnd; Условный onevamov «?», который обычно f/then/else, и вдоб абельность исходного кода. Иными °Tpi ( тельную прибыль (потерю читабельности) с пулевого дохода (п,х водителыюсть остается прежней) -- любой дилетант в экономна жет вам, что это плохой бизнес. В Сосуществование массивов, указателей и ссылок. Именно эта хар терная черта C/C++, как никакая другая, разоблачает его врождещ ориентацию на системное программирование и вытекающую из этг врожденную неуклюжесть в качестве языка прикладного прогр, мированпя. Вы когда-нибудь пробовали обучать программистов^ вичков этой части языка? Я пробовал и могу без преувеличения CI зать: достаточно одного-единственного урока на эту тему, чтобы у(- дить кого угодно в контр-интуитивности этой области синтаксиса. В Строки, завершаемые нулями. С минимальным отрывом занимг второе место после трехголового дракона Массивы-Указатели-Ссыль Этот механизм на пару с более чем сомнительным дизайном ста дартных строковых функций, без сомнения, лежит в корне неисчш мых ошибок существующих сегодня программ. Целочисленные тины, которые меняют свои размеры при изменен! целевого исполняемого кода с 16-разрядного на 32-разрядный. Вьи жете предъявить более безумное архитектурное решение, чем это? сожалению, Borland совершил ту же самую ошибку в 32-разряда версии Delphi — смотрите врезку «Delphi32: Pascal достигает с вершенполетия» на стр. 190.) В Бесконечные опции компиляторов и компоновщиков. Я до сих п< качаю головой от недоумения по поводу того, как легко было созДа в Borland C++ 3 бе неприятностей и собирал программу, моментально вызывавшую ^ при запуске. Проблема (и ее решение) во всех этих случаях кры х сборки; в некоторых случаях мне удавалось доб1 роблем и с программами-примерами от Borland - л* путем изменения опций Отсутствие локальных функций (эту тему я подниму в главе 5)- В Компиляторы, щедро изливающие потоки неправильных или н?У стных диагностических сообщений. Если бы компилятор не тра'
так прекрасен 175 много времени на сборку проекта порядочного размера, я бы просил его останавливаться после первой лее найденной ошибки. Но поскольку компилятор не так проворен, мне приходится за один проход находить и исправлять все ошибки, какие возможно. Это оз- ачает для нас выбор — либо тратить массу времени на компиляции, н 1ибо подолгу таращиться на целые диагностические «романы», пытаясь отфильтровать ложные сообщения. (Поначалу я хотел объявить конкурс на самое большое количество неповторяющихся диагностических сообщений, которого можно добиться в C/C++ «односимвольной проблемой». Но потом я отказался от этой идеи — слишком депрессивным было бы зрелище показательных выступлений конкурс ант ов.) и Пока преданные сторонники C/C++ не сформировали суд Линча и не шли штурмовать Эидвелл, я поспешу сделать одну оговорку: ничто из вышесказанного ни в коем случае не означает, что вы не можете писать перво- кштные прикладные программы на C/C++. Было бы абсурдом утверждать это хотя бы потому, что рынок программного обеспечения тут же предъявит сотни контрпримеров. Суть моей точки зрения состоит лишь в том, что C/C++ возлагает на программистов гораздо более тяжелую ношу и требует от нас гораздо больше стараний для уклонения от целых классов ошибок, которые при использовании других инструментов либо вообще невозможно сделать, либо сделать значительно труднее. Судя по качеству коммерческого программного обеспечения для Windows, такие старания (или адекватные усилия по тестированию, или и то и другое вместе) сегодня явно в дефиците. Можно взглянуть на это и по-другому: несомненно, вы смогли бы писать отличные Windows-приложения и на ассемблере, если бы захотели. Но в том- то и дело, что никто на свете никогда не стал бы рассматривать такой вариант в качестве разумного пути (разве что не в каких-нибудь экстремальных условиях). Суть альтернативы в том, где именно пролегает пограничная линия ме- Л(ДУ «разумными» и «неразумными» программными инструментами в конкрет- НЬ1Х Уровнях и в конкретной задаче. По моему личному опыту, C/C++ и опУтствующие ему инструменты почти всегда оказываются на той же стороне -Поц границы, что и ассемблерный язык. ..a C/P-4--L во так ужасен, то почему Pascal не используют все? Это справедливый СТь много причин, по которым Pascal не занимает сегодня достойного по- 'Кения. jq4 - -"".v/i \j 1ЦЛГ1Ш1, 11U tVUlUpJDIM. JTcia^cll ПС OcirmiHclC 1 tCl иДЛЛ AULlUilllUlU I1U" ия. Но первой на ум приходит серьезная проблема с имиждем, который ^крепился за продуктами Borland, базирующимися на Pascal. Разве
176 Инструмр \ можно было серьезно воспринимать компилятор, в названии которого стоя, I I f слово «Turbo», а цена которого не составляла сотен долларов? Слишком мн гпе разработчики, обращая внимание на подобную чепуху, совершили в те ч. ние времена ошибку, отвергнув Turbo Pascal как «игрушку». Но так или инач, «игрушечная» репутация сохранилась за продуктами Borland. Тем не менее, Pascal-продукты от Borland действительно имели и имек серьезные ограничения, такие как отсутствие поддержки различных модеде пямяти (хотя в моей реальной работе это почти ни разу не было помехой) неспособность генерировать объектные модули в стандартном формате (^ конечно же, является более серьезным недостатком). Но вернемся к порокам Object Pascal. Канонический упрек критиков адрес всех разновидностей Pascal состоит в том, что он якобы представляет со бой «смирительную рубашку» и обычно не дает программистам достаточно свободы при кодировании. Я был бы счастлив разгадать, откуда могло поя виться такое мнение (может быть, оно как-то связано с НЛО?), потому чтое действительности все совсем не так. Если вас достает проверка типов в Object Pascal, вы делаете ровно то же самое, что и в другом строго-типизированщ языке под названием C/C++ — приведение типа. На самом деле, во всех мот разработках я гораздо реже прибегаю к приведению типа в C/C++, чем в Pas cal. Среди 43 000+ строк па Pascal, которые составляют Stickiest, есть всего дюжина приведений типа и только одна вариантная запись (конструкция, ана логичная объединению в языке С). И это -- учитывая все те рукопашные бои которые Stickiest ведет с Windows API и OWL. Нет, лично мне это отнюдь не кажется работой в «смирительной рубашке». Другая, на этот раз действительно существенная, проблема заключается том, что Pascal-продукты от Borland всегда были слабоваты в поддержке проек тов и управлении ими. Borland Pascal и Delphi позволяют вам задать все ошДО в компиляции прямо из интегрированной среды, по не дают возможность сохранять поименованные наборы этих настроек для проекта в целом (подобна тому, как обстоит дело с конфигурациями проекта «Release» и «Debug» в Vis ual C++, к которым давно привыкли программирующие на C/C++). В Delpb вам приходится заводить несколько отдельных проектов, включающих в одни и те же исходные файлы — глупая и ненужная морока. се& al Единственной моей огромной и категорической претензией в адрес Pasc продуктов от Borland всегда была иеудовлетвореность тем, что они сиги'1 лизируют об ошибках, но не дают предупреждений. Я прекрасно понимаю, чЬ это одна из причин их поразительного быстродействия при компиляции, по М1*' кажется, что обязательно должен быть обеспечен и компромисс, при котор0 т° компилятор мог бы опционально обнаруживать и потенциальные проблемы, 1" кие как возвращение функцией указателя па локальную переменную, заверь ние функции без явной установки возвращаемого значения, неиспользу^1Ь локальные переменные п т. д. и т. п. Для языка, задуманного как инстрУ^
pascal так прекрасен • ф Л77 9 ;tV [OC'l знейшими ограничениями. (Н ама компания Borland, вы моя iW [ 11^ Delphi32 Hah thumb с C++? C/C++ не исчезнет на следующее утро. Да и вообще никогда не исчезнет, .цг Ка то пошло. Он уже так глубоко окопался, что все мы будем жить и рабо- с шш, в тон или. другой форме, на протяжении десятилетий. Как же лучше всего делать это? Как я упоминал выше, когда вы выбираете инструмент, процесс изучения и эксилуацтации не кончается, а продолжается на микроуровне, в границах выбранного инструмента. Вероятно, для компиляторов C/C++ это характерно более, чем для большинства других инструментов. (Посмотрите врезку «Оптимизация: бесплатный сыр подай!» на стр. 182, и вы прослушаете поучительную [екцию об одном из наиболее обманчивых свойств компиляторов C/C++.) Во-первых, вы должны удостовериться в том, что до конца понимаете возможности C/C++ и вашего компилятора: что они могут сделать для вашего проекта, и чего не могут. Существует много хороших общедоступных книг о языке C++, и в главе 16 я даже предложу вам конкретные рекомендации. Ос- t » мотритс полки вашего местного книжного магазина, найдите две-три книжки, которые покажутся вам наиболее подходящими, купите их, а затем прочтите их полностью, от корки до корки. Вы будете поражены множеством интереснейших деталей, которые почерпнете из них. Во-вторых, решите для себя, насколько глубоко вам необходимо погрузиться в язык. Бьярн Страуструп, отец-основатель C/C++, в своей превосходной книге The Design and Evolution of C++ говорит следующее: По моему опыту, наиболее безопасный путь — изучать C++ снизу вверх, то есть вначале изучить те возможности, которые C++ предлагает для традиционного процедурного программирования (т. н. «улучшенный С»), затем научиться использовать и понимать средства абстрагирования данных, а потом освоить использование классовых иерархий для организации наборов взаимосвязанных классов. о ДЦ\ Л полностью согласен с вышесказанным, но хотел бы доб шеище Мо*ное обнаружите, что исследуете намного больше <• геи, Н C++/ II в |-ч *-'|-'^iTJ.^I IJlVUV/^IUU у IVy «.' 1. V/ J-> 1'lV/HiV UV/141'IVvj^ IV-'V IVIl^l JIUVVvlVLUA. Л-/ 1CIV. IIIUV^ L 11^ с1ммируя на C++ для Windows, я использую тот самый диалект «C++ как
178 Инсъ f^Hn улучшенный С» - применяю MFC в качестве набора ячеек, в которые встц ляется мой код. В противоположность этому, мой код на Object Pascal выг 1Я дит как взрыв на фабрике объектов — а все потому, что я чувствую Се* значительно комфортнее с объектной моделью Pascal, которая по количестр сюрпризов для программиста даже близко не стоит к C++. Например, в Sti^ icst я использую десятки объектов с огромной пользой для программы. Такая стратегия применения возможностей C++ годится для меня ц д,„ моих проектов. Что годится для вас - решать вам. Но, собираясь принят, решение, обязательно помните, что нет такого правила, которое заставляло б' I Г вас задействовать все возмолсиости языка или другого инструмента. Программа Stickiest является хорошей иллюстрацией к рекомендацц, «You(now) != Yoii(later)», о которой я говорил в главе 2 на стр. 79 Первоначальную версию я начинал писать в начале 1992 года, во временс моего сильного (я бы даже сказал, почти маниакального) увлечена ООП В результате самый старый код в Stickiest оказался более объ ектно-ориентированиым, чем если бы я писал его сегодня. Код, которыг я добавлял для следующих версий, был уже не таким объектно ориентированным, поэтому в нынешних исходниках смесь стило- кодирования и дизайна бросается в глаза настолько сильно, что я пора сам не верю, что все это было написано одним человеком — мной Также как и у вас, у меня вечно нет времени переписывать уже отлаженный! распространяемый пользователям рабочий код только лишь для тою чтобы привести его стиль к общему знаменателю. Поэтому Stickiest оси нется в своем нынешнем, слегка беспорядочном состоянии до тех пор KJ 9 • пока не перепишу эту программу целиком по какой-то другой причине Каркасные Библиотеки Ваш выбор каркасной библиотеки (начиная с вопроса, выбирать ли что-* вообще) так же важен, как выбор языка программирования. Он во много* определяет, что вы сможете делать (или, по крайней мере, что вы сможете Дс лать легко). Уверен, многие из вас сделали из вышесказанного вывод о том, что неЛ1 пользование каркасной библиотеки тоже может быть обоснованным выбор0' Та небольшая доля истины, которая заключена в этом выводе, сегодня стан1 вится все меньше и меньше, благодаря таким вещам как OLE и забота о rrepehl симости на другие платформы. Тем не менее, работа без какой-либо каркасе библиотеки все еще остается возможным сценарием разработки и, в завис11'1 сти от конкретных условий, может оказаться наиболее правильным выбор0 Например, если вы собираетесь написать простую утилиту, которая Д°Л)Ь уместиться в тоненькую щелку, оставшуюся на одном из дистрибутивны*'' ков, то написание ее без применения каркасной библиотеки может окяза , решением, в каких-то случаях - даже единственно возможным (если в() ^
[САИ Pascal гак прекрасен Ф Ф Ф 179 d ится так — либо использовать библиотеку и тем самым добавить еще один к дистрибутиву, который будет затем разослан десяткам или сотням тысяч иск \ ,1ей либо не использовать библиотеку, но сохранить работу — выбор Совершенно очевиден). Если вы собираетесь использовать каркасную библиотеку, вы должны оце- альтернативы очень тщательно и убедиться, что они действительно нить о I Л'спечивают те возможности, которые вам нужны, и что они делают это удов- гворяющим вас (и ваших пользователей) способом. Это особенно актуально > icx случаях, когда важным требованием к проекту является переносимость между различными платформами. Поддерживается ли создание свернутых (минимизированных) программ? Как насчет поддержки перетаскивания-бросания, DDE и OLE? Как насчет новых элементов управления Windows 95? Если вы считаете, что вам понадобятся автоматически рисующиеся окна-списки, позво- 1яг ли вам сделать это, или вам придется добиваться логически эквивалентного нгерфейса лишь теми средствами, что имеются в каркасной библиотеке? Насколько широко охватывается Windows API в этой библиотеке? Более широкий охват, сам по себе, не является безусловным достоинством; например, если вы собираетесь много работать с «рисовальной» графикой, то мощная поддержка GDI не имеет значения для вас и не принесет вам прибыли, какой бы крутой она ни была по описаниям в полноцветных рекламных буклетах. и не йобавления и аксессуары Подобно тому, как ни один человек не является совершенством, ни один компилятор не является полным п законченным средством разработки (несмотря на то, что многие производители хотели бы убедить вас в обратном). Вы можете и должны наращивать ваши основные инструменты при помощи более мелких добавлений и аксессуаров. И я хотел бы немного подробнее поговорить 0 Некоторых из наиболее общих дополнительных средств. Ъретъесторанние библиотеки и компоненты D hni напичканы практически все каталоги программного инструментария. 4 ли вам удастся открыть хотя бы одну страничку такого каталога и не быть ,0 v )Вс1нным рекламой или описанием какого-нибудь VBX-компонента. Вот- наступление бросятся компоненты для Delphi и OCX-компоненты для Ual Basic 4 О "0л ;ЖНо ли вам их использовать? Как всегда, ответ гласит: когда как. Это \tCIT Зс1висеть от вашей задачи, от ваших навыков, от имеющихся в вашем 1оГп >Кепии библиотек, от вашего кошелька и, наконец, от плана-графика ва- пРоекта.
180 Инструм^Тк Идти по этой дороге следует предельно осмотрительно. Я слышал профессионалов многочисленные жалобы об ужасном качестве лицензирова, ных компонентов и об отсутствии поддержки. Если вы пишете публичную о i о .** программу, тщательно исследуйте и продукты, и их производителей до т0Г( как привяжетесь к кому-то из них. Удостоверьтесь, что полные исходные те} о сты либо поставляются вместе с'продуктом, либо будут охотно предоставлен вам производителем в случае необходимости. Учитывая стремительное размн жение разнообразнейших компонентов на сегодняшнем рынке, следует ожидат самых неожиданных пертурбаций среди многочисленных поставщиков увеличения числа «продуктов-сирот». А значит, все выше pi выше становитс риск заработать серьезные неприятности на свою голову, неосмотрительн выбрав третьесторонний продукт и наткнувшись в нем на серьезную ошибк или фатальное ограничение. Третьесторонние отладчики Все основные инструменты для Windows-разработки поставляются с своими собственными отладчиками. Зачем же тогда покупать какой-нибу^ третьесторонний отладчик, если эти деньги можно пустить на существенное m полнение своих запасов Jolt Cola и Doritos1? Да просто потому, что существу! такая замечательная вещь, как Bounds Checker от Nu-Mega Technologie Bounds Checker использует отладочную информацию, помещенную в ваи приложение, и поднимает тревогу по поводу самых разнообразных пробле передача недопустимых или неправильных параметров при вызове API, утеч! ресурсов, некорректное размещение pi освобождение памяти и пр. Сам. замечательная черта этого инструмента — способность указать точную стро! исходного кода, в которой была обнаружена проблема. Bounds Checker — превосходный пример высокопроизводительно ч_» инструмента, который достоин включения в арсенал каждого разработчика. Windov Системы управления версиями Вы — не стадное животное, вы — одинокий волк, независимый и с3°' ный, идущий своей дорогой через кибер-равнины так, как считаете нуЖнь1'' потому зачем вам использовать систему управления версиями? Разве это не полезный в пути хлам? 1 Jolt Cola п Doritos — популярные среди американских программистов (да н не толь4 них) питье (кола с повышенной концентрацией кофеина) и легкая закуска (что-к* чебуреков) [Примечание переводчика ] цЛ,(
прекрасен Ф 0 181 тт т Даже если вы выполняете 6ольи1Инство ваших работ в одиночку, вы извлечь немалую пользу от применения системы управления 1 >[0/. гми. Как часто вы замечаете, что вам необходимо поэкспериментировать ^ <• й-нибудь новой деталью программы или сделать специальную версию для ili "о или нескольких из ваших пользователей? Такие случаи возникают и система управления версиями может значительно облегчить вам жизнь. • зчно вы можете вручную создать новое дерево каталогов и скопировать все файлы проекта. Ыо тогда как вы будете собирать и соединять изменена разных вариаций одного проекта в новую версию? И как вы будете осу- •гвлять частичный отказ от изменения, затронувшего дюжину исходных файлов? Разумеется, чем больше ваша команда, тем критичнее становятся подобные блемы. Если, работая в командном проекте, вы хоть раз сталкивались с тем, ак чьи-то важные изменения пропадали или, еще хуже, лишь частично переноси пкъ в очередную версию, то вы уже знаете, в какую мороку и угрозу для сачества это может вылиться. Одно важное предупреждение: системы управления версиями весьма не положи на остальные инструменты и утилиты. Я работал с несколькими, и все п [ I собственные правила, тонкости и странности. Даже не думайте, что, купив первую попавшуюся вам систему управления версиями, установив ее на ваш(и) компыотер(ы) и проигнорировав докумен- uimno, вы тут же начнете радостно клепать версии, не отрывая задницы от пула У вас это не получится. Зато риск потерять сделанные изменения обеспечен. В лучшем случае, вы зря потратите время и расстроитесь раньше, чем сможете, наконец, разобраться, как же на самом деле работает этот инструмент. (Кстати, по моему личному опыту, качество документации этих систем намного ниже среднего уровня для инструментов разработки. Это 10в°рпт о многом.) Мой совет — сделать фалып-старт, создать проект-пус- Ь1шку и затем проверить на нем все возможности системы, обращая особое • "«мание па средства разветвления и совмещения версий. Это не должно от- / ' Мн°го времени, возможно, всего лишь полдня, но это время будет иесо- П1сп«о потрачено с толком. Нст*ААяциоиные программы ' ногпх из вас появится искушение не обращать внимание на эту ка- л, ^ ^ФУментов, если вы не собираетесь распространять свои продукты . !,11ь КТ1М способом. В конце концов, если ваша программа будет исполь- Лько внутри вашей компании, всего лишь парой дюжин пользовате- , < ем У'груждать себя написанием инсталлятора? ' Но к бывает практика, небольшая простая инсталляционная программа 10 из стокилобайтиых бегемотов, которые рекламируются во всех
182 Инструмент bl V программистских журналах) может сохранить вам (да, именно вам) масс времени в перспективе. Даже если для установки вашей программы требует^ всего лишь копирование нескольких файлов в соответствующие каталоги, ват,, усилия окупятся сторицей. Пользователи вашей программы получат простор автоматизированный способ установки, а вам не нужно будет беспокоиться норой весьма творческом подходе людей к размещению и редактированию фа^ лов, перезаписыванию системных динамических библиотек и т. д. pi т. п. Чем более сложен процесс инсталляции вашей программы, тем ва>к% вопрос об автоматизации этого процесса. С приходом Windows 95 и ее на вязчнвой идеи о реестре, инсталляция вообще стала критическим вопросом, j если вы будете хотя бы какое-то время поддерживать и Windows 3.1, и Win dows 95 (что, я уверен, придется делать многим из нас), то какой-нибудь срав нительно небольшой инсталляционный пакет типа Install Pro от Eshalon Devc opment наверняка окупится легко и быстро. Оптимизация: бесплатный сыр подам! Создайте новый проект при помощи Visual C++ или любой %j другого аналогичного пакета и взгляните на настройки проекта Вы увидите кое-что интересное: настройки для конфигурации «Re lease» не только отключают генерацию отладочной информаци (что естественно), но по умолчанию включают довольно агрессия пую оптимизацию (как правило, «Оптимизацию скорости» и.ф «Оптимизащ 1 ю размера»). Как же, черт возьми, это случилось? Когда это мы сообща ее гласилпсь с тем, что финальные версии продуктов, поставляемы конечным пользователям, должны иметь высокооптимизироваь иый код? И когда это мы решили, что текущее (да и вообще како^ нибудь, если на то пошло) поколение компиляторов C/C++Д°сТ' точно надежно справляется с такой деликатной задачей, как опт: мизация? Насколько я знаю, ответ на оба вопроса одинаков: пнь'1 гда. Потому что нас никто об этом пе спрашивал. каь- а О'кей, давайте потратим минутку и постараемся понять, же предположения кроются за этими настройками компилятор что они означают для реальных проектов Общепризнанная мудрость гласит, что желание использоВ' отладочные настройки па всем протяжении процессов коДИР0 пня и тестирования диктуется следующими причинами: 1. Такие полезные средства отладки, как TRACEO и ASSE* полагаются на правильное значение препроцессорного u ла отладки ( DEBUG).
так прекрасен 183 2. В целевом исполняемом файле должна присутствовать отладочная информация. 3. Компиляции должны проходить настолько быстро, насколько возможно, поскольку масса времени программиста будет тратиться на циклы редактирование/компиляция/тест. Если при этом исполняемый файл не будет максимально быстрым или минимального размера, — не беда, поскольку настоящие пользователи даже не увидят этот файл. Как только версия будет готова, вы захотите нейтрализовать TRACEO и ASSERTO, удалить отладочную информацию из исполняемого файла и выжать максимально возможную производи- 4.J тельность из законченной программы. Все это совершенно разумно и находится в полном согласии с теми настройками проекта, которые по умолчанию заданы в наших C/C++ оболочках. Самой большой проблемой этого розового сценария является тот факт, что оптимизаторы несовершенны. Они иногда вводят ошибки в корректную программу, совершая свои колдовские манипуляции с кодом. (Если вы не верите, что опти- <j <j мизация действительно граничит с черной магией, загляните в мартовский номер Microsoft Systems Journal за 1995 год и почитайте про возможности Visual C++ 2.0 в этой области: оптимизаторы перемещают код с места на место, удаляют его, переписывают его. Если вы примете во внимание общую картину качества больших Windows-приложений, и в том числе компиляторов, и даже после этого не обеспокорггесь по поводу творческого подхода оптимизаторов к вашему коду, я вам уважительно намекну: вы просто не обращали внимания, сэр.) Загляните на Microsoft Development Network CD, и вы найдете несколько подтвержденных ошибок и проблем, для которых рекомендуемым решением является отключение оптимизации (либо полностью, либо частично). А еще почитайте врезку «C++: недоварен, впрочем, как и его компиляторы» на стр. 188, и обратите ** ваше внимание на то, как за тот короткий промежуток времени, в течение которого Windows/DOS Developer's Journal вел свою колонку «Bug++ of the Month» («Ошибка месяца»), были отмечены две ошибки оптимизаторов — одна у Borland C++, а другая у Microsoft Visual C++. Когда вы разрабатываете и тестируете продукт с одними на- стропками оптимизатора, а потом отгружаете его пользователю с Другими, вы делаете ничто иное, как играете в русскую (хоть и компьютерную) рулетку. Когда-нибудь ваша программа щелкнет по каморе, которая окажется непустой.
184 ^ze^ Этот нюанс заслуживает особого внимания: развязы*. ^ присутствие отладочной информации и оптимизацию, произвол' %т\ищ£} Тель компилятора, по сути, гарантирует, что во многих проект' оптимизация будет включена только после завершения тестцр^ ния. Во время тестирования, отладочная информация необходц,' €2fi^ программистам, поэтому они будут использовать настройки «^ bug», но они часто доверяют настройкам, которые производите предлагает для финальной версии. -—*вч Многие полагают, что оптимизацию не следует включат "™ ^ слишком рано (в цикле разработки), поскольку она существен^ замедляет компиляцию и компоновку. Для реальных проектов эт, ^ предположение является довольно обманчивым. Взгляните €sZ^) например, на результаты моего небольшого эксперимента с раз личными установками оптимизации при сборке исходного кодс программы WordPad при помощи Visual C++ 2.2: ж=*Ш Таблица и Установки оптимизации Отключена Минимальный размер Максимальная скорость Время полной сборки 5:07 5:42 5:48 Время перекомпиляции одного модуля и компоновки 35 секунд 36 секунд 38 секунд В случае полной сборки я считаю эти различия несуществен ными. Как только она начинает занимать больше двух минут, и3 менение ее длительности всего на 13% (разница между 5:07 и 5'4о' уже не имеет значения; запуская такую сборку по нескольку ра? на дню, вы вскоре обнаружите, что слишком много времен' тратится уже не на кодирование, а на раскладывание пасьянсов саперное дело или упражнения на гитаре. Что же касается чг( тичных сборок, то там настолько превалируют временные затрат на компоновку, что наличие или отсутствие оптимизации праЬ тически невозможно заметить. Как всегда, в заключение необходимо ответить на законе1 вопрос: и что же со всем этим делать в реальном мире? Для Ф личных программ я могу дать следующие рекомендации:
pascal так прекрасен 185 еЭ <=3> Sq> 1. Будьте предельно скептичны по отношению к любой оптимизации компилятора. Вы должны обязательно исследовать «послужной список» компилятора в этой области: задавать вопросы в соответствующих телеконференциях, тщательно просматривать публикуемую поставщиком информацию об ошибках (если таковая имеется) и т. д. 2. Не соглашайтесь на предлагаемые производителем настройки без предварительной оценки того, насколько хорошо они вам подходят в текущем проекте. Как я у лее не раз говорил, нельзя прекращать ргеследование выбранного инструмета. Не подда- О *-> О ваитесь ни одной из отдельных деталей продукта, не проверив ее, особенно если ошибка в ней может грозить серьезнейшими иепррштностями вам и вашим пользователям. 3. Как можно раньше определитесь с тем, какие опции оптимизации вы хотите использовать для финальной версии, а затем все тестирование осуществляйте именно с этими опциями. Вообще говоря, вы должны использовать оптимизацию только тогда, когда вы уверены, что выигрыш от этого будет реальным и ощутимым, и что он перевесит риск. Мой личный опыт знакомства с компиляторами говорит за то, что в большинстве реальных проектов использование оптимизации не имеет никакого смысла. Так ли это в вашей конкретной ситуации — решать вам. Главное — помните одно простую вещь: собираясь потанцевать с дьяволом, удостоверьтесь, что знаете все па. / Что Будет с OWL? Недавнее (относрггельно времени написания этих строк) прошлое компании Borland может служить интересной и поучрггельной иллюстрацией к разговору о выборе ршетрументария и, косвенно, поставщика. Конец 1994 года и начало 1995 года были для Borland перрюдом тяжелейшего фршанасового положе- нрш: повсюду распространрыись слухи о ее неминуемом банкротстве, и в «кулуарах индустрии» обсуждались перспективы скупки Borland той или иной компанией (общепризнанным фаворрггом тогда считалась Novell, и такой сделки ожрщали буквально со дня на день). Выпуск в свет Delphi в марте 1995 года стал спасительным moctpikom через пропасть, у края которой топталась Borland. Даже несмотря на резкую (pi, на мой взгляд, заслуженную) кррггику в адрес документациР!, продажр! Delphi заметно ширились, и тучи на
и с к. небосклоне Borland стали рассеиваться. К сожалению, нельзя зать, что это вызвало адекватное «просветление» в судьбе 4\, гочисленных программистов — поклонников Borland Pascal t Windows. Да, они получили на вооружение выдающиеся возм0. ности Delphi по повторному использованию кода, с одобрен, приняли некоторые новые расширения Object Pascal и бот ^ _ ^*1г I let современную каркасную библиотеку VCL (Visual Component I brary), обещавшую плавную миграцию на 32-разрядную пл* форму в ближайшем будущем. Но впридачу разработчики лучил и серьезнейшую проблему: VCL оказалась несовместимо! OWL, и при этом не было предложено хоть какого-нибудь бо. менее легкого пути для переноса BWP/OWL-кода на Delphi/VCL В результате, все обладатели приличных наработок на ВР\\Т OWL попали в безвыходное положение. Как выяснилось из моей переписки с двумя высокопоставлен ными сотрудниками Borland по поводу этой проблемы, ловушке на самом деле была двойной: в планы Borland совсем не входи перенос OWL на 32-разрядную платформу (хотя 32-разрядна? версия OWL для C++ уже существовала). Более того, мне сооб щи ли, что Borland усиленно занимается поисками какой-нибуд! третьесторонней компании, которая взялась бы выполнить это- перенос (вплоть до вознаграждения за это в виде передачи это! третьей стороне права на маркетинг результирующей версии). Та ким образом, весь существующий BPW/OWL-код оказался ш только в тисках старой каркасной библиотеки, но и в капкане It бит. В итоге, у разработчиков осталось совсем мало варианта KJ О О дальнейших действии: 1. Переносить код на Delphi/VCL ценой всех необходимых Дг этого переделок. 2. Переносить код на Borland C++ 4.5 (текущую на тот момен версию C++). В этом случае код мог бы по-прежнему опираТЬ ся на OWL (пускай и на несколько иной ее диалект), но91' потребовало бы буквально построчной трансляции с Pascal и C++ (что, как известно, является более чем сомнительны удовольствием, если количество строк в программе превъШ1 пару-тройку сотен). 3. «Выбросить» старый код и переписывать его с нуля, исп° $ t о зуя новый пакет. 4. Ничего никуда не переносить и продолжать жить с 16-р^х3Р' ными версиями своих продуктов.
<•/ / ' ■• *. / sq> прекрасен 0 Ф 187 В такой ситуации для небольшой программы в две-три тысячи строк особой проблемы выбора нет: можно отвести пару недель на изучение VCL, после чего совершить перенос по пути 1, попутно создав себе неплохой плацдарм для последующего прыжка на 32- разрядную платформу (при появлении Delphi32, разумеется). Возможно, именно поэтому не возникло никаких вопросов и у многих (если не у большинства) шумных поклонников Borland по крайней мере, я лишь очень изредка видел обсуждение этой темы в форуме DELPHI в CompuServe. (Может быть, перейдя на Delphi, программисты занимались написанием только новых программ, выбросив свой старый код?) Но что же было делать тем, у кого накопился действительно солидный багаж исходных кодов на BPW/OWL? Так, например, один из моих клиентов — обладатель нескольких проектов с суммарным объемом исходных текстов примерно в 200,000 строк — в один день оказался без единого экономически осмысленного пути для продолжения этих проектов. У меня самого есть более 100,000 строк кода, написанных на BPW/OWL, конвертация которых любым из вышеописанных способов была бы для меня неоправданно дорогой. Поэтому я так и продолжаю до сих пор надеяться, что кто-нибудь «подберет» OWL и перенесет его на 32-разрядную платформу (увы, насколько мне известно, пока к этой задаче еще никто не приступил). Суть вышеизложенных фактов -- вовсе не в том, чтобы лишний раз поругать Borland; наоборот, я на протяжении многих лет был в числе верных сторонников этой компании и предлагаемого ею программистского инструментария. Но после того, как с начала 90-х годов их техническая поддержка покатилась вниз по наклонной, их отказ от поддержки огромного количества BPW/OWL- кода своих заказчиков окончательно заставил меня решительно изменить свое отношение к Borland — оно стало точно таким же скептическим, как и к другим компаниям. И тот печальный факт, что я столь долго давал им фору (в смысле доверия) относительно других производителей, является лишь доказательством моей ошибки — чрезмерной доверчивости к поставщику (ошибки, которую вам не следует допускать). Справедливости ради я хотел бы отметить, что послужной список Borland по части отказов от своих < f *j продуктов и стандартов определенно не самый худший по сравнению с другими компаниями. Borland — это всего лишь отдельный пример. Произойдет ли чудесное возрождение Borland? Лично я надеюсь на это, поскольку всем нам стало бы только лучше от ожив-
Инст> №ен ления конкуренции и расширения выбора на рынке компилятор. Ро Более того, я ожидаю довольно заметного улучшения положен, Borland уже в ближайшем будущем. К C++: недоварен, впрочем, hah и его компилятор Я пишу эти строки в августе 1995 года: стандарт ANSI к\ Лс ;■ И языка C++ все еще не появился на свет, а компиляторы вовс соревнуются друг с другом по количеству реализованных новы элементов грядущего стандарта. Например, Visual C++ 1.5 не по- держивает и никогда не будет поддерживать шаблоны. Ни один известных мне компиляторов не поддерживает новые операщц приведения типа, области определения имен и булевский тип (да мы столько лет на все лады определяли свои собственные вариан ты типа BOOL, и теперь, наконец, C++ вот-вот получит н£ вооружение настоящий булевский тип). Как же долго писали мь публичные программы, фактически используя бета-версию язык; и его прото-компиляторы! И даже несмотря па столь долгое и широкое практичесм применение C++, сегодняшний урожай его компиляторов р разработки Windows-приложений отличается удивительно! незрелостью. Пролистав подшивку старых номеров Windows DOS Developer's Journal и в очередной раз заглянув в превосход ную рубрику Марка Нельсона «Bug++ of the month» («Ошибк месяца»), я подумал, что небольшая выборка из нее могла бы по служить отличной объективной иллюстрацией качества современ ных компиляторов C++: Октябрь 1994 года: Watcom C++ 10.0 не выполняет вызове структора для автоматического объекта, когда возврат и функции производится из тела оператора switch. Ноябрь 1994 года: Borland C++ 4.02 генерирует вызовы Де структоров для несуществующих автоматических объектов. Декабрь 1994 года: Visual C++ 1.5 генерирует сотни ложнь' вызовов конструктора, если включена оптимизация (ошДОЯ Ох). Январь 1995 года: Visual C++ 1.5 не выдает предупрежДей1' о попытке возвращения функцией ссылки на локальФ переменную (далее на самом высоком уровне предупре^ •\ НИИ).
pascal так прекрасен 9 189 Февраль 1995 года: Borland C++ 4.5 не выполняет очистку стека при включенной оптимизации инструкций процессора 1386. О Март 1995 года: Visual C++ 2.0 использует неправильное значение по умолчанию в конструкторе. Апрель 1995 года: Borland C++ 4.5 некорректно обрабатывает 16- и 32-разрядные вызовы iostream, приводящие к испорченным выходным данным. Май 1995 года: Visual C++ 2.0 некорректно обращается с функциями-друзьями и шаблонами. Июнь 1995 года: Borland C++ 4.5 не позволяет отключить RTTI (динамическую информацию о типах), если предварительно не отключена обработка исключительных ситуа- 4 ? ции. Июль 1995 года: Visual C++ 1.5 генерирует некорректный код для процессора i386 (опция /G3). Август 1995 года: Borland C++ 4.5 конвертирует enum в не- KJ связанный с ним класс с целью проверки его значения. Разумеется, если вас интересуют какие-то из этих ошибок — их влияние на ваши конкретные разработки, пожалуйста, не полагайтесь на мои сжатые описания, приведенные выше. Лучше откопайте соответствующие номера Windoivs/DOS Developer's Journal или скачайте соответствующие файлы из CompuServe (SDFO- RUM, x library 7), а затем самостоятельно ознакомьтесь с оригинальными подробными описаниями этих опшбок. Большинство (если не все) из перечисленных мною ошибок должно быть устранено к моменту выхода этой книги в свет, но я и не ставил себе целью снабдить вас более-менее полным списком ошибок компиляторов C++. Просто этот список, совместно с отчетами о проблемах, публикуемыми на Microsoft Developer Network CD, и многочисленными третьесторониими «подборками» ошибок и сбоев — все это неопровержимые свидетельства незрелости существующих компиляторов, практически всех компиляторов без исключения. И если принять во внимание стремительность всех изменении, происходящих ныне в индустрии, отчаянную борьбу производителей компиляторов за 100-процентное соответствие своих творений стандарту ANSI, а также все те бесконечные, следующие одна за другой модернизации, необходимые для поддержки все новых и новых возможностей Windows — трудно ожидать существенного улучшения качества компиляторов в
е» обозримом будущем. (В нашем бизнесе «обозримое будуще довольно растяжимое понятие: от пяти минут до года. Но если ворить именно о компиляторах, то я не думаю, что у них *.. v» о реальные шансы перевести дух pi дозреть по крайней ьщ ♦ течение ближайшего года. А потом — с появлением Windows % Cairo — вся карусель неизбежно закрутится снова. Как же нам жить и работать в таких условиях? Конечно *f нельзя прятать голову в песок и убеждать себя, что эти проблеу не существуют на самом деле — это было бы безответственно и н коим образом не могло бы повысить качество наших публична программ. Наша единственная опора — постоянно помнить ото* что современные компиляторы C++ бурно pi безостаново* эволюционируют и снаружи, и изнутри, и что это неизбежно б) Щ< 'де приводить к наличию в них тех или иных ошибок. Это означает что иногда ошибки наших программ в действительности являются не нашими собственными ошибками, а подножками со сторонь компиляторов, и что мы просто обязаны быть в любую минуту го товы к подобным случаям. (Ошибки компиляторов во много? сродни таким проблемам, как поломка жесткого диска: вопрос ш в том, случится это или нет, вопрос в том, когда это случится.) Разумеется, все мы должны делать и еще одну вещь - стараться сообщать обо всех найденных нами ошибках в компиля торах (да и в других программных продуктах тоже) их авторам' разработчикам. В конце концов, всегда есть шанс на то, что произ водитель устранит именно вашу проблему в ближайшей нова версии или «заплате», или хотя бы предложит приемлемый спосо! обхода. Delphi32: Pascal достигает совершеннолетие Компиляторы языка Pascal от Borland занимали заметное м^ сто на рынке еще со времен первой администрации Рейгана. Пра; тически сразу за ними прочно закрепилась репутация быстроДе! Hi- ctf ствующих, практичных, но в каком-то смысле «игрушечных» струментов разработки, не слишком пригодных для реалы*0 работы. И хотя у этих продуктов определенно были какие-то ограничения и причуды, я думаю, что ярлык «игрушечности» ^ в значительной степени необоснованным. Последнее достижение Borland, Delphi, довольно 6ыс^ устраняет эту проблему с имиджем, а грядущая версия Delp'lb должна окончательно стереть все оставшиеся следы. Поэтому звольте мне обратить ваше внимание на наиболее интересные р
прекрасен 191 можности Delphi32, имеющие отношение к теме этой главы и всей книги в целом: О Полностью 32-разрядный компилятор, без 64-килобайтных ограничений. Я делаю минутную паузу, чтобы вы смогли объявить и отпраздновать День Освобождения Паскалистов. О Полная совместимость с Delphi16, включая поддержку VCL. О Полная поддержка новых элементов управления Windows 95. О Значительно улучшенный компоновщик. Я не могу и не должен сообщать результаты аттестационных тестов бета-версии какого-либо программного продукта, но мой не очень обширный опыт тестирования Delphi32 уже позволяет уверенно утверждать, что эта версия компилирует и компонует очень быстро, создавая исполняемые файлы еще меньшего размера, чем это делала ее 16-разрядная предшественница. • Способность создавать 32-разрядные OB J-файлы, которые можно связывать с проектами, разработанными при помощи C/C++. Возможно, пуристы от Pascal презрительно усмехнут- ч f ся в адрес этой детали, но лично я считаю, что это один из важнейших залогов перспективного успеха Delphi32. До сих пор не существовало ни одного практичного способа «смешивать» Pascal и C/C++ от Borland в одном проекте. (Да, примитивная возможность прицеплять OBJ-файлы к Pascal-проекту уже была, и тем более всегда был вариант обеспечения мирного сосуществования языков через механизм DLL. Но эти способы lie очень пригодны для большинства реальных проектов.) Теперь же можно будет написать модуль на Pascal, скомпилировать его в OBJ-файл и затем использовать в другом проекте точно так же, как если бы этот модуль был изначально написан на C/C++. А это даст возможность большим программистским коллективам и компаниям тестировать Del- phi32 на своих реальных проектах выборочно, без пеобходи- К i6oi объемы невыполненных заказов, что никак не могут позволить себе роскошь изолированного тестирования нового инструментария; они чаще всего вынуждены производить тестирование прямо в боевых условиях. Поддержка стандартных OBJ-фаплов в Dclphi32 как раз и позволит им осуществлять такое тестирование. И я готов предсказать, что разработчики будут широко тестировать Delphi32, а впоследствии выберут
К ш ч Ж ^^Л этот компилятор в качестве основного инструмента для Comoro числа своих проектов. Как серьезный недостаток, изменение размера тина iyty с 16 на 32. К сожалению, в этом вопросе Delphi32 послет дурному примеру C/C++ и даже ввел новый 16-разрядный,! Ювь лочисленный тип — smallint (32-разрядный целочисленнк тип — longint - уже существовал в Delphi и раньше). Все-» мне кажется довольно глупым, поскольку без какой- Щ реальной необходимости делает часть существующего кода и. VJ корректной и хменяет размер многих определенных пользов 1.» телями записей. Вообще говоря, предсказания в области программна обеспечения - скорее дело, подходящее лишь для лунатиков эгоцентристов. Тем не менее, я готов встать на край карниза, предсказать, что быстрый успех Delphi 16 станет хорошим трау плином для Delphi32, a Delphi32, в свою очередь, повернут хо. событий в другую сторону (в сторону, противоположную C/C++ хотя это произойдет, вероятно, не раньше конца 1996 года. Яуб •» о жден, что сегодня существует огромнейший неудовлетворенны спрос на достойную альтернативу для C/C++, pi что в об ласт, программирования для Win32 она уже отчетливо видна.
." }ф я_ ffir-my актина
^ The last thing one knows in constructing a work is what to put first Б лез Паскаль All politics is local Американский афоризм Итак отдавайте кесарево кесарю, а Божие Богу. Евангелие от Матфея, 22:21 ..локальной функции же — то, что принадлежит ей. Л у Гриизоу вокальные функции напоминают фильмы Дэвида Линча: либо они нравятся ам до глубины души, и вам кажется глупой сама мысль, что их существование УЖно как-то оправдывать, либо вы вообще ничего не понимаете в них. Если • подобно мне, работаете с несколькими языками программирования, то п,,^ЯТНо вы так же остро чувствуете отсутствие локальных функций в C/C++, если занимаетесь переносом кода с другого языка, поддерживающего 1еренос кода с Pascal на C++ всегда был настолько трудоемким и неудоб- Для меня, что в конце концов я и выработал идиому локальной функции. Ос°бенио их. ным В Л1 ЭТ°И главе я рассмотрю локальные функции в том виде, в каком они реа- аны в Pascal, и объясню, почему они являются очень полезным Рументом, достойным находиться в арсенале каждого программиста. А за- «н тем Фун ПокажУ> как вы можете без труда создавать и использовать локальные кцци Конг в C/C++, несмотря на то, что сам язык не поддерживает эту стРУкщно.
1 96 Илиолла локальной фуцк Локальные функции: Pascal реализует их так, как науо Мое стремление использовать локальные функции (так, как они реализова в Pascal) основывается на непоколебимом и эгоцентричном прагматизме чисТг воды: они поддерживают абстракцию, скрытие данных и инкапсуляцию, самым позволяя мне организовывать мой код в той форме, которая максима^ но близка к логике решаемой задачи. А это значит, что мои программы лег писать и, что намного важнее, они легче читаются спустя несколько месяце когда абсолютно другому программисту (возможно, мне самому) необходц. что-то добавить в код или исправить в нем ошибку. Листинг 5.1 демонстрирует очень простую программу, написанную на О'; ject Pascal и показывающую, как локальные функции взаимодействуют с of ластями видимости переменных. Я постоянно использую эту модель в мое реальной работе и считаю ее весьма мощной, гибкой и интуитивной о; новременно. (Программа PASCAL.PAS всего лишь вычисляет и показывает); военную сумму последовательности целых чисел, причем умышленно делае это довольно странным способом. Едва ли она может служить великолепны примером алгоритмизации, но такой цели перед собой я и не ставил.) Листинг 5.1. PASCALS program Pascal, uses WinCRT, var pre_Rtn1. longint; { Видна повсюду в программе } function Rtn1(R1Parm- longint)- longint; var pre_Fn1. longint, { Видна только для Rtn1 и Fn1 { function Fn1(F1Parm: longint) longint; var m_Fn1: longint, { Видна только для Fn1 } begin if FlParm > 1 then Fn1 = FlParm + Fn1(FlParm - 1) else Fn1 = 1, end, { var post_Fn1 longint, { Видна только для Rtn1 } begin Rtn1 . = 2 * Fn1(R1Parm), end, var
иые функции: Pascal реализует их так, как нало 197 ро G t RtnV longint; { Не видна ни для Rtn1, ни для Fn1 } De9JriteLn('Rtn1(lO) = *,Rtn1(lO)), е nd Построение примера: PASCAIJPAS Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chap05/ pascal Платформа: Win 16 Инструкции по сборке: откройте при помощи Borland Pascal for Windows или Delphi файл PASCAL. PAS и скомпилируйте его. (Хотя мне и непонятно, зачем вам вообще может захотеться собирать и запускать эту программу. Как, впрочем, и другие примеры из этой главы.) В программе PASCAL.PAS есть пять переменных, интересных для нас с точки зрения областей видимости (хотя на самом деле они никак не используются программой): В pre_Rtn1 видна во всей программе, включая подпрограммы, и является «максимально глобальной». В post_Rtn1 также находится в глобальной области видимости, но, благодаря ее местоположению, не видна ни подпрограмме Rtnl, ни ее локальной функции Fnl. й preJFnl локальна для Rtnl и поэтому видна только внутри Rtnl и Fnl. В post_Fn1 также является локальной для Rtnl, но из-за ее расположения после Fnl она видна только в Rtnl, но не в Fnl. Это классический пример объявления в Object Pascal переменной, которая должна быть В видна только в главной подпрограмме. in_Fn1 является классической локальной переменной и видна только внутри Fnl. ^сли вы внимательно посмотрите на эту программу и ее переменные, вы Дйте, что у программиста довольно много степеней свободы при определе- ОГо, какие объекты программы доступны из других объектов. В частности: V а Переменные могут быть глобальными помещенных после определенной позиции ( Pre Rtnl и post Rtnl)
198 Илиома локальной <& В Переменные могут быть локальными для подпрограммы, локально для локальной подпрограммы (in_Fnl) и даже локальными подпрограммы, но недоступными для локальной подпрограммы э подпрограммы (post_Fnl). (Попробуйте положить в рот пару кубЙКг льда и повторить это втрое быстрее.) в Подпрограммы могут быть доступными глобально (Rtnl) или лока- но для охватывающей их подпрограммы, а также могут быть недос^ ны для любого другого объекта (Fnl). На самом деле, все эти возможности являются расширением базовой ког цепции параметров и локальных переменных; вы можете минимальными уС1 лиями создавать переменные и подпрограммы, области видимости которы очень точно соответствуют логической структуре и порядку выполнения ваше программы. Такой гибкий контроль над областями видимости, в свою очередь позволяет практически полностью устранить целый класс ошибок (например порчу кодом тех данных, к которым он, согласно логике программы, вообщен должен иметь доступа). В конце концов, никто не станет оспаривать общею вестное утверждение, что чрезмерное использование глобальш переменных — плохая практика. И те возможности, которые демонстрируе PASCAL.PAS, просто-напросто являются более полной реализацией этой фр лософии (более полной, чем то, что доступно программистам в C/C++). Попытки достигнуть предельного контроля над областями видимости ш гут потребовать довольно витиеватого оформления кода независимое того, используете ли вы Pascal или C++. Например, вы можете попробс вать группировать подпрограммы в отдельные единицы компиляции, ест эти подпрограммы должны иметь эксклюзивный доступ к некоторым^ щим для них данным. Такой подход плох, и не стоить тратить на иегосво силы. В общем случае вы все равно не сможете удовлетворить всем таки1 требованиям. Например, в случае, когда лишь подпрограммы А и t должны иметь доступ к переменной X, и лишь подпрограммы В и С -' переменной Y. Нет оптимального (необходимого и достаточного) спос» решить такую проблему на уровне единиц компиляции. И даже если6 вам удалось этого добиться, в конечном итоге вы получили бы некотор1' извращение оформления программы в целом (так как оно стало бы Д1! товаться низкоуровневыми деталями реализации, а не логикой более в> сокого уровня). Кроме того, такой подход всегда неминуемо веДеТ чрезмерному размножению единиц компиляции. Например, я исслеД013 одну программу, написанную мной на Pascal и имевшую 32,000 стро^к°' в 32 единицах компиляции. Выяснилось, что если переструктуриров^ с целью имитации локальных функций (но без модификации областей димости переменных) при помощи отдельных единиц компи ляД*^ результате получится более 150 единиц компиляции (и отличный для преждевременного умопомешательства вашего покорного слуг гй)
перенос на С 199 попытка решить таким способом проблемы видимости переменных вообще привела бы к кошмару. srl: грубый перенос на С калению, С не поддерживает концепцию локальных функций. Это значит, ели бы мы захотели превратить PASCAL.PAS в С.С и при этом сохранить о icTii видимости переменных настолько, насколько это возможно, мы бы по- или нечто вроде того, что приведено в листинге 5.2. Листинг 5.2. С.С ^include <windows.h> «include <stdio.h> int pre_Rtn1; // Видна повсюду в программе int pre_Fn1; // Видна повсюду в программе, если не поместить ее вместе // с Rtn1 в конец файла. int Fn1(int FnlParm) { int in_Fn1; // Видна только для Fn1 if(Fn1Pann > 1) return(Fn1Pann + Fn1(Fn1Parm - 1)); else return 1; } int Rtn1(int RtnlParm) { int post_Fn1; // Видна только для Rtn1 return 2 * Fn1(Rtn1Parm); wt main() / t int post_Rtn1; // He видна ни для Rtn1, ни для Fn1 printf("Rtn1(10) = %d\n\n'\Rtn1(10)); 9etchar(); return 0; / ^ Построение примера: СХ Местоположение: http://www.symbol.rU/russian/library/prof_prog/source/chap05/c Платформа: консольное приложение для Win32 Инструкции по сборке: откройте при помощи Visual C++ 2.2 файл СМАК и скомпилируйте, как обычно. Те}, 4 Р°гРамма С.С — это консольное приложение, которое выполняет точно ^мысленные вычисления, что и PASCAL.PAS. Я использовал те же са-
200 Илиома локальной мые имена подпрограмм и переменных. Обратите внимание на то, как теп обстоит дело с областями видимости этих подпрограмм и переменных: in_Fn1 по-прежнему локальна для Fnl, post_Fnl по-прежнему BJI. только для Rtnl, a pre_Rtnl все еще является максимально глоба ной, — пока все так же, как и в PASCAL.PAS. Аналогично, post Pt В этс по-прежнему видна только для главной подпрограммы, хотя тещ она расположена после первой открывающей фигурной скобки mainO — там, где традиционно располагаются такие переменные в г В preJFnl, которая в PASCAL.PAS была локальной для Rtnl и при доступной для Fnl, теперь является глобальной (с точки ее объяв] ния и до конца единицы компиляции). В реальной программе это леп может стать причиной проблем, так как ниже точки объявлеш' pre_Fnl наверняка будут добавляться новые подпрограммы. Э] функции будут иметь доступ к этой переменной, хотя во мноп случаях им это не требуется. (Эта проблема напоминает старые добрь I. М( времена DOS, когда нам то и дело приходилось жонглировать резг дентными программами, подбирая тот порядок их загрузки, прию тором они не конфликтовали бы друг с другом. Так же, как и в случ< с упомянутым ранее методом распределения подпрограмм и перемен ных по единицам компиляции, это было битвой, обреченной н поражение — достаточно захотеть загрузить несколько резидентны программ, и вы быстро сталкиваетесь с комбинацией требований, и торые нельзя удовлетворить одновременно.) В Fn1 теперь оказалась «повышеной в чине» — до того же уровня вид! мости, что и Rtnl. Это значит, что вызывать функцию Fnl теперь жет не только Rtnl, но и любая другая функция. В данном конкретно примере этот факт может показаться незначительным. Однако, реальном проекте такая подпрограмма зачастую может использовать параметры, и локальные переменные охватывающей ее подпрограмм (например, pre_Fnl в PASCAL.PAS), и выполнять некоторую зад^ имеющую смысл только в контексте этой охватывающей подпрогр^ мы. Помещение такой функции в общую область видимости — плох* практика, потому что в дальнейшем она может легко привод11 программиста к заблуждениям. Все мы наслышаны о таком понят11 как само документированный код. Оно означает исходный текст, #а11 значение и способ использования, что он не нуждается в коМ>|е тариях. (Смотрите рекомендацию 3 в главе 2 на стр. 79 на тему #° отношения к комментариям и к понятию «самодокументированног0 да».) Выставление напоказ функции, которая для этого не пре^ значалась, является абсолютно противоречащей данному определи I! санный настолько ясно и имеющий настолько очевидное
2: г++ и илиома локальной функции 201 и, следовательно, является самонедокументированным кодом, требующим комментария уже лишь для того, чтобы немедленно рассеять неправильное представление о себе, которое разумный программист составит, исходя из ее местоположения. Таким образом, очевидно, что из-за отсутствия в С поддержки локальных р-ций физическая реализация С.С не может полностью поддержать ло- ское предназначение программы, хотя эта программа также очевидно явля- корректной и выдает правильный (если это кому-то важно) результат. Как днократно подчеркивал в этой книге, проблема не только в том, дает ли или иной инструмент возможность создать корректную программу. Пробле- \л как раз в том, насколько хорошо он обеспечивает создание корректных, надежных и сопровождаемых программ. Пока дело касается такого простого примера, как в этой главе, все эти рассуждения могут казаться слишком абстрактными и чисто теоретическими. Но как только*вы столкнетесь с реальной C/C++ программой из 100 000 строк кода, и перед вами встанет задача исправить в ней ошибку или добавить в нее новую возможность, вам придется часами исследовать области видимости данных и функций. И вот тогда проблема вдруг предстанет перед вами совсем в другом свете. Шаг 2: С++ функции Но Бог с ним, со старым добрым С. Давайте обратимся к C++. К сожалению C++ все еще не поддерживает локальные функции. Но он поддерживает нечто > !няще блргзкое к желаемому: классы. В конце концов, о чем мы все это ш говорили, как не об оформлении и контроле за доступом к коду и дан- ? Не для этого ли и предназначены классы? Да, для этого. И мы приходим иоме локальной функции. Листинг 5.3 лемонстоиоует СРР.СРР - аналог PASCAL.PAS функц include <windows h> «include <ctdio h> cla { cPackageRtnl Public cPackageRtnl(void), mt Rtn1(mt RtnlParm), Private mt pre_Fn1, // Видна только для Rtn1 и Fn1 mt Fn1(mt FnlParm), // Видна только для Rtn1 Private Листинг 5.3. СРР.СРР
202 Илиома локальной =-**ь // Зачем определять эти методы, не реализуя их9 См. главу 5. cPackageRtn1& operator=(const cPackageRtn1& x); cPackageRtn1(conot cPackageRtn1& x); } mt pre_Rtn"l; // Видна повсюду в программе cPackageRtnl:'cPackageRtnl(void) { } int cPackageRtnl:: Fn1(mt FnlParm) { int x; // Видна только для Fn1 if(Fn1Parm > 1) return(Fn1Pann + Fn1(Fn1Pann - 1)), else return 1; i mt cPackageRtnl::Rtn1(int RtnlParm) { int post_Fn1; // Видна только для Rtn1 pre_Fn1 = 0; return 2 * Fn1(Rtn1Parm); • V i mt poct_Rtn1; // He видна ни для Rtn1, ни для Fn1 int main() { cPackageRtnl fred; printf("fred.Rtn1(10) = %d\n\n",fred.Rtn1(10)). printfC'Preso Enter... "); getchar(); return 0; } #V/ '% •• V 9 #*V Построение примера: СРРХП Местоположение: http://www.symbol.rU/russian/library/prof_prog/source/chap05/4 Платформа: консольное приложение для Win32 Инструкции по сборке: откройте при помощи Visual C++ 2; файл СРР.МАК и скомпилируйте как обычно. Обратили ли вы внимание на то, как я определил приватный констру^1 копирования и приватный оператор присвоения для класса cPackageR*11 и как я не реализовал их? Зачем? Причина проста: таким образом предотвращаю любую возможность передавать объект этого класс»1 качестве параметра функции или делать копию такого объекта л)'Тс присвоения. (Пропущенный оператор присвоения вызовет ошибку D} компиляции, если кто-то попытается использовать его, а пропуШ#^ конструктор копирования вызовет ошибку при компоновке.) Мне $ изредка приходится прибегать к подобным трюкам в C++, но поскок данная идиома в какой-то степени деформирует обычный механизм сов, предназначая его для других целей, я полагаю эта предостороЖ**0 будет кстати. В конце концов, данный класс представляет собой не об*' #
q++ и илиома локальной функции /* _——■—■ ——————————————_____ 203 9 в классическом понимании ООП (например, данные этого объекта вовсе не обязаны сохраняться от вызова к вызову), а лишь функцию с интересными, не объектно-ориентированными характеристиками, поэтому нет разумных причин, по которым кому-нибудь понадобилось бы или захотелось бы копировать и куда-либо передавать объекты такого класса. Кстати, это один из тех немногих «дурацких трюков C++», которые, на мой взгляд, могут оказаться воистину полезными. Хотя при их применении я часто чувствую неприятный холодок страха: я декларирую методы, которые никогда не реализуются, и при этом ни компилер, ни компоновщик даже не пикнут. Программа, приведенная в СРР.СРР, включает в себя класс cPackageRtnl, торый является примером того, что я называю классом-упаковкой. Этот •iacc содержит в качестве своих методов и саму охватывающую функцию, и ее юкальные функции, а также те переменные, которые будут доступны для всех этих функций (например, preJFnl). Для того, чтобы воспользоваться идиомой локальной функции: в Создайте класс-упаковку для каждой функции, которая доллсна иметь одну или более локальных функций. 3 Сделайте охватывающую функцию и все ее локальные функции членами класса-упаковки, и только охватывающую функцию сделайте публичной. Это будет означать, что локальные функции можно будет вызывать только из охватывающей функции, и что сама охватывающая функция будет единственным публичным членом класса (кроме конструктора). Приведенный выше пример написан только для одной локальной функции, но, конечно же, никакого ограничения на количество локальных функций в одном классе-упаковке нет. / в Те переменные, которые должны быть доступны для охватывающей функции, но недоступны для локальной функции, поместите в саму охватывающую функцию. Например, взгляните на местоположение переменной postJFnl в методе cPackageRtnl::Rtnl. Этот прием позволяет задать для этой переменной ту область видимости, которая была у нее в PASCAL.PAS, и которая была «утеряна» при переносе на С. ■■ Глобальные переменные, а также переменные, локальные для отдельных функций, поместите точно так же, как это делалось бы в Pascal, С и C++. 68 Сделайте так, чтобы вся необходимая инициализация совершалась не в конструкторе, а в той подпрограмме, которая выполняет реальную работу (например, в Rtnl) объекта Жете легко нарваться на неприятности, если вызовете метод одного и того же объекта несколько раз и при этом будете полагать, что кон-
204 Илиома локальной структор проделал свою работу. Поскольку класс-упаковщик испп зуется немного не так, как это обычно делается в ООП, вам следъ быть в курсе подобных проблем. Определите объект класса-упаковки в той же области видимости, в к торой находится самая глобальная функция программы, вызывают вашу охватывающую функцию. Например, если ваша охватывают, функция является подпрограммой общего назначения и будет щ^ ваться из многих единиц компиляции вашей программы, помести этот объект глобально. Если же охватывающая функция будет испод о о зоваться только одной единицей компиляции и только нескольким функциями, поместите объект в эту единицу компиляции и сделай] его локальным для нее. е При помощи этой идиомы вы достигаете такого же гибкого контроля за об ластями видимости, какой обеспечивается в Pascal. При этом вам не придете? изменять структуру единиц компиляции вашей программы. Тем не менее, ис пользуя этот прием, старайтесь не перехитрить и не запутать самого себя. И Е Pascal с его локальными функциями, и в старом добром C/C++, в котором ло кальных функций нет, вам следует все время следить за проблемами нормали зации доступа к данным (такими, как, например, неуместное использование глобальных переменных). Обратите внимание на тот факт, что некоторые данные класса-упаковки (pre_Fnl) сохраняются на протяжении всего времени жизни объекта, поэтом) вы должны избегать наложения вызовов метода у одного и того же объекта Например, если к нему одновременно обратятся две функции из разных пото ков вашей программы, результаты обоих обращений будут совершенно непред сказуемы. Вы можете легко избежать подобных конфликтов, используя в раз ных потоках разные объекты класса-упаковки. Как известно, C++ поддерживает вложенные (или локальные) классы Эта возможность могла бы показаться как нельзя более подходящей > теме данной главы, если бы не страдала от одного очень серьезное ограничения* методы вложенного класса не могут ссылаться на авто матические переменные охватывающей их области видимости. Интересе деталь: это ограничение, оказывается, имеет довольно характерное обо1 нование. Если заглянуть на страницу 189 Annotated C++ Reference Mcl ual, на которой обсуждаются объявления вложенных классов, то можно прочитать следующее: «допустимость ссылки на автоматичен1' переменные охватывающей функции означала бы введение вложен^ функций». Это ограничение покажется еще более странным, если вс11° нить, что вложенные классы могут ссылаться на типы, на о6ъ&* перечисляемых типов (enums) и на статические объекты из охватываК)^ та-' области видимости. Еще хуже то, что вложенные классы не могут i ■ю\* статические члены.
lV€ C++ и имома локальной функции Л* ■ " ———————————————————————— 205 Я уверен, у всех этих ограничений есть какое-то уважительное обоснование, но я его не знаю. А глядя на это с точки зрения программиста и рассматривая Pascal, C++ и прочие языки просто как инструменты для своей работы, я и не хочу знать этого обоснования. Все, что меня интересует это характеристики и возможности этих инструментов. (На самом деле я только что солгал: я вовсе не уверен, что у всех этих странных KJ ограничении есть веские причины, но не имея возможности детально разобраться в сути этого вопроса, я склонен оправдать архитекторов C++ за недостаточностью улик.) Давайте остановимся на минутку и рассмотрим вопрос о рекурсии. Во всех программах — PASCAL.PAS, С.С и СРР.СРР — подпрограммы Rtnl и Fnl могут вызывать друг друга или сами себя, как угодно. В этом ничего не- жиданного нет. Но есть одна тонкость, касающаяся переменных: те перемен- о кое 1е которые не являются локальными для охватывающей подпрограммы (Rtnl), не перегенерируются при рекурсивном вызове этой подпрограммы. Та- поведение вполне осмысленно, поскольку эти переменные (такие как pre Fnl) находятся в той области видимости, которая охватывает обе подпрограммы Rtnl и Fnl. Поэтому, когда Rtnl вызывается рекурсивно и затем вызывает Fnl, Fnl увидит то же «поколение» переменной pre_Fnl, которое существовало до рекурсивного вызова. В Pascal же (как, впрочем, и в других языках, поддерживающих настоящие локальные функции) перегенерация автоматических переменных из такой области видимости происходит. Решение этой проблемы несложно: если необходимо, чтобы локальная функция видела о о новое «поколение» этой переменной при рекурсивном вызове охватывающей функции (Rtnl), сделайте эту пременную локальной для Rtnl и передавайте ее \ в Fnl через параметр. В заключение, отмечу еще один серьезный недостаток идиомы локальной Функции: она в буквальном смысле слова является поверхностной — работает лишь для одного уровеня вложения (хотя на этом уровне может быть и несколько локальных функций). Если вы попытаетесь сымитировать двойное вложение функций, вы моментально столкнетесь с проблемой доступа к временным. Вложенные классы не дадут вам возможности адресоваться к лояльным переменным охватывающей области видимости, а создание иерархий лассов-упаковок, очевидно, приведет к такой таинственной навороченности °Да> которая будет совершенно неоправданной и слишком дорогой для пРовождения. Правда, я должен признать, что в реальных проектах второй D ень вложенности функций является скорее всего экзотикой (например, я вообщ е не могу припомнить, когда я в последний раз прибегал к ней в своих Раб°чих кодах).
206 Илиома локальной .„.„ Реальное применение Если вы желаете увидеть идиому локальной функции в действии, а не пп в надуманном примере, посмотрите на реализацию компонента WIN32VP представленного в главе 14 на стр. 515. Те части кода WIN32VER.CPp торые имеют отношение к идиоме, приведены прямо здесь (смотп* листинг 5.4), но именно в главе 14 вы найдете детальное описание использо ния этого компонента. Я мог бы реализовать метод DoItO класса pckComplainlfNot в ш простой отдельной функции. Но это привело бы к проблеме с вспомогательн функцией AddPlatformO, которую вызывает DoItO. Если бы я сделал д dPlatformO также отдельной функцией, мне пришлось бы либо делать глоба.- ными те данные, которые она использует совместно с DoItO, либо передава их ей как отдельные параметры. Кроме того, мне пришлось бы поместить э- две функции в отдельную единицу компиляции, что было бы глупо и некрасш для такого небольшого количества кода. Таким образом, ни одна из эй альтернатив не годилась для той функциональности, которая требовалась, бы далее сказал, что код просто-таки «хотел» быть написанным через механиз ./ локальных функций.) Используя же идиому локальной функции и «оберточную» функцию Сое plainlfNotO, я получил именно ту схему областей видимости, которая требов лась от всех частей программы. Вызывающий код имеет доступ только к фут цин ComplainIfNot() и находится в счастливом неведении о деталях реализащг этой функции. Листинг 5.4 Сокращенная версия WIN32VER.CP «include "stdafx.h" «include "win32ver.h" // Замечание* многие детали реализации удалены из этой копии примера для // большей ясности. // Идиома локальной функции. За объяснением обращайтесь к книге. class pckComplainlfNot { public: pckComplainlfNot(void); BOOL DoIt(DWORD Aneeded.OS, char *Atitle); private. int platforms_needed, platforms_reinainmg; char *title; char msg_text[500]; DWORD needed_0S; mt Count0neBits(DWORD x); void AddPlatform(DWORD platform, char *platform_name); pckComplamIfNot& operator=(const pckComplainIfNot& x); pckComplamIfNot(const pckComplamIfNot& x); v • pckComplainlfNot.:pckComplainlfNot(void) { } v
о ( / olainlfNot DoIt(DW0RD Aneeded_OS, char *Atitle) * .f(ihave_version_info) X QetVersionlnfoO, // Мы имеем то, что требуется вызывающему коду, так что катапультируемся f(Aneeded_OS & Wmdows.version) return FALSE; / модифицируйте следующий оператор должным образом, если будут определены // дополнительные платформы Этот оператор всего лишь обеспечивает наличие // только допустимых битовых флагов в needed_0S. needed_0S = Aneeded_OS & (WV_WIN32S | WV.W95 | WV.AnyNT), Dlatformc_reinaining = platformc_needed = CountOneBits(needed_OS); title = Atitle, -trcpy(mcg_text, Извините, эта программа требует "); AddPlatform(WV_WIN32S,"Win32G"), AddPlatfonn(WV_W95, "Windows 95"); AddPlatfonn(WV_NTWS, "Windows NT Workstation"); AddPlatform(WV_NTSERVER,"Windows NT Server"); AddPlatfоrm(WV_NTAS, "Windows NT Advanced Server"); MecsageBox(0,insg_text, title, MB_0K | MB.ICONEXCLAMATION); return TRUE; int pckComplainlfNot. •CountOneBits(DWORD x) { int count = 0, for(int 1=0; l < sizeof(x) * 8; i++) { if(x & (DW0RD)1) count++; x = x » 1; } return count; void pckComplainlfNot: :AddPlatform(DWORD platform, char *platform_name) { if(needed_0S & platform) { strcat(msg_text,platfonn_name), platforms_remaining--; switch(platforms_reinaining) i \ \ i case 0' strcat(msg_text," "); break, case 1: if(platforms_needed > 2) strcat(msg_text,", или '), else strcat(msg_text," или "); break; default strcat(msg_text,", "); Bool гСТаЯ °^еРточная функция (функция-шлюз) l°mplainIfNot(DWORD needed_0S, char *title)
208 Илиома локальной { pckComplamlfNot worker, return worker DoIt(needed_OS,title); После того, как первоначальная версия этой главы была опубликована в& в? отдельной статьи в Software Development, я получил по электронной по письмо, автор которого настойчиво интересовался, почему я не пользуюсь кг пилятором Watcom, поддерживающим локальные функции в C/C++. И я от. тил этому человеку, что такой вариант не может рассматриваться в качест приемлемого решения. Я охотно допускаю, что автор письма был прав, и« компилятор Watcom действительно поддерживает локальные функции; но я этого никогда не проверял, потому что мне совершенно все равно, так это и не так на самом деле. Данная возможность совершенно нестандартна и, еле С: вательно, ставит ваш код в зависимость от конкретного поставщика компи.1 тора. Причем здесь такая зависимость оказалась бы чрезвычайно сильной. Ц\ не нужны дополнительные кошмары с переносимостью кода, и, я подозрева! вам тоже. Вообще говоря, использование или неиспользование подобных СЕ' цифических возможностей компилятора это просто еще один прш проблемы эксплуатации инструментария, которую следует решать только контексте вашего конкретного проекта. Если вы пишете приватш программу — вперед, используйте нестандартные расширения на ваш собстве; ный страх и риск. Это ваша программа, и вы — единственный, кто будетп: жинать все плоды ваших собственных решений. Но если вы пишете публичнр программу (особенно такую, которую вы создаете совместно с друпя' программистами, и которая требует сопровождения и переносимости), тог; это совсем другое дело, и использование подобных нестандартных возможна KJ стеи практически всегда является недопустимым. Есть ли надежда на то, что архитекторы C++ когда-нибудь смягчатся и} бавят в язык поддержку локальных функций? Честно говоря, я не слишком о тимистичен в моих прогнозах. Более того, я даже не уверен, что нам слеД}с хотеть такого изменения. В то время, когда я пишу эту книгу, стандарт А№ ISO языка C++ приближается к заключительной стадии разработки, и в ре3У тате C++ будет еще более сложным и объемным языком, чем он являетсясе[ дня. Например, сегодня ни один из известных мне компиляторов C++ для ^ dows не поддерживает пространства имен (namespaces). Но вы можете^ поставить на кон шарик от вашей мышки и утверждать, что эта поддержка вится сразу же, как только стандарт будет зафиксирован. Если не рань#е несмотря на мощь и полезность механизма локальных функций, я дума*0* было бы ошибкой включить его поддержку в данный язык на данном этаПе развития. flf
той шил. Though we travel the world over to find the beautiful we must carry it with us or we find it not. Ральф Уолдоу Эмерсон If we do not find anything pleasant, at least we shall find something new. Вольтер Блиц-вопрос: если 16-разрядная Windows-программа вызывает функцию Load- LibraryO, то в каком месте 16-разрядная Windows ищет требуемый DLL-файл? Не буду зря скромничать и томить вас ожиданием ответа. Полный порядок каталогов для поиска таков: 1. Текущий каталог. 2- Главный каталог Windows (например, C:\WINDOWS). 3. Системный каталог Windows (например, C:\WINDOWS\SYSTEM). 4- Каталог, в котором находится исполняемый файл программы. 5- Каталоги, перечисленные в переменной окружения PATH. "• Каталоги, доступные по сети. Обратите внимание на то, что я явно указал 16-разрядную программу и 16- в РЯднУю версию Windows. Для 32-разрядной программы и 32-разрядной 1И Windows правильный ответ немножко отличается: 1. 2. Каталог, в котором находится исполняемый файл программы. Теку Щий каталог. 6- 32 -п; 4. 5 6. разрядный системный каталог Windows. 1 о-разрядный системный каталог Windows. Главный каталог Windows, каталоги, перечисленные в переменной окружения PATH.
210 Как жить г л \ И это еще не все отличия — Windows есть Windows: когда Windows ищет 16-разрядную DLL, она проверяет 32-разрядный системный ката (например, C:\WINDOWS\SYSTEM32) сразу после 16-разрядного систем/ каталога; когда Win32s ищет 32-разрядную DLL, она в первую очередь заь дывает в свой собственный домашний каталог, а потом уже переходе обычной последовательности поиска; a Windows 95 вообще предлагает испо о о .] зовать специальный ключ в реестре, который позволяет задать для каждогоi дивидуального 32-разрядного приложения свой собственный путь для поио DLL-файлов, который будет проверяться перед проверкой каталогов, переч! ленных в PATH. к Ну что, теперь мои слова привлекли к себе ваше внимание? Вы ужез интригованы вопросом, как же ваши программы будут находить свои DLL реальном мире, и что будет происходить при загрузке не тех файлов, которы- в действительности требуются? Хорошо. Именно для обсуждения такш вопросов я и написал эту главу. Отчаянный поиск £Ш Итак, одна и та же операционная система использует разные последовательно1 сти поиска DLL для программ, совершенно незначительно (с точки зрения' пользователя) отличающихся друг от друга. Этот факт, мягко говоря, является; очень интересным (и заставляет нас призадуматься — а не был ли Вольтер; случайно знаком с самой ранней альфа-версией Windows, когда писа' «Простодушного», откуда взят эпиграф для данной главы?). Как бы там ни бы, ло, это факт. И 16-разрядные, и 32-битные программы не могут рассчитывать на одинаковый порядок поиска той же самой DLL во всех воплощениях Win dows, или даже на тот же самый порядок поиска при последующих своих за пусках (в зависимости от текущего каталога и переменной окружения PATH) Я обращаю ваше внимание на эту ловушку в Windows по двум причинам Во-первых, подавляющее большинство Windows-программистов провалило^ бы при ответе на упомянутый выше блиц-вопрос (включая меня самого; $ крайней мере, до того, когда я провел необходимые для написания этой главь исследования). Во-вторых, те способы, которыми Windows осуществляет пои^ динамических библиотек, может иметь серьезнейшее влияние на поведение ших публичных программ. В особенности сегодня, когда огромное количеств на людей покупают свои первые компьютеры, на которых, как правило, уже Уста новлена операционная система Windows. Например, в далеком туманном прошлом, перд самым выходом в свет dows 3.1 я приобрел новый компьютер, на котором уже были установлены dows 3.0 pi несколько программных продуктов для нее. Спустя несколько МеС. цев, я установил на этот компьютер Windows 3.1 и переустановил те же саМ программные пакеты, после чего я немедленно столкнулся со странно**
0тча*нный поиск DLL 211 ясающей ошибкой в работе стандартного диалога для выбора шрифта. Р разговоров с несколькими работниками группы технической поддержки, же после проверок размеров и дат файлов в моей системе, я выяснил, что аТ иной проблемы было наличие на моем компьютере двух копий нашей nF и подрУжки ~ динамической библиотеки стандартных диалогов СОММ- Т(Г DLL. Финальная, корректная версия этой DLL находилась в каталоге r\WlNDOWS\SYSTEM, а ошибочная, предварительная версия была установ- одним из программных пакетов в C:\WINDOWS. Благодаря той последо- льности поиска DLL, которую соблюдает Windows 3.1, корректная версия пр roMMDLG.DLL никогда не загружалась. Я удалил предварительную версию той DLL, и, словно по мановению волшебной палочки, диалог выбора шрифта починился. Пока я писал эту главу, один мой приятель столкнулся с двумя замечательными примерами из той же области. В первом случае, некая программа при ее установке заменила CTL3DV2.DLL в системном каталоге Windows на более старую версию этой DLL, в результате чего Procomm Plus for Windows 2.0 перестал запускаться. И это несмотря на бесконечные предупреждения и рекомендации о необходимости проверять даты и версии системных файлов перед их заменой! (По-видимому, написание инсталляционных программ все еще является в глазах многих лишь очень второстепенной частью всего процесса разработки программного обеспечения.) Данный случай является замечательным примером того, как программный продукт может неуважительно относиться к ресурсам пользователя (о недопустимости подобного поведения я уже писал в главе 2 на стр. 90). Другой случай, который наверняка выиграл бы какую-нибудь награду на конкурсе извращений, произошел с двумя программами по обработке документов, которые использовали и включали в свою поставку две разные версии од- о ной и той же динамической библиотеки (DLL-файлы с одинаковым именем; Для определенности назовем ее FRED.DLL). Продукт X помещал все свои DLL (включая старую версию FRED.DLL) в свой собственный каталог, а продукт п°мещал более новую версию FRED.DLL в системный каталог Windows. Заметите программу X, и она загрузит FRED.DLL из своего собственного ката- 'ога (по-видимому, именно он является текущим в момент запуска Р°граммы). Программа загружает именно ту версию FRED.DLL, которую иДает, поэтому пока все работает нормально. К сожалению, при завершении u 0TbI программа X ошибочно оставляет FRED.DLL в памяти. А это приводит °Му, ЧТо пользователь больше не может запустить программу Y, которая не Dl тСТ загРУзить более новую версию FRED.DLL с диска. Windows видит, что с требуемым именем уже загружена, и заставляет программу Y использо- Y /^1Менно ЭТУ копию библиотеки — старую и несовместимую с программой Hiw СМотРя на то> что формальной причиной проблемы является ошибка в к РогРамм е X, именно авторы программы Y явно не заслуживают премиальных
212 Как жить за изящный отказ их программы от работы в отсутствие правильной ВерС]< FRED.DLL. В данном случае сама Windows не дает работать программе V скольку та требует наличия в уже загруженной, старой версии FRED,DLl о «-* кои-то несуществующей там подпрограммы, и динамическое связывание тепп неудачу.) Динамические библиотеки могут приводить к интереснейщ,, приключениям даже тогда, когда они должным образом установлены изолированы в системе. Например, моя программа Stickiest использует Д! СО F те} { намическую библиотеку по имени STKB.DLL для перехвата клавиатурных бытии и обеспечения глобальных «горячих клавиш» для своей работы. И я раз встречал пользователей, которые правили ее (да и другие DLL тоже) стовыми редакторами, удаляли ее, переименовывали ее и перемещали ее совершенно неожиданные каталоги файловой системы. На самом деле, многк пользователи Windows (вероятно, даже большинство из них) сегодня дажеш догадываются о том, что динамические библиотеки, по сути, являются испод няемыми файлами (даже несмотря на то, что Проводник помечает их яр лыко:1 «Application extensions»). И, как правило, прозрение наступает только тогда когда какая-нибудь программа вдруг перестает работать, вызывая всем m хорошо известное сообщение типа «Cannot find BARNEY.DLL. Windows need' this file to run C:\TEMP\FRED.EXE» («Невозможно найти BARNEY.DLL Этот файл нужен Windows для запуска C:\TEMP\FRED.EXE»). Что еще хуже, пользователи редко могут обнаружить причинно-следственную связь ме жду каким-то файлом, удаленным пару дней назад, и программой, котора? вдруг перестала работать сегодня. В конце концов, что за ерунда — эта BWCC.DLL, и почему она должна заботить пользователей? Даже самые опытные пользователи умудряются добиться неправильное местоположения DLL-файлов. Часто это происходит попросту из-за того, динамические библиотеки хранятся отдельно от использующих их программ результате, когда пользователь пытается перенести программу с одной систем* на другую, он конечно же не видит никакой явной связи между этим ЕХЕ-фа11 лом и той DLL. А иногда он, вероятно, вообще не подозревает, что эта D^ существует в природе. Случалось, те клиенты, которых я консультирова' теряли динамические библиотеки в самые неподходящие для этого момента что приводило к самым неприятным последствиям, вплоть до истерик. Пока я не забыл: помните, что динамические библиотеки — это не тольь1 файлы с расширением DLL. Windows и многие третьесторонние проДУк просто-таки безумно любят использовать динамические библиотеки. Напр ДО1* чтс файлы шрифтов (.FON) AfterDark (.SCR) Ф Visual Basic (.VBX) — все эти с виду специаль^ как динамическими библии ками.
Кто заГружает^я_первым1 213 $0* интересная проблема, связанная с DLL — конфликты имен. Механизм ^Pw" жКЙ 16-разрядных динамических библиотек (не только в Windows 3.1, П в Windows 95 и Windows NT 3.51) до смешного извращенным способом ивает имена загруженных модулей, будь то исполняемые файлы или Дру гая п Ю отс т (Умопомрачительные примеры и детальное описание того, как все это де i сЯ вы можете найти в главе 3 замечательной книги Мэтта Питрека под на- зва нием ныи «Windows Internals».) Я не собираюсь вдаваться в кровавые обности, но СуТЬ дела в том, что исполняемые программы и динамические б1б1ИОтеки могут блокировать работу друг друга и тем самым создавать пол- й хаос в системе. Например, если вы запускаете FRED.EXE, которая загружает BAR- \JEY.DLL, а затем, пока FRED.EXE работает, пытаетесь запустить BAR- MEY.EXE, Windows откажется это сделать и даже не выдаст никакого сообщения об ошибке. Windows просто ничего не сделает, оставив пользователя в недоумении, какого черта BARNEY.EXE не хочет запускаться (хотя на самом деле с программой абсолютно все в порядке). В действительности, на этом дело не кончается. Каждый раз, когда происходит попытка запустить BARNEY.EXE, и она проваливается из-за наличия в пямяти загруженной копии BARNEY.DLL, Windows увеличивает счетчик использования BARNEY.DLL на единицу. В результате этого, даже по окончании работы FRED.EXE (которая при этом добросовестно пытается выгрузить BARNEY.pLL) динамическая библиотека остается в памяти и продолжает блокировать работу ни в чем не повинной BARNEY.EXE. И такая ситуация сохранится до тех пор, пока пользователь не перезагрузит Windows, или пока он не запустит какую-нибудь утилиту, которая может выгрузить BARNEY.DLL (например, путем достаточного количества вызовов функции FreeLibraryO). Аналогично, программа FRED.EXE не сможет загрузить нужную ей BAR- ^tY.DLL, если программа BARNEY.EXE уже успешно запущена. При этом Г*№.ЕХЕ вовсе не прекратит работу из-за этого — она загрузится, и можно °лько фантазировать на тему, к каким фейерверкам это может привести. Если вы желаете поэкспериментировать со всеми этими загрузками-выгруз- ' |ш Динамических библиотек, на WWW-сервере по адресу http://www.sym- ,rU/russian/library/prof_prog/source/chap06/win16 находятся несколько 16- Рядных программ, которые вы можете использовать для этих целей. 11 BARNEY.DLL — динамическая библиотека, которая экспортирует °Дну единственную функцию SayHelloO, которая, в свою очередь, просто выводит на экран соответствующее сообщение FRED.EXE — программа, которая просто вызывает SayHelloO из BARNEY.DLL
214 Как жить г * BARNEY.EXE — программа, «по случайности» имеющая то лее о имя модуля (BARNEY), что и BARNEY.DLL Ч BARNEY2.EXE — копия BARNEY.EXE, имеющая то же самое модуля (BARNEY) ' И': UNBARNEY.EXE — программа, которая просто делает вызов Freel braryO Будьте чрезвычайно осторожны при обращении с этими программам Если вы запустите BARNEY.EXE, а затем запустите FRED.EXE, вы увиди- блокировку загрузки DLL, которую я описывал выше. Вероятно вы также у* дите мертвое зависание FRED.EXE при попытке выйти из нее, а в конечна итоге сеанс работы с Windows будет испорчен, и вам придется пользоваться сетевым выключателем для возврата машины в рабочее состояние. По крайне; мере, именно такое поведение я наблюдал под Windows 3.1 и Windows 95. По Windows NT 3.51 программа FRED.EXE не повисала, но все еще работающа? программа BARNEY.EXE пропадала с экрана, оставляя на месте своего окш прямоугольник-привидение, который больше не перерисовывался (были i другие последствия: система становилась нестабильной, хранители экрана от казывались нормально рисовать свои картины и т. д.). По моему опыту, NTr в самом деле заслуживает репутации «прочна как сейф», однако даже этг операционная система не имеет 100-процентного иммунитета против подобных проблем — мои маленькие тесты с очевидностью это доказывают (вероятно мне следовало назвать файлы этих двух программ иначе — например TBOLT.EXE и LITEFOOT.DLL). Мораль: если вы собираетесь поиграть с эти ми программами, вы сами берете на себя ответственность за все необходимые предосторожности в отношении сохранности ваших даннных и друпв программ, потому что мои «примеры» ОБЯЗАТЕЛЬНО приведут вашу систе му к краху. (Жуткая сторона всех этих экспериментов заключается в том, вы можете совершенно неумышленно и случайно встретиться с подобно] проблемой и потратить часы или даже дни на выяснение ее причин.) На мой взгляд, данная проблема с блокировкой модулей является своег рода приговором основам архргтектуры Windows и приговором довольН1 серьезным. В конце концов, в приведенных мною примерах программ и Д11 намических библиотек нет абсолютно ничего некорректного: они предел ляяют собой совершенно легальные ЕХЕ- и DLL-файлы, и они не использу10 никаких недокументированных возможностей или чего-то в этом духе. Пр°сТ' две из них - BARNEY.EXE и BARNEY.DLL - оказались носителями 00^ кового имени модуля, а этого в свою очередь оказалось достаточно для т°г чтобы привести в замешательство 16-разрядную Windows (или ее поддерг чте i на уровне WOW). Кстати, это напоминает о том, что от данной проблемы11 возможно увернуться просто переименованием файла BARNEY.EXE. В к° фликте участвуют не имена файлов, а имена модулей, которые хранятся внУт"
до сеТлиЛ*£. Win32? 215 Вы можете убедиться в этом путем использования в тех же э \ аМИХ рентах файла BARNEY2.EXE вместо BARNEY.EXE, а также применяя КСП v Heap Walker (она поставляется вместе с Visual C++) для проверки раз- тИ «ия памяти при работе BARNEY2.EXE. Опять же, если вас всерьез щения есует техническая подоплека всех этих проблем, возьмите почитать книгу lHr joWS Internals», которую я упоминал выше. Только чур потом не прихо- ко мне плакаться, если после ее прочтения вас начнут мучить ночные кошмары- i « жеш ли нас Я уверен, что хотя бы некоторые из читателей уже задают вопрос: все вышеописанное интересно так же, как посещение места прошлой автокатастрофы; разве эта книга не призвана повествовать прежде всего о программировании для Windows 95 (и, следовательно, о Win32), а не о старых дряхлых проблемах Winl6? Конечно же нет. Как я уже много раз повторял в этой книге, меня (и, я надеюсь, вас тоже) беспокоит именно то, как хорошо будут работать ваши программы для Windows 95 в реальном мире. А реальный мир таков, что беспокоиться нужно о том, как ваши программы будут взаимодействовать и с разными версиями Windows, и с различными ее составляющими (включая динамические библиотеки). И вряд ли кого-то удивит тот факт, что миграция компьютерного сообщества с 16-разрядных программ на 32-разрядные динамические библиотеки готовят нам еще больше занимательных сюрпризов, чем мы встречали до сих пор. Чтобы пояснить мою мысль, я замечу, что проблемы блокировки выполнения 16-разрядных программ, упомянутые выше, не происходят с 32-разрядными программами под Windows 95 и Windows NT. Вы можете запускать любые программы и загружать любые динамические библиотеки с конфликтующими именами модулей, и при этом Windows 95 и Windows NT будут правильно манипулировать ими, и все будет работать просто замечательно, фичиной такой необычайно высокой разумности Windows 95 и Windows NT отношении 32-разрядных программ и DLL (по сравнению с тем, как это елают их же собственные службы поддержки Win 16) кроется в деталях архи- КтУры, которые в сущности не имеют прямого отношения к теме данной вы. За более подробной информацией об этом вы можете обратиться к не- °РЬ1м статьям из Microsoft Systems Journal или к книге Хелен Кастер под Званием «Inside Windows NT». вопреки всем самым искренним и заветным желаниям Microsoft, суще- УЮщая сегодня база Windows не станет слепо принимать Win32, следовать за\ ^ецендентномУ продажному буму, случившемуся 25-го августа, и срочно п ЯТЬ все программное обеспечение на новые сверкающие 32-разрядные Р^ммы. Я не могу представить, чтобы мало-мальски существенная часть
216 Как жить пользователей Windows могла посчитать это экономически осмысленным л. етг t если эти новые 32-разрядные программы появятся в изобилии. Что каса лично меня, то я знаю, что я в течение нескольких лет не заменю по Крайь мере дюжину моих 16-разрядных программ на их 32-разрядные аналоги ( говоря уже о нескольких DOS-программах), потому что производители программ их больше не поддерживают или вообще исчезли с рынка, а программы просто замечательно работают под 32-разрядной Windows. В KOHi концов, зачем мне надо пускаться в перипетии затрат и апгрейдов и заменят то, что исправно работает? Дело в том, что практически все системы, работающие под Windows 95 и- Windows NT, еще в течение нескольких лет будут представлять собой скопищ. 16- и 32-битных программ и динамических библиотек. И нам, программистам необходимо признать этот факт, понять его последствия и приспособиться ним. Многие пользователи будут заменять хотя бы часть своего программное обеспечения на 32-разрядные программы, а значит, они будут соверцщ многочисленные манипуляции со своими системами, полагаясь на все эти оше ломляюще самонадеянные, сумасшедшие инсталляционные программы которые и так уже обеспечили нам столько проблем, что и не сосчитаешь. Ас появлением OCX-компонентов мы получим еще целый класс внешних файлов которые будут неправильно располагаться, перезаписываться, удаляться к т. д., что без сомнения только усилит остроту проблемы. Пожалуйста, не поймите меня неправильно. Мое недовольство направ лено вовсе не в адрес производителей инструментов для создания ишь ляционных программ (некоторые из таких инструментов поистик превосходны), а именно в адрес тех, кто либо не использует это! инструментарий, либо использует его неправильно. Мой личный опыт ш установке многих Windows-программ убеждает меня, что большая часгг поставщиков программного обеспечения смотрят на инсталляцию ли& как на накладные расходы, либо как на нечто, о чем стоит заботите только в самый последний момент. Главное — собственно создание1 маркетинг продукта, а все, что связано с установкой, не так «секс* пильно», чтобы всерьез интересовать, и является чем-то таким * скучным как упаковка, как выбор картона для изготовления проклаД'1 внутри коробки. Я мог бы попробовать организовать Доску Позора ИнсталляциоН^ Программ, но это было бы бессмысленным занятием, поскольку алия** большая часть коммерческих продуктов резонно претендовала бы включение туда. Конечно же, смешивание 16-разрядных и 32-разрядных программ й^' намических библиотек формально не разрешено (хотя и возможно при Я0*1 использовании переходников). Но вы должны знать, что програмы будУт . таться делать это (в частности, из-за пресловутой путаницы с порядком fl0*1 \
Зяи^г^й^ применения линамических библиотек 217 DLL) :.\^ 1 Как вы догадываетесь, результаты такой практики будут совсем не тана которые хотелось бы надеяться. Например у если 16-разрядная программа пытается использовать 32- Ре 6veT но яную DLL под Windows 3.11, пользователь увидит DOS-окно со знако- ообшением «This program requires Microsoft Windows» («Эта программа 1Ы Microsoft Windows»), хотя все программы — участники событий на са- ^" леле являются настоящими Windows-приложениями. Когда я запускал же тест под Windows NT 3.51, я получал практически навечно появляв- 1ся курсор типа «ждите, идет загрузка программы», программа ни разу не пускалась, и я ни разу не получал никакого сообщения ни от NT, ни от File Manager. Много раз я пробовал завершить работу системы в такой ситуации, это происходило только после задержки в несколько минут. (Кажется, это еще одна трещинка в броне «прочного сейфа».) Подобные вещи могут оказаться серьезнейшей проблемой для публичной программы. Представьте себе, вы пытаетесь объяснить заплатившему вам деньги заказчику (или тому, за чью поддержку вам платит ваш наниматель), что это странное поведение вовсе не является виной вашей программы, что оно является побочным эффектом того, что у пользователя случайно оказалась 32-разрядная DLL, по несчастливому стечению обстоятельств имеющая то же имя модуля, что и ваша 16- разрядная DLL, и к тому же очутившаяся в каталоге, который не имеет никакого отношения к вашей программе... Я думаю, все мы знаем, чем, скорее всего, закончатся подобные переговоры. что Знаете ли вы какие-нибудь достоверные «ужастики», касающиеся DLL? Если да, то дайте мне знать о них (мой электронный почтовый адрес вы найдете во вступлении к данной книге). Мне будут нужны все подробности, которые вы только сможете сообщить (например, «FredCalc 2.0 не выгружает свои динамические библиотеки», или «WordOpolis 3.0 заменяет MFC30.DLL на более старую версию, не сообщая об этом пользователю и не спрашивая разрешения на это», и т. д.). Если у меня наберется достаточное число подобных примеров (а я уверен, так и должно случиться), я, возможно, организую Доску Позора Динамических Библиотек. ^маошческих г>иб °Т^Я На все вышесказанное, я не испытываю ненависти к динамическим * отекам. Они, очевидно, являются очень полезным инструментом и, при и и правильного использования, благом для всех д^ * — »-^x^w—*„^, ^.x^wxx w„. ^^v — и для разработчиков, я Во °ЛЬзователей. Будучи и разработчиком, и пользователем в одном лице, Не хотел бы отказываться от таких выгод, как, например, наличие MFC
218 Как жить в виде DLL на всех системах, работающих под управлением Windows 95 ъ к сожалению, как только речь заходит о наших собственных программа* т, пользуемых ими динамических библиотеках, понятие «условия правильн использования» слишком часто трактуется как «в руках экспертов», а это пп положение воистину превращает DLL в классический пример «тепличного дания», которому неимоверно трудно выжить под напором настоящего цуНа, новых пользователей Windows, которые будут использовать наши публичн программы. В свете всего вышесказанного, я полагаю, что и разработчики, и пользов тели смогут извлечь максимальную выгоду из той новой модели использованц; динамических библиотек в публичных программах, которую я называю «этике том разумного применения динамических библиотек». Главная цель протокола — повысить удобство и надежность Windows-программ. Он стремит ся достичь этого путем уклонения от мистических проблем с последовательн стью поиска DLL (насколько это возможно) и предложения наиболе разумного подхода к использованию динамических библиотек приложениям!! Итак, этот протокол заключается в трех основных рекомендациях: б Используйте DLL только при необходимости Правильно располагайте DLL Загружайте все DLL явно этогс о { Используйте DU только при необходимости Бывают случаи, когда действительно серьезные основания заставляют як помещать код в динамическую библиотеку (или, по крайней мере, делают так» решение наиболее разумным и эффективным). Например, когда несколь» программ должны совместно использовать значительный объем кода (хороши иллюстрацией подобного случая может служить динамическая библиотек» MFC). Тщательно, всесторонне оцените ситуацию в вашей конкретно1 программе (или в наборе программ), и, если вы убеждены в осмысленности# пользования DLL в вашем проекте, делайте это с чистой совестью. Конечно же, подобные рассуждения являются самым что ни на ест классическим примером «скользкой дорожки». К сожалению, я видел слиШК0 много кода, совершенно необоснованно запиханного в динамические теки. Поэтому, выражаясь лаконично, вам следует всегда следовать принн11 презумпции ненужности динамических библиотек: весь код должен принэД-1' жать монолитному ЕХЕ-файлу, если не доказана необходимость обратного. * мещение кода в ЕХЕ-файл не только позволяет избежать патологичен случаев, описанных выше, но и упрощает распространение и инсталлЯ1* программного продукта.
?1^rJ0^^ применения линамических библиотек 219 Я не знаю, почему это происходит именно с динамическими библиотеками, но они, кажется, имеют очень странное воздействие на программистов, только что познакомившихся с ними- динамическая библиотека представляется воображению эдаким сочетанием Супермэна с Никлаусом Виртом, позволяющим одним прыжком перемахнуть через высоченное здание и через небрежно составленные спецификации программного продукта. Если бы меня попросили назвать какую-либо одну возможность Windows, наиболее подверженную синдрому блестящего молотка (см. главу 4), то я ответил бы: динамические библиотеки. Ключевым моментом является то, что вам ни в коем случае нельзя воспринимать мой (или чей угодно) совет «использовать DLL по своему усмотрению» как своего рода официальное разрешение на начало кутежа с динамическими библиотеками. Такое понимание может плохо кончиться и для ваших программ, и (что более важно) для ваших пользователей. И даже не думайте, что тут вас не подстерегают никакие банальные соблазны. Стоит вам начать использовать динамические библиотеки, и станет очень легко убедить себя в том, что вам следует помещать в DLL весь «ненадежный» код, так чтобы если (читайте: когда) его потребуется обновить, вы сможете «просто» распространить только одну DLL, а не весь программный продукт. Аналогично, очень легко убедить себя и в том, что ваша первая программа в перспективе обязательно разрастется в целый набор взаимосвязанных программ, и поэтому вам следует с самого начала планировать наперед и помещать в DLL каждую подпрограмму, которая возможно будет использоваться совместно двумя или более программами из этого будущего набора. Кстати, последний сценарий является особенно подозрительным: действительно ли вы испытываете необходимость пла- нировать на перспективу и начать подобное оформление кода прямо сейчас, или вы просто ищете оправдание для того, чтобы немедленно помахать своим новым блестящим молотком? Только вы сами сможете ответить на такой вопрос. правильно располагайте DLL Как и другие, относительно простые, темы программирования для Win- Ws» вопрос о расположении динамических библиотек стал намного интерес- с появлением Windows 95. Без сомнения, Microsoft болезненно переживала осведомленность о всех тех ужасных историях и случаях, о которых я пи- * binie, и наконец они решили что-нибудь предпринять по этому поводу. Как зо "?Идите» они действительно сделали достаточно серьезные изменения в 6а- Модели расположения динамических библиотек, и эти изменения должны Hor °ТИ В ПоРяД°к наши дела при условии, что все мы будем следовать уста- То I HHbIM правилам. (Почему вы пшроко раскрыли глаза? Разве я сказал что-то?)
220 Как жить Для начала, нам придется четко различать 16-разрядные и 32-разрЯл программы, а также те случаи, когда операционная система W indo поддерживает и не поддерживает новые средства и правила управления намическими библиотеками. 32-разрядные программы, работающие в Wind** 95, подчиняются новым правилам игры, которые я вскоре опишу, в то вп, как 16-разрядные программы, работающие во всех версиях Windows, атак 32-разрядные программы, работающие в Windows NT 3.51, этим правилам подчиняются. В ходе дальнейшей дискуссии, я для упрощения буду объедини те комбинации программа/ОС, которые подчиняются новым правилам под общим названием «новая обстановка». игрь Нам также следует различать типы динамических библиотек, использ\ мых вашей программой, в смысле области их видимости (или доступности) системе. В этом смысле есть три типа DLL, о которых нам нужно беспокоить 'е ся Специфичная для программы. Это означает именно то, что вы подум ли: такая динамическая библиотека, которая используется и будет 2 ис и и пользоваться только вашей программой, так что другим программа* нет и не будет до нее никакого дела. Специфичная для набора (пакета) программ. Как правило, к этойка тегории относятся динамические библиотеки производства одной и тог, же компании. Такие пакеты программ, как Norton Utilities m\ программы от Aldus/Adobe издавна включали в свой состав DLL этогс, 1 типа, равно как это теперь делает большинство новомодных офисные программных пакетов. Кстати, в данной ситуации доволык критичным является вопрос о том, чтобы различные программы пакета (в общем случае, установленные в различное время) всегда могли оты екать такие динамические библиотеки. Глобальная. Эти динамические библиотеки, как правило, исполь зуются широким кругом программ, о существовании которых созда тель DLL может даже и не знать. Одним из наиболее ярких пример0' такой DLL модет служить VBRUN300.DLL — библиотека времени и( полнения от Visual Basic 3.0, которую использует огромнейшее к° личество программ, абсолютно не известных фирме Microsoft. TaKi- динамические библиотеки должны быть легко находимы и доступны!1 любой части системы в целом, не требуя никаких дополнительных Де! и ствии при установке программ. Старые времена Поскольку все мы будем вынуждены в течение какого-то времени 6есЯ°ь о иться об операционных системах, отличных от Windows 95 и предшество ф ших Windows NT 4.0, и поскольку именно эти условия породили многочй^
применения линамических библиотек ясастики» из жизни динамических библиотек, позвольте мне 221 начать .менно с этого случая. (См. таблицу 6.1, иллюстрирующую все случаи, 'Jopbie я буду обсуждать.) Таблица 6.1. Расположение DLL Тип DLL Специфичная ая для прогр аммы Специфичная для пакета Глобальная Новая обстановка Системный каталог программы, плюс настройки Арр Paths в реестре Каталог общих файлов, плюс настройки Арр Paths в реестре Главный или системный каталог Windows, плюс настройки SharedDLLs в реестре Старая обстановка Домашний каталог программы, больше никаких настроек не требуется Либо специальный, специфичный для пакета каталог с указанием его в PATH или в другой переменной окружения, либо центральный каталог Главный или системный каталог Windows При такой комбинации программы и окружения динамические библиотеки, специфичные для программы, должны находиться в домашнем каталоге этой программы. Неправильное местоположение этих DLL, вероятно, является одной из самых типичных ошибок, допускаемых разработчиками Windows- программ. И когда такие динамические библиотеки дружно сваливаются в главный каталог Windows или в системный каталог Windows — это наивернейший признак того, что вы имеете дело с первым большим вкладом данной компании- производителя в копилку Windows-приложений. Я почти слышу возгласы БТоР°в подобных «свалок DLL» в свою защиту типа: «Эй, эта схема прекрасно Работает красно на тестовых системах, так в чем же проблема?» Все мы теперь знаем, чем плохи такие «свалки», и мне не хочется вновь напоми- текц 41 нться IT Гч ' ь о всевозможных последствиях. В конце концов, все динамические библио- помещенные в домашний каталог программы, будут прекрасно нахо- при обеих схемах поиска DLL, причем этот каталог будет первым в - еД°вательности поиска под 32-битной Windows — небольшой, но всегда ^тствуемый выигрыш в производительности. °Мещение динамических библиотек, специфичных для программы, ог имеет и другую полезную особенность: это облегчает пользователю де- лляцию вашей программы или запуск ее под разными версиями Windows, в ее
222 Как жить установленными на одной и той же машине (последнее, кстати, будет прой дить особенно часто в период миграции с одной Windows-платформ^ другую; посмотрите врезку «Размещение ваших динамических библиотек» стр. 233, в которой для вас, как для пользователя, найдется несколько по-т ных советов о том, как действовать в подобных условиях). Динамические библиотеки, специфичные для пакета программ - проблема. В принципе, у вас есть две возможности, ни одна из которых не Эт Я.' ляется полностью удовлетворительной. Во-первых, вы можете поместить в такие динамические библиотеки в специальный каталог (вроде подкаталог MSAPPS, который создает Microsoft в главном каталоге Windows для помеще ния туда некоторых внешних компонентов Word). Если вы пойдете этим путеу вам придется обеспечить какой-нибудь способ нахождения этих библиоте всеми программами вашего пакета (конфигурационная настройка программы добавление этого специального каталога в список переменной окружения PATH или создание и использование для этого специальной переменной окружена подобно тому, как С-компиляторы издавна позволяют задавать порядок поиске статических библиотек и заголовочных файлов). В любом случае, это решение окажется довольно неудобным и неэффективным прежде всего потому, что затруднит пользователю изменение конфигурации его системы и, в конечное итоге, практически не оставит ему реального выбора, куда же все-таки помес тить эти библиотеки. Кроме того, этот подход накладывает дополнительное бремя на процесс инсталляции и создает другие, более мелкие источники проблем, которые рано или поздно обязательно сработают в реальном мире. Второй путь значительно более прост и ясен, но потенциально гораздо 6о лее смертоносен: вы можете поместить эти динамические библиотеки в главны! каталог Windows. Этот подход чреват опасностями и часто приводит к «ужа стикам» из серии перекрытая динамических библиотек, о которых я говори о ИГ ,» ранее в этой главе. Наконец, мы плавно приходим к вопросу о глобальных DLL, которые определению должны помещаться либо в главный, либо в системный катало Windows. Как и во многих других ситуациях Windows-программирования должны быть максимально осмотрительны, когда дело доходит до размеШ^ файлов здесь: вы приступаете к игре с основным компонентом конфигураЦ11' системы пользователя и должны двигаться очень осторожно. Важно все^ проявлять особую заботу при замене существующих динамических библией Убедитесь не только в том, что заменяемый файл не является более новым по возможности, и в сохранении совместимости. Я встречал много намических библиотек, более новые версии которых оказывались несовМесТ мыми с теми программами, которые ориентировались на старые версии эТ DLL. Эта проблема является как никогда более серьезной именно в случае г1 бальных динамических библиотек, поскольку в этой ситуации по определи речь идет о замене ресурса, свободно доступного всей системе. Д1'
' 1 паз умного применения линамических библиотек 223 Помещение глобальных динамических библиотек в домашний каталог программы — это почти настолько же плохая идея, как и помещение специфичных для программы библиотек в общие каталоги Windows. По каким-то непонятным мне причинам, библиотека времени исполнения от Visual Basic (VBRUN300.DLL) постоянно обнаруживается мною в самых странных местах. Я часто нахожу на компьютерах своих друзей и клиентов по несколько копий этой динамической библиотеки, причем каждая такая копия отъедает примерно 400 Кб на жестком диске. Новые времена Windows 95 привносит новые детали в этот колоритный пейзаж. Прежде всего, в реестре появился новый ключ под названием Арр Paths, который вы можете использовать для указания одного или нескольких добавлений к значению переменной окружения PATH вашей программы. Этот ключ — почти идеальное место для указания местоположения динамических библиотек, специфичных для программы. Например, если имя вашей программы FRED.EXE, соответствующая запись в реестре могла бы выглядить примерно так же, как и в моем тестовом варианте, изображенном на рис. 6.1. Обратите ваше внимание на то, что значение по умолчанию для данного ключа совпадает с полным именем приложения (Windows 95 изменит это значение автоматически, если вы переместите или переименуете приложение). &■ В«ш51(> ЫШ i:;Vkw £$р'"'''. Z + ■ +; ^J Shared Tools Windows ::y.) CurrenlVersion -: ^-J App Paths U BACKUP.EXE CCDIALE R EXE ENGCU.EXE EXCHNG32.EXE EXPL0RER.EXE fred. exe FTMCLEXE GUIDE.EXE M0SCP.EXE MSPAINT EXE SIGNUP EXE U WORD PAD EXE „J Applets _77p ■M .^(Default) *$Pahj .'.^"«.V?-:-: щ*щ ^->v.-_ -v.y,_... ■ ■ ■ .■■■■ ■ ^J Controls Folder ^J D ebugO b|ectR PCE nabled Detect ■ ■ ■ ■ ■ в в в i 111 <-!+ + + ■■ + ■ -1*1 в hi h< "cAternp\fred.eKe" "СЛТЕМР" i И f f | M iVi f'l ■ ■MMi'i лл ■ ■ | p' т |f ■V- ■"h->"-Jrr"»L-i- ■■■ ■■ d I i.l I вв.ввв-ввв-r-l-i bTb'-I i.b i в ' '■'■_■_■_■ в в.в m't +" в -г + в""-"-в" -m Л^Цв^и VbN'b^ ■w.i.b.I.b.b.b * -г.t.I- "-.г.в. ■ +. '.■"♦"■■'V.TJ .V >4-*l^-> J4.m x -i и.в.1 "■-VA В В В -В В ■ I I 1.4. i]4_4 Рис. 6.1. Настройка Арр Paths с одним каталогом. В дЬ1в Се ЭТо работает так: когда Windows 95 запускает FRED.EXE, она загля- в реестр, находит данный ключ и добавляет каталог «c:\temp» в начало
224 Как жить с текущего значения переменной окружения PATH. Это, в свою очередь, дае шей программе возможность найти свои динамические библиотеки, даже они находятся в специальном месте, а не в главном или системном катал Windows, и не в каком-то другом каталоге, указанном в стандартном значе PATH в вашей системе. Microsoft предлагает вам • помещать динамические библиотеки вац; программы в какой-нибудь подкаталог стандартного каталога Program Fik также устанавливать для вашей программы ее собственный системный ката> под этим каталогом. Последним шагом, конечно же, должно быть соотве ствующее изменение в реестре, чтобы Windows 95 узнала о том, что вы толь о что сделали, и могла следить за остальной частью сделки. Что касается динамических библиотек, специфичных для пакета програ^ ситуация немного упростилась, и вы можете решать ее, используя все тот> ключ Арр Paths в реестре. Microsoft предлагает вам создать для вашего паке] отдельный подкаталог в каталоге Common Files: С \Program Files Common File s Fred Files а затем добавить этот каталог в ключ реестра Арр Paths вашей программы так, как показано на рис. 6.2. ftqiettv E&tat I. bill' J- mm ■■■■■■-■'■■■■■■■■■■'■■'■■■■■■■■■■■" —■ ^ CuirentVersion т D *PP Path-. ■Ll bAlMJK -Xt _] CCDIALER.EXE _J EN3CU EXE £j EXCHNG32.E/E EXPLORER EXE fred ene 1Я _J FTMCL FXE lj GUIDE EXE МОьиР. EXE rj} MSPAiNT.EXE bIL-NUKLA UJ V ORDPAD EXE AppbU Control Folded D ebug J bjeciR PCE nat^d D elect explore Extensions ■ J I— I J Л Л Л Л Ш Л Л Л 4 .Напчг: ■ ■ ■ ■ bvvvv ■ 4 v v v ■ BTVTrr ^ (Defaultl Path . .'. . _ Л ^«AwhJ H.'.H.I.i " I.M' IN M P Mt P.VVt ■ P*t " " " "."." ".".'.■.»" '.РМ >.l I 'j >' 'J 1.H 4 I > Ш И * 1 >Ml>tlM Г ■■■ ■ F1 - ^.".v.-.-v-.v^.,.. -■ (■■■f rillMllirill ■■■■ ■■■ h b, ., "ftv*ii. rid ■ ►VY "D\TEMP\FRED2.EXE" "DMEMPxAPrDgram Fi1e$\Commcn F leiVFred Files **. - -r r -fr jt_- -*kP V ~ *_^ i ■ *» Ь i ■ ■ ■ 4 . BIF ^ 40 A f> «■ on Ли*: Еа№£ШЕЯ1& F- J -- III! ■ _■■■■ 14 l|H*aa**d*4A*fc* M^***»**M***^* 4WM*MMf " ^ - Рис. 6.2. Настройка Арр Paths с двумя каталоге
3^jU^^ применения линамических библиотек 225 ■ iiiiM<iieiiiinnHiHMMMHHiHiHif#iHiftHH>ttHj^tMHt<iw^w*wwwiifH<MMMiMnttMnHMHHHMM< iHHmH iwHiHiniMMM fletftHy- Edit Sfw» +; -J MS DOSOpbons Nel work Name Л Ш Л I _ _ I ■ £Й С. \Progiem Fies\Syman ее \sym evntl dH ' '2 ii-iii Run :_J RunOnce ..„ j RunSi trvcesOnce _J Set i SharedDLL ;,_J ShettScrap ■J Time Zones UnnstaH £*3 С \Progiam Ffles\Symantec\sym glosc hip ' '1 ЙЗ CAProgram Fles\Symantec\symkrnl8 dll ££j С \Program Ftfes\Symantec\sym krnl8 vxd ' "21 *Я С. \Program Fites\Symantec\tkkfc168 dH *-""-■ + _J Symantec *■ .J System - _j HKEY_USERS # Cj HKEY_CURRENT_CONFIG Ф _J HKEY_DYN_DATA 0#a tt^H ■ ■-■"tall 7 ъч+ыч 2 £& С \Program F-tes\Symantec\tkkt328 * Й CAProgram F les\Symantec\ueclib oil ll^ll * i чм 2 »£j С \WI N D OWS \SYS ТЕМ \mtc30 dll 01 00 00 00 $3 C: \WI N D OWS \S YS ТЕМ \mf cans32 dll 01 00 00 00 01000000 01000000 01 00 00 00 01000000 01 00 00 00 l>4> С \WI N D OWS \SYS ТЕМ Wcd30 dll Щ С \WI N D 0WS\SYS ТЕМ \mlcn30 dH Щ C\WI N D OWS \SYS ТЕМ \mf соЗО dll Щ С \WI N D OWS \SYS ТЕМ \mfcuia32 dll t$ C: \WI N D OWS \SYS ТЕМ Uhreed vbx *"*"Л Ш III it -■J. 4MriM№4i **fab*4A* *«*M^4**-** Рис. 6.З. Строковые счетчики Обратите внимание на то, как работает настройка Арр Paths. Windows 95 добавляет значение этого ключа в начало списка, содержащегося в переменной окружения PATH, копия которой затем передается вашей программе. Вы помните, на каком месте стояло упоминание PATH в ответе на первый блиц-вопрос данной главы? Для Win32 оно было пятым по счету или даже шестым, если в системе существует 32-разрядный системный каталог Windows. Иными словами, поиск доходит до каталогов, указанных в Арр Paths только в том случае, если ничего не было найдено ни в домашнем каталоге программы, ни в текущем каталоге, ни в системном каталоге Windows, ни в главном каталоге Windows. А это означает, что прием с Арр Path решает лишь часть обсуждаемой проблемы — обеспечивает то, что программа сможет найти свои динамические библиотеки (я называю это проблемой «ложного отрицания»). Но этот прием никак не помогает в решении другой, возможно более серьезной, части проблемы нахождения и загрузки не тех динамических библиотек, Реест НамНч к которые на самом деле нужны (проблема «ложного подтверждения»). Я думаю, было бы гораздо более осмысленным поместить каталоги, указанные в Арр Paths, в самое начало последовательности поиска, на место с номером ноль. И тогда ваша программа могла бы всегда и совершенно безопасно находить именно свои динамические библиотеки, помещая их именно в каталоги, указанные в Арр Paths. Таким образом, этот прием успешно решил бы обе части проблемы — и ложное отрицание, и ложное подтверждение. Что ж, может быть в Windows 98?. глобальных файлов также предусмотрена специальная обработка в Ре- Всякий раз, когда вам действительно нужно инсталлировать ди- Для ее кую библиотеку, которая должна быть глобально доступна широкому пРограмм, предпочтительный в Windows 95 метод (согласно Microsoft)
226 Как жить заключается в размещении ее в системном каталоге Windows и одновремек добавлением в реестр ссылки на эту DLL так, как показано на рис. с стр. 225. Значением такого ключа в реестре является так называемый «счето количества использования» глобальной DLL, который должен увеличиват" на единицу каждый раз, когда эта DLL нужна очередной инсталлируй программе (то есть когда сама DLL уже установлена и ссылка на нее у^е ществует в реестре). Все это выглядит достаточно хорошо задуманным, и на практике это ствительно работает нормально, по крайней мере если говорить о 32-разрядн программах. Если мои слова звучат так, как будто бы я даю слишком лончивую оценку, то это правда. Я действительно не хотел бы спешить окончательным приговором для всех этих нововведений. Как я уже говори ь ранее, динамические библиотеки являются классическим примером «Tt о пличных создании», а это означает, что они подвержены неправильному их пониманию и неправильному их использованию больше, чем другие возможное! Windows. На момент написания этого раздела я своими глазами видел всег только одну марку программного обеспечения, использующую эти новые воз можности. И даже если большинство производителей программное обеспечения начнет использовать их завтра, разве достаточно одного лиш этого для того, чтобы авторы инсталляционных программ вдруг стали болееде ликатными, начали грамотнее размещать динамические библиотеки и перестал! портить жизнь другим программам? Выступая в роли пользователя Windows причем такого пользователя, который в течение последних двух месяцев ие сталлировал и деинсталлировал несколько дюжин новых программных продук тов и этим жестоко истерзал конфигурации четырех компьютеров, я конечн( же хотел бы надеяться на это. Но я смогу обоснованно судить об этом тол№ после того, как получу шанс инсталлировать еще пару дюжин программ, р' полностью осведомленных о новых возможностях Windows 95. А до тех пор* лучше подержу за спиной скрещенные пальцы. (Посмотрите врезь «Стандарты для слабаков?» на стр. 234, посвященную нескольким новы* несчастьям при инсталляциях, которые всплыли уже почти после того, как закончил эту главу. Кажется, мой первоначальный пессимизм был вполне обос нованным.) Помимо всего прочего, при использовании ключа Арр Paths есть еще оД1, нюанс, касающийся 16-разрядных программ. Экспериментируя с этим клю1*0' я обнаружил следующую ошибку (или, по крайней мере, абсолютно неудов при работе аномалию). Я указал только путь «c:\temp» в Арр Paths Д FRED.EXE и поместил BARNEY.DLL в этот каталог (для повторения эТ° теста вы можете использовать уже предлагавшиеся ранее файлы с этими нами). Когда я запустил FRED.EXE, Windows 95 пожаловалась, что не с^° найти BARNEY.DLL. Тогда я модифицировал FRED.EXE так, чтобы пр11ь пуске она показывала значение переменной PATH из своего окружения, п° 0
применения линамических библиотек 227 Windows 95, возможно, вообще никак не использует настройку Арр ra\ и запуске 16-разрядных программ. Но к моему удивлению я оказался ?№ -«amV ГГПТТПЧПРНГТСТУ* ТГЯТЯ 7ТПГ <$гГ*\ fpm П* И R Г.ЯМПМ П&П& ПТГЯЧЯ ПГЯ ГЯМКТТМ не прав первым ГТВЙЮ| своих подозрениях: каталог «c:\temp» и в самом деле оказался самым в списке PATH. Следуя мгновенно возникшему нехорошему предчув- я загружала тало еще модифицировал FRED.EXE так, чтобы эта программа явно BARNEY.DLL при помощи LoadLibraryO. И после этого все зарабо- как часы. Стало очевидным, что при запуске 16-разрядных программ iv'ndows 95 осуществляет динамическое связывание неявно загружаемых DLL того, как модифицирует PATH в соответствии с настройками Арр Paths. Я сам не исследовал тот код, который Windows 95 использует для загрузки 16-разрядных исполняемых файлов, однако я читал в книге «Windows Internals» (Мэтт Питрек) описание работы функции LoadModuleO, которая занимается этим в Windows 3.0 и Windows 3.1. После этого прочтения волосы стали дыбом на моей голове, и я теперь очень искренне надеюсь, что Microsoft заменила (или, на худой конец, хотя бы серьезно переработала) этот код. Интересно, какова причина того, что ключ Арр Paths прекрасно срабатывает для 32-разрядных программ, но не слишком корректно работает для 16-разрядных? Мне кажется, что в наши дни, когда Советский Союз практически стерся из памяти, большинство советологов переключилось на чтение мыслей Microsoft и IBM, причем делают они это примерно теми же самыми методами. (Я думаю, что все это является бесполезной тратой времени, хотя и довольно подходящим занятием для программистов во время распития Jolt Cola и поедания Doritos.) Поэтому у меня нет сомнений, что проблема «странных» взаимоотношений между Арр Paths и 16- разрядными программами непременно приведет некоторых людей к приступам негодования и жалоб: вот очередное доказательство того, что Microsoft пытается «заставить» разработчиков программного обеспечения забросить 16-разрядные платформы. Такое возможно, но, на мой взгляд, маловероятно. Я сильно подозреваю, что данная конкретная проблема является ничем иным, как закономерным следствием приоритетов Microsoft при разработке Windows 95: именно поддержка 32-разрядных приложений была приоритетом номер один, и поэтому взаимодействию новых возможностей с 16-разрядными программами наверняка не было уделено достаточно времени на тестирование и отладку. В результате обсуждаемая проблема проскользнула в эту брешь и создала программистам еще одну интересную особенность, о которой придется побеспокоиться. 3isrl>ytkaQme все DLI явно Что *анДартный, неявный способ динамической компоновки подразумевает, Up mdows будет искать и загружать требуемую динамическую библиотеку в Ссе запуска вашей программы. И если динамическая библиотека не будет ваша программа так и не получит управления, а пользователь увидит най Аена
228 Как жить с лишь скудное сообщение, в котором не будет никакой полезной инфор^а щ о ваше кроме имени ненайденного файла. Для многих новых пользователей Wind такое знакомство со сложностями подстерегающих их конфигурадион проблем является по меньшей мере грубым и наверняка не внушит им люби той программе, которую они только что безуспешно попытались запуск Было бы намного лучше, если бы ваша программа сама обнаружила недоСт, ность нужной ей динамической библиотеки, выдала на экран свое собствен! сообщение и даже предложила пользователю кнопку для получения дополн тельных подробных разъяснений (например, прямо в справочном файле программы). Такой подход решает сразу две важные задачи: во-первых, ваши пользов тели смогут получить немедленные ответы на самые очевидные вопрос которые у них появятся в такой ситуации, а во-вторых, у самой ваше программы появится шанс изящно сократить свою функциональность в отер ствие той или иной динамргческой библиотеки. Такое сокращение функционал ности может быть совершенно простым, например, таким, как запрещенц почтовых функций вашей программы в отсутствие MAPI.DLL, или работа бе глобальных горячих клавиш в отсутствие DLL, отвечающей за это (именнота поступает моя программа Stickiest, когда не может загрузить STKB.DLL) если ваша программа pi в самом деле может совершать осмысленную работу бе какой-то из своих динамическргх библиотек, пользователь очень часто буде благодарен за предоставленную возможность выбора — продолжить работу: условиях сокращенного набора функций или прекратить работу. В случае не явной загрузкрг динамргческргх библрютек такого выбора нет pi быть не может - это классический пример ситуацрш типа «либо все, либо ничего», когда отсут ствие даже несущественной дршамической библиотеки не позволит работа! о о всей вашей программе. Даже если все динамические библиотеки, которые использует ваш программа, являются жрюненно необходимымр! для ее работы, все равно ва1 следует загружать их явно. Хотя бы для того, чтобы в ответ на невозможное загрузить какую-то из них вы могли выдать пользователю дртлоговое окно кнопками YES/NO и примерно таким сообщением: «Сожалею, программа Fi'e( Calc не может работать в отсутствие BIGNUMS.DLL. Вам нужна дополните ная помощь?» И когда пользователь нажмет кнопку YES, выдайте хотя одну страничку Pi3 справочного файла вашей программы, которая объясня бы, что такое BIGNUMS.DLL, и зачем она нужна. Еще одна важная деталь: ршея полный контроль за ерпуацией при яв^ загрузке дршамической библиотеки, ваша программа может также произве° проверку версии успешно найденной DLL, pi в случае чего-лргбо отказа^ работать (разумеется, внятно сообщив об этом пользователю), л\ скорректировать свою дальнейшую работу в соответствии с определен*1 версией DLL.
^ja^SLUh применения линамических библиотек 229 Помните, что проверка версии ваших собственных динамических библиотек вовсе не обязана заключаться в таких относительно утомительных процедурах, как использование ресурсов типа VERSIONINFO или проверка размеров и дат файлов. Все может быть намного проще: просто добавьте в вашу DLL функцию GetVersionO, которая возвращает мажорный и минорный номера версии. Затем, когда ваша программа загрузит эту динамическую библиотеку, она сможет получить адрес функции GetVersionO, вызвать ее и в зависимости от полученных значений поступить так или иначе. Можно поступить еще проще, если хотите. Создайте в вашей DLL ничего не делающие функции с именами типа IsVerlO, IsVer2() и т. д. И тогда ваша программа сможет судить об истинной версии только что загруженной DLL уже по тому, насколько успешно она может определить адрес соответствующей функции из этого набора. В любом случае, определив истинную версию загруженной динамической библиотеки, ваша программа сможет совершать условное связывание с остальными функциями этой DLL. To есть ваша программа может быть сделана существенно более гибкой, вплоть до способности успешно работать с предыдущими версиями своих компонентов. Очевидно, невозможно всегда загружать все динамические библиотеки явно. Например, программы, написанные при помощи Visual Basic, загружают свою динамическую библиотеку времени исполнения неявно, до того, как их код получит управление. Точно также обстоит дело с программами, написанными на C++ и использующими динамическую версию библиотеки MFC. И все же, всякий раз когда ваша программа (неважно, на каком языке она написана) использует свои собственные, специализированные динамические библиотеки, вы должны следовать духу вышеописанных рекомендаций и обеспечивать вашим пользователям осмысленные подсказки и варианты дальнейших действий в отсутствие этих DLL. Как и в случаях с другими советами, вы должны применять этот совет Разумно. Прежде всего, я загружал бы (и загружаю) явно только динамические иолиотеки, специфичные для моей программы, и не делал бы этого с системой компонентами, которые необходимы для работы самой Windows и, сле- вательно, просто обязаны находиться в положенном месте. Это вовсе не что все глобальные динамические библиотеки могут смело загружаться значит н°. Например, в той или иной ситеме могут отсутствовать такие глобаль- " библиотеки, как MAPI.DLL, и вашей программе, использующей их, все- следует элегантно реагировать на их отсутствие. w B заключение, еще одна немаловажная рекомендация: пожалуйста, с а РШайте загрузку всех необходимых динамических библиотек Раньще как можно Не те в ходе выполнения вашей программы. Я видел много коммерческих и дкч М^РЧеских программ, которые успешно стартовали и позволяли пользова- айти довольно далеко в своих действиях, прежде чем обнаруживали, что
230 Как жить та или иная функция недоступна в связи с отсутствием соответствующе намической библиотеки. При этом даже тот факт, что они вежливо ставг пользователя в известность об этом, уже был не так полезен: как прав^, выбора к этому моменту уже не оставалось — пользователю приходи т, просто согласиться с отказом программы выполнить то или иное действие т о С"» кои вариант поведения программы-крайне неинтеллигентен, и программист ответственных за принятие такого подхода, я бы наказывал — заставлял бы дельку поработать без клавиши ENTER. Правильный же подход прост: лайте так, чтобы ваша программа явно загружала все динамические библцг теки, которые ей могут понадобиться, в самом начале своей работы и тут,; выдавала пользователю все необходимые сообщения, подсказки и вопросы, чтобы остальной код уже об этом не беспокоился. /г- Т2 §Ш$о t В качестве демонстрационной реализации этикета применения DLL, описанно го в данной главе, я предлагаю вам посмотреть на безумно простую дина мическую библиотеку HELLO.DLL и 32-разрядное консольное приложени RUNHELLO.EXE, которое просто явно загружает вышеупомянутую DLL и вы зывает несколько ее функций. С точки зрения структуры вызывающе, программы, наилучшим решением я считаю использование отдельной ш терфейсной единицы компиляции, которая служит абстрактной прослойкой ме жду самой программой и явно загружаемой динамической библиотекой. I данном примере такой абстрактной прослойкой служит модуль HELLOINK исходный текст которого приведен в листинге 6.1. Обратите внимание нас. ле дующие детали: Функция LoadHello() возвращает единственное значение типа BO0L которое является ответом на самый животрепещущий (для вызь вающего кода) вопрос: «Загружена ли DLL?». Заметьте, что вопрс вовсе не ставится в виде «Загружена ли DLL только что?» В том-то дело, что LoadHelloO вначале проверяет значение анкерной перемег ной hHello и возвращает TRUE, если эта переменная имеет ненул^с значение. Это позволяет дать вызывающему коду разумный отве даже если по случайности функция LoadHelloO была вызвана ^ сколько раз подряд. (Как всегда, оборонительное программирован1 включает в себя предвидение наиболее вероятных ошибок и устра'1 ние всех вариантов их появления.) Функция LoadHelloO является весьма подходящим местом, где вЫ**' жете сконцентрировать код, отвечающий за работу с предыдУ11111 версиями данной динамической библиотеки. Например, вы моглП менить имена каких-либо экспортируемых функций в послеД1
f\p^eb 231 в версии DLL. И тогда именно в этой функции вы можете обеспечить совместимость новой версии RUNHELLO.EXE со старыми версиями HELLO.DLL очень просто — путем корректировки соответствия между именами функций и соответствующими им анкерными переменными. Вообще говоря, вы можете завести анкеры и функции-шлюзы, которые буду выполнять целые последовательности вызовов истинных функций DLL в ответ на одиночный вызов из главной программы. Таким образом, интерфейсный модуль (в данном примере — HELLOINT.C) является не просто примитивным коммутатором, вместилищем анкеров и функций-шлюзов, а по сути может служить еще одним, дополнительным уровнем абстракции для общения вашей программы с динамической библиотекой. В функция UnloadHelloO проверяет анкерную переменную DLL и выгружает динамическую библиотеку только в том случае, если она действительно была загружена. При этом она всегда обнуляет анкерные переменные и тем самым обеспечивает перевод их в строго определенное состояние. б Функция GotHello() обеспечивает простой и быстрый способ проверки присутствия динамической библиотеки, который может использоваться в любой момент работы основной программы. Например, результат, возвращаемый этой функцией, может использоваться для условного добавления или удаления пунктов меню основной программы, для изменения режимов ее работы и т. п. И Доступ ко всем функциям динамической библиотеки осуществляется через функции-шлюзы (SayHello, GoBeep), которые всегда проверяют, загружена ли DLL. Именно такой подход (инкапсуляция «.» о в отдельный модуль и скрытие от вызывающей программы всех анкерных переменных для функций DLL) позволяет достичь максимально безопасного протокола общения основной программы с динамической библиотекой. Вам следует позаботиться о том, чтобы 16-разрядная программа всегда выгружала динамическую библиотеку (в данном примере — вызывала UnloadHello) перед окончанием своей работы. Это необходимо для уменьшения «счетчика количества использования» этой DLL. И хотя Для 32-разрядной программы Windows может сделать это автоматически (если программа не сделала это сама), более мудрым решением было бы обеспечение вызова UnloadHello вне зависимости от типа в^шей программы. Расчет на то, что операционная система соберет за вас ваше грязное белье для стирки, всегда считался плохой практикой (за исключением экстремальных ситуаций, разумеется).
Как жить с д •• л ** о Построение примера: RUNHELLO и Нйц* Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chap06/heii Платформа: приложение для Win32 (RUNHELLO) и дИн, мическая библиотека для Win32 (HELLO) Инструкции по сборке: откройте при помощи Visual C++ 2^ прилагающиеся МАК-файлы и скомпилируйте, как обычн (сначала HELLO, потом RUNHELLO). Листинг 6.1. HELLOINT.C /* Copyright Lou Grinzo 1995 Zen of Windows 95 Programming */ #mclude <wmdows h> #mclude "hellomt. h" typedef mt (*pfnHello)(LPCSTR IpszText, LPCSTR IpszTitle, UINT fuStyle); typedef void (*pfnBeep)(void), typedef int (*pfn\/ersion)(void); // Анкеры подпрограмм, расположенных в DLL. tatic pfnHello HelloAnchor = NULL; static pfnBeep BeepAnchor = NULL, static pfnVersion VersionAnchor = NULL, // Анкер для самой DLL. static HINSTANCE hHello = NULL; // Загрузить DLL и установить связи с необходимыми подпрограммами BOOL LoadHello(void) г» о { if(hHello '= NULL) return FALSE, if((hHello = LoadLibrary('hello dll")) ■ = NULL) { if (((HelloAnchor = (pfnHello) GetProcAddress(hHello,"SayHello")) == NULL) ((BeepAnchor = (pfnBeep) GetProcAddress(hHello,"GoBeep")) == NULL) ((VersionAnchor = (pfnVersion) GetProcAddress(hHello,"Version")) == NULL)) { UnloadHelloO; return FALSE; } else { UnloadHelloO, return FALSE; } return TRUE, } // Выгрузить DLL и должным образом установить значения всех анкеров void UnloadHello(void) {
ueo: Hell° 1f(GotHello()) FreeLibrary(hHello), hHello = NULL; HelloAnchor = NULL, BeepAnchor = NULL; VersionAnchor = NULL, И Загружена ли DLL вместе со всеми необходимыми функциями9 BOOL GotHello(void) { return (HelloAnchor ' = NULL), 233 // Шлюз для нашего самозванного варианта MessageBox int SayHello(LPCSTR IpszText, LPCSTR IpszTitle, UINT fuStyle) { if(GotHelloO) { HelloAnchor(lpszText,lpszTitle, fuStyle), return 1. else return 0, } // Шлюз для нашего самозванного варианта MessageBeep void GoBeep(void) { lf(GotHelloO) BeepAnchor(), // Шлюз для функции - определителя версии int HelloVersion(void) { if(GotHello()) return VersionAnchor(); else return -1; } Ничто из того, что представлено в этом примере, не является высоким [но-техническим достижением, и я уже представляю себе, как некоторые читатели скажут: «Тоже мне пример. Ничего нового». И тикет применения DLL отнюдь не требует больших ус разработки или (о, ужас!) покупать инамические библиотеки. При этом он значительно улучшит полезность и Рактичность ваших программ, особенно в глазах новых пользователей Win- Размещение ваших динамических библиотек Если вы держите на вашем компьютере несколько разных версий Windows, вы можете облегчить себе жизнь, используя тот
234 * ■.-j*" й* ^* *£1Ё& i ' ?.' .-• £ •* '%*/•>*- < л ^ / / У А ',/- ••Л ;•'• * ЛГ .• , лг * ** г А ' v<*' / г-?- >л> &02?' - \ •• *. *v > „ > л •• %' ч -. •« •* •V % v <л •' -« *" -- --.^-х /Са/с ж##ть с ни факт, что перечисленные в переменной окружения PATH катал всегда просматриваются при поиске динамических библиотек сути, вы можете просто сымитировать то, как при пом0 переменной среды LIBPATH в OS/2 задаются пути для поиг DLL. Вначале создайте в вашей системе новый каталог, напри^ с именем C:\DLLS и переместите туда столько динамических бдо лиотек из главного и системного каталогов Windows, сколько жете. Мой каталог C:\DLLS содержит огромное число мических библиотек от разных приложений, плюс общие смо Дина Дина версии мические библиотеки (такие, как BWCC.DLL и три библиотеки времени исполнения от Visual Basic). Практически только библиотеки, являющиеся частью самой Windows, и состав ляют то немногочисленное сообщество DLL, которые не помещены мною в C:\DLLS. Далее, сделайте так, чтобы каталог C:\DLLS был упомянут в переменной окружения PATH для всех версий Windows, установленных в вашей системе. Разумеется, этот трюк далеко недостаточен для достижения нирваны при работе с несколькими версиями Windows: вам по- прежнему придется беспокоиться об INI-файлах, реестре, о перемещении в ваш DLL-заповедник динамических библиотек от новых инсталлированных программ и т. д. Однако, этот подход несомненно устранит часть ваших забот и головных болей. Стандарты для слабаНоъ! Как я уже упоминал в этой главе, когда вы инсталлируй вашу программу, предполагается, что вы должны увеличивать значение ключа SharedDLLs в реестре для каждой глобальной ДО* намической библиотеки, которая используется вашей программой Это правило кажется до смешного тривиальным, но только дот$ пор, пока вы не обнаружите, что имеет место некоторая неопреДе' ленность в отношении типа данных, который должны иМеТЬ значения этих ключей. Я обнаружил это, когда инсталлировал финальную ве рс# Norton Navigator от Symantec и вдруг заметил, что она установи строковые значения счетчиков в SharedDLLs для всех своих Д*1*1 мических библиотек — весьма странный выбор типа для счетчй^ > неправда ли? Кроме того, я заметил, что счетчики для некотор** общих файлов, установленные самой Windows 95, имеют № нарный числовой формат. Посмотрите на рис. 6.3 на стр. 225, ^
лр0- Hello 235 / / •• / / -• / ^ <• изображен «снимок» экрана, демонстрирующий всю эту занятную смесь. Я обратился за разъяснениями в Microsoft, и некто из группы поддержки разработчиков ответил мне, что я должен создавать новые ключи в формате REG_DWORD (а не в формате REG_BINARY). И еще он добавил, что при необходимости увеличить счетчик моя программа должна быть готова встретить строковое значение и произвести соответствующие конвертации. Вывод? Поздравим себя: начиная с того самого дня, когда Windows 95 вышла в свет (24 августа 1995 года), мы получили еще одну бессмысленную проблему при инсталляции, о которой нам придется побеспокоиться. И все мы знаем, что произойдет «.» дальше: как только достаточное количество людей станет запускать Norton Navigator и другие программы, которые совершают %ф£%'/"' слишком творческие манипуляции с этими счетчиками, инсталляционные программы, ожидающие определенного типа данных от этих счетчиков, начнут ошибаться — они будут либо неспособны наращивать значения этих счетчиков, либо вообще будут перезаписывать старые значения новыми (скорее всего, единицей), тем самым напрочь уничтожая ту полезную для кого-то информацию, которую несли в себе эти старые значения. В конце концов, все эти счетчики начнут казаться бесполезными, и разработчики просто- напросто перестанут их использовать. Правда, как мы вскоре увидим, такое развитие событий вовсе не было бы таким уж плохим делом в конечном итоге (см. главу 14, где я рассказываю о считывании значений из реестра и сюрпризах, связанных с их типами данных). Из того, что мне ответил по этому поводу какой-то из сотрудников Microsoft, следовало, что у него самого нет четкого представления о том, какой же все-таки тип данных следует использовать для этих счетчиков. Я проверил все стандартные справочники, доступные мне на момент написания этой главы, и также не смог найти никакого ответа (хотя я совершенно уверен, что строковый тип не должен стоять тут на первом месте). В поисках ответа на мой вопрос касательно типов данных я наткнулся на некий фрагмент в документации по Win32 SDK (тема «Adding Entries to the Registry», ее можно найти по образцу текста «shareddlls»), который отвечал на другой мой вопрос — а именно на вопрос, зачем нужны эти счетчики. Тем не менее, я считаю весьма уместным дословно процитировать этот фрагмент именно здесь:
236 •* * ? •• •* / *V / •* Как жить «Ваша инсталляционная программа должна следить за сов стно используемыми динамическими библиотеками. При инст4 ляции программы, использующей такую динамическую библии ку, инсталляционная программа должна увеличить на едийй счетчик использования этой DLL в реестре. При деинсталлящ программы, этот счетчик должен уменьшаться на единицу. £с после уменьшении счетчик обнуляется, пользователю должна бьт предложена возможность удалить динамическую библиотеку, п следует предупредить что на самом деле эта библиотека все еще может быть нужна другим приложениям, и что ном приложения могут перестать работать после ее удаления Иными словами, Microsoft говорит нам, что именно наш [ляционные программы будут отвечать за политику раз] совместно используемых динамических библиотек в cv каталоге Windows, и что именно наши деинсталляпис ин программы ответственны за предоставление пользователю возможности удалять те файлы, счетчик использования которых достигает нуля. Все, крыша поехала. Как только несколько миллионов людей будут использовать Windows 95, как вы думаете, насколько часто программы будут использовать общие динамические библиотеки, которые имеют «-f ужасно маленькое значение своего счетчика использовании в реестре? Подсказка: соберите несколько программ, использующих MFC30.DLL, и проверьте, изменяется ли автоматически счетчик этой общей DLL. А теперь обобщите этот пример. Как вы думаете, XJ " что произойдет, когда некая компания, создающая свои первый продукт для Windows, наивно последует букве закона в данном вопросе (буквально так, как это описано в вышеприведенной цитате) и предложит пользователю удалить MFC30.DLL при своей деинсталляции? (В конце концов, их программа совершенно легально использует MFC30.DLL, они деинсталлируют свою программу, уменьшают счетчик в реестре, и счетчик обнуляется Предполагается, что теперь именно их деинсталляционная программа должна предоставить пользователю возможность уДа' лить MFC30.DLL, правильно? И если пользователь даст разрешение, то так тому и быть.) Так не звучит ли вышеприведенная данном # с» о тата как свидетельство явной недоваренности дизайна в вопросе или как описание катастрофы, возникновение которой всего лишь вопрос времени? Я уверен, что это именно так и зву чпт (особенно в свете того факта, что та же популярная динамически библиотека MFC30.DLL выходит на старт с единицей на св°е{ счетчике использований, то есть ставится прямо на самый кр аИ
• Hello 237 еэ 9 пропасти — на расстоянии всего лишь одной ошибки программы- деинсталлятора от смертельной опасности). Все это может показаться уже достаточным для того, чтобы я срочно задумался над заменой всех PC в моем офисе на компьютеры Мае. А лучше — над еще более решительным шагом: бросить все к черту и уехать вместе с Лиз в Гренландию, желательно до того момента, как наступит 2000 год, и все программы на COBOL сойдут с ума в один день. Но это уже вопрос для обсуждения в другом месте и в другое время.
s To see a World in a Grain of Sand, And Heaven in a Wild Flower, Hold Infinity in the palm of your hand, And Eternity in an hour. Уильям Блейк Внимательно приглядитесь к инструментам разработки и книгам по программированию для Windows, и вряд ли вам придет в голову мысль, что Windows- программы могут работать в минимизированном виде, или что может существовать хоть какой-то смысл, польза в таких программах. Аналогично, многие пользователи Windows считают, что минимизация программ — это всего лишь сподруЧный способ убрать программу с глаз долой, чтобы переключиться на Работу с другой программой (то есть просто еще один способ жонглирования z- ПоРядком окон в Windows). Все это представляется мне довольно загадочным, потому что я уверен, что Минимизированные программы бывают достаточно гибкими и полезными. В со- °кУпности с популярной и естественной в Windows метафорой drag-and-drop, ""Химизированная программа образует весьма мощную комбинацию, которая ^Дставляется мне своего рода Windows-аналогом программ-фильтров в DOS. Сли вьг не знакомы с этим жанром, я поясню: программа-фильтр — это, как вило, простая в использовании утилита, которая запускается из командной Р°ки и использует возможности ОС по перенаправлению ввода-вывода для -^ Ия Каких-либо данных с клавиатуры или из файла, некоторой обработки Данных и затем вывода результатов на экран или в выходной файл. гРДммы такого типа существуют с давних времен и являются идеальным
240 Минимизированные Windows-программы: вселенная внутри инструментом для таких задач, как трансляция символов «перевод строки текстовых файлах в пары символов «возврат каретки»/«перевод строк перевод символов в верхний регистр, замена во всех документах имени ват бывшей жены на имя вашей нынешней супруги, и так далее. Практике ' любое преобразование файла, осуществимое за один проход, являет отличным кандидатом для создания соответствующей программы-фильтра. М, нимизированные Windows-программы, конечно же, далеко не так ограничен'• теми типами задач, которые они могут решать, но, благодаря своей литарной ориентации, они близки к программам-фильтрам с философское точки зрения.) Вы увидите, что минимизированные программы являются еще одним типом всех этих интереснейших закоулков Windows, которые есть смысл исследовать и использовать. Воистину, каждая минимизированная программа может представлять собой целую Вселенную, заключенную в значке. Ути- Большим сюрпризом для многих пользователей Windows может оказаться тот факт, что многие приложения, распространяемые сегодня, находясь в минимизированном состоянии, прекрасно воспринимают drag-and-drop (перетаскивание и сбрасывание файлов мышью). Например, запустите редактор Microsoft Word, минимизируйте его а затем перетащите из Проводника (или из Диспетчера файлов в Windows 3.1) Word-документ и бросьте его на значок. (При работе в Windows 95 это потребует выполнения специального маневра мышью, о котором я расскажу позже.) Word загрузит брошенный на его значок файл и восстановит нормальное состояние своего окна так, что вы сможете немедленно приступить к работе с только что загруженным документом. Следует отметить, что в Windows 95 реакция раскрытого окна Word на сбрасывание в него ф^И' лов несколько изменилась по сравнению с Windows 3.1: раньше для загрузки файла было достаточно сбросить его в любом месте окна Word, теперь жеДлЯ этого надо сбрасывать его только на заголовок окна (если же вы сбросите фай-1 прямо в рабочую область раскрытого окна Word, сброшенный файл будет загружен отдельно, а вставлен в уже открытый документ). не Некоторые программы более хитроумны — даже слишком хитроумны ? на мой взгляд. Они воспримут drag-and-drop, загрузят сброшенные на их значк1 документы, но при этом вовсе не станут раскрывать свои окна или еще как^1 либо способом давать пользователю понять, что они что-то сделали в ответ #1 сбрасывание файла. Это ужасная разновидность минимизированных програ^ Я называю те программы, которые обычно используются «в откр ъгт°м виде», но будучи минимизированными воспринимают drag-and-drop, прогр аМ
ылности минимизированных программ 241 рВого типа. Создать программу этого сорта очень просто: достаточно *'J*1 - ^т тттиЛО ТТ1"\ТТ ГТГ^М/*^ТТТ>Т^ ГПЛТ,Т1*1Л\ЛНТК Г&ЛЛГМТТС*XXTJT^ri^ TJTC1 £*ГГ\ Г ГГОТЭХХГЧО Г\ТГХТГ\ rtlH^b обычное приложение принимать сброшенные на его главное окно (как вам известно, это делается при помощи вызова функции DragAc- ^VlesO с параметром TRUE), и оно будет так же успешно воспринимать '^..mrl-drop, находясь в минимизированном состоянии. Такая функциональ- , 'id-ell^*- *• 10 несомненно повышает полезность программ, но не они являются основой темой данной главы. Минимизированные программы второго типа — это такие программы, орые принимают сброшенные на них файлы и всю свою жизнь проводят в визированном состоянии. Именно эти программы так дороги pi близки мо- сердцу- Моя программа Stickles! является пред став ительницей как раз того типа минимизированных программ, хотя при своей работе она и показы- ает множество дочерних и диалоговых окон для диагностических сообщений, а данных пользователем и, наконец, собственно для отображения заметок. в ввода Механика минимизированных программ... Итак, как же программа может оказаться в минимизированном состоянии? Это несложный вопрос, но именно этот вопрос встает первым. Если обратиться к уровню API, то ваша программа оживленно работает, возбуждаемая время от времени потоками сообщений от Windows и внешнего мира вообще, и в какой- то момент она получает сообщение WM_SYSCOMMAND с wParam равным SCJVQNIMIZE. Если ваша программа не «проглатывает» это сообщение (например, она может специально отслеживать его, и при его получении просто ничего не делать и не отдавать его Windows для обработки), то стандартная реакция превратит вашу программу в кнопку-значок на панели задач (или в значок на поверхности стола, если дело происходит под Windows 3.1). Ваша иРограмма по-прежнему остается активной, но только с этого мгновения показывает миру другое свое лицо. Обычно пользователю явно видны два способа мигошизировать пР°грамму: пункт Свернуть (Minimize) в системном меню программы и кнопка •'Химизации в правом верхнем углу программы (разумеется, предполагается, Ваша программа имеет обе эти стандартные интерфейсные возможности). с самом деле, оба эти способа — всего лишь два разных фасада для одного и 0 Же механизма: доставки вашей программе системной команды ^MINIMIZE. Это очень важная деталь. Если по какой-либо причине вы (и захотите, чтобы вашу программу никогда нельзя было минимизировать Ример, если вы в душе максималист), то удаления кнопки минимизации и что с Нас *1Но^ к°манды минимизации будет отнюдь недостаточно. Если какая-либо \1{ ВаШей программы предполагает, что программа никогда не находится в зцн П13ированном состоянии, вам придется взять на себя труд по проглаты- системной команды SC_MINIMIZE и по блокировке минимизации
242 Минимизированные Windows-программы: вселенная внутри программы непосредственно при ее запуске (более подробно об этом я скажу ниже). При этом, во имя удобства пользователя, пожалуйста, не 0 ляйте на виду вышеупомянутые интерфейсные элементы — кнопку и пук ° системном меню! Иначе вы наверняка дезориентируете ваших пользователе" интерфейс будет с очевидностью свидетельствовать о наличии функции ш мизации, тогда как программа не будет реагировать адекватно. Помните Программистском Аду есть специальное место для тех, кто позволяет себе, лать такие вещи, и место это не где-нибудь, а в самом центре Страны СОВп: Когда Windows запускает вашу программу, вместе с командной строкой KJ о Е шеи программе передается еще один параметр, который указывает, в каком (г стоянии должна стартовать ваша программа: в нормальном, минимизирование или максимизированном. Обычно программы используют этот параметр д01/ ным образом, но это вовсе не обязательно. И именно для минимизирована программ второго типа существует принципиальное отличие в этом месте: он должны всегда игнорировать этот параметр и использовать значещ'. SW SHOWMINIMIZE. В зависимости от каркасной библиотеки, которую bi используете (если вы используете хоть какую-то), это достигается по-разном\> но это всегда делается в самом начале работы программы, перед тем, как ohJ создает на экране свой интерфейс. Например, при использовании MFC вымс жете присвоить значение SW_SHOWMINIMIZE члену m_nCmdShow вашек объекта-приложения прямо внутри метода InitApplicationO перед открытие1 окна или диалогового окна, которое будет служить главным интерфейсом ва KJ i г шеи программы. Несколько сложнее дело обстоит с отменой минимизации ваше программы. Как известно, минимизированная программа может быть восстг нов лена (ее окно будет возвращено в последнее неминимизированное и немаь симизированное состояние и местоположение на экране) или максимизирован. (ее окно будет развернуто во весь экран). Когда пользователь щелкает мышы по значку вашей программы на панели задач (или, если речь идет' Windows 3.1, делает двойной-щелчок мышью по значку), Windows посыле вашей программе сообщение WM_SYSCOMMAND с wParam paBHbI SC_RESTORE. Если же пользователь выбирает из системного меню пункт становить (Restore) или Развернуть (Maximize), тогда ваша прогр^1' получает системную команду SC_RESTORE или SC_MAXIMIZE соотв^. венно. Кроме того, ваша программа может получить SC_MAXIMIZE, K°r' Вое и о пользователь производит двойной щелчок мышью по заголовку окн программы. Таким образом, если вы хотите, чтобы а ваШс напрашивается очевидное и простое решение: пере\ тывать эти самые системные команды SC_RESTORE и SC_MAXIMIZE й **е давать их Windows для стандартной обработки, правильно? Да, это ^° сработать, однако есть и другой прием, о котором вы должны знать. Сра-3)
243 iaaHOCTH минимизированных программ лучения вашей программой этих сообщений, но до того как программа '1е ,т в самом деле восстановлена или максимизирована, Windows пошлет ей ^ одно сообщение — WM_QUERYOPEN. Это сообщение является ^ боазным запросом со стороны Windows на разрешение открыть вашу В° амму- Если ваша программа ответит на это сообщение значением TRUE, ^ ация будет продолжена; ответ же значением FALSE — это для вашей ° паммы способ сказать «спасибо, не надо». Поэтому все, что нужно делать визированной программе для того, чтобы оставаться в этом состоянии, — возвращать значение FALSE в ответ на приход сообщения \\rM QUERYOPEN. На первый взгляд, впечатление такое, что вам даже нет бходимости беспокоиться о системных командах SC_RESTORE и SC MAXIMIZE. Но, как мы вскоре увидим, на самом деле это случай, когда первое впечатление оказывается обманчивым. Взгляните на листинги 7.1 и 7.2 которые демонстрируют изменения стандартной MFC-программы, необходимо > мые для превращения ее в минимизированную программу второго типа. Листинг 7.1. DROP1.CPP // DROPLcpp . Определяет классовое поведение для программы // «include tfinclude #mclude «include «include stdafx h" DR0P1 h" inamfrm h" DROPIdoc.h" DROPIvw.h" i «lfdef .DEBUG «undef THIS_FILE static char BASED_CODE THIS_FILE[] =,__FILE__, ftendif ///////////////////////////////////////////////////////////////////////////// // CDROPIApp BEGIN_MESSAGE_MAP(CDR0P1App, CWinApp) //{{AFX_MSG_MAP(CDR0P1App) 0N-COMMAND(ID_APP_ABOUT, OnAppAbout) // NOTE - the ClacsWizard will add and remove mapping macros here // DO NOT EDIT what you see in these blocks of generated code //}}AFX_MSG_MAP ' Стандартные команды для работы с файлами-документами ON_COMMAND(ID_FILE_NEW, CWinApp:■OnFileNew) ^C0MMAND(ID_FILE_OPEN, CWinApp: :0nFile0pen) tND,MESSAGE_MAP() /z7^//////////////////////////////////////////////////////////////////////// rno 1App construction p0p1App::CDROP1App() // n aclcl construction code here, x lace all significant initialization in Initlnstance /////////////////////////////////////////////////////////////// 11 T ■ •' 1111111111111111111111111111 CDRno °ne and оп1У CDR0R1APP object UPlApp theApp,
244 Минимизированные Windows-програ, внут О ///////////////////////////////////////////////////////////////////////////// // CDROPIApp initialization BOOL CDROPIApp-•InitlnstanceO { // Обеспечить старт в минимизированном состоянии m.nCmdShow = SW_MINIMIZE, // Если вы не используете эти возможности и хотите уменьшить размер финальной // версии вашего исполняемого файла, вы можете удалить из нижеследующего // фрагмента конкретные инициализирующие подпрограммы, которые вам не нужны LoadStdProfileSettingsO, // Загружаем стандартные опции (включая список последних // использованных файлов) из INI-файла // Register the application's document templates Document template // serve as the connection between documents, frame windows, and views CSmgleDocTemplate* pDocTemplate, pDocTemplate = new CSmgleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CDR0P1Doc), RUNTIME_CLASS(CMamFrame), // main SDI frame window RUNTIME_CLASS(CDR0P1View)), AddDocTemplate(pDocTemplate); // create a new (empty) document OnFileNew(); if (m_lpCmdLine[0] '= '\0') { // TODO add command line processing here } return TRUE; } ///////////////////////////////////////////////////////////////////////////// // CAboutDlg dialog used for App About class CAboutDlg public CDialog { public CAboutDlg()t // Dialog Data //{{AFX_DATA(CAboutDlg) enum { IDD = IDD_AB0UTB0X }, //}}AFX_DATA // Implementation protected virtual void DoDataExchange(CDataExchange* pDX), // DDX/DDV support //{{AFX_MSG(CAboutDlg) // No message handler //}}AFX_MSG DECLARE_MESSAGE_MAP() о CAboutDlg CAboutDlgO CDialog(CAboutDlg-•IDD) { //{{AFX_DATA_INIT(CAboutDlg) //}}AFX_DATA_INIT v void CAboutDlg-•DoDataExchange(CDataExchange* pDX) { CDialog DoDataExchange(pDX), //{{AFX_DATA_MAP(CAboutDlg) //}}AFX_DATA_MAP i
илности минимизированных программ 6 rrn MESSAGE_MAP(CAboutDlg, CDialog) .«AFX MSG_MAP(CAboutDlg) / No mesc age handler /l 1 AFXJISG_MAP P'NDMESSAGE.MAP() "/ ApP command to run the dialog '' d CDROPIApp OnAppAboutO CAboutDlg about Dig, aboutOlg DoModaK), // CDROPIApp command Листинг 7.2. MAINFRM.CPP О // inainfrin cpp ' реализация класса CMainFrame // ^include 'Gtdafx h include "DR0P1 h" tfinclude "inainfrin h' tfifdef .DEBUG #undef THIS_FILE tatic char BASED_CODE THIS_FILE[] = __FILE__, tfendif ///////////////////////////////////////////////////////////////////////////// // CMainFrame IMPLEMENT_DYNCREATE(CMainFrame. CFrameWnd) BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) //{{AFX_MSG_MAP(CMainFrame) ON_WM_CREATE() 0N_WM_DROPFILES() 0N_WM_QUERYOPEN() 0N_WM_SYSCOMMAND() //}}AFX_MSG_MAP END_MESSAGE_MAP() W////////7///////////////////////////////////////////////////////////////// // CMainFrame construction/destruction CMainFrame CMainFrame() // TODO add member initialization code here CMainF fame. ~CMainFrame() '• CMainFrame diagnostic *lf(jef _DEBUG VOld CMainFrame AssertValidQ const i \ л tF,ameWnd AssertValid(), ! ld CMainFrame Dump(CDumpContext& dc) const { ; ra,neWnd Duinp(dc).
246 Минимизированные Windows-программы: вселенная внутри 3 tfendif //_DEBUG III IIllllll/111/1IIIIIIIII II III/I/III I//IIIIIIIIllll/III 11/II//IIIIII/I III III II CMainFrame message handlers mt CMainFrame1 OnCreatedPCREATESTRUCT lpCreateStruct) { if (CFrameWnd OnCreate(lpCreateStruct) == -1) return -1, // Взять дескриптор нашего системного меню и удалить из него "Restore' and "Maximize CMenu *sysmenu = GetSystemMenu(FALSE), ysmenu->DeleteMenu(SC_RESTORE,MF_BYCOMMAND), ysinenu->DeleteMenu(SC_MAXIMIZE,MF_BYCOMMAND), // Сообщить Windows мы готовы к DND DragAcceptFiles(TRUE), return 0, void CMainFrame ,OnDropFiles(HDROP hDropInfo) { char file_buffer[MAX_PATH]; UINT num files, l, // Выяснить количество сброшенных файлов num_files = DragQueryFile(hDropInfо,Oxffffffff, file_buffer,sizeof(file_buffer)); // А теперь выяснить их имена и обработать каждый файл for(i = 0; 1 < num_flies; i++) { DragQueryFile(hDropInfo,i,file_buffer,sizeof(file_buffer)); ProcessDroppedFile(file_buffer); i II Сообщить Windows, что мы закончили обработку DND DragFinish(hDropInfo); } BOOL CMainFrame 'OnQueryOpenO { // Мы всегда отказываемся работать иначе, как в минимизированном виде return FALSE, i II Создать видимость работы со сброшенными на нас файлами. // Обратите внимание на высокую интеллектуальность алгоритма. void CMainFrame ProcessDroppedFile(char * fn) i MessageBox(fn,'Сброшен файл ,МВ_0К), i void CMainFrame. OnSysCommand(UINT nID, LPARAM 1Param) { UINT the_real_nID = nID & OxFFFO, // Проглотить эти системные команды, даже несмотря на то, // что мы отвечаем FALSE на сообщение WM_QUERYOPEN if((the_real_nID == SC_RESTORE) || (the_real_nID == SC_MAXIMIZE)) return, CFrameWnd. OnSysCominand(nID, lParain), } ■* В заключение, еще несколько слов об удобстве и надежности. Как я, рил, если вы решили убрать из интерфейса вашей программы кнопку'
илности минимизированных программ jtfUD^.—■ 247 . тши или максимизаЦр1И> вы должны одновременно убрать соответ- •ч^НЗ^ - — "I1 (1J *' уЮ команду из системного меню, и наоборот. Ваша программа всегда . Ц'х предоставлять пользователю последовательный, согласованный ;°Л гЬейс Нейтрализуя системные команды SC_RESTORE и -Й MAXIMIZE, вы также должны устранить из интерфейса соответствующие "* енты управления. Естественно, это провоцирует немедленный вопрос: д' м же тогда вообще нужна вся эта таинственная возня с нейтрализацией со- 3 ений, если все-равно придется удалять из интерфейса источники этих сооб- ний? Разве этого не достаточно? Нет, недостаточно. Потому что это один из , паТологических случаев в Windows, о котором следует побеспокоиться ( отя этот случай один из наименее страшных). Даже изменив интерфейс вашей программы так, чтобы лишить пользова- 1Я возможности возбуждать эти сообщения непосредственно, все равно останется лазейка: пользователю достаточно будет щелкнуть по значку вашей программы на панели задач, и сообщение SC_RESTORE вам обеспечено. Существует и еще одна, менее очевидная лазейка: любая другая программа может лучить от Windows дескриптор окна вашей программы и послать этому окну сообщение WM_SYSCOMMAND с нежелательным для вас значением wParam. И не стоит обманывать себя: такие программы действительно существуют повсюду, включая условно-бесплатные поделки, и благополучно позволяют пользователю опосредованно перемещать, минимизировать, восстанавливать и еще черт знает что вытворять с другими программами. Причем такие «программы-лазутчики» весьма популярны. И уж если ваша программа не рассчитана на существование в отличном от значка виде, то неожиданное возникновение ее виде нормального окна может весьма озадачить вашего пользователя (в лучшем случае заставив его усмехнуться при виде глупости вашей программы). по не -* как Windows 9S поменяла правила игры Минимизированные программы (первого, второго или какого угодно типа) Должны иметь никаих неприятностей при переносе из Windows 3.1 в Windows 95 правильно? Сюрприз: неприятности есть. Причем программы второго а> которые целиком полагаются на механизм drag-and-drop при получения 1 Лов для обработки, страдают сильнее всех. Причина проста, хотя и за- * чна: до появления Windows 95 вы могли сбрасывать файлы на мини- 95 ^0Ваннь1е программы с той же легкостью, что и на обычные; но в Windows иа М Для ЭТого придется совершать специальные маневры мышью, которые я I aio «замиранием мыши». Если вы схватите какие-нибудь файлы в Пр0 0д;нике, перетащите их на панель задач и немедленно сбросите на значок 71 ^dIVIIVibi, готовой их принять, вы получите сообщение, изображенное на рис. я^сящее, что вам это не удастся. Предполагается, что вы должны прита-
248 Минимизированные Windows-программы: вселенная вн щить файлы на панель задач, но не сбрасывать их сразу, а подержат которое время над значком и подождать, пока Windows не восстановит соответствующей программы так, чтобы вы могли потащить файлы да„ прочь от панели задач, и бросить их в нужное место теперь уже открытого OKi ■^^ ;ЙЧЕ 1&* ч лллллллллллл i'»w**ffl»4 ■* •V г^-Г- ^ ^—^^""^ ¥*^^*^**^ т^щ * Ч \ ■$ -" 1 Vuu cdnriJttftag an item qntu a outtp&on U le T^^bar, However;Ёчш .do №w№o^:(е1зо|щ*Не №Ш suttGit the /йшэм -willoperand;w : ■■. 'T "i ■ ill r ■ i ■ "*^^ ■ ■ i i i" "* r" * i л " ean then drag the item Mo *He vindow£ ■ * ц .4" 4*4 n ■ ■ н г ш ш I I I I III * * v,,/t 111111Г^,л,У"' 'i "'■ *"■" i» - - J* »- - h ■ ■ л J ш ш --.Ц-Щ^-ШХ^ I J.-MJ.*~J.J.J. J^_l- ^ J X X >*"■'■■" ^Ц'1 ■ --""-■■■'*■■ ■ inn Рис. 7.1. Панель задач в Windows 95 является зоной свободной от сброса файлов Суть проблемы с минимизированными программами второго типа в тог что они отказываются восстанавливаться (по определению), и следовательно Windows 95 вам никак не удастся сбросить на них файлы. Когда я в первый раз столкнулся с таким режимом работы drag-and-drop я подумал, что это, вероятно, просто чья-то злая шутка. Я уверен, чтотс1 кой интерфейс будет жутко раздражать людей, в особенности тех, и только учится работать с мышью, или тех, у кого есть проблемы с коорди нацией. Я нахожусь в совершенно искрением недоумении, почему Micrc soft заменила очень простой, практичный интерфейс этим тестом i-; ловкость рук. И если у вас есть этому объяснение (хорошо бы официаль ное), я бы с удовольствием его выслушал. Другая незначительная, но странная проблема при переходе к Windows9 касается z-порядка окон. Как вы вероятно знаете, z-порядок описывает сте' окон, открытых пользователем в Windows; окно, находящееся наверху этоГ1 стека, выглядит как перекрывающее собой все отстальные окна и находяш^ ближе всех к пользователю; в это же время сама рабочая поверхность cf0' (desktop) всегда находится внизу стека и поэтому всегда представляв перекрытой любым другим окном. В Windows 3.1, когда вы сбрасывали фа11! в окно какой-нибудь программы, это окно автоматически получало фокус вн0" и оказывалось на самом верху z-последовательности (даже если перед этим °h было частично перекрыто окном самого File Manager). Эта маленькая.^ весьма существенная для удобства деталь делала честь инте рФе in Windows 3.1. К сожалению, разработчики Windows 95 либо позабыли пр° ' деталь, либо сознательно изменили замысел очень странным образом. Потащите файлы из окна Проводника и сбросьте их на программ)7 чг окно гастично перекрыто тем же самым Проводником. Обработка сброШей файлов будет произведена абсолютно корректно, однако окно этой програ>'
9 ^ лности минимизированных программ 24" жнему окажется позади окна Проводника. Именно поэтому вторая П1 . мМа-аример из этой главы, DROP2, вызывает функцию SetForeground- ' ^\v() сразу же, как только начинает обрабатывать сброшенные файлы в "П с OnDropFilesO; без этого вызова и само окно программы, и ее диалог, с1 1ЯюШИЙся в ответ на сброс файлов, оставались бы перекрытыми окном , одника. При таком поведении пользователь вашей программы совершенно 1 ' номерно оказывался в недоумении, почему ваша программа никак не • гировала на сброшенные в нее файлы, хотя на самом деле она это сделала. и интересно, данная проблема затрагивает не только антикварные 16-разряд- е программы (которые все мы будем использовать еще десятилетиями просто тому, чт0 они пРекРасно выполняют свою работу), но и 32-разрядные прогр аммы тоже. Tiny Elvis Лив! Windows 95 приготовила и еще один сюрприз для минимизированных программ: они больше не смогут рисовать на своих значках. На практике такой запрет не очень серьезен, поскольку не так уж много программ делают это. Даже те, которые, казалось, делали это (такие, как популярная бесплатная программа Tiny Elvis), на самом деле просто сменяли значки и тем самым производили простой анимационный эффект, поэтому они продолжают успешно работать и под Windows 95. Такой же прием использует Stickiest, которая содержит 16 значков для изображения заметок с разными цветами фона. Когда вы меняете цвет фона у заметки, Stickiest делает следующее: / DestroyIcon(GetClassWord(hwnd,GCW_HICON)); SetClaGsWord(hwnd,GCW_HICON,LoadIcon(hInGtance,<new_icon>)); где <new_icon> — ресурсный идентификатор того значка, который соответствует необходимому новому цвету заметки. Таким образом, значок каждой заметки на панели задач или на поверхно- о сти стола соответствует внешнему виду самой заметки. А для тех, кто до сего дня вел пустой pi никчемный образ жизни и еще не видел Tiny Elvis, эта программа есть в WWW-библиотеке по адресу http://www.symbol.ru/russian/library/prof_prog/ source/chap07/t_elvis. Когда эта программа работает, ее значок изображает миниатюрный портрет Элвиса Пресли. Через некоторое время мини-король вдруг пробуждается к жизни, делает несколько всем известных характерных телодвижений, выдает комментарий по поводу чего-либо на вашем рабочем столе и вновь замирает. Программа нормально работает под Windows 95, однако
250 Минимизированные Windows-nporpaMMbi: вселенная внутри з KJ / ' ' s ъ V / слишком маленький размер знат нее зрелищной. Под Windows 3 >> •* ■* * '> <• Tiny Elvis — это общедоступная программа, написан Мэттью Т. Смитом Кстати, даный трюк с SetForegroundWindowO является отличи- примером тех самых досадных идиотизмов Windows, которые мы все знае,\ ненавидим. Прежде чем я смог найти это магическое заклинание, я эксперт тировал с RedrawWindowO, SetFocusO, BringWindowToTopO, SetWindov PosO, InvalidateRectO и UpdateWindowO. Я выяснил, что можно добить- желаемого при помощи двух вызовов SetWindowPosO подряд — первого 3 ставляющего окно быть «всегда наверху» (используя парамеТ[ HWND_TOPMOST), и немедленного второго, отменяющего первый (с параметром HWND_NOTOPMOST). Но у меня не хватает цензурных слов чтобы выразить, насколько уродлив этот способ. Кстати, я был весьма удивлеь тем, что не срабатывает метод CWnd::BringWindowToTopO, описание ко торого гласит буквально следующее: Выносит CWnd на верх стека перекрывающихся окон. Кроме того BringWindowToTop активизирует всплывающие (рор-щ верхнеуровневые (top-level) и дочерние MDI-окна. Функцию-ют BringWindowToTop следует использовать для полного показа любого окна, полностью или частично скрытого другими окнами. Вызов этой функции аналогичен вызову функции SetWindowPos дм изменения положения окна в z-последовательности. Функция Вгщ WindowToTop не изменяет стиль окна для превращения его в сам верхнее окно на поверхности стола. Я уверен, что есть какая-то причина, по которой BringWindowToTop неде лает то, что мне хочется, но я также уверен, что вышеприведенное описан^ никак этого не проясняет. Поэтому, когда я обнаружил SetForegroundWin dow() и тем самым нашел хорошее решение, я прекратил усилия по разъясни о нию этой ситуации. SetForegroundWindowO является элементом Win32 и, следовательно, # доступна 16-разрядным программам. В то же время обсуждаемая «странность* поведения Проводника затрагивает эти несчастные 16-разрядные программ поэтому при написании 16-разрядного кода вам ничего не остается как заЖаТ' нос и прибегнуть к трюку с двойным вызовом SetWindowPosO. Просто заМ ните вызов SetForegroundWindowO на... : .-SetWindowPosCm.hWnd.SWP.TOPMOST.O.O.O.O.SWP.NOSIZE | SWP_N0M0VE), ::SetWindowPos(m_hWnd,SWP_N0T0PM0ST,0. О,0,О,SWP_N0SIZE | SWP_N0M0VE); ...и после этого пообещайте никому не говорить, что я предложил вам сДед это. К сожалению, этот омерзительный hack — лучшее решение, которое яс}
251 илности минимизированных программ £сЛи вам удастся отыскать что-нибудь более приличное, пожалуйста, art1"*1 мной. итесь вашим открытием со fme более странной деталью является тот факт, что минимизированные ' аммы вообще не всегда имеют значок, когда отображаются на панели ^ в Windows 95. Создайте при помощи АррWizard стандартную программу, n lM окном которой является диалог, и вы увидите, что соответствующая Ut" пр°гРамме кнопка на панели задач содержит только текст заголовка диа- и никакого значка там нет. В тот день, когда я заканчивал написание этой г вы я получил по подписке мою копию Microsoft Visual C++ 2.2, и в ней же не было полной реализации поддержки сообщений WM_GETICON и \\rM SETICON, которые являются новинкой Windows 95 и служат для управ- нИЯ Теми значками, которые показываются в заголовке окна и в соответствующей кнопке панели задач. Чтобы понаблюдать этот эффект, запустите программу-пример DROP2 из этой главы под Windows NT 3.51: при минимизации этой программы вы увидите стандартный значок MFC, как и ожидалось. Но запустите ее под Windows 95, и значок появится на панели задач только в том случае, когда стиль окна содержит одновременно биты WS_CAPTION и WSJSYSMENU. И у меня нет абсолютно никаких догадок о резонных причинах такого поведения. Вот вам еще один странный выверт Windows 95, который нам всем придется как-то учитывать при написании новых версий наших программ. В части 3 этой книги вы найдете еще массу подобных штучек. Теперь позвольте мне провести вас по всей цепочке поиска решения, поскольку, как мне кажется, это замечательно проиллюстрирует некоторые проектировочные проблемы, с которыми нам всем приходится иметь дело при написании публичных программ. Предположим, что у вас есть минимизированная программа второго типа, и вы хотите при минимуме усилий заставить ее нормально работать под Windows 95. Окончательная версия найденного мной решения включена в книгу в виде программы-примера DROP2. Исходная же версия, демонстрирующая причуды Windows 95, включена в виде примера DROP1. Как вы, наверное, уже догадались, суть необходимой для решения пробле- Мы Уловки заключается в отслеживании системной команды SC_RESTORE и ^которой реакции на нее, поскольку именно это сообщение получает от Win- ows 95 ваша программа, когда пользователь выполняет маневр с «замиранием 1Ши>>- Самое легкое и очевидное действие, которое вы можете произвести, заставить программу показать диалоговое окно, которое примет сброшен- е в него файлы, запомнит их и затем позволит основной программе обрабо- q о эти файлы. В MFC-программе, соответствующий код метода ysCommandO главного окна мог бы выглядеть примерно так: |f(nlD == SC_RESTORE) (DoDropDlgO) // Диалог для получения сброшенных файлов , Pr°cessDroppedFileG(), // Обработка
252 Минимизированные Windows-программы: вселенная вн Пользователь, разумеется, не поймет, что именно основная прогр-> полняет реальную работу, а диалог всего лишь служит у нее посыльным Id не страшно. Пользователь увидит лишь то, что после его маневра «зам ° мыши» ваша программа вежливо покажет дружественный диалог (с б нарисованной мишенью или с чем-нибудь таким же призывным и очевич который примет сброшенные файлы, и затем программа среагирует д0г образом. Ч Смотрится как простое, работоспособное решение, неправда ли? Нч деле, нет. Все будет работать так, как описано, но у этой схемы есть одц желательный и потенциально очень неприятный сторонний эффект. Де том, что даже при простом щелчке мышью по значку вашей программы н- не л и задач (с целью восстановить ее), на экране появится все тот же диа. мишень для сбрасывания файлов — очевидно, вовсе не то, что ожидал пои ватель. Вспомните, ваша программа отслеживает физическое событие (с темную команду SC_RESTORE), но обрабатывает его как логическое (желан пользователя сбросить файлы на окно вашей программы). Это классичец проблема идентификации, и, насколько мне известно, не существует надежно способа определить, пытается ли Windows 95 восстановить вашу программу г. еле одиночного щелчка мышью, или она делает это в ответ на маневр «замиранием мыши». Помните самое начало этой книги и мои рассуждения; поводу разницы между физическими и логическими событиями, и су- программирования как процесса превращения первых в последние? Вот bl %j <_f теперь и отличный пример из реальной жизни. Разумеется, вы можете поместить в диалог-мишень какой-нибудь текс говорящий пользователю, что ваша программа на самом деле не работает «открытом состоянии», и что этот диалог предназначен исключительно д сбрасывания в него файлов. Это объяснит пользователю, почему вдруг он дел этот диалог в ответ на попытку раскрыть окно вашей программы. Но точ: так же это разъяснение может вызвать реакцию типа «Что за туп программа!» — реакцию, которой вам следует избегать как чумы. в Когда Windows 95 пытае^ Быть слишком удо1№ Когда вы захотите изменить внешний для Windows 95, вам нужно быть готовым к некотор- Предположим, вы указывете Windows 95, что 03 программа, например, не должна иметь кнопки минимиз^1 правом верхнем углу. И вы ожидаете, что этой кнопки и в cJl деле не будет, правда? Ведь именно так обстоит Де- , Windows 3.1. Как бы не так. Если вы удалите только кнопФ нимизации (путем отключения бита WS MINIMIZEBOX в с
ти минимизированных программ 253 окна), то кнопка останется на месте и будет лишь заблокирована. То же самое случится и с пунктом Свернуть (Minimize) в системном меню. В итоге, пользователь, глядя на такой интерфейс, наверняка окажется весьма заинтригован, когда же эти элементы интерфейса будут разблокированы (если это вообще произойдет). Аналогично, заблокируйте кнопку максимизации, и она также останется на месте. А вот если вы попробуете заблокировать обе кнопки, они наконец-то исчезнут с глаз пользователя, но при этом соответствующие пункты меню все еще будут присутствовать, оставляя вам развлечение по их удалению. (Как говорится, нет худа без добра. Программисты довольно часто манипулируют пунктами меню, отталкиваясь от относительного их местоположения. Ко- о нечно, такая практика является порочной, но в данном случае интересно не это. Представьте себе, что произошло бы, если бы код, модифицирующий подобным способом системное меню, вдруг перестал бы корректно работать после удаления кнопок минимизации и максимизации.) Я не обсуждал эту проблему с кем-либо в Microsoft, но она явно носит философский характер. Проектируя интерфейс Windows 95, Microsoft приложила неимоверные и болезненные усилия к тому, чтобы максимально угодить компьютерным пользователям-новичкам (зачастую в ущерб интересам опытных пользователей). И в данном случае, я подозреваю, они просто хотели обеспечить неизменное местоположение кнопок минимизации и макси- N \J мизации в заголовке окна, пусть даже ценой существования заблокированной кнопки, которая никогда не будет разблокирована. Я думаю, что такое решение является еще более путанным для всех пользователей, особенно для новичков, поскольку оно создает обманчивую иллюзию: пользователь думает, что при каких-то условиях заблокированная кнопка будет доступной, тогда как на самом деле этого никогда не произойдет. На мой взгляд, более правильным было бы все-таки допустить изменение местоположения кнопок (например, чтобы была возможность поместить кнопку минимизации на то место, где обычно находится кнопка максимизации, если последняя не нужна для данного окна) и всегда показывать только те кнопки, которые могут работать. Независимо от того, на чьей вы стороне в данном конкретном вопросе, он несомненно является еще одним примером того, насколько Windows 95 полна сюрпризами — маленькими и не очень. И сколько бы документации по Windows 95, написанной самой Microsoft или другими авторами, вы ни прочитали, ее будет недостаточно для того, чтобы подготовить вас ко всем этим сюрпризам.
254 Минимизированные Windows-программы: вселенная вн *, Лучшее, что вы можете делать в таких условиях — СТа смотреть на все глазами пользователя и оперативно адаптиро Ь ся ко всем неожиданностям по мере их возникновения. ' Если вышеупомянутых неприятностей вам недостаточно, в данной гу есть и еще одна сложность: вам пришлось связаться с модальным диало возникающим прямо на рабочем столе пользователя. Это очень нежелатель для вас ситуация, потому что такой диалог тут же монополизирует весь ввп^ вашу программу до тех пор, пока пользователь не вспомнит, что этот дца следует закрыть (а очень многие пользователи мгновенно забудут об этом к- только диалог окажется скрыт другими окнами). Поэтому первое же, что в XJ U наверняка начнете выслушивать от пользователей вашей программы — это of винения ее в том, что она «замораживается» и больше не желает реагироват T0.V II не на щелчки мышью по ее значку. Вспомните главу 1 и мои рассуждения о насколько бывает трудно новым пользователям Windows преодолеть один первых барьеров интерфейса и воспринять трехмерность рабочего стола. И смотря на то, что панель задач в Windows 95 существенно облегчает им караб кание вверх по кривой обучения, люди по-прежнему теряют из виду окна. I! лучше не давать им еще одного шанса для этого. Конечно, можно было бы использовать немодальный диалог. Но этотмето: тоже был бы слишком сложным и чреватым, поскольку вам пришлось бы еле % f к ь дить за невозможностью случайного запуска нескольких копрш этого диалога потерями памяти и за еще Бог знает какими непредвиденными проблемами. Может быть есть еще какое-то, более простое решение, которое бы ближе стояло к тому, как «хочется» работать самой Windows 95? На самом деле, та кое решение есть. Поскольку мы говорим о программе второго типа, у нее определению есть один ценный, но невостребованный ресурс — главное окно Почему бы просто-напросто не задействовать его? Действительно, ва№ программа может совершенно традиционным способом реагировать SC_RESTORE и показывать пользователю свое главное окно, которое теперь1 будет служить для сбрасывания в него файлов. В этом случае, даже если поль зователь забудет о том, что окно вашей программы уже открыто, ШеЛЧ° мышью по значку на панели задач просто переместит фокус ввода на У*' открытое окно, и никакого конфуза не произойдет. Иными словами, это стратегия уступки и превращения вашей програ^1 из минимизированной программы второго типа в то, что я называю третьим1 пом — в программу, у которой есть главное интерфейсное окно (или диаЛ° но оно предназначено только для приема сброшенных файлов. Некотор^ программисты тут же восстанут против такого решения, которое по сути з'сХСЬ\ ляет сменить главного героя программы (и, следовательно, документ^1 справочную систему, работу группы поддержки и т. д.). И я могу лишь сй> тизировать такому благородному возмущению, особенно всП<# изначальную причину проблемы — странный и необъяснимый ^а
лности минимизированных программ 255 9 -а iflp лния мыши», придуманный Microsoft. Но, к сожалению, именно такие ' миссы часто являются оптимальными и для вас, и для ваших пользование говоря уже о ваших программах). ieR лЯНите на листинг 7.3, в котором сделаны те изменения, которые щают MFC-программу с диалогом в качестве главного окна в мини- ^ •зиров анную программу третьего типа. Листинг 7.3. DROP2DLG.CPP / DR0P2dlg cpp implementation file • ? s include 'stdafx.h „include "DR0P2 h ' «include DR0P2dlg h" tfifdef _DEBUG ffundef THIS_FILE tatic char BASED_CODE THIS_FILE[] = __FILE__; #endif «define id_timer 0 ///////////////////////////////////////////////////////////////////////////// // CDR0P2Dlg dialog CDR0P2Dlg: CDR0P2Dlg(CWnd* pParent /*=NULL*/) CDialog(CDR0P2Dlg- IDD, pParent) { //{{AFX_DATA_INIT(CDR0P2Dlg) // NOTE the ClascWizard will add member initialization here //}}AFX_DATA_INIT // Заметьте, что Loadlcon не, требует последующего вызова Destroylcon в Win32 mjilcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME), void CDR0P2Dlg •DoDataExchange(CDataExchange* pDX) { CDialog- DoDataExchange(pDX), //{{AFX_DATA_MAP( CDR0P2Dlg) 4 NOTE the ClascWizard will add DDX and DDV calls here //!}AFX_DATA MAP i BEGIN_MESSAGE_MAP(CDR0P2Dlg, CDialog) //{{AFX_MSG_MAP(CDR0P2Dlg) 0N-WM_PAINT() 0^WM_QUERYDRAGICON() 0N-WM_TIMER() 0N-WM_DROPFILES() '/}>AFX_MSG_MAP ^.messageImapo '//////////////////////////////////////////////////////////////////////////// Rnn, DR0P2D19 meccage handlers < L CD№2Dlg--0nInitDialog() Sn?°0 '0nInitDialog(), lenterWindow(), etTl"ier(id_tnner, 1500, NULL),
256 Минимизированные Windows-программы: вселенная DragAcceptFileG(TRUE), return TRUE, // возвратить TRUE, если вы не перемещали фокус } // Если вы добавите в ваш диалог кнопку минимизации, вам потребуется нижеследующий // код для того, чтобы отобразить значок В MFC-приложениях, использующих модель // document/view, каркасная библиотека делает это автоматически void CDR0P2Dlg OnPaint() { if (IsIconicO) { CPamtDC dc(thiG). // контекст устройства для рисования SendMeGsage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0), // Поместить значок в центр клиентской области окна int cxlcon = GetSysteinMetncs(SM_CXICON), int cylcon = GetSyGtemMetncs(SM_CYICON); CRect rect, GetClientRect(&rect), mt x = (rect Width() - cxlcon + 1) / 2, int у = (rect Height() - cylcon + 1) /2, // Нарисовать значок dc DrawIcon(x, y, m_hIcon); else { CDialog OnPaintO, i } // Система вызывает этот метод, чтобы получить курсор, который она должна использовать, // когда пользователь перетаскивает минимизированное окно HCURSOR CDR0P2Dlg OnQueryDragIcon() { return (HCURSOR) m_hIcon, void CDR0P2Dlg OnTimer(UINT nIDEvent) { KillTimer(id_tnner), PostMessage(WM_SYSCOMMAND,SC_MINIMIZE,0); CDialog OnTnner(nlDEvent), \ i void CDR0P2Dlg OnDropFiles(HDROP hDropInfo) { char file_buffer[MAX_PATH], UINT num_files, l, // См главу 7, где вы найдете поразительную историю о нижеследующей строчке SetForegroundWindow(), // Сколько было сброшено файлов9 num_files = DragQueryFile(hDropInfo,Oxffffffff,file_buffer,sizeof(file_buffer)), // Иеперь выяснить имена и обработать по очереди for(i = 0, 1 < num_files, i++) { DragQueryFile(hDropInfo,i,file_buffer.sizeof(file_buffer)), ProcesGDroppedFile(file_buffer), i // Сказать WmdowG, что мы закончили обработку DND я/yyr
н0сти минимизированных программ > 3* ^-——"^ 257 agFiniGh(hDropInfo). '' убрать долой диалог-мишень для сброса файлов 4 onage(WM_SYSCOMMAND.SC_MINIMIZE.0). Di rGtMeoG 4^ г япать видимость работы со сброшенными на нас файлами г тите внимание на высокую интеллектуальность алгоритма. ^ opR0P2Dlg ProceGGDroppedFile(char * fn) (с :sageBox(fn. Сброшен файл , МВ_0К), Ч $ \9 Преобразование существующей минимизированной программы второго о *j к третьему типу является относительно простои процедурой: ;Ш \ Заставьте вашу программу игнорировать значение параметра для ShowWindowO, которое она получает при запуске (в MFC — член m nCmdShow объекта-приложения). Чтобы стартовать в миними- должна SW SHOWM1NIMIZED 2. В ответ на сообщение WM_OUERYOPEN возвращайте TRUE (стандартный отклик в Windows). Тем самым вы говорите Windows, что ваша программа способна, готова и даже желает быть раскрытой из минимизированного состояния. 3 Убедитесь, что ваша программа не проглатывает системную команду SCJRESTORE. 4 Сделайте так, чтобы ваша программа проглатывала системную команду SC MAXIMIZE. Помните, что теперь вы отвечаете на WM_QUERYOPEN положите ся сглотнуть SC_MAX1MIZE полноэкранном виде и будет i KJ мишень 5 Ф г о обработку сообщений в нем, которая необход о показа (такую, как отрисовка растровой кар эт на WM_PA1NT.) В частности, ваша программ возвращаться в минимизированное состояние cj обработки сброшенных файлов. по- буст небо Если вы хотите, чтобы ваш P'iMMa стартовала в минимизированном виде, использование именно диало- 4<i «естве главного интерфейса может стоить довольно хитроумных усилий } с '11симости от того, какую каркасную библиотеку вы используете. д-, ' IIMeP, некий старый код, который я писал при помощи Borland Pascal for 'А\'т ^S' достигал требуемого эффекта очень легко, благодаря тому, что в \щ" ^сть специальный класс для диалогов, выступающих в роли главных ^DlgWindow), и этот класс прекрасно подчиняется указанному
1/ 258 Минимизированные Windows-программы: вселенная внутри 3 значению CmdShow (эквиваленту члена m_nCmdShow в MFC) и может со- диалог в минимизированном виде. Классы же MFC не так нодатли}/0 обычный диалог (а больше там нечего использовать) игнорирует знач* m_n CmdShow, когда он используется в качестве главного окна программы '- вынуждает вас выбирать между имитацией диалога окном и запуском цр0г мы со сразу же открытым главным интерфейсом. Есть один трюк, который i нравится использовать в подобных ситуациях — посылка самому себе сооб ния по таймеру. Например, в программе DROP2 в качестве главного нате Рфе. i са используется диалог, который устанавливает таймер и сам себе носы системную команду SC_MINIMIZE по истечении секунды или около того, у создает эффект «всплеска окна» на экране и тем самым показывает пользой телю, что программа действительно стартовала. Если внешний вид диал01 спроектирован правильно, этот эффект может быть хорошим дополнением интерфейсу вашей программы. Обратите внимание, что при вышеописанном приведении программь второго типа к третьему типу весь код, отвечающий собственно за обработк сброшенных файлов, не меняется ни на байт. Поэтому, если с самого наш отделить такой код от чисто интерфейсного кода (см. рекомендацию 12 цс стр. 136), ваша работа вообще сведется только к изменению последнего. Если вы достаточно увлекающийся человек, вы можете без особых усилю о сделать вашу программу универсальной — автоматически адаптирующейся!, той разновидности Windows, под которой она запущена. Ваша программа мо жет проверить версию Windows, и если она выяснит, что данная версия Ht имеет характерного для Windows 95 интерфейса (опять мы встречаемся i проблемой соответствия физических и логических условий), то она может рабо тать как классическая минимизированная программа второго типа. И топь ваши пользователи, которые все еще используют Windows 3.1, вообще не заме тят никаких отличий новой версии вашей программы от старой. Пользователе же Windows 95 (а также Windows NT 4.0) будут иметь дело с функционально! и более приспособленной к окружающей среде программой третьего типа. Обратите внимание, что такая универсальность может сводиться только- следующим нюансам в коде: при работе в системе с интерфейсом Windows 9 ваша программа должна отвечать TRUE в ответ на WM_QUERYOPEN! обрабатывать системные команды SC_RESTORE и SC MAXIMIZE, а яр' льй работе в других версиях Windows - отвечать FALSE в ответ 1 WM_QUERYOPEN и проглатывать SC_RESTORE и SC_MAX1MIZE- т° факт, что во втором случае в программе будет напрасно дремать главна интерфейс и еще некоторые дополнительные функции, не должен вас си беспокоить, поскольку возникающие при этом накладные расходы в дей тельности пренебрежимо малы. (Если угодно, можно опять вспомнить я°с ' новку вопроса в терминах отношения затраты/выигрыш. В данном случае'., имеем средней величины приобретение ценой практически нулевых усШ111
DROP2 259 что сама по себе проблема проверки версии Windows стала теперь не- l<^iH Од0вной болью (см. часть 3 и в особенности — главу 12). ion г ш о уже упоминал выше, программы-примеры этой PR0P2 главы DROP1 и :лп являются минимизированными программами второго и третьего "соответственно, и просто показывают имена сброшенных на них файлов. л /< / "• / Построение примера: BROP I if DKOP 2 Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chap07/ clropl http://www.symbol.ru/russian/library/prof_prog/source/chap07/ drop2 м^;-<: Платформа: приложения для Win32 Инструкции по сборке: откройте при помощи Visual C++ 2.2 f'VI'rfc прилагающиеся МАК-файлы и скомпилируйте как обычно. Запустите DROP1 под Windows 95, и вы увидите, насколько подавленной и невыразительной (чтобы не сказать бесполезной) она окажется: будет просто сидеть на панели задач, к^ак картофелина на грядке, и ничего не сможет делать. Программа же DROP2 будет работать значительно лучше, поддерживая прежнюю функциональность и не смущая при этом пользователя. А описанные выше трюки с таймером и автоминимизацией после сброса файлов лишь делают ее Работу еще немного более изящной. в! Ее: "гь много различных типов программ, к исследованию и использованию ко- но Но. минимизи роваиные программы являются замечательным ь. И имеи- примером. Вы можете доб те R 4Цд "*^ix*V J^€.XkJJXX'X XXXUXS^ XXVJXXKIJXXX V 1-/CIJ.I, I IV^XI UUIV, 11 X JL СЛ, V/ L J У \S ^ 1Д11Л °борот показывать такие диалоги только после сбг Вы може- файлов в До тех пор, пока ваши 6Ь1ТЬ смущ Тс Щ °tIeiIb практичными, хотя и довольно специализированными, инструмен- в вашем арсенале.
Минимизированные Windows-nporpaMMbi: вселенная внутри зн Идиотские выкрутасы % Kb и Любой программный продукт такого размера и такой сдо- сти, как Visual C++, просто обязан иметь некоторое количе.", ошибок в документации, а также несколько оплошностей в т* не. Uo крайней мере, они ни для кого не являются неожидан стъю. Однако всему должен быть разумный предел. Одной из,/ более удивительных является комбинация странного дизайна ц кумектацип метода OnSysCommandO класса CWnd в Щъ Прототип этого метода имеет следующий вид: afxjncg void OnSysCommand(UINT nID. LPARAM IParatn), В справочной: системе Visual C++ 2.2 параметр nID ОПИс; следующим образом: «Указывает тип системной команды. Это %j параметр может иметь одно из следующих значении:...» и да к следует список констант вида SC_* с краткими их описаниями включая SC_MINIMIZE, SC_PREVWINDOW ' SC_SCREENSAVE. Здорово, правда? Правда, но это еще не коне. истории. Если вы щелкните мышкой по значку, находящемуся самом низу справочного окна, рядом с текстом «То jump to Bool Online, and click the icon or press F2», Visual C++ выдаст вам (к лее подробную и полную версию этой справочной страницы, ви торой описание nID снабжено следующим дополнением: В сообщении WM_SYSCOMMAND четыре нижних бит параметра пЮ используются для внутренних целей Windo\ Когда приложение проверяет значение параметра nID, fo получения правильного результата оно долМ предварительно скомбинировать значение пЮ с величине OxFFFO при помощи побитовой операции AND. Таким образом, мы всегда должны помнить о необходимое отсеивать эти самые четыре бита. Это, конечно, досадная мор01* но это еще не самое плохое. Подумайте еще немного. Предп0, жим, вы добавили в вашу программу еще одну команду, \\ Vist C++ сгенерировал для нее новый идентификатор, и затем решили завести для этой команды пункт в системном меню Ь° чем вероятно, идентификатор этой команды не окажется кратН 16, и следовательно будет иметь значимые биты в первых четЫ! разрядах. Л значит, ваша программа (следуя правилам i1 пП продиктованным Microsoft) никогда не сможет отреагиро^аГЬ вашу команду И что бы вам пи говорили, вы действительно Д ны отсеивать эти пресловутые четыре нижних бита. НапР*1'1 программе-примере LOCKER из главы 11 па стр. 360 выбор11
минимизируй те 261 та Maximize в системном меню посылает программе системную команду с кодом ОхРОЗО. Двойной же щелчок мышью по заголовку окна, который также максимизирует программу, посылает команду с кодом 0xF032. Очевидно, отсев первых битов необходим, но только для стандартных системных команд. Как вы думаете, какое количество таинственных ошибок в публичных программах спровоцирует такой дизайн? Моя оценка: больше, чем несколько. И я не знаю, что хуже — то, что Microsoft упомянула о важности этих четырех бит только в Books Online (тем самым гарантируя, что многие программисты не заметят этой детали и сделают дурацкую ошибку), или то, что Microsoft ни ело- о ва не сказала о легкости, с которой вы теперь сможете исковеркать коды ваших собственных командных кодов. И я также не знаю, почему в MFC не был предложен другой, менее провоцирующий на ошибку, интерфейс для обработки стандартных команд; * t например, такой: afxjncg void OnSysCommand(UINT nID, BYTE bMagicBits, LPARAM lPararn); Мой метод борьбы с этим патологичеким случаем заключается в том, что я добавляю следующую строчку в самое начало метода OnSysCommand (): UINT the_real_nID = nID & OxFFFO, Вы будете замечать такую строку во многих примерах этой книги. А имя вспомогательной переменной призвано не дать забыть о причине происходящего. Потом я использую значение the_real_nID для проверки стандартных системных команд, и изначальное значение uID для своих собственных команд, а также для передачи сообщения обработчику по умолчанию.
Trust me, there are evil doobies out there Мария Валлоне К сожалению, Мария права. И вредные доброжелатели не только существуют, ни еще и солидно вооружены разнообразными ресурсными и шестнадца- теричными редакторами, при помощи которых они могут (и будут) разрывать вашу программу на клочки и наносить самые немыслимые увечья вам и вашим пользователям. Думаете, я шучу? Тогда взгляните на рис. 8.1 и посмотрите, что я смог сотворить всего за несколько минут со стацдартным дршлогом для открытия файлов из Windows 3.1. Самое ужасное то, что эта версия диалога все еще работает. Если кто-нибудь сделает что-то менее пышное с вашей программой, всегда ли смогут ваши пользователи догадаться, что это не ваших РУК дело? П Режде, чем перейти к вопросу о методах защиты программы от подобного ван- *зма, позвольте мне напомнить вам, что могут сделать с вашей программой ЛоУмыш ленники, даже не имея доступа к ее исходным текстам. Они могут: • Хцалять или заменять диалоги. • Видоизменять диалоги, 3 что может подразумевать показ значков, которые вы и не думали показывать, изменение размера и местоположения элементов управления, их удаление и т. д. и т. п. Изменять текст в строковых ресурсах, включая название вашей компании, утверждения об авторских или имущественных правах на программу, сообщения и прочую важную информацию. Если это
264 4 Зашита программ звучит недостаточно угрожающе, представьте себе парочку 12-7 беспризорников, дорвавшихся до такой замечательной игрущ,, '' ресурсный редактор, и решивших испытать его прямо на в. программе: они заменят все сообщения на то, что по их мнению я ется смешным и забавным, после чего пользователям представится / можность оценить этот юмор, в диалогах вашей программы. Изменять или удалять ресурс с информацией о версии вашей пр0Г[у мы. S«veAa 1 picked Ihis one . < »ave File as Луре All Hies Г ж) Vhere it comes from: c: \zen\chapQ9\uglv &c:\ & chap09 i N b way$ 11 otherships c: wd420 Рис. 8.1. Метаморфоза стандартного диалога Я должен сознаться в том, что уже в течение долгого времени име1° пристрастие калечить ресурсы программ Я ненавижу окна-списки и я01 редактирования размером с замочную скважину, которые прогр аммпсТЬ за элем<* то и дело всучивают своим пользователям, поэтому я постоянно нимаюсь тем, что увеличиваю размеры диалогов и определенных тов управления. Я хочу сказать, неужели есть кто-то в этом мире, действительно нравится редактировать текст и просматривать списки всех этих почтовых марках? Почему диалог для открытия файло8 Проводнике так узок, что не может показать детальную информаДО11 файлах, не заставляя прокручивать их список по горизонтали?
265 ппжаловать в наш кошмар если не рассматривать вопрос об умышленных искажениях, остается / ма ошибок при пересылках и копировании. Если ваша программа будет ./)U* копироваться и передаваться но модемным каналам связи (например, 1 pc4t> идет о свободно распространяемой демо-версии или об г06еснлатном продукте), то всегда есть маленькая, но ненулевая вероят- 1013 )С искажения данных при передаче. Программа может быть покалечена и ' 'i-нибудь альфа-версией третьесторонней утилиты для архивации. (Если -отя бы изредка просматриваете каталоги условнобеснлатных или де- .трацнонных версий программ, вы наверное видели множество бета-версий тамм-архиваторов. Программы этого типа очень популярны, и даже их , ч.версии идут нарасхват. Подумайте над этим.) Короче говоря, далее если бы вы могли каким-то образом исключить воз- жность злонамеренных повреждений (можете мне верить, это недостижимо, мюме как в самых экзотических условиях), все-равно существует еще целая масса совершенно невинных причин, по которым та программа, которую будет спускать ваш пользователь, не будет стопроцентно совпадать с той програм- мой, которую вы выпустили. И пока кто-нибудь не сказал вам, что это вряд ли может произойти, позвольте мне уверить вас, что это случится обязательно. Кстати, обратите внимание, что я еще ие упоминал ужасающего слова на uvkby В: вирус. Я уверен, что заразить вирусом Windows-программу можно лаже без доступа к исходникам, и не прибегая к утомительному «взлому». Но МО Я9 KJ я не думаю, что именно вирусы являются существенной угрозой для ваших программ и ваших пользователей. Возможно, я провел слишком укромную жизнь в моем маленьком живописном городке, по я еще ни разу не слышал о aiuix вирусах Если вам известны достоверные примеры, дайте мне знать, по- i жа гунста. Пу что ж, теперь, когда я добавил в вашу жизнь еще один совершенно но- ьып повод для паранойи, давайте наконец поговорим о том, как защитить ваши "рограммы и ваших пользователей. Что вы смоЯете сделать, л чего № сможете *^к бы вы ни подходили к этой проблеме, очень быстро выясняется, что 1111как не можете добиться желаемого результата: вы не можете создать ни- ч01° препятствия для всех желающих модифицировать вашу программу, и Ке можете сделать так, чтобы ваше чудесное творение было бы неуязвимо „ ильного разряда статического электричества в телефонной линии. А это °/Шт нас к классической постановке задачи массового дизайна: если мы не il Достичь 100-процеитной защиты, существует ли какая-нибудь меньшая ij Нъ защиты, которая оправдала бы затраты всех заинтересованных лиц? слц бы вам удалось научить вашу программу наделено обнаруживать факт
266 Зашита программ от искл^ своего искажения и при этом должным образом оповещать пользовате X *Л Т Г X /<-_ ._..___ A f\f\ __ "Щ nv; проблеме? Конечно, это было бы не так здорово, как 100-процентная непробиваемость, но без сомнения было бы полезным. И тогда вопрос свол к следующему: как достичь этого разумной ценой. (' Разумеется, самый легкий способ обнаружения модификаций - Tv сравнение «подозреваемой» копии с "эталоном. Но поскольку на рабочем ст ' вашего пользователя эталона нет, данный способ не годится. (Кстати, если б! вы имели способ без искажений передать пользователю эталон, то вы сдела." бы это сразу, и проблемы вообще не существовало бы.) Спасение в CMC 01 Самый лучший из оставшихсяя вариантов — использовать CRC-код1. Эт способ позволит с приемлемой степенью достоверности обнаружить факт мода фикации вашей программы, но не сможет помочь ни выяснить характер изме нения (сколько байт, где и как были изменены), ни тем более устранить повреждение. Впрочем, для наших целей достаточно и того, что есть. По смотрите врезку «Краткая азбука CRC» на стр. 279, чтобы бегло ознакомиться с тем, что такое CRC. Итак, метод прост: ваша программа должна открыть для чтения свой глав ный исполняемый файл, вычислить по нему CRC-код и проверить, совпадает ли этот код с ожидаемым значением: да (все в порядке) или нет (внимание,в файле затаился дьявол!). Что может быть проще? Ничего, но есть одна малень кая деталь: где же, собственно, хранить то эталонное значение CRC кода, с которым ваша программа будет производить сравнение? Вы не можете хранить это значение в самом коде программы (например, в виде константы), поскольк) это поменяло бы CRC код всей программы, и проверка всегда давала бы *j отрицательный результат. Хранение эталонного CRC кода вне самой программы было бы слишком рискованным, поскольку это подвергает данные опасности быть легко изменен иыми. (Я часто воображаю себе кого-то, кто хранит результат CRC открыт^ текстом в lNl-файле, выставляя его на обозрение всему миру. Я ни разу неви дел такого своими глазами, но я уверен, что подобные попытки имели место8 истории.) Кроме того, внешнее хранилище допускает такую ситуацию, когДа эталонный код просто не окажется на месте в нужный момент, pi тем самыми хаиизм не сработает. Как я уже говорил где-то в этой книге, вы никогда не М° жете быть уверены в том, что происходит с данными вашей программы, еС сама программа за ними не следит. 1 CRC — сокращенпс or «cyclic rcdimclancy checking»; метод циклической проверки избыточности. [Примечание переводчика ]
267 9 >е iaM пожаловать в наш кошмар Ппостой и гениальный фокус заключается в том, что программа должна гтт эталонное значение CRC прямо в своем файле, а вычислять его по *' с 1 файлу за исключением тех байт, в которых оно и будет храниться. Если \г0 удастся, то проверяемая часть файла никогда не будет меняться, и мы niiiM постоянное значение CRC. При таком подходе ваша программа * ia знать, в каком месте ее исполняемого файла хранится результат CRC 1 -re того, как программа собрана, независимо от того, как она компилируется ! компонуется. Может показаться, что мы только что поменяли шило на ю - заменили одну проблему (знание CRC во время работы) другой (зна- м Места хранения CRC). Но это не совсем так: на самом деле, все это тачает, что нам нужно хранить две величины — само CRC значение и место- чожение его в исполняемом файле. по Ш тисках места для тайника Программисты привыкли рассматривать свои программы и данные в них с логической точки зрения: этот массив записей, тот связанный список, те вещественные числа и т. д. Вы привыкли иметь поименованные переменные и константы, на которые ссылаетесь, не заботясь о том, где конкретно они располагаются в памяти: программа стартует, и все они в вашем полном распоряжении. Разумеется, с константами, хранящими значение CRC и его местоположение, все обстоит точно так же. Но нам важно знать местоположение этих данных не в памяти, а на диске — в исполняемом файле программы. Зная это местоположение, ваша программа сможет пройтись по всему файлу, перескочить через эти данные и тем самым сосчитать интересующее нас значение CRC. Обратите внимание, что такой механизм совершенно не зависит ни от изменений формата исполняемого файла, ни от структуры сегментов кода и дан- ИЬ1Х в вашей программе, ни от языка, на котором программа написана. Когда ваша программа вычисляет CRC для своего собственного исполняемого файла, Эг°т файл является для нее просто потоком байт, в котором лишь небольшой * чисток следует исключить из расчета. Тот факт, что данный файл является ис- шяемым файлом самой этой программы, — всего лишь случайное (хотя и J (ьма удобное) стечение обстоятельств. ПО.' И'гшс, мы разгадали половину загадки: выяснили, какие данные pi алгоритм °ход1щы дЛЯ самопроверки программы, и как программа узнает, какие с lbl в исполняемом файле необходимо будет пропустить в процессе вычисления ГРГ П S J i J i V'J*<^. Вторая половина проблемы — выяснить, как же, собственно, помес- 13 финальную версию исполняемого файла эталонное значение CRC и све- Гя о его местоположении. Для этого нам понадобится вторая программа, ^ JP^a посчитает это значение и вставит необходимые данные в нужное место. 1 tl Программа не сможет сделать этого, поскольку после ее запуска Windows \
268 Зашита программ от иСк^ • Hi сделает ее исполняемый файл недоступным для записи. Кроме того другая простая причина, по которой следует привлечь вторую прогп- ' нельзя оставлять в защищаемой программе никакого средства, которое \ бы быть использовано кем-то другим для несанкционированного пере • CRC [I пометки исполняемого файла как легитимного. Остался нерешенным последний вопрос: как же эта вторая программ- нает о местонахождении данных CRC в исполняемом файле др.. программы? Для решения этой проблемы я предлагаю прибегнуть к одном\ моих излюбленных программистских приемов, который \JnV сформулировать так: «В самом сердце каждой высоконнтеллектуальн программы находится грубый и простой алгоритм». В данном случае, мыпоуе тим местоположение CRC-данных «маячком» — какой-нибудь необычной щ следовательностыо байт, -- и программа будет находить CRC-данпые просты линейным поиском этого самого маячка по всему файлу. Тем самым \ о гарантируем, что программа не сможет случайно изменить посторонний участо файла: все, что программа должна сделать - это найти одну и только одн уникальную последовательность байт. Б у вот и все. Все вопросы решены теоретически: обе программы знают, ка им следует подсчитывать значение CRC по всему исполняемому файлу i исключением нескольких байт, основная программа знает, с чем следует срав нивать результаты этого подсчета, а вспомогательная программа — куда и\ Геиег гбо Построение примера: 5ELFCHCK и PATCHED Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chap08/ selfchck •), http://www.symbol.ru/russian/library/prof_prog/source/chap08/ patcher Платформа: приложения для Win32 Инструкции по сборке: откройте при помощи Visual C++ прилагающиеся МАК-файлы и скомпилируйте как обы1'110 Затем переместите один из свежеиспеченных исполняем1 файлов в каталог другого. (SELFCHCK.EXE и Р^ , ER.EXE должны находиться в одном каталоге, так, чТ° PATCHER мог SELFCHCI
и PATCHER 269 и PATCHER ы *13 это^ главы — SELFCHCK и PATCHER — воплощают все выше- ^ниые теории в жизнь и являются реальными (и я бы даже сказал, до- функциолальными) программами. Обе они используют одну общую irv компиляции — CHECKER, которая собственно и предназначена для ' еиая реального вычисления значения CRC. Путем хранения CRC-дан- менно в CHECKER (в виде записи по имени crc_data), то есть в общем \ име обеих программ модуле, мы практически сводим к нулю вероятность того } л13е наши программы по ошибке будут пытаться использовать разные 1чки (Да> конечно же, у нас все еще остается шанс нарваться на неприятно- Например, изменить маячок в CHECKER, пересобрать программу p\TCHER и применить ее к еще не пересобранному исполняемому файлу sFLFCHCK. В этом случае PATCHER может непредвиденным образом пока- и It •чнть основную программу, перезаписав совершенно невинный участок исполняемого файла. Но, как говорят математики, вероятность такой беды меньше осилен. Если вас это беспокоит, или если вы используете этот прием с программой, абсолютно нетерпимой к ошибкам, вы всегда можете сделать рограмму PATCHER более осторожной — например, научить ее проверять и да г у последней модификации исполняемого файла и сравнивать ее с собственной датой. Честно говоря, я — изрядный параноик, но даже я не являюсь та- параноиком.) Исходные тексты программы PATCHER pi модуля т ч CHECKER приведены в листингах 8.1 и 8.2 соответственно. Листинг 8.1. PATCHER.CPP IIIIIIIIIIII/III III III III III/II1/11/11/1/IIII1/11//II III III III III III III III/III/ II PATCHER CPP Утилита для латания файла в виде консольного №^32-приложения // // Copyright 1995 (с) Lou Gnnzo «include •otdafx.lV «include <otdio h> «include qo h> «include 'checker h FILE* f COnst buffer_cize = 16384, Signed long i, crc_ctart, file.pos, eVe_catcher_type pattern, ""signed short the_crc, BYTE the_byte, Jhar buffer[buffer_size], x°n9 Duffer_poG, data_oize, ^ NextByte(void), У°1С| ReadNextCatcher(eye_catcher_type & c), *nt main() Измените следующую строку, для указания имении другого файла, '' подлежащего обработке1
270 Зашита программ от char * file_name = "celfchck exe ', // '! м ' '' '! ' '' ' '' ' '' ' ' '('' ' ' " !'' • > printf("Собираемся обработать файл \'%g\ ' . \n\n',file_name); if('(f = fopen(file_name,' rb"))) printf( 'Невозможно открыть файл \n\n\n'), else { buffer_pos = buffer_size, data_oize = 0, file_po3 = 0; printf("Поиск начала CRC-данных \n"), // Заполняем образец первыми байтами из файла for(i = О, 1 < sizeof(pattern), i++) pattern[i] = NextByteO, crc_ctart = 0, // Инициализируем значением "не найдено" // Сканируем оставшуюся часть файла в поисках маячка // Обратите внимание, что мы НЕ останавливаемся на первом найденном // маячке1 Мы должны обязательно просканировать весь файл и убедиться, // что больше нигде не затесалась вторая копия маячка unsigned long limit = _filelength(fileno(f)) - Gizeof(pattern), ford = 0, l < limit; i++) { if ('ineincinp(crc_data. eye_catcher, pattern, sizeof(pattern))) if(crc_Gtart > 0) { printf('B файле обнаружено несколько копий маячка'\n\n"), fclose(f)■ getchar(), return 0, j elce crc_Gtart = file_poG - Gizeof(pattern), ReadNextCatcher(pattern), V i fclose(f), if(crc_Gtart == 0) { pnntf( 'He найден маячок! \n\n"); getchar(), return 0, \ I // В данном месте можно было бы сделать проверку, не является ли файл // уже подлатанным должным образом и, если это так, катапультироваться // с соответствующим сообщением. Но латание не является деструктивной // операцией и кроме того работает довольно быстро, поэтому, вероятно, // не стоит тратить силы на такую проверку, особенно если учитывать, // что речь идет о создании простого программистского инструмента printf( 'Маячок найден Выполняется расчет CRC. ,\п\п"); if(' FindCRC(file_name,crc_Gtart,&the_crc)) { printf( 'Невозможно вычислить CRC-значение для файла!\п\п"); getchar(); return 0, if(* (f = fopen(file_name,"rb+"))) { printf("Невозможно открыть файл заново1\п\п '),
и PATCHER 271 getchar(); return 0; I // Мы вычислили необходимые данные, теперь пора подлатать // структуру crc_data в файле. // Перемещаемся на начало той области файла, которую мы собираемся // перезаписать (которое является НЕ началом CRC-данных, а определяется // полем crc_Gtart). // // В данном месте предполагается определенный вид структуры crc_data_type foeek(f ,crc_Gtart + cizeof(pattern),SEEK_SET), // Записываем данные в файл fwrite(&crc_start, 1,sizeof(crc_start),f), fwnte(&the_crc, 1. Gizeof (the_crc), f); // А теперь домой fcloce(f), printf( "Файл \'%g\" обработан \n", file_name). printfC смещение CRC %d\n",crc_Gtart), printf(' значение CRC %d\n",the_crc), printf( ЛпГотово1 ' >\n\n"), printfC (Нажмите ENTER) "); getchar(), return 0, ///////////lllllllllllIII/llllIllllllll/l/1llllllllll/llllll/ll/lllllllllll/lll II NextByteO* Возвращает следующий байт исходного файла, используя // буферизованный ввод char NextByte(void) { if(buffer_pos >= data Gize) { data_Gize = fread(buffer,1,buffer_Gize,f), buffer_poG = 0; i buffer_poc++, fiie_poG++ return buffer[buffer_poG - 1]; /////////////////////////////////////////////////////////////////////////////// // ReadNextCatcher(): Считывает следующий символ и добавляет его к нашему // текущему 'кандидату" на совпадение с маячком V01d ReadNextCatcher(eye_catcher_type & с) i ll'eimnove(&c[0],&c[1]l Gizeof (с) - 1); cCoizeof(c) - 1] = NextByteO; Листинг 8.2. CHECKER.CPP ' CHECKER CPP Ядро, осуществляющее собственно CRC-вычисления // ; c0PVnght 1995 (с) Lou Grinzo *lnclude ctdafx h" Jlnclude checker h" delude <ctdlib.h>
272 Зашита программ отиск^ «include <ctdio h> «include <io h> «include <malloc.h <assert h: У tfinclude static long buffer_pos, data_size, static BYTE *buffer, static const alloc_buffer_size = 4096. * static FILE *f, // Локальные функции tatic unsigned short UpdateCRC(BYTE new_byte, unsigned short crc); static char NextByte(void), // Эта переменная типа crc_data_type должна быть именно здесь И именно // она будет использоваться в приложении в качестве носителя вычисленного // значения crc_data_type crc_data = { CRC с а "Г, 'с 'h', 'e 0, 0 // eye_catcher // crc_start // crc_value \ / /////////////////////////////////////////////////////////////////////////////// // Таблица, которую использует функция UpdateCRCO для вычисления CRC static unsigned short crctab[256] = { 0x70e7, Oxflef, 0x62d6. 0xe3de, 0x5485, 0xd58d, 0x46b4, 0xc7bc, 0x3823. 0xb92b, 0x2a12, ОхаЫа, 0x1c41, 0x9d49, 0x0e70, 0x8f78, 0xe16f, 0x6067, 0xf35e. 0x7256, 0xc50d, 0x4405. 0xd73c. 0x5634, 0xa9ab, 0x28a3, 0xbb9a, 0x3a92, 0x8dc9, OxOcd, 0x9ff8, 0x1ef0 0x0000 0x8108 0x1231 0x9339 0x2462 0xa56a 0x3653 0xb75b 0x48c4 0xc9cc 0x5af5 Oxdbfd Охбсаб Oxedae 0x7e97 0xff9f 0x9188 0x1080 0x83b9 0x02M, OxbSea 0x34e2, 0xa7db, 0x26d3, 0xd94c, 0x5844, 0xcb7d, 0x4a75, 0xfd2e, 0x7c26, Oxeflf, 0x6e17, , 0x1021 0x9129 , 0x0210 , 0x8318 , 0x3443 . 0xb54b , 0x2672 , 0xa77a . 0x58e5 , 0xd9ed , 0x4ad4 , Oxcbdc , 0x7c87 , 0xfd8f . ОхбеЬб , Oxefbe , 0x81a9 , OxOOal , 0x9398 , 0x1290, , 0xa5cb; , 0x24c3, , 0xb7fa, , 0x36f2, , 0xc96d, , 0x4865, 0xdb5c, 0x5a54, OxedOf, 0x6c07, 0xff3e, 0x7e36, , 0x2042 . 0xa14a 0x3273 , 0xb37b . 0x0420 , 0x8528 . 0x1611 , 0x9719 , 0x6886 , 0xe98e . 0x7ab7 , Oxfbbf , 0x4ce4 , Oxcdec , 0x5ed5 , Oxdfdd , OxMca , 0x30c2 , 0xa3fb: , 0x22f3, , 0x95a8, , 0x14a0, , 0x8799, , 0x0691, , 0xf90e, , 0x7806, 0xeb3f, 0x6a37, 0xdd6c. 0x5c64, 0xcf5d. 0x4e55, . 0x3063 , 0xb16b , 0x2252 , 0xa35a . 0x1401 , 0x9509 , 0x0630 , 0x8738 , 0x78a7 , 0xf9af , 0x6a96 . 0xeb9e , 0x5cc5 , Oxddcd , 0x4ef4 , Oxcffc, , 0xa1eb: , 0x20e3 , 0xb3da , 0x32d2, . 0x8589, , 0x0481, , 0x97b8, , 0x16b0, , 0xe92f, , 0x6827, Oxfble, 0x7a16, 0xcd4d, 0x4c45, 0xdf7c. 0x5e74, , 0x4084 . 0xc18c , 0x52b5 , 0xd3bd . 0x64e6 , 0xe5ee , 0x76d7 , 0xf7df , 0x0840 , 0x8948 , 0x1a71 , 0x9b79 , 0x2c22 , 0xad2a , 0x3e13 , Oxbflb , OxdIOc . 0x5004 , 0xc33d , 0x4235 , 0xf56e , 0x7466: , 0xe75f, , 0x6657, , 0x99c8, , 0x18c0, 0x8bf9, OxOafl, Oxbdaa, 0x3ca2. 0xaf9b. 0x2e93. . 0x50a5 , Oxdlad . 0x4294 , 0xc39c . 0x74c7 . 0xf5cf , 0x66f6 , 0xe7fe , 0x1861 , 0x9969 , 0x0a50 , 0x8b58 , ОхЗсОЗ , OxbdOb . 0x2e32 , 0xaf3a , 0xc12d , 0x4025 , 0xd31c , 0x5214 , 0xe54f , 0x6447 , 0xf77e, , 0x7676, , 0x89e9, , 0x08e1, , 0x9bd8, OxIadO, 0xad8b, 0x2c83. Oxbfba, 0x3eb2. , ОхбОсб, , Oxelce, , 0x72f7, , 0xf3ff, , 0x44a4. , 0xc5ac, , 0x5695, , 0xd79d, , 0x2802, . 0xa90a. , ОхЗаЗЗ, , ОхЬЬЗЬ, , ОхОсбО, , 0x8d68, , 0х1е51, , 0x9f59, , 0xf14e, , 0x7046, , 0xe37f, , 0x6277, , 0xd52c, , 0x5424, , 0xc71d, , 0x4615, , 0xb98a, , 0x3882, , Oxabbb. 0x2ab3, 0x9de8, OxIceO, 0x8fd9, OxOedl, I ///////////////////////////////////////////////////////////////////////////////
и PATCHER 273 iindateCRC() Обновляет текущее значение CRC для потока данных, учитывая '"/ ледующий ^айт из этого потока (new_byte) '\ пС unsigned short UpdateCRC(BYTE new_byte, unsigned short crc) :ta turn crctab[(crc & OxOOff) " new_byte] ~ (crc » 8); } //; 'not FindCRC(char *fn. unsigned long crc_start, unsigned short * the_crc) aGsert(fn ' = NULL), aGG6rt(fn[0] '= '\0'), aoGert(crc_start != 0), lf(fn == NULL) return FALSE: if(fn[0] == '\0') return FALSE, *the_crc = 0, unsigned long i, if((buffer = (BYTE *)malloc(alloc_buffer_size)) == NULL) return FALSE. if(' (f = fopen(fn,' rb" ))) return FALSE // Выясняем длину файла (не прибегая ни к каким циклам1) unsigned long limit = _filelength(fileno(f)), // Убеждаемся, что crc_start указывает на осмысленную позицию в файле if(limit - sizeof(crc_data_type) < crc_start) { fclose(f), return FALSE; \ i buffer_pos = alloc_buffer_size, data_size = 0; // Вычисляем CRC для той части файла, которая предшествует // структуре данных CRC for(i = 0, 1 < crc_start, i++) *the_crc = UpdateCRC(NextByte(),*the_crc), // затем перешагиваем через всю структуру. for(i =о, 1 < sizeof(crc_data_type), i++) NextByte(), •'/ и заканчиваем вычисления, обрабатывая остаток файла. for(i = crc_start + sizeof(crc_data_type), i < limit, i++) *the_crc = UpdateCRC(NextByte(),*the_crc), free(buffer), fclose(f); return TRUE; B°0L GoodCRC(void) { char fn[MAX_PATH]; unsigned short the_crc; lf(crc_data start •= 0) { GetModuleFileName(NULL,fn.sizeof(fn)); // Если что-то неладно, мы возвращаем FALSE 1f('FindCRC(fn,crc_data start,&the_crc)) return FALSE, else return the_crc == crc_data crc_value,
274 Зашита программ от иск^( е* % i / else return FALSE: // Данных нет, неит смысла выполнять вычисления //1///1////////////////////////////////ИИИ/1Ш///////////////////////////// I/ NextByteO Возвращает следующий байт исходного файла, используя // буферизованный ввод char NextByte(void) i i if(buffer_poG >= data_cize) data_size = fread(buffer,1,alloc_buffer_Gize,f), buffer_poc = 0, i buffer_pos++, return buffer[buffer_poG - 1], Как вы можете заметить, тот способ, которым PATCHER ищет в фай. шле маячок, действительно является грубым и простым. Вероятно, я мог бы исполь зовать другой подход и соптпмизировать эту процедуру. Но эта программа не будет предоставляться моим пользователям, и, кроме того, я собираюсь запус кать ее довольно редко. Поэтому нет особого смысла утруждать себя какой либо дополнительной оптимизацией. Обратите ваше внимание на то, что в по исках маячка PATCHER не останавливается, встретив его в первый раз Программа всегда сканирует файл до конца. Эшо очень важная деталь. Если \ KJ вы этого не сделаете, вы подвергаетесь риску, пускай и незначительному, на ткнуться на другой фрагмент файла, по случайности совпадающий с искомых маячком. В результате, заплата могла бы быть поставлена в совершенно неправильном месте, и это привело бы к непредсказуемым последствиям. Когда PATCHER находит единственную копию маячка, работа продолжается; есль V же маячок не обнаруживается, или если обнаруживаются несколько его копии программа выдает пользователю соответствующее сообщение и прекращу свою работу. Программа SELFCHCK делает совсем немного в ответ на запрос пользов^ теля произвести самопроверку. (В реальном приложении интерфейсом к Bbl полпеишо такой проверки может быть, например, специальный пункт меню Внутри обработчика сообщений OnSelfCheckO производится простой в^30 функции GoodCRCO из модуля CHECKER, и, в зависимости от результат этого вызова, пользователю выдается то или иное краткое сообщение: //I///////III/I/////////////////!///////I//////////II//IIII/ll/1/l/l/ll/lllll // CMamFrame meGcage handler void CMamFrame OnSelfcheck() { lf(GoodCRCO) MeGGageBox(0,"Самопроверка произведена успешно1'." \MB_OK); elGe MeGGageBox(0, 'Самопроверка дала отрицательный результат! ',"",МВ_0К);
и PATCHER 275 И функция GoodCRCO возвращает TRUE, только если все осуществляемые оверки проходят успешно. Во всех остальных случаях она возвращает с1°т с£. (В случае более длительной процедуры самопроверки, вы должны сде- . так, чтобы ваша программа показывала пользователю немодальный диа- 1 с примерно таким сообщением: «Выполняется самопроверка. Это займет не- 1 рое время».) Обратите внимание на тот уровень абстракции, который V сцечивается модулем CHECKER. И специальный код, и все необходимые Него данные находятся в этой единице компиляции, и поэтому вызывающей ' оГрамме остается только лишь вызвать функцию GoodCRCO, не имеющую араметров, pi получить в ответ просто «да» или «нет». CHECKER совершает реальные вычисления CRC для обеих программ для SELFCHCK, и для PATCHER, и предоставляет всего две функции FindCRCO и GoodCRCO. Функция FindCRCO используется программой PATCHER после того, как она просканировала файл и нашла в нем уникальное местоположение структуры crc_data. Эта функция вычисляет значение CRC для заданного файла на диске с учетом заданного местоположения структуры crc_data в этом файле. Функция GoodCRCO вызывается вашей основной программой для выполнения самопроверки. Для определения полного имени исполняемого файла вызывающей программы эта функция использует GetModuleFileNameO Windows API, а местоположение эталонных CRC-данных она извлекает из соответствующего элемента структуры crc_data. Затем она вызывает FindCRC, используя эти значения, pi просто сообщает о результате сравнения вычисленного значения CRC с эталонным. Функция GoodCRCO даже не утруждает себя вызовом FindCRCO, если видит, что местоположение все еще указано как 0; в этом случае она сразу возвращает FALSE. Такая эвристика является стопроцентно пуленепробиваемой, потому что ни в каком исполняемом файле структура crc_data не может оказаться в самом начале. Использование функции GetModuleFileNameO обусловлено двумя причинами. Во-первых, это избавляет вас от необходимости «зашивать» имя исполняемого файла в код (например, в виде константы), что являлось бы поминальным источником опасности. Конечно, вряд ли ваши пользователи бу- *т Переименовывать исполняемый файл вашей программы. Но я уверен, что v K только ваша программа распространится достаточно широко, кто-нибудь гДа-нибудь обязательно сделает это по совершенно резонным, но непредска- Уемьщ заранее соображениям. оо-вторых, при осуществлении самопроверки необходимо знать именно иое имя файла (включая название дискового устройства и имя каталога, где годится файл), чтобы не беспокоиться о том, какой каталог является теку- 41 Hi* момент выполнения самопроверки. Вы не можете быть абсолютно 1ены в том, что в момент запуска вашей программы ее домашний каталог
276 Зашита программ от иска* будет текущим. Кроме того, наши старые добрые друзья, стандартные дца. *j могут изменять текущий каталог, если не указывать им не делать этого Ч Итак, предположим, что вы хотите добавить возможность самопроверки в v. существующую программу. Эта задача очень проста и может быть решена! *j пр помощи следующей схемы: 1. 2 3 4 5 Добавьте в интерфейс пользователя какой-нибудь элемент, ко: горьп позволит пользователю запустить процедуру самопроверки. Этот aie например, дополнительны» мент может быть совершенно простым пунктом системного меню. Добавьте в программу соответствующий обработчик сообщения лайте так, чтобы ваша программа вызывала его в ответ на запро< зователя о самопроверке. На этом этапе вам придется включить проект файл CHECKER.CPP и использовать в вашем коде соотвек вующий заголовочный файл CHECKER.H. еде оль Выбег нибудь мудреную последовательность б ш маячка и соответствующим образом подправьте код в CHECKER.CPP Помните, что в качестве маячка должна использоваться такая строка которая, с одной стороны, наверняка будет уникальной во всем резуль тирующем исполняемом файле, а с другой стороны не будет бросаться в глаза человеку. Тот пример, который я использовал в исходном да CHECKER.CPP ker решительно образцом и была выбр ие те льны Поэтому, когда вы будете выбирать свой маячок, будьте изобретя укажите в качестве него номер вашего первого водительско го удостоверения, шестиадцат возлюбленной и тому подобное. ч t код даты рождения ваши Приведите имя программы в PATCHER в соответствие с настояшп' именем исполняемого файла вашей программы. Скомпилируйте вашу программу и программу PATCHER. Запуск PATCHER. Если PATCHER сообщит об успешном завершении нр°11с дуры латания, запустите вашу программу и выполните самопро1зерьЧ Программа должна тут лее отрапортовать о том, что проверка пр0111 В качестве дополнительной проверки вы можете взЯ >нное прогоаммой РАТСН^ успешно мещення CRC-даиных, сообщ и, вооружившись иибу шестнадц б ред хктор о? что заданный вами мая1 10
ация самопроверки в вашей программе 277 0 действительно находится (Кстати, я сам делаю это всякий раз после очередной серьезной модификации основной программы и первого ее латания при помощи PATCHER. Эй, говорил я вам, что я — параноик? Я не доверяю даже своему собственному колу.) Разумеется, теперь вам придется запускать PATCHER перед вы- t з 1 ♦ *_• .iniero приложения бывайте об g Решите, когда ваша программа должна производить самопроверку. Только по запросу пользователя? При установке? Каждый раз при запуске? На это решение может влиять целый ряд вопросов, которые будут подробнее расмотрены ниже. Итак, на данном этапе в вашей программе присутствует работающее, хотя и минимальное средство для осуществления самопроверки. Но разве стоит останавливаться на достигнутом? Вы можете легко придать ауру профессионализма вашей программе и значительно поднять уверенность пользователя в ее (и ваших) возможностях путем автоматической самопроверки при первом же запуске после установки. Я предпочитаю такой вариант решения этой задачи: сделайте так, чтобы при запуске ваша программа могла осуществить самопроверку при наличии какого-либо ^задокументированного ключа в командной строке (например, «/CRC»), а затем пусть инсталляционная программа запустит ваше приложение с этим секретным ключом. Именно такой подход используется в Stickles!. Он отлично срабатывает и освобождает вас от бессмысленных попыток определять со стопроцентной достоверностью факт первого запуска вашего приложения на данной системе. 7 Если ваше приложение состоит не из одного-единственного исполняе- иеобх Н с динамическими библиотеками и с файлами данных (не изменяющимися во время работы программы). Для каждого нового файла, ко- необ вы можете просто добавит элемент в структуру crc_data, который будет хранить эталонный CRC для данного файла. (В данном случае совсем не нужно xpai местоположение чего-либо, поскольку контрольные данные CRC будут затронуты.) Р <i Дсриизировать и программу PATCHER (так, чтобы она обрабатывала все необходимые файлы), и функции модуля CHECKER (так, чтобы °нп умели работать с файлами, не содержащими встроенных в них CRC-даииых), и само главное приложение (так, чтобы оно проверяло
278 Зашита программ от иска^ *л несколько файлов). По все эти изменения являются лишь пепрт ппальным расширением приведенных выше программ-примеров мой большой проблемой была проверка программой своего собстве го исполняемого файла, и эта проблема уже решена. Немаловажным является также вопрос о проверке файлов с данными U t t торые распространяются вместе с вашей программой, но вовсе не обязаны таватьея неизменными. Например, в поставку вашего приложения могут в* дить шаблоны документов, с которыми пользователь скорее всего 6v экспериментировать, редактируя п даже удаляя их. В такой ситуации вы; Мог I бы сделать так, чтобы в ответ на секретный ключ командной строки ва программа проверяла и все такие файлы (но при этом не забывала, что она нерь должна молча пропускать ненайденные файлы, а никак не жаловать' пользователю от том, что они не прошли проверку). Затем вы можете сдела- так, чтобы инсталляционная программа строго проверяла все установленщ файлы па целостность. И, наконец, тот вариант проверки, который доступе пользователю в любой момент, вообще может не затрагивать эти файлы с Даь пыми (полагая, что вероятность их изменения к данному моменту улсе мож считаться стопроцентной). Как я уже упомянул выше, вполне законным является вопрос о провер! целостности файлов программы при каждом ее запуске. С одной стороны,! кой вариант автоматической самопроверки может показаться слишком болыш бременем для программы и прямой дорогой к тому, чтобы вызвать раздражен! у пользователей. Однако, это не всегда так. Здесь мы имеем дело как раз с т. и «» кпм вопросом, на который нельзя дать однозначный ответ, годный на bi случаи жизни. Насколько большим будет ваше приложение? Что вы знаете о аппаратном обеспечении, на которм будет работать ваша программа? (Ее ваша программа будет распространяться широко, тогда она наверняка будет* пускаться на самых разнообразных компьютерах: от античных 386-х, тсоторь следовало отправить служить в качестве подставок под принтеры сше времена администрации Буша, до самых новейших моделей типа BelchFive5w Multiprocessor Wonder box. И в этом случае вы не сможете сделать никаЫ разумных предположений. Но если ваша программа будет использоваться,сЬ жем, только в пределах вашей компании, то кое-какие предположения1 б вооб В /я: никакой выгоды от этого пег; проверка сразу после установки, плюс возМ° пость осуществить такую проверку в любой момент по требованию пол: теля этого вполне достаточно Но, опять же, не всегда. Возможно, программа будет запускаться в таком окружении, в котором она будет дос одновременно многим пользователям, среди которых есть несколько мас'^ ль умышленно или нечаянно калечить случайные файлы. В обе за»111
вы числяйте CRC 279 * т длительности процедуры самопроверки, автоматическое ее выполне- JC и каждом запуске может иметь смысл (если при этом сообщения будут D чТЬСя пользователю только в случае обнаружения проблем). Подобным образом следует рассматривать и вопрос о проверке файлов с 1И В обычных условиях такая проверка была бы излишней, ведь жест- лискн сегодня являются настолько надежными, что в данном вопросе но рассматривать их как совершенные. Однако достаточно критичные дан- помещенные в окружение с достаточно непредсказуемыми действующими тми, могут сделать проверку целостности данных весьма разумным и даже необходимым делом. вычисляйте Как видите, описанные выше основные приемы достаточно гибки, и без особых сомнении они могут оказаться отличным оружием вашего арсенала, которое обеспечит ваши программы pi ваших пользователей недорогой и эффективной страховкой. Но не будьте самоуверенными^ Как pi в случаях с другими инструментами, данный экземпляр будет полезным ровно настолько, насколько полезным его сделает тот, кто держит его в руках (чрггайте: вы). И никогда не забывайте предостереженрге Марии о вредных доброжелателях — ohpi по-прежнему бродят где-то вокруг и по-прежнему охотятся за каждым из нас. Краткая азбука CRC По-видимому, всякий, чья работа хоть как-то связана с программным обеспечением, в общих чертах представляет себе, что такое CRC-коды. Но при этом никто не знает, откуда именно они берутся, какрш ошибки они могут, а какие не могут обнаруживать, и какрши другами относительно важными свойствами ohpi обладают. Это был один из наиболее интересных и странных фактов, обнаруженных мной при написании этой книги; другим интересным фактом было то, насколько мало я сам знал о CRC. Но не надейтесь, я не собрфаюсь даже пытаться рпложить вам полную теорию CRC-кодов. Я полагаю, что гораздо более осмысленным является разговор о них на более общем, логаческом уровне — о том, насколько они могут быть полезны при проверке целостности данных. Говоря коротко, вычисление CRC-кода — это процесс преобразования целого потока данных в одно короткое «проверочное слово», удобное тем, что является очень «чувствительным» к малейишм изменениям в этом потоке данных. Hapi6o- лее часто в поршоде встречаются две оазноврщности CRC колов -
280 Зашита программ от иски* CRC-16 и CRC-32, которые являются 16- и 32-разрядн проверочными словами соответственно. В программах-прщ данной главы, а также в программе-примере MegaZero цСп' зуются алгоритмы вычисления CRC-16. В теории, алгоритм CRC представляет весь поток данных, одно огромное число и делит его на другое, тщательно выбрци, огромное число, называемое «образующим полиномом». Част от этого деления игнорируется, а остаток как раз и является к j шпм призовым проверочным словом. При подсчете CRC для вес файла понятие «огромное число» может приобретать весь- непривычное значение. Например, копия исполняемого файла F ecl 5.0 (EXCEL EXE) имеет размер 4,185,600 байт. Если ра, сматривать этот файл как одно большое число, то мы получим Б[ личину порядка 2^(8*4,185,600) - 1. Это число настолько огро>, но, что я с удовольствием оставлю задачу его представление- читателю в качестве упражнения. К счастью, описанная выше операция деления на самом де* <-» является чисто теоретической, и вам никогда не придется мучитьс; с такими немыслимо большими числами. Как правило, CRC кодь вычисляются табличным методом (таким, который я применилi исходном коде примера из данной главы), который очень легк> реализовать, и который работает весьма быстро. По сути, этот ж тод является простым аналогом так называемого «метода отбрасы вания девяток», которому всех нас обучали в средней школе. «Me год отбрасывания девяток» позволяет взять произвольное большо- число и быстро определить остаток от деления его на девять путе последовательного просмотра значащих цифр слева направо и которых простых операций над ними. Трюк заключается в ' не гоу U что вам нужно начать с первой цифры и прибавлять к иен с it дующие цифры до тех пор, пока не получится число, большее и1 равное девяти, затем вычесть из этого числа девять и продолжит прибавление следующих цифр аналогичным образом. КогД цифры кончатся, вы получите не что иное, как остаток от делен! исходного числа на девять. Например, используя этот метод для моего идентифпк^0!1 CompuServe (71055,1240), нужно начать с первой семеро прибавить к ней единицу, ноль и первую пятерку. На этом №\ мы получим первое число, большее или равное девяти " Вычтите из пего девять и прибавьте следующую цифру " терку. И так далее до самой последней цифры. Когда вЫ беретесь до последнего нуля, вы получите 7, что и является ocV ком от деления 710551240 на девять.
^ вычисляйте CRC 281 ^ 9 Совершенно аналогичным образом работает табличный метод вычисления CRC-16, реализованный в моем примере. На каждом шаге он берет текущее значение проверочного слова и следующий байт из потока данных, а затем, используя эти значения, специальную вспомогательную таблицу из 256 16-разрядных чисел и *J простои алгоритм, выдает следующее значение проверочного слова. То есть поток данных рассматривается как одно большое число, первой цифрой которого является первый байт. А вычисление контрольной суммы для всего файла производится просто путем побайтного пропускания всего файла через одну корневую процедуру (функцию UpdateCRCO в файле CHECKER.CPP). Когда эта функция обработает последний байт, мы и получим финальный CRC-16 код для всего файла. И все-таки — какую ike степень защиты обеспечивает метод CRC? Если посмотреть на этот метод иод другим углом, то CRC код — это просто проекция множества файлов самой разнообразной длины и содержания на множество целых чисел. Вычисление CRC-16 по любому файлу дает одно из 65 536 чисел, находящихся в диапазоне от 0 до OxFFFF (a CRC-32 — от 0 до OxFFFFFFFF). Это делает CRC-значение достаточно непредсказуемым заранее. Очевидно, что сам получаемый код является бессмысленным — на основании его значения нельзя делать какие-либо предположения о данных, по которым он был вычислен. Единственно возможное применение таких кодов — проверка их совпадения для двух потоков данных и на основании этого вывод о совпадении или различии этих потоков данных. 16-разрядное проверочное слово обнаруживает все однобитовые различия, все различия длиной 16 бит и более pi все различия в двух битах, расположенных не далее, чем на расстоянии 65,536 бит друг от друга. (Об этих и других подробностях, касающихся CRC Джона Код1 «Fletcher's Checksum» в майском номере Dr. Dobb's Journal за 1992 год.) С одной стороны, все это здорово, а с другой — очевидно, что весьма значительной является вероятность того, что два совершенно разных файла (и по содержанию, и по длине) могут дать одно и то же значение CRC. Ведь количество уникальных значений CRC-16 равно 65 536, а уникальных файлов во Вселенной, несомненно, много больше. (Именно поэтому некоторые люди любят говорить, что CRC — это не более чем причудливый хэш-алгоритм для «всего лишь» 21Ь или 2Л корзин; выполните
282 ч Sf f \ 4 / J*. • *. s •* 4 *• Зашита программ от искаН( вычисления для достаточно большого набора потоков дант конфликты CRC-кодов не замедлят произойти.) Еще несколько слов о *чру Ч ее3 CRC -коды хороши для обнаружения неумышленных, сл\щ. ных изменений файла. А как насчет умышленных (а тем бол намеренных) изменений? Оказывается, зная CRC-код для ф можно всегда поменять его произвольным образом, а затем вить некоторую комбинацию байт так, что результирующий фа будет иметь тот же самый CRC-код, что и исходный файл. И метод «подделки» файла работает значительно быстрее, случайный подбор дополнительно последовательности байт. air такс Разумеется, защитный прием, описанный в данной главе \ может защитить ваш файл от такого рода умышленной «подде ки». Но он существенно затрудняет ее. Помните всю нашу таищ венную возню с местоположением CRC-кода в исполняемом фай и с «обучением» нашей программы находить этрг данные? Тот п «_» о >< потетическии вредитель, который захочет подделать ваиг программу, не изменив ее CRC-код, должен будет сначала найг этот самый CRC-код. И если вы используете достаточно неброски маячок (например, просто похожий на бинарный мусор привзгл де на него в шестиадцатеричном редакторе), вы тем самым веш и весьма затрудняете жизнь потенциальному злоумышленнику
При And the best and the worst of this is That neither is most to blame, If you have forgotten my kisses And I have forgotten your name. Олджсриои Чарльз Суинбори Action is transitory,—a step, a blow, The motion of a muscle, this way or that Tis done, and in the after-vacancy We wonder at ourselves like men betrayed: Suffering is permanent, obscure and dark, And shares the nature of infinity. Уильям Уордсворт всем уважении к господину Уордсворту, если уж говорить о постоянстве тРаданий (точка зрения, с которой я категорически не согласен), то ему сле- вало бы поближе посмотреть на некоторые файлы и форматы файлов на на- к°мпыотерах. А что касается «мрачности и невразумительности» иге»), то я бы предложил ему поглядеть на ближайшую систему с Win- (<obsc . JO и ее регистрационной базой данных. 11опрооуи-ка переплюнуть это, 0Ц| иг! 'слц лее быть абсолютно серьезным, то задача проектирования и реализа- ,10с'1оянпого хранения данных в ваших Windows-программах действи- ° °чень непроста. Решая ее, вы неизбежно столкнетесь с целым рядом ло- s п вам придется прибекпъ ко множеству компромиссов. Обойти эту ^Ц|, М*' 11ево^можно, поскольку грамотный подход к постоянному хранению Pop /' Являет<-*я еще одним важным фактором, определяющим качество ваших Г)Д\ 71М" ^ R данной главе я хочу рассказать об одном из гибких и мощных °в К этой проблеме - о так называемых умных файлах данных.
284 Умные файлы хранилища Как правило, большинство программистов даже не задумываются ни 0 . зачем и когда нужны файлы данных, ни о правильном их использовании у только появляются какие-то данные, которые необходимо где-то хранить от ° ного запуска программы до другого, эти данные просто складываются в кар иибудъ файл, после чего программист беспечно продолжает разрабо/ программы. Разумеется, в реальном мире все далеко не так просто и легко Вопрос о постоянном хранении данных молено разбить на две части: как| именно данные (и для чего) нужно хранить постоянно, и как и где следует ц хранить. Поскольку даже первый вопрос далеко не всегда рассматриваете программистами всерьез (особенно в условиях спешки pi мучительных поиско того самого решающего заклинания, которое заставит что-нибудь работать по Windows), давайте начнем с рассмотрения именно вопроса «Что хранить?» ; к вопросу «Как и где хранить?» мы обратимся в следующем разделе. В рамках нашей основной задачи — разработки приложений ДО Windows — те данные, которые должны храниться постоянно, можно разбии на три основные категории: системные данные, настроечные данные вашеа приложения и документы, с которыми работает ваше приложение. Системные данные Я уверен, что многие из читателей этой книги видели телевизионный рек ламный ролик фирмы Apple, в котором два человека ведут тяжелейшую бито за то, чтобы заставить работать систему под Windows. Один из них держитi руках открытое руководство и диктует из него инструкции по правке файл0 SYSTEM.INI и WIN.INI, а второй старательно стучит по клавиатуре под эг диктовку. Оставив в стороне вопрос об эффективности такой рекламной кая паиии, молено с уверенностью сказать, что этот ролик наглядно иллюстрирУе тот факт, что любая современная операционная система нуждается в огромно количестве конфигурационных настроек, если она хочет иметь хоть какой'1 шанс работать со всем этим умопомрачительным разнообразием аппарате средств для PC. (Если вы сами уже сталкивались достаточно часто с пр°6г мамн настройки и поддержки конфигураций на машинах ваших сотруДн11К°' друзей или родственников, вы наверняка знаете, что одна только мультим^1 ная часть компьютерной Вселенной может заставит вас потратить абсурд количество времени па невольное подражание тем ребятам из рекл^1Н ролика Apple. На мой взгляд, вряд ли стоит ожидать действительно всемИР поддержки Plug and Play в ближайшее время.) Все эти конфигурации . настройки должны где-то храниться, и они должны быть легко достУ операционной системе, особенно при ее старте.
pro используются постоянные хранилища ланных net —.—- 285 q отличие от двух других категории данных, существует одно особенное / вание по отношению к системным данным: они должны быть доступны (в 1цсле Для изменения) даже тогда, когда сама операционная система не в ке (На самом деле, именно эта особенность является причиной, по ■юн многие специалисты так нерешительно используют «родную» фай- систему Windows NT - NTFS; они просто-напросто знают — случись нибудь страшное с файловой системой FAT, всегда остается возможность v3iiiь с дискеты DOS и получить доступ к файлам.) Кстати, страстные 0ы между пользователями Windows и Мае по поводу вышеупомянутой рек- \\ъ\ зачастую касаются именно этой особенности системных данных: многие Оперты Windows говорят, что это наоборот хорошо, когда конфигурация сис- ы может быть изменена так просто и даже без запуска Windows; в ответ шзователи Мае возражают, что если бы PC имели более продуманную по ,ппаратную и программную архитектуру, вся эта возня с INI-файлами вообще была бы не нужна. В данном случае обе стороны в чем-то правы. А по поводу 1\'1-файлов я поговорю подробнее чуть ниже. Настройки приложения Так же как и сама Windows (пли любая другая операционная система), прикладные программы нуждаются в постоянном (от одного запуска программы до другого) хранении каких-то своих конфигурационных данных. При этом следует различать данные, относящиеся к отдельным документам ШрИ(1 ображения мента), ц более глобальные данные, относящиеся к самому приложению (например, шрифт, ном побого вновь созданного документа). В [аю различия между конфигурационны лпьного приложения и целого пакета программ. (особенно те, которые довольно просты) даже ю б б ж. Другие д< Word for W со многими асгроиками форматирования — он хранит их в документах-шаблонах (в DOT- <11[лах, по умолчанию - в файле NORMAL.DOT). Кстати, такой подход ih о побочный эффект: многие пользователи бук 4озревают, что у программы есть специальный «потайной карман» для ,а1снця этих настроек. Пользователи знают лишь то, что, когда они укажут •ulKropy Word использовать шрифт Arial/12 для стиля «Normal» во всех но- документах, программа запомнит это указание и будет выполнять его в *Иьн<мщгем.
286 Умные файль1^ляНц Аокументы приложения В противоположность данным предыдущей категории, зачастую KJ I невИди мым для пользователей, данные документов приложения являются именно т что пользователи именуют «мои данные» — это их письма, бюджетные отче базы данных заказчиков, отсканированные картинки из «Playboy» и тому п добное. В эту категорию входят не только тысячи файлов, физически распоп женных в настольном компьютере пользователя, но и многочисленные удале ные базы данных, работа с которыми осуществляется по методу клиент- сервер С одной стороны, эту категорию данных порой весьма трудно отделить 0 предыдущей, поскольку почти все файлы с документами включают в себя какую-то конфигурационную информацию для программы. Например, Word for Windows хранит в файле-документе такую настройку как способ просмотра этого документа (обычный (normal), разметка страницы (outline) или главный документ (master document)), так чтобы при повторном открытии этого доку. мента Word мог показать его соответствующим образом. С другой стороны, есть одна интереснейшая деталь, которая очень резко «.» отличает документы от остальных двух категории данных: как пользователь эксплуатирует эти данные. Вы наверняка ни разу не слышали о том, чтобы пользователи перемещали с места на место файл W1N.INI; многие вообще не подозревают, что эта и другие подобные ей твари населяют их файловые системы. Но когда дело доходит до «их данных», пользователи легко и непринужденно начинают перемещать эти файлы, хранить их там, где захочется, переименовывать их, совместно использовать их с другими людьми и так далее Таким образом, с точки зрения системы, все три категории данных практически неразличимы — все они, в конечном счете, не более, чем дисковые файлы. Но в зависимости от того, к какой категории относятся те или иные данные, рази тельно отличается пользовательский взгляд на них — его ожидания по части того, что он может и должен иметь право делать с этими данными. постоянного Другая ось координат в нашем двумерном пространстве данных определи вопросом, который обычно приходит программисту на ум первым: физпче реализация хранилища данных. В этом программисты абсолютно протШ ложны пользователям, которые озабочены лишь возможностями и х* теристиками своих программ pi, как правило, вообще не задумываются такими вопросами. В сущности, пользователи и не должны этого делать. я&
0BHbjejt£E^L постоянного хранения ланных 287 ,егД ,5ирая физическую реализацию постоянного хранения данных, мы, как будем идти на компромиссы. При этом нам придется иметь дело с двумя 1 переплетающимися проблемами: g Производительность и тип доступа. С какой бы стороны вы ни подходили к этому, вес детали (такие как размер данных вашей программы, способ их использования, последовательный пли произвольный доступ к данным) в конечном итоге сводятся к вопросу о производительности. Даже если вам необходим произвольный доступ к данным, вы могли бы при каждом запросе осуществлять последовательное чтение данных, начиная с нулевой позиции. Для достаточно большого массива данных такой подход, очевидно, был бы глупым, но он без сомнения работал бы. Единственная причина, по которой программисты тратят так много сил и времени на махинации со всякими индексами и другими подобными приспособлениями, — улучшение производительности. Одним из самых ярких и всем известных примеров данной проблемы (среди коммерческих продуктов) являются те самые наборы CD, которые содержат огромные телефонные справочники но Соединенным Штатам. Очевидно, что такого рода продукты были бы абсолютно бесполезны, если бы при их создании не была проведена определенная работа по индексации и оптимизации форматов хранения данных. Некоторые из этих справочников содержат около сотни миллионов имен, адресов и телефонных номеров, и еще не изобретены такие быстрые устройства CD-ROM и процессоры, которые позволили бы достаточно быстро производить простой линейный поиск среди такого количества данных. К счастью, при выборе формы хранения данных производительность является действительно критическим фактором лишь в экстремальных случаях. В обычных условиях мы можем беспечно игнорировать это1 фактор, хранить данные в простейшей, наиболее удобной для работы форме, и уделять больше внимания другим вопросам. Безопасность. Я имею в виду безопасность не в привычном смысле, не такие вещи, как использование паролей для предотвращения несанкционированного доступа к данным. Я говорю о безопасности на более низком уровне. Как уже упоминалось где-то в этой книге, некоторые пользователи становятся весьма изобретательными авантюристами, когда получают возможность свободно побродить по Windows-системе с текстовым редактором в руках. Тем не менее, большинство пользователей весьма уважительно относятся к неизвестному, если можно так выразиться. И они не будут редактировать файл, имеющий явно иерас- шпфровываемый бинарный формат. Поэтому, в зависимости от тина
288 Умные файлы л-> - —^н данных, хранение их в непонятном для человеческого глаза ф0п может стать эффективной (но, конечно же, не стопроцентной) зат от случайных пли умышленных искажений. 4[. IN I-файлы Этот вид постоянного хранения данных наиболее знаком Windo, программистам (и, как это ни грустно, пользователям тоже). В теории 1\| файлы являются удобным и простым способом хранения ограниченнь наборов системных данных и настроек приложений. (Конечно, вы могли б хранить в lNI-файлах и данные документов приложения. Но, на мой взгля- такое решение может быть оправдано только для случая очень небольшого объ ема -- буквально в несколько байт - таких данных.) К сожалению, lNI-файлы являются еще одним примером тепличного соз даиия Я могу смело утверждать это, зная, как они используются в реально;- мире. Для начала, можно вспомнить, что практически никто из программисте й не утруждает себя проверкой кодов ошибки, возвращаемых функциями дл чтения и записи INI-файлов. Такое впечатление, что программисты имеют просто-таки религиозное отвращение к таким проверкам. (Правда, это может показаться не таким уж неожиданным, если, например, принять во внимание тот странный способ, которым GetPrivateProfileStringO сигнализирует об ошибках. Подробнее об этом я расскажу в главе 10.) В самом дела, если у вас вдруг появится настроение позаниматься относи тельно безвредным вандализмом, lNI-файлы являются отличнейшим полиго ном для таких развлечений. Только попробуйте слегка отредактировать IN1 файл какой-нибудь программы, и почти наверняка она сойдет с ума. Например если вы возьмете файл EXCEL5.INI, найдете секцию [Microsoft Excel] и заме ните значение ключа Font на «Fred» (то есть иа заведомо неправильное значение, поскольку программа ожидает найти здесь имя и, через запятую размер шрифта например, что-то типа «Arial,10»). Результат этого малень кого вандализма будет таким, как изображено на рисунке 9.1. Ясно, что Ехсе> считывает настройки из lNI-файла и затем использует их без какой--^111 проверки. Обратите внимание на то, как попытка использовать слишком >IJ ленысий шрифт свела с ума логику рисования в Excel — приглядитесь рис. 9.1, и вы заметите короткую широкую прямоугольную область в пр^в°' верхней части окна (между панелью инструментов pi сетлеой), через котор) просвечивает содержимое части экрана, скрытой окном Excel. В ДаИ1{ примере вы видите лоскуток окна Проводника. Это очень занятный и >111Л глазу эффект, хотя Microsoft, вероятно, этого не предусматривала. На ,N взгляд, этот пример интересен тем, что проблемы могли бы быть устр дне** очень легко. Программе следовало бы просто проверять считанные tf>J> размер шрифта и, если что-то не так, использовать какой-либо подход*1
формы постоянного хранения ланных 289 9 .г типа «Arial,10». Я бы даже рассмотрел вопрос о проверке попадания '' .па шрифта в некоторый разумный диапазон - например, от 4 до 36. '/ <> значение за пределами этого диапазона вызывало бы соответствующее ' некие для пользователя и предложение использовать другой, более нодхо- „i размер шрифта. (Да, конечно, это сообщение-предложение должно 5ы включать в себя флажок, позволяющий пользователю отключить поде этого предупреждения в дальнейшем.) Но я слишком отвлекся. '' Др\'гая интересная проблема, связанная с INI-файлами, заключается в том, при изменении отдельного значения в INI-файле не происходит пересозда- !я всего файла. Это означает, что после любого заведомо некорректного изменил настроек вручную, неправильные данные могут благополучно оста- мгься в файле на протяжении многих запусков программы. Если в качестве пмера Снова взять Excel и его EXCEL5.INI, указать в качестве [Microsoft Ex- (elj/0ptions строку «Hello, world!» (вместо оригинального значения, которое 1[а моей машине было равно 339), то такое приятное глазу но, очевидно, ^правильное значение будет оставаться в силе до тех пор, пока вы не измените teiip wuue-нибудь настройки через интерфейс самого Excel и не сохраните эти изменения Я не хотел бы, чтобы мои последние слова и примеры звучали как упреки именно в адрес Microsoft или Excel. В отношении обсуждаемой проблемы, Mi- iiosft поступает вовсе не хуже огромного большинства других разработчиков программного обеспечения. Я использую в качестве наглядного примера Excel *J О %J рост потому, что он оказался иервоп программой, с которой я 11 подходящим |енных мною целей. будут "икакой проблемы тут пет, поскольку INl-файлы без сомнения являются во INI ( ); • i Я не согласен. Т Во-первых, в борьбе за нормальную работу Гем пользователям уже настолько часто приходилось редактировать INI- Фаплы, AUTOEXEC.BAT, CONFIG.SYS и даже MSDOS.SYS, что они давно №ыкдп к одной простой мысли: если вам необходимо заставить вашу сис- 1МУ работать так, как вам хочется, вам наверняка придется раскочегарить ваш он М) ■МП Ьакт, что lNI-ф ь 'Ч|ц10 Уже сам по себе является громким приглашением пользователя к прове- :)ксперпментов с ними. 4 ,1рсвшнх настроек. Я пособ Uv()i WIN
290 Умные яйлы з течение последних месяцев или даже лет. н популярных типов программного обеспечение ( Windows являются программы-деинсталляторы — те, которые предназн ф ненужных файлов, INI-файлов и настроек в общи Этот факт что-нибудь w^—^^ W* ^™^т Ш:Е$? £dft •''')&№* Fyrotf £&tfte Ш¥$. )*£ft&QW Й^Р L>.V.V Ш\ S heetl У ■ ■ "л ■■■"■ ^ л ЗЬйеЮ *■ She ItY"'" J'J'r -^j 1 ■'■'■'"'■*■ ■'■'■'■'l'-' *'■'■'- J *'■■'* **'*'** ■■'*'"''* JJ'* ^ ■■'■J'" ■■■■»■■■■■- лГ*". "V" ■■ л. л.--. -Гл. ■ м**Ги"Г|1iV iiiii +»* 1ШЬф il*h '♦ii4^tJi'i>V<'iifc' 4*4^4iMV*»4 uhEMUtJhuHUuuu JJ---"J-JJJ Рисунок 9.1. Результаты махинаций с EXCELS. Не содрогнулись ли вы, когда я сказал, что теперь пользователи бул^ редактировать MSDOS.SYS? Это сущая правда. Одной из наиболее страинь' вещей в Windows 95 является то, что файл C:\MSDOS.SYS — обычный тестовый файл. На мой взгляд, это весьма неудачное сочетание имени и содер^ ния. Уж лучше бы они назвали его как-нибудь по-другому, нанри^ MSDOS.CFG. Однако я полагаю, что причина данной аномалии довол# проста: Microsoft было просто необходимо присутствие в корневом катал0 файла с именем MSDOS.SYS (см. их комментарий в приведенном ниЖе1 держимом этого файла в моей системе), и они предположили, что такой пр°- воречнвын выбор имени, а также атрибуты hidden, read-only и sys предохранят этот файл от редактирования хотя бы какой-то частью ноль30 *j те леи. [Paths] WinDir=C \W95 WinBootDir=C \W95 HostWinBootDrv=C
ie формы постоянного хранения ланных 291 options] =0otMulti=1 3ootGUl=1 ..eiwork=l following lines are required for compatibility with other programs n not remove them (MSDOS.SYS needs to be >1024 bytes) xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxb УХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХС xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxd xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxe xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxf xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxg xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxh xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxi xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxj xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxk xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxl xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxm ,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxn xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxp xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxq .xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxr , xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Реестр Windows Конечно же, реестр Windows не был изобретен специально для Windows 95, но именно с появлением этой версии Windows появился и нынешний акцент на использовании этого инструмента (влияние Windows NT в данном вопросе можно практически не учитывать, благодаря относительно малому числу пользователей этой операционной системы). Признаюсь, это изобретенрге вызывает У меня двойственные чувства. С одной стороны, реестр является более «чис- 1ьщ»( более централизованным хранилищем для конфигурационных данных uMoii системы и прикладных программ (по сравнению с INI-файлами). Но с другой стороны, это — еще одно «вертикальное» архитектурное решение, что, ч'°>калению, означает следующее: эти данные могут быть доступны только из- '*ТРИ Windows, л следовательно наши варианты их использования "винчены. U казалось бы, тут нечего обсуждать: поскольку реестр и в самом деле раз и СегДа является важной составной частью Windows 95 и Windows NT, нам Дс'гея считаться с этим и принять это как очевидный и неоспоримый факт Не Щ Чтицей мере, нам - как пользователям и программистам). Но это вовсе а1Шт, что мы должны наивно принять реестр в паши объятия и, сломя го- V f\ *' "Росаться хранить в нем все и вся. ^а> вы правы, реестр является очередным тепличным созданием. Просто У> что он страдает ровно теми же самыми болезнями, что и INI-файлы.
292 Умные Файлы л^ * _да^ Не существует никакой централизованной политики использования хранилища, а значит пользовательские настройки опять отдаются на щ1л °f разработчикам того программного обеспечения, которое установлено и раб0 Ci на компьютере этого пользователя. Используйте Windows 95 достаточно ■' тивно в течение года, попнсталлируйте и поудаляйте приличное количег программных продуктов, и, я уверен, в результате вы получите VN помрачительное количество неправильных и устаревших данных в реестре шей системы. (Надо отдать должное фирме Microsoft — она убедите и' призывает и стимулирует разработчиков создавать грамотные программы деп. сталляции, которые призваны вычищать из реестра подобный мусор. Но вспомните те следы, которые оставляют разработчики в наших системах, кален наши динамические библиотеки, изменяя без нашего ведома и разрешения кон фигурационные файлы и совершая еще множество самых разнообразны, грехов при инсталляции. После этого, я надеюсь, вы простите мой сильнейшиг скептицизм в данном вопросе. В самом деле, вернитесь к главе б и посмотрите еще разок, как инсталлятор Norton Navigator от Symantec использует строко вый счетчик использования динамических библиотек в то время, как Microsft твердит, что следует использовать тип REG_DWORD.) Разумеется, если вы хотите использовать некоторые специальные возмож ности Windows 95, вам придется использовать реестр для хранения некоторы\ настроек вашего приложения (например, местоположение динамических биб лиотек, используемых только вашей программой). Но даже это хорошо только до тех нор, пока ваша программа собирается работать только под Windows 95 Если же речь идет о поддержке нескольких платформ Windows, то, как только дело коснется специфичной только для Windows 95 возможности, вам придется либо вообще отказаться от ее использования, либо по-разному хранить i' обрабатывать соответствующие настройки в разных версиях Windows Что касается вопросов безопасности, то реестр Windows уязвим пере,' пользователем не меньше, чем lNI-файлы — спасибо замечательной программ REGEDIT, которая берет всю эту бинарную кучу данных и столь любезно!' призывно предоставляет ее в иерархическом виде для обзора и редактирования Поймите меня правильно — в тех случаях, когда мне необходимо что-то лоМе нять в реестре, я очень счастлив, что Microsoft предоставляет мне эТ' программу; я бы первее и громче всех запротестовал, если бы Microsoft застав ляла нас писать собственные программы только для того, чтобы редактир°ваТ элементы в реестре. Но в то же время я испытываю немалую тревогу за ге пользователей, которые смогут неряшливо обращаться со своими реестра (просто из-за отсутствия элементарного опыта и осторожности). К момент}' писания этих строк я видел всего несколько 32-разрядных программ, но V встретил среди них одну - все тот же Norton Navigator от Symantec» открытым текстом предлагающую пользователю совершить кое-какие мазс1,, ШШ с бинарным ключом в реестре для отключения красочной заставки
ie формы постоянного хранения ланных 2"э - жжется, что чем больше вещи меняются, тем больше они на самом деле ;а10тся прежними. Специальные бинарные файлы Третий и самый древний способ постоянного хранения данных — •юный файл специального формата. Именно этот метод я стремлюсь исполь- 1ть для хранения большей части документов и настроек в моих программах, , /кочьку он немного более безопасен, чем INI-файлы, и гораздо более перено- м с одной Windows-платформы на другую (включая несколько инсталляций Windows на одной системе), чем реестр. Использование бинарных файлов для хранения конфигурационных данных страдает от тех же проблем, что и использование INI-файлов: загрязнение и итога Windows. Инсталлируйте, запустите, а затем деинсталлируйте дос- 1аточное количество программ, и результат не будет зависеть от того, как они хранят свои конфигурационные данные — в INI-файлах или в бинарных фай- шх" вы обнаружите целые залежи конфигурационных файлов в вашем ката- юге Windows, и их вычищение по-прежнему будет головной болью для вас. Для тех программ, которые запускаются не с совместно используемого дискового устройства, есть более удачное решение (как раз его я и использую в Stickles!) - помещать конфигурационные файлы в свой домашний каталог (туда, где расположен главный исполняемый файл программы). Помимо уменьшения загрязнения каталога Windows, данный подход дает несколько дополнительных, более важных реальных выгод. Во-первых, пользователи могут запускать такую программу из нескольких разных конфигураций Windows, установленных на одной машине, не прибегая к такой идиотской процедуре, как повторная инсталляция уже установленной программы. Лично мне довольно часто приходится заниматься этой ерундой, "скольку на всех моих компьютерах установлено по нескольку разных версий Windows (а на некоторых даже стоит OS/2), и установка одного и того же пРиложения на один компьютер порой требует трех- или четырехкратного за- llVcKa инсталляционной программы, что почти всегда является сомнительным д°вольствием. Во-вторых, значительно облегчается деинсталляция вашей 1|)ограммы. Если она использует бинарный конфигурационный файл, то, как "№1до, никакие INI-файлы ей не нужны, и пользователь может деин- |/ЛЛ1фовать вашу программу просто путем удаления ее домашнего каталога. ^исого «осадка» ни в каталоге Windows, ни в реестре при этом не останется. нечно же, при такой схеме на диске, в других каталогах останутся нетрону - 1,1 Документы приложения. Но это в любом случае не вопрос хранения дан- [j '' а вопрос создания очень агрессивной программы деинсталляции. "ни Зч1аться> если бы я встретил такую программу, я бы побоялся ее использо-
294 Умные файлы Ал Наиболее типичное применение специальных бинарных файлов — х ние приложениями своих рабочих (не конфигурационных) данных п давляющее большинство используемых нами программ опираются на одцн несколько таких форматов — сложные форматированные докумен предварительно скомпилированные заголовочные файлы, графические фа/ (порой мне кажется, что последние имеют уже миллиард и еще одну разнов"- ность формата). Даже некоторые семибитовые ASCII-форматы (такие RTF, например) могут вполне резонно считаться бинарными, благодаря свое^ запутанному (для человеческого глаза) виду. Для большинства приложен, специальные бинарные файлы вообще являются единственным жизнеспособ ным решением. Я хотел бы чуть подробнее остановиться на одном особом случае хранена данных в специальном формате — на базах данных. Как известно, существуй много различных библиотек, которые ваша программа может использовать д# работы с базами данных, — начиная от ODBC и кончая специализированным!' библиотеками, ориентирующимися на конкретные форматы баз данных. Ос новными проблемами при работе с такими библиотеками являются их размер (некоторые библиотеки прибавят вашему приложению до мегабайта и больше) и дополнительные заботы при инсталляции результирующей программы (как минимум, вам придется побеспокоиться о правильной инсталляции самой биб лиотеки, а также о проблемах совместимости различных версий этой библио теки). Тем не менее, в некоторых случаях выигрыш от использования таких биб лиотек намного перевешивает дополнительные затраты, вызванные их приме пением, поскольку вы получаете возможность работать с третьестороннимн форматами баз данных или использовать готовые возможности индексирования данных, а не тратить недели своего времени на создание и тестирование собст венного аналогичного кода. Но это лишь обобщенная теория. Мой опыт под сказывает, что в большинстве случаев вопрос об использовании или неисполь зовании таких библиотек сводится к более простым вопросам. Должна ли ваШ" программа работать с уже существующими базами данных стандартных форма тов? Должна ли она хранить данные так, чтобы их потом мождно было исполь зовать из других программ? Если ни одно из этих требований не ставится,tl скорее всего будет лучше избежать накладных расходов и дополнительно* нагрузки на инсталляцию, возникающих при использовании стандартных 6П лиотек для работы с базами данных. Кстати, это еще одна ситуация, в которой вы можете cli:ib'\ разочароваться в средствах БРП (быстрой разработки приложений), исполь. их в реальных проектах. Да, вы можете очень быстро создать протри- которая будет успешно работать с файлами dBase, Paradox или Access, # ^ этом вы не коснетесь и строчки кода, что-либо понимающего в форматах э ^ файлов. Здорово! Но вдруг приходит время распространять вашу програ>
файлы ланных 295 •талкиваетесь с массой проблем при создании дистрибутива и инсталляцией программы. файлы данных умные файлы данных являются бинарными, но не все бинарные файлы явится умными. Это очень важный момент, потому что и для вас, и для ваших , чьзователей есть огромная разница между простым сбрасыванием бинарных иных в файл (такую практику я встречаю на каждом шагу) и эксплуатацией "мных файлов с данными. Как будет описано ниже, умные файлы данных яв- 1ЯЮТСЯ не просто файлами с определенными характеристиками. Этот термин подразумевает еще и некоторые правила использования таких файлов приложениями. Вы можете возразить, что, мол, вопрос о том, как используется файл (или любые данные), лежит несколько в стороне от обсуждаемой темы. Но на самом деле это не так — формат файла данных и методы использования этих данных являются тесно переплетающимися проблемами. Довольно теории. Пора перейти к сути. Умные файлы данных и использующие их программы имеют следующие характеристики: б Они могут использоваться для хранения любых типов данных (системных настроек, настроек приложения или документов приложения). В Они являются полностью самоидентифицирующимися. Под самоидентификацией я подразумеваю такую ситуацию, когда любая программа, рассматривающая данный файл, может недвусмысленно pi без особых усилий определить, с каким форматом файла и с какой версией этого формата она имеет дело. Такие вещи, как имя файла, его расширение, местоположение файла в системе, не могут использоваться при таком опознании; вся информация должна браться из содержимого самого файла. (Это очень важная деталь, поскольку всем известно, как любят пользователи переименовывать файлы, и как они порой тяжело воспринимают соглашения о соответствии между форматами и расширениями имен файлов.) Для достижения этой цели нужно совсем немного — небольшая последовательность символов (своеобразный «отпечаток пальцев») и номер версии в самом файле, а также ваша предусмотрителъиость в использовании этой информации. Эта характеристика вовсе не означает, что программа должна иметь возможность с такой же легкостью определять все содержимое файла или какие-то другие данные (например такие как количество страниц в документе). Во многих случаях программа может получить такую информацию лишь более трудоемким способом, вплоть до старательного прочтения всего файла. Тем не менее, ясно, что вы должны стремиться Делать ваши файлы интерпретируемыми достаточно быстро, и в неко-
296 Умные файлы л* 4^ торых случаях вам следует делать часть информации действие доступной мгновенно. Например, представьте, что вы проектип/ специальный формат базы данных с записями постоянной длины ' чтобы ваша программа могла легко и быстро считывать из файла^ одной записи и позволять пользователю редактировать данные п такой постановке задачи'вам, очевидно, незачем беспокоиться от, чтобы, например, хранить в заголовке файла количество записей нем. Но, спустя некоторое время, вы услышите от пользователей ty dows 95 многочисленные просьбы сделать для ваших файлов прогпам му быстрого просмотра, которую они могли бы запускать по нажата правой кнопки мыши и мгновенно получать «конспекты» ваших фа^ лов. И тогда вам наверняка захочется хранить в заголовке вашего фай ла такой параметр, как количество записей в нем, чтобы программе бы строго просмотра не приходилось старательно прочитывать весь фай] для подсчета количества записей или вычислять это количество, исхо дя из размера файла и размера записи. Этот гипотетический, но весьма жизненный пример очень хорошо иллюстрирует, как в игру могут вступать «расширения формата» и новые версии формата — еще одна важная характеристика умных файлов данных, о которой я подробнее поговорю чуть позднее. Хочу заметить, что не все программы, стремящиеся идентифицировать файлы (бинарные или текстовые) и умеющие это делать, работают так как ожидает пользователь. Я получил массу напоминаний об этом, как только впервые стал работать с Visual C++ 2.x и Windows 95. Всякий раз, когда я случайно дважды щелкал по МАК-файлу от старого проекта, сделанного при помощи Visual C++ 1.5, запускалась оболочка Visual C++ 2.x (которая теперь была ассоциирована с расширением МАК). Visual C++ 2.x успешно распознавал неправильную, старую версию таких файлов, однако реакция его при этом была весьма далека от разумной. Происходило примерно следующее: 1. Visual C++ 2.x показывал свою красочную заставку. 2. Visual C++ показывал свое главное окно, на фоне которое буквально на мгновение мелькал диалог, который я никак успевал рассмотреть. (Мне лишь потом удалось увидеть, что эт° диалог на самом деле сообщал об обнаружении файла стар01 формата и предлагал мне сконвертировать такой файл.) 3. Фокус ввода перемещался обратно на Проводник, который св°! окном закрывал и главное окно Visual C++, и упомянутый вь1 диалог. 4. Передо мной возникал модальный системный Д*1а' объявлявший, что МАК-файл (или один из его компонентов/
файлы ланных 297 найден. Взгляните на рис. 9.2, где изображено и это модальное сообщение, и диалог, упомянутый в п. 2. Возможно, я не понимаю чего-то совершенно очевидного, но именно появление этого модального сообщения больше всего меня удивило. Ведь ясно, что Windows строго последовал букве закона — ассоциировал МАК-файл с Visual C++ 2.x, точно как я и просил. В свою очередь, Visual C++ 2.x был успешно запущен, следовательно с этим тоже не было никаких проблем. Я не стал утруждать себя дальнейшим исследованием всей этой истории, но она явно чудна и свидетельствует о некорректности поведения Visual C++ 2.x. Попробуйте, наоборот, взять МАК-файл от Visual C++ 2.x и открыть его при помощи Visual C++ 1.5 — результат получится еще менее впечатляющим. Visual C++ 1.5 не признает МАК-файл более нового формата и выдает два диалога. Первый диалог спрашивает, не является ли данный МАК-файл внешним, и, если вы ответите «нет», появляется второй диалог и сообщает о синтаксической ошибке в первой строке. Разумеется, я далек от того, чтобы требовать от старой версии программы успешной работы с файлами от новой версии; такое требование было бы абсурдным. Но я бы желал, чтобы формат файла был спроектирован хоть чуточку предусмотрительнее — достаточно было бы, чтобы старая версия программы по-прежнему могда идентифицировать тип файла и адекватно среагировать на новизну его версии (то есть вежливо отказаться работать с таким файлом, с которым она заведомо не может работать). В данном конкретном примере, вместо того, чтобы задавать дурацкие вопросы о типе файла, а затем рапортовать о несуществующих синтаксических ошибках, Visual C++ 1.5 мог бы выдать пользователю примерно такое сообщение: «Файл FRED.МАК может быть использован только более новой версией Visual C++». Самым непревзойденным специалистом в вопросах проверки версий является сама Windows — я имею в виду уже обсуждавшийся ранее штамп «ожидаемая версия Windows» в исполняемых файлах. Например, в Win32s 1.30 этот механизм испорчен Microsoft настолько, что операционная система позволяет запускать даже те программы, у которых олшдаемая версия Windows превышает 4.0. (В главе 14 я гораздо подробнее расскажу о проблемах определения версий в Windows.)
298 Умные файлы •is DME*tf*UfWTESr\LraiTeerMAK Tbift ргорчП wa. create) usflig Vi i lal rt+ VeHore 1 5 « earlier C&nl ruin'} vjitf с orwetf* to a VUwsl C++ 2 Оргфес! -and jettir g* 'vdl be cow erted ko the ft.itt32 .ердйУаЬэяЬ Succfr^ v*f depend ph.the:portability of the cryioal.pfojeif,: If уйц .^f A to соя»шме to и €■ the ob proie*: i> be *we to sfcvs tbe nsw or* undet^a cWtoierrrtatrte. >ow 4bep».iiTFtedto>d^*Ueftbe on^fer^m Де* >.qu tf'fe yog want Id *~orv erf ? C^rngj-find.(he;rife p \тЩдаТЕШ1ЖТ£5?#№': (cf :$w■ pf ::j&- feptHf'CiheHtЩ tvta'e >-.MO tb^ parfi and fetw№Mb:&~ii9& &d th^^rapu^dhtrfaties ire $v A$fa :DK Рисунок 9.2. Странный способ обнаружения старой версии Одна из первейших выгод от использования умных файлов данных за ключается в том, что они облегчают задачу написания разумных, ус лужливых программ. И некоторые примеры таких программ уже стали для нас привычными, когда дело касается поддержки нескольких спе циализированных форматов документов. Например, сохраните файл созданный при помощи Word for Windows 6.0, и в нормальных уело виях вы получите файл в формате Word 6.O. Но вы с такой же легко стью можете сохранить файл и в форматах Word 2.0, RTF, Word Per feet, MS Write и т. д. Такая практика в современных программах стала настолько обычной, что мы редко задумываемся над этим вопросом. Менее заметной разновидностью такого правильного подхода к файлам данных является работа с конфигурационными данными разных версий приложения Когда вы модернизируете какое-либо приложение (устанавливаете в вашей системе его более новую версию), вам никогда не придется запускать какую-то вспомогательную утилиту, которая будет переделывать ваши настройки в INI-файлах под новую версию А ведь совсем недавно это было чуть ли не общепринятой практикой " при обновлении программы (например, какого-нибудь коммуникан11 опиого пакета) принуждать пользователя запускать подобную утилиз для конвертации старых данных (например, справочника телефон^1 номеров) в новый формат. Как это ни удивительно, но даже некотор^ современные программы, к сожалению, все еще требуют от пользой теля подобных мероприятии. Отдельные утилиты для конверт^1" файлов почти всегда являются признаком программистской лени, и Y смотря па их кажущуюся простоту, код таких утилит 6Ырс значительно худшим по сравнению с самими программами (как пра
до, приступая к написанию этих утилит, программист с самого начала уверен, что рано или поздно они все-равно будут «выброшены на помойку»). Ца мой взгляд, никакгш программа не должна требовать от пользователя большего, чем просто ответ типа «да/нет» на предложение скон- вертировать данные в новый формат. Хорошим примером является обращение Word for Windows 6.0 с файлами (документами) в формате Word 2.0. Загрузите такой старый документ в Word 6.0 и редактируйте его сколько вашей душе угодно — вы даже не узнаете, что на самом деле работаете с файлом от Word 2.O. Однако, когда вы попробуете сохранить этот файл, Word сообщит вам о «древности» его формата и предложит очень естественный выбор — сохранить файл в старом формате или сконвертировать его в новый. На самом деле, этот пример, вероятно, является одним из лучших известных мне примеров разумной, услужливой программы, поскольку демонстрирует успешное сочетание гибкости и ненавязчивости. Они используют зарезервированные (и обязательно нулевые) поля для минимизации изменений формата. Это еще один из тех приемов, которые поразительно просты в исполнении и в то же время удивительно редко используются программистами. Когда вы проектируете специализированный формат для хранения данных, вы почти всегда приходите к разбиению его как минимум на две логически различимые секции: заголовок, содержащий «отпечатки пальцев» pi номер версии (а иногда еще какие-нибудь служебные данные), и тело, содержащее собственно данные. После того, как вы определите, что где располагать, вы можете долго и пристально рассматривать свои свежеиспеченные структуры данных, фантазировать насчет самых разнообразных возможных новшеств, но вы почти наверняка не предусмотрите тот самый лишний байт, который вам впоследствии придется или захочется добавить. А спустя две недели после начала распространения программы случается что-то непредвиденное, и вы вдруг замечаете сильнейшее раздражение от того, что вам теперь придется прибегать к расширению формата только потому, что вы с самого начала не включили в него достаточное количество зарезервированных полей. Именно подобные жизненные примеры в свое время убедили меня в том, что Мэрфи (я имею в виду автора одноименных законов) наверняка был программистом. Главный фокус не в том, чтобы просто добавить в заголовок несколько байт и обеспечить заполнение их нулями, это тривиально. Главное — осторожность и аккуратность в тот момент, когда наступает время использовать эти драгоценные зарезервированные поля для реальных, ранее неизвестных целей. Ключевым моментом является тот факт, что
300 Умные файлы л* —4^ все эти ноля всегда заполняются бинарными нулями (или тем, в транслируются эти нули для различных типов данных — число u H пустая строка, FALSE и т. д.). Поскольку нулевое значение всегд- ляется недвусмысленным, полезным и безопасным значением, вы/ жете смело размещать новые данные в предварцТе1' зарезервированных полях-и не беспокоиться о смене формата. Одн.. в некоторых случаях вашему коду придется соблюдать некоторые первый взгляд неожиданные условия игры. Скажем, пусть вы захт ли отвести поле типа BOOL для новой конфигурационной оптг нормальное значение которой равно TRUE, а значение FALSE являе- ся более рискованным pi предназначено для более опытных пользов телей (например, это может быть какая-нибудь подсказка со сторож программы в какой-либо критической ситуации). Здесь, как это н обидно, лучшим решением является использование обратной логики вместо того, чтобы назвать эту опцию \varn_about_something (чтобыц бы наиболее естественным), вам следует назвать ее not_\varn_about_something, а значением по умолчанию сделать, соот ветственно, FALSE. И тогда вам не придется ни изменять формат, hi тратить время на написание утилиты конвертации, ни бояться за опас иость того значения, которое получит ваша новая опция из старых кон фигу рационных файлов. Что касается конкретного количества байт, которое следуй резервировать, то я обычно использую от 20 до 50. Они обеспечиваю! достаточно места для приличного числа маленьких полей и в то же время не дают файлу значительно распухнуть. Если у вас есть веские основания быть уверенным, что вам придется добавить какие-то конкретные поля, не полагайтесь на резервные байты. Вместо этого лучше разместите ожидаемые поля прямо сейчас (даже если они буД}' задействованы только в следующей версии) и используйте что-нибуД" наподобие трюка с «подвешиванием функции на булевскую перемен ную», который я описывал в главе 2. И даже после этого не за6уДьТс включить в формат несколько зарезервированных полей. Когда какие-то из ранее зарезервированных байт пускаются в дело, п1 жалуйста, посвятите часть времени тщательной проверке всех стр^ тур данных п убедитесь, что вы случайно не изменили размер cTpv туры или смещение какого-нибудь элемента этой структуры. Оди11,м простых и эффективных инструментов, помогающих при гаЬ проверках, является утилита для дамшшга бинарных файлов, ° торой я расскажу подробнее чуть ниже.
файлы ланных 301 9 Когда вы начнете возиться с резервными полями, вы можете легко увлечься и очень скоро обнаружить, что в очередной раз делаете какие-то причудливые фокусы лишь для того, чтобы втиснуть в оставшиеся несколько байт еще немножечко дополнительной информации. Всеми силами сопротивляйтесь этой тенденции с самого начала Она является типичным примером противоестественных действий, и в перспективе неизбежно сделает сопровождение вашей программы чудовищной мукой. В конце концов, расширения формата не так уж плохи и страшны. Верьте мне. g Когда это необходимо, они используют расширения формата, зависящие от версии. Не стоит себя обманывать: как бы умело вы ни обращались с резервными полями, рано или поздно настанет момент, когда вам понадобится расширить ваш специализированный формат файла несколькими полями. Как правило, эти новые поля объединяются в отдельную структуру, которая в свою очередь помещается в конец заголовка и образует прослойку между старым заголовком (самым последним расширением формата) и собственно данными. Вам необходимо сделать следующее: 1. Убедитесь, что вам действительно требуется расширение формата. Вам придется тщательно обдумать реальные нужды вашей программы — действительно ли вам необходимы изменения в заголовке файла (и тогда расширение формата будет неизбежно), или будет более разумным изменить лишь формат самих данных? Даже в последнем случае вам все равно следует изменить номер версии формата, чтобы избежать конфликта со старой версией программы — при попытке открыть файл новой версии она не должна пытаться его интерпретировать. Имейте в виду, что иногда вам может понадобиться изменять и формат заголовка, и формат самих данных одновременно. (Например, если вы добавляете еще одно поле в базу данных, то очевидно вам придется поменять формат данных. А если при этом ваша программа позволяет задавать значения по умолчанию для любого поля, то вам придется также поменять и формат заголовка, чтобы отвести место для хранения такого значения для нового ноля.) 2. Определите новую структуру для расширения формата. И не забудьте добавить некоторое количество резервных полей. Может случиться так, что вам потребуется не добавление новой структуры, а серьезное изменение всей старой структуры. Это не беда, однако два особых поля заголовка — «отпечатки пальцев» и номер версии формата — обязательно должны остаться на прежних местах для совместимости со всеми версиями программы.
302 Умные файлы л* Ж ^^Jj. Лучший способ изоляции этих двух полей — поместить отдельную структуру и потом никогда не менять эту структу0 протяжении всей жизни программы. # * '' Я решительно предпочитаю использование расширений, По потому, что, с точки зрения перспективы, расширения бот облегчают жизнь, чём введение совершенно новых формат Полное переопределение формата неизбежно заставит Ва " программу тратить массу сил на преобразование данных старого формата в новый всякий раз, когда она встретит старь- файл. Почти всегда гораздо легче просто оставить стары структуры без изменения и лишь добавить новые. 3. Добавьте в код программы новую (или измените старщ структурную переменную, соответствующую новому формату < инициализируйте ее значениями по умолчанию. Эта переменна? должна находиться в такой области видимости в программе которая наиболее всего подходит для соответствующих данных Например, если речь идет о конфигурационных данных, то соответствующая структурная перемеиая, как правило, должна быть глобальной для всей программы. (Наверное, дочитав книп до этого места, вы немало удивитесь, что такой параноик, как я вдруг предлагает поместить что-то в глобальную область видимости в программе. Опыт показывает, что для некоторых данных, в частности — для конфигурационных данных, такой подход является весьма и весьма осмысленным, поскольку эти данные в конце концов считываются и записываются в самьп разных местах программы.) 4. Выберите новый номер версии для новой разновидной^ формата файла. Помните, что последовательность номеров версий должна быть монотонной: если версия 2.1 используй исходную версию формата, и вы добавляете расширение Р версии 3.0, то все версии, следующие за 3.0, также должн* использовать это расширение. 5. Модифицируйте код программы так, чтобы он проверял ноМ версии и затем считывал расширение из файла только в т случае, если оно там на самом деле присутствует. При встреЧ файлом старого формата никаких проблем не будет — 3t\ программа будет работать с правильными значениями умолчанию, которые использовались для инициализации ДаН11 [ и пункте 3. При встрече же с новой версией файла эти значе ' по умолчанию благополучно перекроются данными, считай^' из файла.
303 Решите, как вы будете осуществлять сохранение файла. Если ваша программа будет автоматически преобразовывать файл к новому формату, то она должна будет всегда использовать и новый номер версии, и новое расширение формата файла. В зависимости от типа вашей программы и сути этих данных, вы можете предоставлять пользователю возможность выбора формата при сохранении файла (подобно тому, как это делает Word for Windows). 7. Скорректируйте должным образом утилиту для дампинга файла, чтобы она могла правильно работать с новым расширением формата. В Они используют идиому «обнаружения и переименования подмены» для файлов с фиксированным местоположением на диске. В основном, это касается конфигурационных данных. Когда вы сохраняете файл в каталоге Windows или в другом «популярном» месте файловой системы, всегда существует вероятность столкнуться с конфликтом имен файлов. Каким бы уникальным ни казалось вам имя, выбранное для вашего конфигурационного файла, конфликт может произойти. Разумеется, я вовсе не утверждаю, что есть хоть сколько-нибудь значительный риск одновременного выбора несколькими программистами имени BAZOOKA.DAT для конфигурационных файлов своих программ. Но даже с такими именами нельзя заблуждаться насчет безопасности: в конце концов, пользователь .может просто переместить, переименовать, удалить этот файл или подменить его в то время, когда ваша программа за этим никак не следит. Именно поэтому мои программы, работающие с умными файлами данных, всегда проверяют файл не только, когда считывают его, по и когда собираются такой файл перезаписать. Если пользователь случайно (или умышленно, не предполагая о возможных последствиях) поместил другой файл в то место, в котором ваша программа рассчитывает найти свой конфигурационный файл, вы не можете просто так взять и переписать его, уничтожив данные пользователя. (Конечно, я допустил некоторую вольность, использовав только что слово «не можете», — я видел много программ, которые именно это и делают. Но, надеюсь, вы поняли мою мысль.) Решение, которое я предлагаю, — это идиома «обнаружения и переименования подмены». Всякий раз, когда ваша программа собирается прочитать или перезаписать свой конфигурационный файл, °на должна предварительно проверить «отпечатки пальцев» и номер версии этого файла. Если этот файл в самом деле окажется «ее файлом» , то программа может продолжить работу — читать или перезапи-
304 Умные файлы л*. з: -jja^ сывать его с чистой совестью. Но если файл на поверку 0кц> «чужим», то программа должна переименовать этот файл (Чт'(/ сохранить его содержимое) и известить пользователя о случивще\ / Я хотел бы чуть-чуть задержаться на одной маленькой детали, упомяну выше, - на перекрытии значений по умолчанию при считывании даиных файла. Строго говоря, эта деталь не рассматривается как обязательная хап- теристика умных файлов данных. Однако, этот прием настолько хорошо cpaf тывает практически во всех случаях, что я использую его почти без раздув Как я описывал выше, прием заключается в создании структурных перемер ных, соответствующих рассматриваемым данным, и их инициалнзаци значениями по умолчанию. Когда данные считываются из файла, они про^ перекрывают значения по умолчанию. А когда нужно записать файл, в неге просто сбрасывается то, что находится в структурных переменных. Выгода такого подхода проявляется не только при работе с расширениями формата, не и просто в отсутствии конфигурационного файла (например, такой файл может отсутствовать на диске при первом запуске вашей программы после ее инсталляции). В этом случае программа будет замечательно работать с тщательно выбранными значениями по умолчанию. Во время ее работы она будет изме нять эти значения прямо в памяти и при необходимости запишет их на диск Я уже несколько раз упоминал утилиту для дампинга файла, которую вам следует создать и поддерживать при всех изменениях формата хранения данных. Поскольку эта утилита является чисто приватной программой, она не должна быть сложной и навороченной. Например, она может быть выполнена в виде простой маленькой утилиты, которая считывает ваш специализированный файл данных и показывает его содержимое в форматированном, удобно: для человеческого глаза виде. Если файл может содержать очень много дан ных, следует сделать так, чтобы эта утилита умела записывать результат!/ своей работы в чисто текстовый файл; если же данных немного (как в примера для данной главы), то вам будет достаточно простого 32-разрядного консоль ного приложения. Вы обязательно убедитесь, что такая утилита сослужит^ отличную службу при тестировании вашей программы и создаваемых ею Фа1! лов. Кроме всего прочего, утилита для демпинга может оказаться отличным^ мощником при обнаружении проблем с выравниванием данных. В ДаНН° случае иод выравниванием данных понимается физическое размещение по11 в ваших структурах данных, определяемое компилятором. При одпобап?015 выравнивании никаких проблем быть не должно, в других же случаях ^0% происходить так называемая «набивка» ваших структур данных — .вставке полнительиых байтов между полями для того, чтобы адрес начала ка^ поля был кратен двум, четырем или восьми (наиболее типичные значен*1*1 ции выравнивания у компиляторов языка С). В результате такой наб*1
хранения конфигурационных ланных 305 tec кий размер вашей структуры данных мождет отличаться от jil3 четкого» (от суммы физических размеров ее полей). И если ваша 10 чмма и файл на диске не согласованы по части размеров структур дан- •'° серьезные неприятности вам гарантированы. Метод борьбы с этой пробле- Ь ювольпо прост: выберите с самого начала какой-то вариант выравнивания °'' сцечьте его неизменность в будущем. В программе-примере для данной я сделал это при помощи стандартных заголовочных файлов РОР- ,'\ГК Н и PSHPACK1.H из Win32 SDK — включив их в мои собственные замочные файлы, содержащие описания структур данных, я, тем самым, /,спечил однобайтовое выравнивание этих структур. ]/[ последнее замечание по поводу утилиты для дамиипга: она должна по- зьпзать все поля так, чтобы вы могли с максимальной определенностью оценивать происходящее. Например, если в вашу структуру входит такая пара по- ieii, как текстовый буфер и реальная длина текста в этом буфере, то не следует выводить только лишь начало этого буфера — выводите его целиком, а также показывайте значение второго поля (как отдельный элемент данных). Ана- юшчио, во многих случаях вы получите серьезный выигрыш, если ваша ути- шга даст вам возможность смотреть на ваши данные не только физически, но и югнчески (например, при показе значений зарезервированных полей утилита может выдавать броское предупреждение в том случае, если эти поля имеют ненулевые значения). Программирование такой утилиты может справедливо показаться весьма утомительным и скучным делом, однако овчинка стоит выделки — эта утилита наверняка поможет вам обнаружить некоторые ошибки задолго до того, как это сделают пользователи финальной версии вашей программы. Щшер хранения конфигурационных данных Решил не создавать никакой программы-примера специально для данной гла- Ь1' поскольку счел более правильным привести в качестве такого примера ту !|1,0ТУ <•' умными файлами данных, которую проводит программа MegaZero из Ложения А. Эта простая программа запоминает имена последних десяти Лов. вброшенных на се окно, а также дату своего последнего запуска. Эти Ь1е хранятся в дисковом файле MEGAZERO.DAT в каталоге Windows ,,ь,11*1о в C:\WlNDOWS). . На листингах 9.1 и 9.2 приведены файлы CONFIG.H и CONFIG.CPP - 'еги исходного кода MegaZero, которые имеют отношение к теме данной 0}Калуйста, обратите особое внимание на следующие детали:
306 Умные файлы л-» Л 4^ В Использование заголовочных файлов PSHPACK1.H и POPPACju CONFIG.H. Это обеспечивает желаемое выравнивание (по границ ного байта) для интересующих нас сохраняемых данных и, в То' время, не дает распространить это выравнивание на другие файлы пользующие CONFIG. H. В Оба компонента файловых данных (config_header и config) сч ваются и записываются только в одном месте. Такая централизап чтения/записи облегчает сопровождение кода и обеспечивает больцг безопасность. Разумеется, этот подход годится и для многих дрУ1 аналогичных случаев, но он имеет особое значение именно ^ сопровождении кода, работающего с бинарными файлами, когда ш внесении очередного «несущественного изменения» в программу I большие ошибки могут незаметно закрадываться в код или файлы д^ ных и потом очень долго никак не проявлять себя. В При том оформлении кода, которое вы видите, основная программ может делать лишь две вещи — вызывать функцию LoadConfigData( при запуске и функцию SaveConfigDataO при завершении работь Обратите внимание, что LoadConfigDataO никак не показывает вьш вающему коду, есть ли на диске правильный конфигурационный фай (ранее существовавший или только что созданный). При этом основ ная программа будет работать без каких-либо проблем — она будет ж пользовать либо реально считанные данные, либо соответствуют^ значения по умолчанию. Но в некоторых случаях (главным образом это зависит от природы самой программы) такой подход может быт. недостаточно хорошим. И тогда вы можете слегка модифицировав функцию LoadConfigDataO так, чтобы она возвращала значение ш BOOL, индицирующее успех или провал, а сама программа могла б* элегантно отказываться работать в том случае, когда LoadConfigD1 ta() возвращает FALSE. В В примере экстенсивно используется функция OutputDebugStiw Мой личный опыт показывает, что это может сохранить массу ваши нервов и времени при работе с бинарными файлами и при полЫТК' раскрыть всевозможные таинственные проблемы, связанные с ниМ' Например, при выяснении, почему же новая версия вашей програ>{} упорно отказывается распознавать и правильно обрабатывать стар. версию файла данных. В подобных случаях функция OutputDeD" StringO и утилита WINDBG из Win32 SDK могут оказаться ваШ«} лучшими помощниками. В Код использует локальную подпрограмму GetBaseDirectoryO ^ определения каталога, в котором должен находиться файл даннЫ*- первый взгляд, данная реализация GetBaseDirectoryO может $
конфигурационных ланных заться лишь претенциозным украшением, так как она представляет собой ничто иное как простую обертку стандартной функции GetWin- dowsDirectoryO. Однако, именно такая «мишура» является тем самым дополнительным уровнем абстракции, который может реально сохранить вам жизнь в тот момент, когда вам понадобится или захочется изменить поведение программы. Например, вы можете захотеть, чтобы при работе под Windows 3.1 или при запуске с сетевого диска ваша программа хранила свой конфигурационный файл в главном каталоге Windows, а при работе под Windows 95 — в своем домашнем каталоге или в специальном каталоге данных. При наличии вышеупомянутой «простой обертки» вы получаете несомненный выигрыш и в скорости выполнения соответствующей модификации кода, и в безопасности: необходимое изменение кода оказывается предельно локализованным. Всякий раз, когда вы работаете с каким-либо файлом в Windows, пожалуйста, постарайтесь исключить любую возможность случайного использования неполного имени файла (то есть его имени без указания диска и полного пути). Если вы используете «голое» имя файла, Windows будет предполагать, что файл находится в текущем каталоге текущего диска (которые могут сильно отличаться от того, что ожидаете вы и ваша программа). Это еще один представитель того самого класса странных мелких ошибок, которые могут целыми днями сводить вас с ума: вы будете долго пытаться понять, какого дьявола ваша программа не может прочесть файл, который заведомо существует, или почему программа то и дело создает свои файлы в совершенно неожиданных и странных местах на диске Если вы выработаете в себе привычку пользоваться функциями, подобными BuildFNO в CONFIG. CPP, у вас будет намного больше шансов избежать подобных приключений. 8 Очень критичной частью кода является функция IsOurFileO — именно ее задачей является взгляд на открытый файл и четкое определение, является ли он тем, что ожидается. Если файл оказывается слишком коротким или в нем отсутствуют искомые «отпечатки пальцев», эта функция просто возвращает FALSE. Если используется одно или несколько расширений формата, именно эта подпрограмма считывает Данные согласно указанному в файле штампу с номером версии (в этом случае функция возвращает TRUE). Менее очевидным является ответ па вопрос, что делать, когда «отпечатки пальцев» на месте, но номер версии оказывается неизвестным. Очевидно, функция IsOurFileO не имеет права воспринимать этот файл как пригодный для дальнейшей °бработки, однако она обязательно должна либо выдать пользователю хранения
308 Умные Файли. ——£2^ соответствующее сообщение (при помощи стандартной функцш sageBoxO или еще как-нибудь), либо каким-то образом оставить нейшую обработку такой ситуации на усмотрение вызывающей лО ции. Как обычно, единственно правильного рецепта для лч случая не существует, — все зависит от контекста вашего проект природы вашей программы и ее пользователей. Mega2err Листинг 9.1. CONFIG.H из #ifndef __C0NFIG_H__ #define __C0NFIG_H__ «include <pshpack1.h> // Хотим однобайтовое выравнивание - пожалуйста void LoadConfigData(void), void SaveConfigData(void), struct config_type { char fn1[MAX_PATH], char fn2[MAX_PATH], char fn3[MAX_PATH], char fn4[MAX_PATH]; char fn5[MAX_PATH]; char fn6[MAX_PATH]; char fn7[MAX_PATH]; char fn8[MAX_PATH]; char fn9[MAX_PATH], char fn10[MAX_PATH]; int last_run_inonth, int last_run_day; int last_run_year, BOOL locked, char password[MAX_PW_LEN +1]; mt reserved[10]; }; // Наш глобально доступный масив конфигурационных данных extern config_type config; // Наш заголовок, который обычно не используется никем извне данной // единицы компиляции #defme eye_catcher_len 9 struct config_header_type { char eye_catcher[eye_catcher_len], WORD major_version; WORD minor_version, ». «include <poppack h> // Возвращаемся к тому выравниванию, которое было раньше #endif Листинг 9.2. CONFIG.CPP из №&1* /* Copyright Lou Grinzo 1995 Zen of Windows 95 Programming */ #include "stdafx.h" #include "config.h' #include 'io.h
хранения конфигурационных ланных 309 Clude tb.int h 'toolbox h" -include , и ui глобальный массив конфигурационных данных, который будет перекрыт '! итаннымч из файла данными (при условии, что этот файл существует) ;onfx9_type config = [no Getting] . [n0 Getting] . [no Getting] . [no Getting] [no Getting] [no Getting] [no Getting] [no Getting]" [no Getting] . [no Getting]", 0. 0, 0 // fn1 // fn2 // fn3 // fn4 // fn5 // fn6 // fn"7 // fn8 // fn9 // fn10 // laGt_run. // laGt_run_ // laGt_run_ FALSE, // locked - , { 0, 0, 0, 0, 0, 0. 0, 0. 0. 0 // password // reserved ; .month .day .year static char data_fn[] = '"megazero dat", config_header_type config_header = megazero", // eyecatcher (9 chars, mcl term null) 1, // major_version 0 // minor_vet'Gion i • /V///////////////////////////////////////////////////////////////////////// // Заголовки для локальных подпрограмм Hlfl////////////I////////////////////////////////////////////////////////// static BOOL BuildFN(char *fn, size_t max.len), static BOOL CreateNewFile(void), ^atic UINT GetBaseDirectory(LPTSTR IpBuffer,UINT uSize), static BOOL IsOurFile(FILE *f) static BOOL Renamelmpostor(void), static void SetUpData(void), static BOOL StoreData(FILE *f), static BOOL strlcat(char *dect. conGt char *src, size_t max_len). ^atic BOOL strlcatAtomic(char *dest, const char *src, size_t max_len), ^jflllll/llllllllllllll/lII till/IlllllllllllIIIllll/lll/ttlttlltllllllllIII 'I Экспортируемые подпрограммы ';!; II///////1///III III II III III//IIIlll/l/lIII/I II III III I III II III IIIIII/I III ')/ili///////////ll///llllllll/ll//llllll/lllllll///l/l//lll//lll/lllll/llll '' L°sdConfigData() Загружает в память массив конфигурационных данных, считанный из нашего специализированного файла данных Эта функция ', обР°батывает все патологические случаи файл не найден, файл найден, но в Действительности не является нашим файлом, и т д Если пригодного файла е нашлось, то все значения по умолчанию для configjieadet и config , °*Раняются, и их можно использовать как есть \^1'11/1//////(III И//////!!!//////(/!//////!/////(///////(////11//1/ПП/ - L°adConfigData(void) Char fn[MAX_PATH],
310 Умные файлы л FILE *f, if(lBuildFN(fn,Gizeof(fn))) { tfifdef .DEBUG OutputDebugStrmgC'*** LoadConfigData Невозможно сконструировать имя файла дани #endif ** return; i // Обработать особый случай и убрать его с дороги if('FileExists(fn)) { #ifdef .DEBUG OutputDebugStrmgC'*** LoadConfigData Файл данных отсутствует; \п"); OutputDebugString("создаем новый.\n"), #endif CreateNewFile(), return; i if((f = fopen(fn,"rb")) == NULL) { tfifdef .DEBUG OutputDebugStrmgC *** LoadConfigData Невозможно открыть файл данных \п ), #endif return, if(IsOurFile(f)) { #ifdef .DEBUG OutputDebugStrmgC"*** LoadConfigData IsOurFileO == TRUE\n"), #endif // Перешагиваем через заголовок, который уже прочитан fseek(f,sizeof(config_header),SEEK_SET); if(fread(&config,sizeof(config),1,f) == 0) { #ifdef .DEBUG OutputDebugStrmgC*** LoadConfigData: Невозможно прочитать файл данных\п"), #endif i // Если бы нам было нужно обрабатывать более чем одну версию файла данных, // то именно в этом месте кода мы прочитывали бы те или иные расширения // формата, в зависимости от номера его версии. fcloGe(f); j. else i #ifdef .DEBUG OutputDebugStringC'*** LoadConfigData: IcOurFileO == FALSE\n"); #endif fcloce(f); if(RenamelmpoGtorO) CreateNewFile(); 11//l/l/llIII I Ill/Ill/11/III IIIl/ll/lIII/I/I/!///Il/ll//III/////////I/////I/ II SaveConfigDataO: Сохраняет находящийся в памяти массив конфигурационных // данных в конфигурационном файле, имеющий специальный заголовок. Эта функция // обрабатывает все патологические случаи1 файл не найден; файл найден, но
*е£ «ранения конфигурационных ланных 311 ойгтвительности не является нашим файлом, и т. д /saveConfigData(void) cnar fn[MAX_PATH]; PILE *f. ,f('BuilciFN(fn, cizeof(fn))) -lfdif -DEBUG OutputDebugStringC'*** SaveConfigData. Невозможно сконструировать имя файла данных.\nM); -endif return; i // Как же это могло случиться9 Возможно, пользователь или какая-то другая // программа удалили наш файл данных, пока мы работали? Не важно. В любом // случае эту ситуацию можно обработать. jfCFileExistcCfn)) { «ifdef _DEBUG OutputDebugStnng( *** SaveConfigData- Файл данных отсутствует,\п'), OutputDebugString( создаем новый.\п"), fiendif CreateNewFileO. return, // Открываем файл if((f = fopen(fn,"rb+ )) == NULL) { Jfifdef .DEBUG OutputDebugStringC'*** SaveConfigData. Невозможно открыть файл данных \п"), tfendif return; // если он наш lfdsOurFile(f)) { tfifdef _DEBUG OutputDebugStringC*** SaveConfigData. IcOurFileO == TRUE\n"); uendif // .. записываем заголовок и данные... StoreData(f); // . .и дело сделано. fclose(f); else Wdef _DEBUG OutputDebugStringC'*** SaveConfigData1 IcOurFileO == FALSE\n"), -endif // ..если файл не наш, то мы закрываем старый файл, оставляя его // нетронутым, переименовываем его, а затем создаем новый файл и // сбрасываем в него наши конфигурационные данные из памяти. fcloGe(f); lf(RenaineImpoGtor()) . CreateNewFileO;
312 Умные файлы //////////////////////////////////////////////////////////////////////////// // Локальные подпрограммы //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// // BuildFN() Конструирует полное имя нашего файла данных Результирующая // строка помещается прямо в буфер, предоставленный вызывающим кодом // // Возвращает TRUE, если строка была успешно сконструирована В противном // возвращает FALSE // // max_len включает завершающий нуль //////////////////////////////////////////////////////////////////////////// static BOOL BuildFN(char *fn, size_t max_len) { UINT re = GetBaseDirectory(fn,max_len); if((rc ==0) || (re > max_len)) { #ifdef .DEBUG OutputDebugString("*** BuildFN: Невозможно выяснить базовый каталог \п"), #endif return FALSE, } // Нам не нужно проверять, сделала ли что-нибудь функция AppendSlash и было // ли достаточно места для завершающего символа обратной косой черты (если // ее нужно было добавить). Функция strlcatAtomic() обработает за нас // проблему нехватки места. AppendSlash(fn,max_len - 1), return strlcatAtomic(fn,data_fn,inax_len), i )l11/III II/11//I III I/I/III III/1 III IIIIII/I IIII/I I/1/I/III I/11 III/1/I IIIII III II CreateNewFile() Создает новую копию нашего специализированного файла // данных, заполняя его данными, находящимися в памяти Обратите внимание, // что эта функция НЕ проверяет наличие уже существующего файла - она молча // грохнет одноименный файл Все мероприятия по обнаружению такого файла и // сохранению его содержимого должны быть проведены до вызова данной функции1 // // Возвращает TRUE, если файл был успешно создан, в противном случае // возвращает FALSE // //////////////////////////////////////////////////////////////////////////// static BOOL CreateNewFile(void) { char fn[MAX_PATH], FILE *f; if('BuildFN(fn,sizeof(fn))) { #ifdef _DEBUG lf OutputDebugStringC'*** CreateNewFile* Невозможно сконструировать имя файла данных #endif return FALSE, i if((f = fopen(fn, wb')) ' = NULL) { BOOL re = StoreData(f); fclose(f), return re;
хранения конфигурационных ланных 313 else .,fdef -DEBUG OutputDebugStringC'*** CreateNewFile: Невозможно открыть файл данных\п"); tfndlf га, ОС return FALSE, '///////////////////////////////////////////////////////////////////////// '//6etBaseDirectory() Определяет каталог, используемый для хранения нашего / файла данных В текущей версии это всегда главный каталог Windows, и //поэтому данная функция имитирует синтаксис GetWindowsDirectory(). lllllllllllllllllllllllll/llllllllllllllllllllllllllllllllllllllllllllll/ll yiNT GetBaceDirectory(LPTSTR lpBuffer,UINT uSize) ' !f(lpBuffer == NULL) { mfdef _DEBUG OutputDebugString( *** GetBaseDirectory Обнаружен нулевой указатель\п"), #endif return 0, return GetWindowsDirectory(lpBuffer,uSize); i /////////////////////////////////////////////////////////////////////////// //IsOurFile()- Проверяет заданный открытый файл и определяет, является ли // он носителем данных в нашем специализированном формате. // // Если файл является "нашим", возвращает TRUE, в противном случае возвращает // FALSE Вне зависимости от содержимого файла, по окончании работы этой //функции позиция чтения/записи всегда выравнивается влево (в начало файла). /////////////////////////////////////////////////////////////////////////// static BOOL IsOurFile(FILE *f) { // Проверяем длину файла. Если она не совпадает с тем, что нам нужно, // значит файл точно не 'наш". lf(_filelength(_fileno(f)) != sizeof(config_header_type) + sizeof(config_type)) return FALSE; // Проверяем содержимое заголовка config_neader_type testjieader;7 lf(fread(&teGt_header,sizeof(test_header),1,f) < 1) return FALSE, ' ^азад, в начало файла. f;/eek(f.0,SEEK_SET), '' Если нужный маячок не найден, значит файл не "наш". lf('neinciT)p(&test_header eye_catcher, &config_header. eye_catcher, sizeof(tect_header eye.catcher)) '= 0) return FALSE; ' пока мы признаем только версию 1.0 ((teot_header major_version > 1) || (test_header.minor_version > 0)) // Возможно, здесь нужно будет выдать пользователю сообщение о // проблеме с версией, return FALSE, Делаем это копирование, чтобы другие части программы могли корректно
314 Умные файлы л —-% // использовать заголовочную информацию, включая код версии. inemcpy(&config_header,&teGt_header,sizeof(test_header)), return TRUE; } /////////////////////////////////////////////////////////////////////////// // RenaineImpoGtor() Кто-то другой определил, что существует файл точно с // тем же полным именем, которое мы хотим использовать, и что этот файл не // является 'нашим' Поскольку нам неизвестно содержимое этого файла, мы не // можем просто удалить его Но нам нужно сохранить наши данные, поэтому мы // сначала переименовываем блокирующий файл /7 // Данная функция НЕ проверяет ни существование блокирующего файла, ни его // формат' // // Возвращает TRUE если переименование завершилось успешно, в противном // случае возвращает FALSE /////////////////////////////////////////////////////////////////////////// static BOOL RenainelmpoGtor(void) { char blocking_fn[MAX_PATH], new_fn[MAX_PATH], win_dir[MAX_PATH]; if(! BuildFN(blocking_fn,3izeof(blocking_fn))) { #ifdef .DEBUG OutputDebugStringC *** Renamelmpostor Невозможно сконструировать имя блокирующего фа, \гГ), #endif return FALSE, } UINT re = GetBaGeDirectory(win_dir,cizeof(win_dir)); if((rc ==0) || (re > sizeof(wm_dir))) { #ifdef _DEBUG OutputDebugStringC*** Renamelmpostor: Невозможно определить базовый каталог\п ), tfendif return FALSE. i if(GetTempFileName(win_dir,"OLD",0,new_fn) == 0) { #ifdef .DEBUG OutputDebugStringC'*** Renamelmpostor. Невозможно сконструировать "); OutputDebugStrlng("имя временного файла.\п"); #endif return FALSE; } #ifdef .DEBUG OutputDebugString('*** Renamelmpostor: имя блокирующего файла- "), OutputDebugStnng(blocking.fn); OutputDebugStringC \n*** Renamelmpostor новое имя файла- "); OutputDebugStnng(new.fn), OutputDebugString("\n' ); #endif if(CopyFile(blocking_fn,new_fn,FALSE)) { char buffer[1024], wsprintf(buffer, "Ваш существующий файл: \п \"%з\"\пбыл переименован:\п \'%s\ \п\пВам нужна помощь9", blocking.fn,new.fn);
хранения конфигурационных ланных 315 1f(MessageBox(0,buffer,"Внимание1',MB_YESNO | MB_ICONEXCLAMATION | MB.JASKMODAL) == IDYES) MessageBox(0, Представьте, как в этом месте появляется превосходная, интуитивная, контекстно зависимая подсказка ", ■Помощь",МВ_0К | MB_IC0NINF0RMATI0N | MB_TASKMODAL), return TRUE; else glfdef -DEBUG OutputDebugStringC jtendif return FALSE; Renamelmpoctor: ПРОВАЛ.\n"); qillllllllIII III III IIIIIIIIIIIIIII III 11II III/IIIIllllIIII11III III IIIIIIIII II SetUpDataO Устанавливает текущую дату в соответствующие поля нашего '/ массива конфигурационных данных, а также гарантирует, что все строки с // именами файлов должным образом завершаются. /////////////////////////////////////////////////////////////////////////// static void SetUpData(void) // Заносим сегодняшнюю дату в заголовочные поля timejt now, tin now2, time(&now), now2 = *localtnne(&now), config last_run_month = now2 tmjnon + 1; // месяц отсчитывается от нуля ('?) config last_run_day = now2 tmjnday; config last_run_year = now2 tm_year, // Если мы собираемся сконвертировать файл данных в формат новой версии, // тогда именно в этом месте следует должным образом обновить мажорный и // минорный номера версии. // Вероятно, это смотрится как вершина паранойи, но по крайней мере это // гарантирует нам, что все переменные с именами файлов, что бы в них ни // оказалось, завершены нуль-символом config fn1[sizeof(config.fn1) - 1] = '\0\ config fn2[sizeof(config fn2) - 1] = '\0' config fn3[sizeof(config fn3) - 1] = '\0' config fn4[cizeof(config fn4) - 1] = '\0' config fn5[cizeof(config.fn5) - 1] = '\0' c°nfig fn6[sizeof(config.fn6) - 1] = '\0' c°nfig fn7[Gizeof(config fn7) - 1] = '\0* C0^ig fn8[sizeof(config fn8) - 1] = '\0' Config fn9[cizeof(config fn9) - 1] = '\0' Config.fn10[cizeof(config.fn10) - 1] = *\0* return, 'l/^JI/l////////////////////////////////////////////////////////////////// II t°reDataFile(): Сохраняет заголовок и сами данные в заданный открытый I Файл (предполагается, что файл открыт для бинарной записи). II Возвра // Gtati, 'ащает TRUE, если запись прошла успешно, в противном случае ^звращает FALSE ЦП!/!!//////////////////////////////////////////////////////////////// B00L StoreData(FILE *f)
316 Умные файлы { // Очищаем данные, обновляем номер версии и т д. SetUpDataO, if(fwrite(&config_header,sizeof(config_header) 1,f) < 1) #ifdef _DEBUG OutputDebugStnng( *** StoreData Невозможно записать в файл данных\п'), #endif return FALSE, i // Если у формата заголовка есть какие-то расширения, зависящие от версии, // их следует записать в файл именно здесь. if(fwrite(&config,cizeof(config), 1, f) < 1) #ifdef _DEBUG OutputDebugStringC'*** StoreData Невозможно записать в файл данных\п ), #endif return FALSE, // Если у формата самих данных есть какие-то расширения, зависящие от версии, // их следует записать в файл именно здесь fclose(f), // Если мы дошли до этого места, значит все прошло согласно плану return TRUE, ^S.
lAHX. 0страгирование I (действии: создание своих мртуаАъных машин Works of art, in my opinion, are the only objects in the material universe to possess internal order, and that is why, though I don't believe that only art matters, I do believe in Art for Art's sake. E. M. Форстер By indirections find directions out. Уильям Шекспир. «Гамлет» Ькуже было сказано где-то в этой книге, я уверен, что именно функциональное абстрагирование является самым мощным и стоящим в стороне от других ,,НстРУментом для создания полезных, надежных и переносимых программ. Абонирование может выступать в самых разнообразных формах — от удобных Эффективных локальных программ, до старательно продуманных и спроек- ,1Р°ваиных библиотек. В этой главе я собираюсь поговорить о том, как вы мо- v Ге использовать абстрагирование в ваших собственных программах, а также r°Nl» какие проблемы и приемы специфичны именно для этой области )0гРаммИрования. ***, ОД это надо? 1 C'i\ ,r,0VIOM деле> вы постоянно используете инструмент абстрагирования в своей , • хотя при этом, возможно, и не всегда думаете об этом понятии. (В кон- \и **0в> даже использование подпрограмм является простейшим примером 11°налыюго абстрагирования.) Самый быстрый и прагматичный ответ на
318 Абстрагирование в лействии: созлание своих виртуальных поставленный выше вопрос можно было бы сформулировать так: мыщле терминах абстракции, а также сознательное решение использовать (илин^ пользовать) его в каждом проекте имеет решительное влияние на уСп ' качество вашей работы. Чтобы проиллюстрировать мое высказывание об использов абстрагирования как инструмента,-давайте обратимся к более традици0н, взгляду на проектирование программного обеспечения. Общая стратегия п ния многих задач описывается общеизвестной формулой: разделяй и властв» Вы берете какую-либо большую и сложную, изначально невыполнимую зал- (например такую, как сортировка огромного набора записей) и начинав физически делить ее на меньшие подзадачи до тех пор, пока каждая из по- задач не окажется разрешимой или по крайней мере приемлемой по объем* Аналогично, программисты могут использовать абстрагирование для разделе ния всей дистанции на несколько уровней абстракции, и тем самым преодолев расстояние до требуемого результата несколькими маленькими и четкими ш; гами вместо одного огромного и рискованного прыжка. Между абстрагированием в программировании и примером с сортировка записей есть еще одна аналогия: в обоих случаях наблюдается нелинейность Например, имея простой алгоритм сортировки записей, загруженных прямо: память, вы смогли бы обработать 1000, 10000 или даже 100000 записей при по мощи одного и того же простого и понятного куска кода (конечно же, продол жительность такой сортировки сильно возрастает при увеличении количеств^. обрабатываемых записей; но в данном случае речь идет не о скорости и времен! работы алгоритма, а о том, что именно один и тот же алгоритм пригоден дя всех перечисленных объемов задачи). В какой-то момент вам может поняло биться произвести сортировку десяти или двадцати миллионов записей, итог> вы вдруг обнаружите, что ваш прежний алгоритм больше не может справиться с задачей, независимо от того, сколько времени вы можете ему на это отвести это алгоритм просто-напросто упирается в свои архитектурные ограничения, < его необходимо заменить каким-то другим алгоритмом, вероятно, 11С пользующим дисковую память. Таким образом, при увеличении объема зада4' время работы исходного алгоритма фактически выросло до бесконечно^1 (Признаюсь, этот пример с сортировкой является сильно надуманным, так^ в реальной практике никто не сортирует такие огромные объемы данных: очевидным соображениям производительности предпочтительным явля^1 изначальное хранение данных в нужном порядке и полное исключение эТ<1 сортировки.) При разбиении логической задачи на части или при столкновении с ^ таточно сложной задачей мы видим точно такую же нелинейность. Добавь^ сколько функций и констант к какому-нибудь API, и программисты, отр^ ких же смельчаков как мы с вами, успешно воспримут это и примутся заД ^ Но стоит добавить сотню-другую новых деталей, и мы вскоре чувству^1
нало? 319 ,^JT°- в1цймися в тумане, а любая работа становится невозможной без герку- № усилий. Именно в этот момент мы попросту прекращаем работать и "С° ем озираться вокруг в надежде найти какое-нибудь решение в виде 11 сторонней библиотеки или каркасной библиотеки. То есть мы начинаем е1 те самые виртуальные машины, которые обеспечивают более высокую, а практичную (а иногда и более надежную и стабильную) платформу для ч дальнейших построений. Именно путем создания промежуточных уров- \ ретракции вы можете в конечном итоге значительно увеличить эффектив- ,[Ь своей работы. Так же, как это бывает с другими особенностями программистского ремес- абстрагированием можно слишком увлечься, и тогда вместо написания заученных программ вы начнете бесконечно создавать и усовершенствовать •вой программистский инструментарий. (Эта опасность действительно очень ерьезна, поскольку во многих случаях конструирование инструментария является гораздо более приятным делом, чем написание законченных приложений.) И тем не менее, вашей целью должна быть такая архитектура, при которой код законченной программы был бы больше похож на псевдокод, чем на демонстрацию возможностей вашего языка программирования. Я очень много раз наблюдал любопытнейшее явление: во время проектирования программист пишет прекрасный, очень понятный и читабельный псевдокод; а затем этот код постепенно превращается в код Медузы (при непосредственном взгляде на такой код вы рискуете немедленно превратиться в камень). Даже если перед вами стоит шая простая задача, как один-единственный раз сосчитать среднее арифметическое, код void IssueWarning(DATA *d, mt nuin_obs) { float mean = CalcMean(d, nuin_obs), if (mean < mmjnean) MesoageBox(0, Данные недостаточно хороши1'. Простецкий пример'",МВ_0К); elce if(mean > maxjnean) MecsageBox(0,"Данные слишком хороши1V Простецкий пример ",МВ_ОК); float CalcMean(DATA *d, int num_obs) fbat total = 0; for(mt i = 0; i < num_obs; i++) total += d[i]; , retum total / num.obs, аг°трится намного лучше и читается намного легче, чем ^°ld IssueWarnmg(DATA *d. int num_obs) for(mt i = 0; I < num_obG, I++) total += d[i]. i0*t mean = total / num_obG; (,r,ean < minjnean) eGGageBox(0, 'Данные недостаточно хороши! ',"Простецкий пример \MB_0K);
320 Абстрагирование в лействии: созлание своих виртуальных л, elce if(mean > maxjnean) МеззадеВох(0. Данные слишком хороши1 , Простецкий пример ,МВ_0К), Если бы C/C++ поддерживал локальные функции, первый пример Мо., было бы оформить еще лучше, сделав функцию CalcMeanO локальной д-, , sueWamingO; но это тема для обсуждения в другое время п в другом месте ( главу 5). Суть в том, что при малейшей возможности любой участок кода полняющий отдельную логическую функцию, следует изолировать, если ^ ко не существует какой-нибудь серьезнейшей причины этого не делать. Иногда такой подход к программированию может отрицательно сказь ватъся на производительности, потому что сам вызов функции, а также вся.^ возня с передачей параметров через стек являются вовсе не бесплатными О нако, реальная цена этих дополнительных действий, по крайней мере в о( щепринятых терминах производительности, почти никогда не является тако, высокой, как это декларируют программисты. Это особенно справедливо в то* случае, когда вы уже используете какую-нибудь каркасную или прост. большую библиотеку, которая добавляет к вашей программе 200 Кб (in, требует присутствия 'динамической бибилиотеки времени исполнения размере в 300 Кб). В такой ситуации те дополнительные 100-200 байт и несколько лиш них шагов процессора никак не заслуживают серьезного беспокойства. Наклад пые расходы, вызванные вашим доморощенным абстрагированием, попрост блекнут в сравнении с тем, сколько изначально стоит использование бибаио геки. Я неоднократно замечал еще один интересный положительный побочны! эффект от интенсивного использования абстракций при написании ирнлож пий: результирующие программы оказываются более практичными тг полез ными. Я знаю, что такое заявление звучит глуповато, поскольку практичное! и полезность программны никак не связана с низкоуровневыми деталями1, реализации; к примеру, ничто не может помешать вам написать прекрасную очень полезную программу на «макаронном ассемблере». Но в реальном м"Р такая положительная связь и в самом деле появляется, как только прогр^[* переживает несколько версий: сильно абстрагированный код легче ноДДаеТ1 модификации, и когда команда разработчиков получает от своих полъзоватек (или от своего руководства) просьбу сделать очередное усовершенствован'" скорее всего оно будет сделано быстро п качественно - просто потому» ' абстрагирование сильно облегчает такую задачу. Этот эффект особенно заМе на Windows-программах, которые в наши дни практически всегда создав0 условиях жесткого давления сроков. (В каком-то смысле, данный эфФе действительности является обобщением того самого трюка с «иодвеип^11 функции на булевскую переменную», о котором я писал ранее, "" программа изначально конструируется таким образом, чтобы быть »м'
эТ0 нало? 321 готовой к последующим быстрым, безопасным изменениям и дополне- Гамый непосредственный и немедленный выигрыш от абстрагирования — е самый, что и от любого повторного использования кода: вы выделяете 'i уже известного, предположительно надежного кода в отдельные самовольные блоки, которые позволяют вам с большей эффективностью созда- качественные программы. Вопросы надежности особенно актуальны нпо в програмировании для Windows, когда вы буквально стоите перед г'ором ~~ играть в «API-рулетку» или предпринять какие-то меры для отражения своего кода от многочисленных проблем Windows API. Вы не можете 1 оСТО решить использовать MFC и затем предполагать, что тем самым вы защитите себя и свои программы; многие метацы MFC являются всего лишь -ончайшими обертками вокруг оригинальных функций и сообщений Windows \PI pi не делают вообще никаких усилий по защите вызывающего кода от причуд и вывертов «голого» API. (На самом деле, я только что упомянул одну из самых незаметных опасностей, скрытых в использовании любой библиотеки для Windows: программист может быть легко убаюкан ложным ощущением безопасности. Как и в случаях с другими инструментами, тщательное изучение недостатков и ограничений библиотеки обязательно окупится сторицей.) Что же касается ваших собственных абстракций, то их форма вовсе необязательно должна быть сложной и мудреной. Во многих случаях вполне достаточно простой библиотеки подпрограмм. И волшебство тут совсем не в количестве строк кода или объектно-ориентированных приемах, втиснутых в вашу программу, весь смысл именно в дизайне отдельных функций. К сожалению, я вынужден заметить, что программисты имеют печальную тенденцию не видеть байтов за битами: они часто забывают, что действительно могут создавать свои собственные платформы для последующего написания нового кода, а не просто использовать те архитектуры (такие как Windows API и Различные каркасные библиотеки), которые предоставлены им изначально. Когда я писал эту главу, мой близкий друг Пол поделился со мной своими личными наблюдениями о том, какую интересную реакцию вызывает у некоторых программистов любой разговор о создании промежуточных прослоек кода, библиотек, компонентов и т. п. Он неоднократно замечал, что некоторые программисты начинают глумливо насмехаться над такими предложениями и отклонять их, рассматривая их как простое оправдание для отлынивания от реальной работы. А еще он замечал, что те же самые программисты порой самыми первыми приходят к вам просить копию вашей библиотеки или класса, которые реализуют некоторую функциональность, неожиданно им понадобившуюся. Я тоже наблюдал такое явление, и мне кажется, что одна из его причин кроется в печально известной разнице между тем, что программы, инструменты и технологии обещают, и тем, что они на самом деле предос-
322 Абстрагирование в лействии: созлание своих виртуальных тавляют в реальном мире Я уверен, что каждый из читателей этой не раз испытывал разочарование в раздутых маркетинговых mv " поднимаемых поставщиками программного обеспечения. Какая-т '* щанная функция либо вообще отсутствует в продукте, либо работ плохо п медленно, что пропадает всякий смысл пользоваться ею т положение дел не может не воспитать изрядный цинизм у Всех ' пыотерных пользователей,- включая программистов. В результате они слышат о создании какого-либо нового инструмента пли техно/ все обещаемые преимущества и выигрыши автоматически делятся щ , два, если не больше. Я заговорил об этой печальной проблеме, потому что она имеет само0 посредственное отношение к коллективным программным проектам любой попытке ввести в проект новую библиотеку или создать в нем полнительный оберточный слой Такая идея должна завоевать поддери (или хотя бы одобрение) у двух весьма разных групп людей: у pVKOb детва, которое порой очень настороженно относится ко всему, что хспя( отдаленно напоминает программистские забавы, а не реальную работ (интересно, откуда вообще у них берутся такие мысли?), и у др\тп программистов, которые могут оказаться еще более несговорчивыми, и, тому что именно они будут потом вынуждены реализовывать или испои зовать данное нововведение. Таким образом, данная проблема тесн переплетается со служебной политикой — как бы мы ее ни ненавидев нам приходится время от времени признавать ее существование и как-* уживаться с ней. «&оверятьа но проверять» или «асфальтировать»? В справедливом и правильном мире (насколько мне известно, эта параллельная Вселенная находится в каком-то нанометре от нас, но в непостижимом и неоио тимом для нас измерении) использование Windows API не было бы источник0 беспокойства: мы просто заглядывали бы в документацию, использовали"' этот API (прямо или косвенно, через каркасную библиотеку) и далее беспечь продолжали бы свою основную работу, заботясь только о том, что происхоД1 «над поверхностью стола» — в наших собственных программах. А еще мы >!° ли бы безо всякой опаски рассчитывать на то, что наш 32-разрядный код 6уДс работать повсюду. Как это ни грустно, но мы находимся в нашей, реальной Вселенной Когда вы видите Win32 API, а рядом - заявление Microsoft о том.1 «это последовательный и унифицированный программный интерфейс ко 131 32-разрядным платформам Microsoft Windows», не верьте глазам своим. На ^ мом деле существует немало мелких (да и не очень уж мелких) различии Ме'^ тремя 32-разрядными платформами (Win32s, Windows 95 и Windows *
но проверять» или «асфальтировать»? 323 такие неуловимые нюансы, которые могут запросто обеспечить вам ° ' головные боли, а вашим публичным программам — мистические /* (Я бы не хотел вдаваться в детали именно сейчас, множество подроб- "' ° • на эту тему вы сможете найти в главе 14.) г- кая ситуация, лишь усугубляемая убожеством документации, вынуждает /^.разработчиков постоянно задавать самим себе один и тот же вопрос: н лц я тестировать этот API?». В качестве одного из примеров, в ' , 14 на стр. 486 я рассказываю про функцию GetShortPathNameO, которая 1ЬЗует различные расширенные коды ошибок (добываемые при помощи л astErrorO) в зависимости от того, на какой платформе работает вы- чюшая программа, и далее может работать успешно или проваливаться при иХ ц тех же входных данных и в одном и том же системном контексте. юзгому, если вы хотите писать надежный код, который радикально решает поблему проверки ошибочных ситуаций и адекватно реагирует на них, у вас Jb всего два пути: 1. Тратить массу времени па тестирование критических элементов API в расчете полностью изучить их поведение, включая реакцию на различные ошибочные ситуации в виде возвращаемых кодов ошибок. Этот подход, который я называю «доверяй, но проверяй», вне зависимости от того, как много времени вы сможете ему уделить, всегда характеризуется значительным риском недостаточно полного исследования поведения API в так называемых общих патологических случаях. Например, в первой версии вашей программы вы можете даже не задумываться над тем, как некоторая функция воспримет передачу ей имени несуществующего файла, потому что в данной версии такого не может произойти. А в следующей версии такая возможность появляется (скажем, пользователь теперь может вводить это имя вручную), и ваша программа вдруг оказывается сильно зависящей от корректного поведения данной функции. И когда ваша программа распространяется, ее могут запустить, к примеру, под Win32s, в которой эта функция реализована несколько иначе: она либо возвращает неожиданный для вашей программы код ошибки, либо вообще успешно срабатывает тогда, когда иод Windows 95 и Windows NT терпит крах. В результате вы тут же получаете сбой в вашей программе и знакомитесь с целой толпой недовольных пользователей, с которыми бесполезно о чем-то спорить. Гамбит под названием «доверяй, но проверяй» (да-да, не следует себя обманывать — это действительно гамбит) особенно рискован тем, что, глядя на отдельный фрагмент кода, очень легко сказать «X никогда не случится. Следовательно, зачем об этом беспокоиться?» И во многих случаях говорящий это абсолютно прав, потому что X буквально не может произойти. По крайней мере, до тех пор, пока не выйдет еле-
324 Абстрагирование в лействии: созлание своих виртуальных м дующаяя версия или не будут произведены очередные меропрПят сопровождению программы (иными словами, лишь до тех пор, ,г программу не будут введены новые взаимосвязи и предположения торые могут оказаться критичными для самых неожиданных частеГ да). 2. Обернуть критические и заведомо опасные API и тем самым ущг. цировать их поведение. Такой подход к делу я называю «асфа тировапием», и правильное его применение в перспективе сэконом вам много времени п сил. Ключевым вопросом при таком подходе рг прежнему является выявление проблематичных API, и эта задача pf прежнему затруднена неудовлетворительным состоянием документ ции по Win32. Чтобы дать программистам возможность эффекту использовать Win32 API, документация должна быть намного полнее чем сейчас, и включать больше информации о таких вещах, как ра; лпчия между платформами и коды ошибок. Может быть, в следующе, версии?.. Итак, первый шаг — определить те элементы API, которые критнчнь для вашей программы п от которых можно ожидать выкрутасов пр, работе на разных платформах Windows. Практически, в эту категории попадают все API, при работе с которыми ваша программа нспользуе GetLastErrorO для получения расширенных кодов ошибок, или ко торым передаются данные подозрительного происхождения. Как толь ко вы найдете потенциального кандидата, запустите несколько тесто? на всех Шт32-платформах, которые вы собираетесь поддерживать Если окажется, что есть какие-нибудь различия в поведении данной API па разных платформах, или его поведение является слишком н? удобным на какой-нибудь из платформ, не спешите сразу же приспо сабливать ваш основной код к этим причудам. Вместо этого попро^'1' те определить, нет ли простого способа обернуть этот API и тем самы' «заасфальтировать» проблему, спрятать се иод дополнительным сЛ°е API. Я уже упоминал функцию GetShortPathNameO. Если присмотреть^ ее причудам, то она окажется отличным примером, когда проб^ легко ликвидируется методом «асфальтирования». Все странности31 го API, которые я обнаружил до сегодняшнего дня, (а я вовсе не} ждеи, что обнаружил их все) относятся к ситуации, когда перед*111'1 ей длинное имя файла не соответствует существующему файлу. *' ' гда эта функция срабатывает успешно, иногда проваливается, Пр11 в последнем случае она проваливается по-разному. Решением еЛ)' оберточная функция uGelShortPathNameO из библиотеки W*11 " представленной в приложении С:
яТЬ/ но проверять» или «асфальтировать»? • 325 iiiiiiiiiiiiiiiiii/iiiiiiiiiiiiiii/ii ill iii/ii in/11 ill in пин и/и/ни/ 11 Унифицирующая обертка для функции GetShortPathNameO Возвращает 0 и // устанавливает подходящий расширенный код ошибки, если заданное имя не // соответствует существующему файлу или каталогу В противном случае // возвращается результат вызова функции GetShortPathNameO // // Проверка параметров- // все нулевые указатели отвергаются //III//////////////////////////////////////////ll/l/llll/III////////I/////I/ DWORD uGetShortPathName(LPCTSTR long_path,LPTSTR buffer,DWORD buffer_len) { #ifdef _DEBUG if((long_path == NULL) || (buffer == NULL)) OutputDebugStringC \nuGetShortPathName. Обнаружен неверный параметр1\n\rf), tfendif // Одновременно отлавливаются строки нулевой длины и нулевой указатель if(FileExists(long_path) || DirExiGts(long_path)) { SetLactError(O), return GetShortPathName(long_path,buffer.buffer_len), i else { if((long_path == NULL) || (strlen(long_path) == 0)) SetLastError(ERROR_BAD_PATHNAME), else SetLaGtError(ERR0R_PATH_N0T_F0UND), return 0, } i Эта функция проверяет, соответствует ли заданное имя существующему файлу или каталогу. Если да, то вызывается оригинальный представитель Win32 API. Если нет, то оберточная функция устанавливает разумный, предсказуемый расширенный код ошибки и возвращает значение, свидетельствующее о провале вызова. Таким образом, обеспечивается унифицированное поведение данной функции, минимально подверженное вывертам различных Win32-платформ. Тестируя различные примеры и приемы для данной книги, я обнаружил парочку интереснейших сюрпризов со стороны отладочной версии Windows 95, которые касались двух наиболее серьезных проблем Win32 API — обрезания графических координат и индексов в окнах-списках. (Суть проблемы в том, что Win32 API принимает 32-разрядные значения
326 Абстрагирование в лействии: созлание своих виртуальных м для этих чисел, но из-за 16-разрядности модулей USER и СГ)1 значения обрезаются до 16-разрядных и далее используется jh результат такого обрезания. Более подробно эта проблема обсуждае ' главе 14 ) Попробуйте запустить под отладочной версией Winc|0. программу, передающую в GDI слишком большое значение графцг1е координаты, и операционная система даже не выдаст никакого c(w ния Я нахожу такое поведение отладочной версии, мягко говоря гадочным В конце концов, разве не отладочная версия призвана б чуть ли не последней инстанцией, в которую программист д0т обращаться за помощью при встрече с серьезными проблемами') ц. любопытно, в случае с обрезанием значения индекса для окна-списка f ладочная версия все-таки выдает сообщение. Но при этом само «вцНг ное» значение индекса упоминается в этом сообщении каким-то странны способом — вместо того, чтобы показать все 32-разрядное значение цет. ком, в качестве неправильного значения указывается лишь верхнее слове Например, попытавшись использовать значение 983040 (OxOOOFOOOO), сообщении вы получите упоминание неправильного значения 15 Жри степени тШстратрованит Прежде, чем обратиться к вопросу «как», давайте ненадолго остановимсян. другом аспекте проблемы абстрагирования — на вопросе «что». Когда речь идет о коде, программисты часто ограничиваются простым, механическим спо собом его категоризации: они выделяют код, который находится «здесь» (имея в виду буквально внутренности той функции, на которую в данный момен. программист смотрит), и код, который находится «не здесь» и который прнхо дится вызывать. (Обратите внимание на то, что в данном случае я оставляюз стороне вопрос о встроенных функциях C++, которые физически относятся! первой категории, а логически — как раз ко второй.) Кроме того, программ1' сты в пылу борьбы нередко имеют такой же суженный и механический взгля* на использование подпрограмм: они хотят отыскать по возможности прости шее магическое заклинание, которое заставит ту ргли иную подпрограмму с^ лать что-то необходимое. К этому нетривиальному моменту я вернусь ч)т позже. Эту дискуссию можно без труда превратить в бесконечно детализируй исследование «экономики» программирования, но я не вижу в этом осо ^ пользы (конечно, если вы не ищете тему для своей диссертации). На Ml взгляд, существуют три основные, первичные степени функционал^ абстрагирования: 1. Обертка. Это слой вокруг оригинального API (Windows АРЬ с' дартной библиотеки С, чего угодно другого), предельно «тесно пр11' тый» к нему и сконструированный исключительно ради вызываю кода. Фактически, определяющей характеристикой этой степей*1'
епени абстрагирования 3£/ страгирования является то, что, меняя некоторым образом взаимодействие между вызывающим кодом и оборачиваемым API, обертка взаимодействует с остальной системой так же, как и оборачиваемый API. Упоминавшаяся мной выше функция uGetShortPathNameO является типичной оберткой, поскольку она не вносит никаких изменений в систему, а всего лишь унифицирует взаимодействие вызывающего кода и оборачиваемого API для разных платформ. Обратите внимание, что обертка вовсе необязательно должна быть маленькой пли вызывать только свой стержневой API; да, именно так обертки и работают чаще всего, однако суть не в том, как они выполняют свои задачи, а во взаимодействии с вызывающим кодом и с системой. Вообще, интерфейс обертки должен очень близко повторять оборачиваемый интерфейс. Особенно важным такое правило является при создании оберток вокруг Win32 API — в этом случае интерфейсы должны быть идентичны. (За исключением тех ситуаций, когда обертка используется как раз для переделки какого-то интерфейса в более практичную форму. Хорошим примером такого исключения может служить оберточная функция uGetDlgltemlntO из Win32u.) Идентичность интерфейсов облегчает вам и другим членам вашей команды использование оберточного API и не требует ненужных изменений существующего кода; кроме того, она облегчает переключение между использованием оберточного и исходного API, которое иногда бывает необходимо. Загляните в приложение С, в котором библиотека Win32u использует этот подход в оберточных функциях для безопасной работы с графическими координатами. Другой ключевой характеристикой оберток для Win32 API является использование функции SetLastErrorO для указания расширенных кодов ошибки. И это одна из тех областей, в которой рассудительные люди могут не соглашаться друг с другом. Например, лично я использую SetLastErrorO в обертке только в том случае, когда сам исходный API использует ее (согласно документации). Причина такого подхода проста: при достаточно долгом сопровождении кода (в течение месяцев или даже лет) весьма вероятно, что он будет модифицироваться программистами, имеющими различные взгляды на использование оберток; и когда кто-нибудь откажется от использования той или иной обертки и перейдет к использованию «голого» API, вызывающий код может по-прежнему полагаться на значения, возвращаемые GetLastEr- ror(). И в этом случае, если оберточный API использует SetLastErrorO, а исходный API этого не делает, неприятности гарантированы — вызывающий код будет обрабатывать совершенно посторонние, остаточные коды ошибок. (Конечно же, все это справедливо лишь в пред-
328 Абстрагирование в лействии: созлание своих виртуальны* положении, что программист ничего не знает о неиепользоващ LastErrorO в исходном API; по именно эта ошибка является точно естественно]! и потому очень вероятной.) При этом очень р- чтобы ваша обертка контролировала все возможные варианты in , ния расширенных кодов ошибки: было бы крайне опасным ост- неправильный код ошибки неизменным и затем позволить вызыва му коду его проверять. Аналогично, если кто-то наоборот переходит от использования «г го» API к использованию обертки, добавление обработки расщц ных кодов не имеет особого смысла (разумеется, за исключением ситуаций, когда без этого просто никак не обойтись). Такое услоич ние будет отпугивать программистов и служить им хорошим предлог для отказа от самого перехода на использование обертки. При создании оберток для Windows API, самым туманным являете вопрос о проверке версии самой: операционной системы. На иервь взгляд, все довольно просто: перед выполнением каких-либо машш ляцпй, специфичных для Windows 95, ваша обертка могла бы прост проверять, действительно ли программа работает под этой версие Windows. Но что же делать, если встает вопрос о возможных разж видиостях самой Windows 95? Например, многие из оберток, пре; ставленных мной в Win32u, необходимы только лишь потому, что в тс кущей версии Windows 95 модули USER и GDI являются 16-разря. ными, и следовательно индексы строк в окнах-списках и графически координаты могут принимать только 16-разрядные значения. (Это вс все не является причиной странного поведения Win32 API, при кс тором 32-разрядные значения принимаются и без ведома вызывающе кода обрезаются до 16 бит. Это поведение является либо следствие сознательного решения Microsoft не оповещать программу о том, Ч' ее данные калечатся системой, либо невиданной мной доселе крупне1 шей ошибкой проектирования.) Что же делать, если вы при помои* оберток защитите вашу программу от этого, распространите ваи программу, а потом вдруг обнаружите, что в Nashville пли МеШр11 (то есть в следующих версиях Windows 95) модули USER и GDI ста 32-разрядными? Разумеется, все не так уж страшно, как кажется худшем случае все будет выглядеть так, как будто ваша программе' гаитно отказывается выполнять то, что она на самом деле могла быс-' лать. (Я предполагаю, что если уж вы возьмете па себя труд по с°0,к нию обертки, то вы также обеспечите в вашей программе доЛ#й проверку возвращаемых этой оберткой значений и кодов ошибок-'' Тем не менее, некоторые предупредительные меры никак не повре>' Например, вы можете реализовать примерно следующий трюк: сЯе '
грени абстрагирования 329 так, чтобы ваши обертки могли «отключаться» (то есть просто транслироваться в вызов исходного, обернутого API) каким-нибудь глобальным флажком в программе. По умолчанию, значение этого флажка должно быть равно TRUE (то есть обертки должны функционировать), но при запуске программы с каким-нибудь недокументированным ключом в командной строке (скажем, «/BAREAPI=YES») этот флажок мог бы сбрасываться в FALSE. Тогда, если у вас действительно возникнет серьезная необходимость отключить обертки, вы сможете предложить вашим пользователям временное решение проблемы: запускать вашу программу с этим секретным ключом до тех пор, пока вы не исправите код и не распространите это исправление вместе со следующей версией вашей программы. Поверьте, я вовсе не любитель подобной тактики вообще, но в зависимости от природы ваших пользователей, вашего приложения п, наконец, самих оберток, такой вариант решения заслуживает как минимум рассмотрения. 2 Слабое функциональное абстрагирование. Это следующая за оберткой ступенька, ведущая вверх по лестнице. Такое абстрагирование может изменять, а может и не изменять взаимодействие между вызывающим кодом и исходным API. Однако взаимодействие исходного API с системой обычно изменяется. Примером такой степени абстрагирования служит функция CopyFileForceRWO, которую я представлял вам в главе 3 па стр. 129: она не только вызывает исходную функцию CopyFileO, но и использует функции GelFileAttributesO и SetFileAttributesO для обеспечения доступности выходного файла для записи. Эта функция уже не является простой оберткой, поскольку она изменяет систему (дополняет ее по сравнению с тем, что делает исходная функция CopyFileO). Как и в случае с обертками, при слабом функциональном абстрагировании важно сохранять близость определения интерфейсов (нового и исходного), особенно если стержнем является Win32 API. «Слабость» абстрагирования в данном случае заключается именно в том, что новый интерфейс, как правило, лишь незначительно изменяет или дополняет исходный. Поэтому, вне зависимости от реальной мотивации перехода от старого API к новому (исправление ошибки пли сознательный переход), чем меньше усилий требует изменение вызывающего кода, тем легче программисту пойти на это. В идеале вызывающий код должен просто вызывать функцию с другим именем п с теми же самыми параметрами. Аналогично обстоит дело и с расширенными кодами ошибок: при слабом функциональном абстрагировании, новый API должен как можно более точно копировать поведение своего исходного API в этом
330 Абстрагирование в лействии: созлание своих виртуальном м вопросе. Например, в CopyFileForceRWO функция SetLastE- применяется потому, что она используется исходной функцией Г° FileQ. Однако вызывается она только при отказе работа ' неправильными параметрами; коды ошибок, устанавливаемые и/ днями CopyFileO, GetFileAttributesO и SetFileAttributesO, Пр0г каются и предоставляются вызывающему коду без изменения. Все в том, что правильное обращение вашей функции с расширенным^, дамп ошибок вовсе не сложно и не требует массы замысловатого к0 Все, что нужно — просто знать об этой проблеме. (Если вн&- обратиться к последнему примеру, то, если бы функции для работы атрибутами файлов не использовали SetLastErrorO для нндикцц,, ошибок, я сделал бы так, чтобы CopyFileForceRWO вызывала SetL; LErrorO в случае провала этих функций.) Помимо прочих проблем, есть еще вопрос о том, как быть с некс торыми странными вариантами расширенных кодов ошибок, которы- встречаются в Win32 API. Например, когда при вызове функции Сор yFileO ей передается имя файла, расположенного на CD, а самого дне ка в дисководе нет, вызов проваливается с кодом ошибы ERROR_ACCESS_DENIED. А я предпочел бы, чтобы при этом возш: кала ошибка с кодом ERROR_PATH_NOT_FOUND, поскольку m две ситуации скорее всего будут одинаково обрабатываться вьдо вающим кодом (пускай даже такая замена кода ошибки приводит к не большой потере информации). Очевидно, что подобные подмены ко дов ошибок очень легко осуществить внутри обертки пли слабо аб страгированной функции, но не советую этого делать. На мой взгляд такой трюк означал бы совершенно нестандартное изменение поведи нпя системы ради слишком маленького выигрыша. Честно говоря, в этом месте я хотел бы вытащить на крыльцо сво тумбочку, взобраться на нее и начать громко вопить о том, что вы ^ должны изменять схему использования кодов ошибок, потому хп*° э' схема является важной, строго определенной частью системы. Но я могу этого сделать по той простой причине, что Microsoft не опреДеЛ11' какие API и когда устанавливают те или иные коды ошибок Более то Microsoft даже не сказала, что в некоторых случаях один и тот #е' устанавливает разные коды ошибок на разных платформах Win^ обратите внимание: простая подмена кодов ошибок и унификация *lN пользования в разных версиях Windows — две совершенно разные 0е Изменение кодов ошибок из пустой прихоти или даже с целью поЛ>'чс незначительных удобств является плохой практикой. Но когда ue-'Ibl f ляется переносимость между теми \У1п32-платфорамами, на к°т собирается работать ваша программа, подобные изменения вполне о1, ленны и, можно сказать, даже необходимы.
грпени абстрагирования 331 о Сильное функциональное абстрагирование. Это именно тот уровень абстрагирования, на котором мы больше всего знакомы с самим понятием абстракции: подпрограмма. Именно в этом обличий функциональные абстракции синтезируют из одного или нескольких системных ресурсов совершенно новые и совсем по-другому ведущие себя логические платформы. При этом под системными ресурсами подразумеваются самые разнообразные вещи — API, функции, файлы, встроенные ресурсы вашей программы и т. д. Помните разговор о явной загрузке динамических библиотек в главе 6? Программа-пример в той главе включала в себя файл HELLOINT.C, который занимался собственно загрузкой требуемой динамической библиотеки, а также обеспечивал безопасные функции-шлюзы для доступа к подпрограммам из этой динамической библиотеки. Представленную реализацию функции LoadHclloO я рассматриваю как очень слабую абстракцию (почти обертку) для стандартной функции LoadLibraryO, поскольку первая практически ничего не делает, кроме как загружает DLL и затем прицепляет переменные-анкеры к соответствующим функциям этой DLL. Но если рассмотреть всю единицу компиляции HELLOINT.C как целое, то я думаю, она с очевидностью является примером слабой функциональной абстракции, а возможно, даже сильной. (Вы могли бы пойти дальше и усовершенствовать Load- HelloO так, чтобы она проверяла версию загруженной: DLL, и в случае загрузки неверной копии выгружала бы динамическую библиотеку и возвращала бы в вызывающий код соответствующий код ошибки. Если вы достаточно амбициозны (или безрассудны), вы могли бы пойти еще дальше — например, научить LoadHelloO еще до загрузки DLL-файла проверять, действительно ли он является 32-разрядной динамической библиотекой. Подобные изменения несомненно перевели бы LoadHelloO в категорию сильных функциональных абстракций, поскольку в дополнение к простой, чисто физической задаче загрузки DLL и обеспечения вызова ее подпрограммой, эта функция решала бы уже целую кучу совершенно новых логических задач.) Возникает естественный: вопрос: к какой из этих категорий следует причис- МТь каркасные библиотеки? С одной стороны, они навязывают программам ^чо архитектуру но сравнению с голым ЛР1 (не говоря уже о том дополни- ^ном времени, которое требуется от программистов на изучение и освоение 11 аРхнтектуры). Как уже упоминалось где-то в этой книге, я думаю, что 4 Росные библиотеки несомненно являются хорошим и нужным пнтструмен- ' Но никак не забавой, которую вы можете освоить за полдня. В то же г Мя' большинство существующих на сегодняшний: день библиотек являются lUen частью обертками и очень слабыми функциональными абстракциями.
332 Абстрагирование в лействии: созлание своих виртуальных ——^fy; В качестве примера вы можете присмотреться к таким файлам, входа MFC, как AFXWIN1.INL и AFXWIN2.INL - вы сплошь и рядом увцд^' тоды и функции, которые являются не более чем тончайшими оберткам, сообщений и функций Win32 API и вообще не привносят никакой доподщ • ной функциональности. В большинстве случаев описания интерфейсов ц стыо совпадают с описаниями вызываемых оригинальных интерфейсов \\ dows (с точностью до отсутствия в списках параметров дескриптора обър который и так известен при вызове метода C++). Поэтому немедлен выигрыш от использования таких оберток ничтожно мал. Существенным м0 являться только потенциальный выигрыш — переносимость и надежда ца щиту от будущих изменений голого API. Другая проблема, тесно связанная с темой данной главы и почти неизбеж- возникающая всякий раз, когда программисты начинают говорить о построещ библиотек, — это ООП и правильный выбор его роли в программных прое тах. Как уже неоднократно отмечалось, ООП является той область программирования, которой очень легко увлечься, особенно при работе ь языке C++. (Что же в C++ так способствует этому явлению? Может бьт именно сложность этого языка бросает вызов — освоить весь этот объектнс ориентированный механизмище и создать в конце концов свой бессмертный ж девр в стиле барокко? Или этот язык пробуждает к жизни потенциальных пг сателей компиляторов и «языковых юристов», которые изначально дремлют душах большинства программистов?) Мой вам совет — остерегайтесь таюг тенденций, поскольку они прямиком ведут к бесполезной потере программа ского времени, когда кто-нибудь забирается в дальний угол и работает надби( лиотекой, которая никому и никогда не понадобится в вашем проекте. Для хот бы частичного решения подобных проблем я использую так называемо «правило наследования», которое гласит: если вы создаете библиотек которая вряд ли потребует наследования, то нельзя писать ее с использование объектов. На первый взгляд, это правило несет явный оттенок неолуддизма. Но,nt жалуйела, выслушайте меня до конца. Что является главной целью написан1' библиотеки и главной ценностью результата? Очевидно, извлечение выгоды1 повторного использования кода. Л если это так, и если код в действительно^ не требует использования механизма наследования, то вы можете достичь № преимуществ скрытия данных, абстрагирования и инкапсуляции просто П}'Тс правильного оформления кода в одной или нескольких единицах компн^и11' Сделайте переменные состояния вашей библиотеки локальными, обеспечь^ обходимый набор «set» и «get» функций для доступа к ним, выставьте иаП01'' лишь минимально необходимое количество функций, и скорее всего постав-11 пая цель будет полностью достигнута. Мне кажется, такой подход имееТ'1 очень существенных достоинства:
рйНиа6страгирования в 333 Он минимизирует риск того, что вы или кто-то другой из участников проекта помешается на объектах. Как только проект стартует, то и дело возникает сильнейший соблазн добавить в иерархию объектов еще один класс, в набор кодировщиков — еще один алгоритм, и так далее. До тех пор, пока диаграмма вашей иерархии объектов не станет похожей на схему административной структуры Пентагона. Помимо создания неоправданно большой кривой обучения для коллег, такие распухшие библиотеки часто добавляют огромное количество ненужного кода в программы. (На этот момент многие не обращают внимания: стимулируя создание сложных иерархий объектов, ООП тем самым стимулирует написание слишком большого количества кода. А благодаря несовершенству всех современных компоновщиков, практически весь реально незадействоваппый код все равно остается в программе. Например, Visual C++ создает тривиальную программу «hello, world» в виде отдельного исполняемого файла размером 195 Кбайт, даже без включения отладочной информации). 2. Он минимизирует вероятность того, что библиотека не будет использоваться. Самым безудержным сторонникам ООП не стоит обманываться — сегодня очень многие программисты далеки от того, чтобы находиться в полной власти чар ООП, а язык C++ используется ими всего лишь как «улучшенный С». (Я не собираюсь осуждать или оправдывать такую точку зрения, я лишь констатирую факт.) Если вы исполните библиотеку в виде набора старых добрых функций, ее сможет использовать всякий, кто использует C++, независимо от степени его одержимости ООП. И в то же время я неоднократно замечал, что принуждение использовать объекты (пускай даже в небольшом количестве, пускай даже очень хорошо спроектированные) является одним из самых надежных способов заставить программистов искать поводы для отлынивания от работы. И это еще одна реальная жизненная ситуация, в которой абсолютно неважно, кто прав, а кто неправ. Если вы пишете библиотеку, которую не захотят использовать другие, значит, вы напрасно тратите свое время. И наверняка не можете себе позволить такой пустой траты времени ни при написании «лишь нескольких маленьких утилит» для вашего отдела, ни при написании крутой, предназначенной для всемирного распространения комбинации издательской системы с электронными таблицами, базой данных и изумительной телефонной звонилкой впридачу.
334 Абстрагирование в лействии: созлание своих виртуальных ВВскдсство Мбстрагырованшя Пора переходить к сути дела. Вы можете создавать Виртуальные Мцт?., использовать Инструмент Абстрагирования, вы можете просто «слеплять торые куски кода в библиотеку, чтобы другие могли ими пользоваться» Вь жете делать что-то среднее — в. любом случае есть несколько общих проб специфичных для такой работы. (Только, пожалуйста, не путайте Божий i с яичницей: поблочное копирование кусков кода из одних файлов в друг последующая упаковка последних в LIB-файл вовсе не являются применен- абстрагирования или созданием библиотеки; такая деятельность — не бо- чем переоформление исходного кода.) И в этом разделе я хотел бы рассмоТр[. наиболее важные вопросы и попытаться ответить на них в форме набора pef мендаций по абстрагированию: В Всегда стремитесь уделять основное внимание самым насущнь потребностям вызывающего кода. Это правило кажется тривиальны пока вы пишете нечто простое, типа традиционных «set»- и «g^ функций или методов, с которыми все мы хорошо знакомы и которь обычно контролируют доступ к приватным данным библиотеки ил объекта. Но стоит вам перейти к более сложным задачам, и становий удивительно легко потерять нить и забыть, что вы пишете эти функци, и методы с одной единственной целью: предоставить вызывающей коду ту или иную специальную услугу. И поэтому вы просто обязаш все время помнить о той обстановке, в которой скорее всего будет рабо тать вызывающий код, и о тех намерениях, с которыми он будет вь1 зывать вашу подпрограмму. Именно нарушение этого правила делает в моих глазах такой загс дочной пресловутую функцию GetShortPathNameO из Win32 API Зачем и кому может понадобиться успешное завершение этой фувь ции, если переданное ей длинное имя файла не соответствует сушес вующему файлу или каталогу? Если само соответствие между Д^1г ным и коротким именем имеет смысл только для существующего фа11 или каталога, то какой смысл в принятии бессмысленного (для реШ[К мой задачи) параметра, копировании его в выходной буфер и ра#°1 ном докладе об успешном завершении работы? В конце концов, Ра3 попытка использовать полученный псевдоним и обратиться к фай-1? является тем, что скорее всего сделает вызывающий код после усП ного завершения функции GetShortPathNameO? Да, документ^ для этого API ни слова не говорит о том, что файл должен суШ^' вать, и это понятно — ведь иначе сама операция не имеет сМЫ1' (Кстати, обратите внимание, что иногда вызов GetShortPathNaf все ж таки проваливается при передаче имени несуществующего ^ ла, значит, в документации написана полуправда? НепоследоВаТ
гв0 Абстрагирования 335 ность такого сорта зачастую является еще большим злом, чем просто неправильный или неудобный интерфейс — она прямиком ведет к неправильному представлению об истинном поведении данного API в целом.) g Всегда стремитесь приспосабливать создаваемый интерфейс к наиболее вероятному способу его использования вызывающим кодом. Если предыдущая рекомендация касается семантики, то в данном случае речь идет о синтаксисе. В этом вопросе хорошим «антипримером» может послужить уже упоминавшаяся ранее функция GetDlgltemlntO. Странность ее синтаксиса в том, что он не поддерживает наиболее естественный, идиоматический способ индикации успеха/провала — возврат соответствующего значения типа BOOL. Сравните эту функцию со стандартной функцией С по имени strcmpO, которая обеспечивает вызывающий код множеством полезной информации и делает это очень простым и удобным способом. Она принимает два указателя на строки и возвращает отрицательное значение, если первая строка лексикографически меньше второй; ноль, если строки эквивалентны и нечто большее нуля — если первая строка больше второй. Такой синтаксис поддерживает очень естественные способы использования этой функции, такие как if(Gtrcmp(Gtring1, stnng2)) pnntf("Строки неодинаковы \п"); else printf("Строки одинаковы \п"); и int гс = strcmp(string"!, string2) if(re > 0) printf("string2 больше\п"), else if(re == 0) printf("Строки одинаковы\п' ), else pnntf("stnng1 больше\п'); На самом деле, многие программисты чувствуют себя настолько комфортно при использовании этих стандартных строковых подпрограмм, что, вероятно, даже не задумываются над обсуждаемой проблемой. (Для сравнения, попробуйте представить, насколько менее удобной стала бы функция strcmpO, если бы она никак не указывала точный лексикографический порядок и возвращала бы лишь TRUE, когда строки совпадают и FALSE — когда они неодинаковы.) Я убежден, что данный аспект проектирования API имеет неочевидное, но весьма существенное влияние на качество программ. Если вернуть-
336 Абстрагирование в лействии: созлание своих виртуальных м . л^ ся к той же GetDlgltemlntO, то «кривизна» ее синтаксиса явгт сильнейшим источником соблазна вообще не проверять, успещн она завершила работу (особенно такой соблазн велик в аврально" становке, когда программист стремится сэкономить каждое наж клавиши в своем редакторе). Разумеется, программисты не должцк питься и совершать подобные проступки. Но в том-то и суть: ест. можете недорогой ценой хотя бы чуть-чуть уменьшить вероятность г добных программистских огрех, сделайте это. В Помните о типичных патологических случаях. Одно из фундаментных правил программирования гласит: компьютеры не умеют дуМа? а программисты — считать. Эта аксиома имеет самое непосредственнг отношение к обсуждаемой теме, потому что ваши программы буд\ разумными ровно настолько, сколько усилий вы потратите на обучещ их этому. Вы должны всегда предполагать, что рано или поздно ка; дая подпрограмма будет вызвана с неправильным значением параметр. (нулевым указателем, числом вне допустимого интервала, незавершен ной нуль-символом строкой и т. д.). Я уже говорил, что неправильны данные все равно возникают, и что вы должны всеми силам. уменьшать возможные последствия. Пожалуйста, используйте вес мыслимые приемы оборонительного программирования при создан ваших библиотек, потому что никакой другой ваш код не будет та; подвержен общим (да и не таким уж общим) патологическим случаях Выше я упоминал функцию strcmpO как хороший пример четко выразительно спроектированного интерфейса. К сожалению, друп|г строковые функции стандартной библиотеки С далеко не так хороша продуманы. Изумительно то, что некоторые из них преспокошь принимают указатель на выходную строку, которую они будут моДО фицировать, и при этом никак не интересуются максимальным ДопУс тимым размером этого буфера. Программисты то и дело использу10 эти функции, передают им указатели на строки (которые, в сво очередь, были получены через параметры), не проверяя и никак не ^ мекая на реальную длину строкового буфера — пристрастие к таК° практике стало сегодня чуть ли не всеобщим. Как я уже говор*1-1 рекомендации И на стр. 109, я наблюдаю это безобразие в р^°ч коде повсюду, равно как и многие другие ошибки, ведущие к поЯв'1 нию незавершенных строк и переполнению буферов. Вы когда-Hi^" видели, как программа выдает сообщение с кучей «мусора» на к°н Я видел такое так много раз, что и сосчитать не могу (что неудир1ГГ . но, поскольку я все-таки программист). Я не могу ручаться за к<* случай подобных проблем, но я готов побиться об заклад, что ° шинство этих случаев порождаются именно неправильным испоЛЬ нием стандартных строковых функций.
г(ТВО_ Абстрагирования , <jJ/ g Используйте продуманные, последовательные соглашение о наименованиях. Фанатизм в данном случае не нужен: просто следуйте общим разумным традициям и избегайте путаных и непонятных имен. В конце концов, ради кого как не ради программистов мы даем функциям такие имена, как IsIconicO, CreateWindowExO и т. п.? Придумывая имена функциям, сообщениям, константам, структурам, помните, что хорошее имя всегда должно быть пророческим: оно должно позволить пользователю, впервые встретившему ваш интерфейс, успешно предугадать назначение, семантику этого интерфейса, основываясь только на его имени. Чем более пророческими будут ваши имена, тем легче будет изучить и освоить вашу библиотеку. Что касается популярной венгерской нотации, то решение о ее использовании или неиспользовании является исключительно прерогативой вашей команды. Лично мне она не нравится — на мой взгляд, она приносит интерфейсу больше вреда, чем пользы (с точки зрения человеческого фактора). б Не бойтесь смешивать данные и контроль выполнения в семантике интерфейса. Это еще один прием, с которым Windows-программисты встречаются чуть ли не на каждом шагу, но при этом не задумываются над такой его формулировкой. Многие функции из Winsows API возвращают «двуликие» значения. Скажем, они возвратят вам дескриптор объекта (окна или значка), если все в порядке, или ноль, если что-то неправильно. Ноль заведомо не является правильным дескриптором, и поэтому такое значение возвращаемого дескриптора служит совершенным индикатором провала. Аналогично, некоторые сообщения для работы с окнами-списками могут возвращать значение LBJERR (определенное как -1 в WINUSER.H) при возникновении ошибки или при попытке выполнить операцию над несуществующим элементом списка. Такая смешанная семантика может быть очень полезным, удобным и выразительным способом возвращения информации вызывающему коду, однако использовать ее нужно осмотрительно и осторожно. Например, сообщение LB_GETITEMDATA позволяет вам извлечь 32- разрядное значение, ассоциированное с заданным элементом списка (предположительно то значение, которое ранее было установлено вашей же программой при помощи LB_SETITEMDATA). В ошибочной ситуации сообщение вернет вам LB_ERR. Но что будет, если указанный элемент еще не был явно ассоциирован с каким-либо значением? Мои тесты показывают, что в этом случае возвращается 0. Несомненно, данный интерфейс является противоречивым: как отличить «реальный» ноль от нуля, свидетельствующего об отсутствии ассоциации? Как отличить «реальное» значение -1 от значения LB_ERR,
338 Абстрагирование в лействии: созлание своих виртуальных возвращаемого, например, при указании несуществующего элс\\ Используя один лишь голый API, вам не удастся легко разрещ11т/ эти противоречия. (Если для ваших данных достаточно 31 бит- меньше, вы, конечно, можете использовать один бит в качестве \ катора «наличия ассоциации» и написать пару простых o6epi0l функций, обеспечивающих должную обработку этого бита. Pa3VN ся, и этот трюк не является абсолютно надежным, если вы не м0; гарантировать, что ваши данные никогда не будут содержать едпи„ во всех разрядах. Такой подход граничит с противоестественными ,, ствиями, о которых я уже говорил ранее в этой книге, и которых должны избегать всеми силами. И тем не менее, данный прием м0/ выручить вас в экстремальной ситуации.) Другой пример из Win32 API — функция FormatMessageO, котор, смешивает в одном параметре флаги и размер буфера. Это, наверно самый эксцентричный из встречавшихся мне вариантов. Обсужден; его вы можете найти в разделе «Win32 API: бинарный кельвинбо? на стр. 341. В Всегда гарантируйте возможность недвусмысленного определения вь зывающим кодом, успешно ли завершил работу ваш интерфейс. Х(г этот совет звучит глуповато из-за своей банальности, как видно i: предыдущей рекомендации (а также из примера с функцией GetS1. sColorO, который будет обсуждаться ниже), еще далеко не все cic дуют простым и очевидным правилам. Рекомендация внутри рекомендации: по возможности используй один и тот же прием для индикации успеха/провала во всех функция* своей библиотеки. Пробегите глазами Win32 API, и вы встретите почти со всеми мыслимыми и немыслимыми способами такой ннд№ цин. Непоследовательность такого сорта лишь усложняет ДрУг1! программистам — пользователям этой библиотеки — жизнь. (Едпнс венный случай, когда у вас, пожалуй, нет другого выхода, кроме1" нарушить это правило, — это написание оберток для таких изнача^- «кривых» API; как уже говорилось ранее, обертке приходится ь пировать весь оборачиваемый интерфейс, включая способ пндпка11' успеха/провала.) В Не бойтесь большого количества уникальных интерфейсов, преД°сТ' ляемых вашей библиотекой. Мне приходится произносить эту PLV мендацию с особой осторожностью, поскольку она вовсе не озна1Ь' что я призываю программистов к безудержному размножен^0 ' терфейсов. Без сомнения, в реальном мире у каждой библиотеки с\ ствует некий предел под названием «слишком много интерфе11С , превышение которого создает серьезные проблемы как при ее псП
Абстрагирования зовании, так и при ее сопровождении. Это незамедлительно снижает удобство пользования библиотекой и, в конечном итоге, люди просто прекращают ее использовать. Перебор с количеством интерфейсов никогда к добру не приводит. Но большинство программистов осторожничают в этом вопросе больше, чем следует. Они понимают, что нужно стремиться к некоторому балансу, но очень часто ошибаются в количественной оценке этой самой точки баланса — устанавливают ее слижком близко к нижнему краю шкалы. Если библиотека хорошо спроектирована (то есть если каждый входящий в нее интерфейс является реально необходимым и ценным), то не имеет значения, сколько в ней интерфейсов — 5 или 500. Эта рекомендация одновременно является призывом к программистам прекратить впихивать в один интерфейс то, что на самом деле должно быть оформлено как два интерфейса или более. Хорошим примером такого необоснованного напихивания нескольких интерфейсов в один может послужить сообщение LB_SELITEMRENGEEX. Вместо того, чтобы использовать два разных сообщения, Microsoft использует соглашение о том, что тип операции (выбор или отмена выбора) определяется порядком передаваемых через wParam и lParam границ интервала: если границы указаны в возрастающем порядке, происходит выбор, а если в убывающем — отмена выбора. В моей библиотеке Win32u я завел две отдельных обертки для этого сообщения — uLB- SelltemRangeO и uLBDeselltemRangeO. В Всегда ясно документируйте, как используются строки, завершаемые нулем. Признаюсь, я ненавижу эти завершаемые нулем строки по двум причинам. Самая основная и серьезная причина — та частота, с которой я наблюдаю (и, каюсь, делаю сам) самые разнообразные ошибки, провоцируемые такой реализацией строк: потеря одного символа, «мусор» в конце строки, pi прочее. Те усилия, которые приходится постоянно тратить на всякие неестественные манипуляции со строками, также не прибавляют очков авторам этого изобретения и языку в целом. Благодаря тому, как глубоко вживлены строки и в Windows API, и в другие области использования С, нам наверняка придется долго терпеть их и уживаться с ними. Если мы и сможем когда-нибудь забыть их как страшный сон, то, как минимум, не раньше, чем Солнце превратится в сверхновую. А может, и позже. В связи с вышесказанным, вы должны превозмочь боль и всюду точно описывать, как ваши функции используют указатели на строки, и когда передаваемая длина той или иной строки учитывает или не учитывает завершающий нуль-символ. Не нужно тратить много слов на такие комментарии — просто выработайте для себя стандартные краткие
340 Абстрагирование в лействии: созлание своих виртуальных ^ -^«^ и недвусмысленные формулировки для всех возможных вариа11 затем используйте эти «коды» в комментариях. Кстати, болезненный вопрос о способе передачи длины строки т ' переплетается с вопросом о последовательности интерфейса. Кар вестно, существуют два наиболее распространенных варианта передача длины строки с учетом завершающего нуль-символа ц г учета оного. Первый вариант я называю «методом sizeof» (потому i- при таком методе в качестве фактического занчения зачастую пере ется именно результат операции sizeofO). Второй вариант я называв соответственно, «методом sizeof-1». Выберите для себя одни ц3 ЭГ[1 вариантов и постарайтесь во всей вашей библиотеке использовать той ко его! Ничто так легко и быстро не приведет к многочисленным щ фузам и ошибкам, как необоснованное смешивание этих двух щщ тов. А если уж сложилось так, что вам никак не уйти от такого смеши ванпя, возьмите на себя труд и задокументируйте ваш код так, чтобь пользователю было до боли понятно, какая функция какой способ не пользует. В Позаботьтесь о том, чтобы ваши функции четко следовали букве зако на в своем поведении. Поскольку в качестве закона в данном случае выступает ваша же документация, данная рекомендация в действитель ностн, состоит из двух частей: ваши функции должны быть ясно г подробно задокументированы, а затем они должны четко выполнят. условия своего контракта с внешним миром. При написании докумен тации так велик соблазн умышленно умолчать о некоторых деталях - либо потому, что вы сами не знаете, что вытворит ваш код в каких нибудь неординарных условиях, либо потому, что «источником не определенности» является какая-то другая функция (например, пре; ставитель Win32 API), на которую полагается ваш код. В подобны' ситуациях не пытайтесь угадывать, а лучше постарайтесь задокум^ тировать все, что можете. И если ничего другого не остается, хотя0 напишите что-нибудь типа «функция возвращает то значение, котор1 устанавливается таким-то и таким-то Windows API в качестве р"1 ширенного кода ошибки, и которое может не быть одинаковым навсс Windows-платформах». Конечно, это далеко не лучший ответ, торого могут ожидать от вас пользователи вашего кода, но он, по кр' ней мере, является честным и, главное, указывает верное напр^вЛе дальнейшего исследования проблемы, если таковая возникнет. (^°' вы сами окажетесь в подобной ситуации, наилучшим решением сь всего будет написание какой-нибудь обертки вокруг такого «неоПр ленного» API.)
ctrq Абстрагирования 341 $Щ32 ЛР1: бинарный кельвинбол? а не сомневаюсь, что, дочитав книгу до этого места, вы уже заметили, что ^огтяюсь большим поклонником Win32 API. Вы правы, я не из их числа. этому я полагаю, что было бы полезным и важным представить на ваше мотрение некоторые образчики глупости из этого API. Приводя эти меры именно в данной главе, я преследую сразу две цели: пояснить, почему ' поДДаюсь очарованию Win32 API, и одновременно продемонстрировать те IClMbi, которых вам следует всеми силами избегать при конструировании ва- ! 1Х собственных библиотек. Q функция GetPrivateProfileString(). Я нахожу этот пример интересным, потому что порок этой функции с одной стороны не слишком бросается в глаза, а с другой — мог бы быть очень легко исправлен. Проблема в определении успешного завершения работы функции. Описание этого интерфейса выглядит так: DWORD GetPrivateProfileString( LPCTSTR lpAppName,// указывает на название секции LPCTSTR lpKeyName,// указывает на название ключа LPCTSTR lpDefault,// указывает на значение строки по умолчанию LPTSTR lpReturnedString,// указывает на выходной буфер DWORD nSize,// размер выходного буфера LPCTSTR lpFileName // указывает на имя Ш-файла ); Документация этого API определяет возвращаемое значение следующим образом: Если функция срабатывает успешно, возвращается количество скопированных в буфер символов, не включая завершающий нуль- символ. Если параметры lpAppName и lpKeyName не равны NULL, и если предоставленный выходной буфер оказывается слишком мал для хранения запрошенной строки, эта строка обрезается и завершается нуль-символом, а возвращаемое значение равно nSize-1. Если хотя бы один из параметров lpAppName и lpKeyName равен NULL, и если предоставленный выходной буфер оказывается слишком мал для хранения всех строк, последняя строка обрезается и завершается двумя нуль-символами, а возвращаемое значение равно nSize-2. Достаточно на секунду задуматься над этим описанием, и вы увидите, что не сможете во всех случаях достоверно определить, возвращает ли эта функция всю требуемую строку целиком. Например, если вы передадите ей 1000-байтный буфер, а требуемая строка в INI-файле
342 Абстрагирование в лействии: созлание своих виртуальных л* ■ 23$^ имеет длину 5 байт, то данная функция успешно поместит в ваш 6vi всю строку, завершит ее нулем и возвратит значение 5. Но если ст окажется длиной в 5 или более байт, а вы предоставите функции в лишь 6-байтный буфер, вы снова получите в буфер 5 символов m завершающий ноль и 5 в качестве возвращаемого значения. Как теп вам узнать, вся ли строка из INI-файла была скопирована в ваш г фер? Никак, потому что функция выдала вам 5 байт данных и верт- вам в точности длину предоставленного ей буфера без учета нуль-с! »• вола, которая равна 5 (она же равна nSize-1, она же по случаю точности совпадает с тем значением, которое используется для сообш ния вам о недостаточном размере буфера). Иными словами, ест случится так, что размер вашего буфера совпадет с размером требуемой строки в INI-файле, данная функция выдаст вам всю строку цели. ком, должным образом завершит ее нуль-символом, и в довершение всего вернет вам значение, неотличимое от сигнала об ошибке. Эта противоречивая ситуация могла бы быть легко исключена - например, следующим переопределением интерфейса: BOOL fantasyGetPnvateProfileStnng( LPCTSTR lpAppName,// указывает на название секции LPCTSTR lpKeyName,// указывает на название ключа LPCTSTR lpDefault,// указывает на значение строки по умолчанию LPTSTR lpReturnedString,// указывает на выходной буфер DWORD nSize,// размер выходного буфера LPCTSTR lpFileName, // указывает на имя INI-файла LPINT IpNeededSize// размер необходимого буфера ). В этом новом определении все старые параметры имеют прежнее значение. Новый параметр — IpNeededSize — является указателем на целое число, которое использовалось бы для указания точного необхо димого размера входного буфера (включая завершающий нуль-сим вол) при его недостаточной емкости, или устанавливалось бы в 0 пр' успешном завершении функции или при ее провале по какой-ли11 иной причине. Возвращаемое значение было бы равно TRUE ПРП^ пешном завершении работы (копировании всей строки целиком обрезания) и FALSE — в противном случае. Кроме того, эта фаИ' стическая функция не изменяла бы содержимое предоставленного фера во всех ошибочных ситуациях (включая ситуацию с неД°с точной емкостью этого буфера). Кстати, мне трудно представить себе реальную ситуацию, в к°'г I- вариант с возвратом укороченной строки мог бы оказаться осмЫс- ным и полезным. Например, если бы ваша программа имела вП определенное представление о формате интересующей строки
аггтво Абстрагирования 343 само по себе является сомнительным условием, учитывая любовь пользователей к текстовым редакторам и легкодоступность INI-файлов для редактирования), она могла бы сознательно использовать данную функцию для извлечения только первой части строки. Однако даже такой пример кажется мне слишком надуманным, так как в реальном мире такая схема была бы слишком рискованной. Конечно, вы могли бы излечить эту проблему двусмысленности возвращаемого значения при помощи обертки, такой как uGetPrivate- ProfileStringO: BOOL uGetPnvateProfileStrmg(LPCTSTR lpAppName,LPCTSTR lpKeyName, LPCTSTR lpDefault,LPTSTR lpReturnedStnng,DWORD nSize,LPCTSTR lpFileName) { // Сначала пытается вызвать исходный API с заданными параметрами. DWORD resultl = GetPnvateProfileStnngQpAppName,lpKeyName,lpDefault. lpReturnedStnng, nSize, lpFileName), // Если мы столкнулись с одним из двусмысленных результатов, придется // попробовать снова (теперь уже с локальным буфером) и выяснить, получили // ли мы строку целиком Подробности об условиях nSize -1 и nSize - 2 см // в описании GetPrivateProfileString() if((((lpAppName == NULL) || (lpKeyName == NULL)) && (resultl == nSize - 2)) || (((lpAppName ' = NULL) && (lpKeyName '= NULL)) && (resultl == nSize - 1))) { char *local_buffer = (char *)malloc(nSize +1), if(local_buffer == NULL) return FALSE, // Повторная попытка: используем наш собственный буфер и его длину, // остальные параметры без изменения DWORD result2 = GetPrivateProfileStnng(lpAppName,lpKeyName,lpDefault, local_buffer,nSize + 1,lpFileName); free(local_buffer); // В любом случае буфер нам больше не нужен // Если получаем тот же результат, значит тревога была ложной if(result2 == resultl) return TRUE; else // Если результат отличается, то оригинальный буфер и в самом деле // оказался слишком коротким return FALSE; else // Повезло с первого раза, просто возвращаем TRUE return TRUE;
344 Абстрагирование в лействии: созлание своих виртуальных ^ Как видите, эта обертка очень проста. Вначале она пытается ср^, пасть в цель и вызывает функцию GetPrivateProfileStringO c l{\ f ными параметрами. Если этот вызов дает недвусмысленный резут обертка немедленно завершает работу. Если же результат полуц неопределенным (то есть, если изначально предоставленный б\м либо оказался слишком маленьким, либо совпал по размеру Q влеченной строкой), тогда она размещает в памяти буфер, па один^. больший оригинального, и вновь делает вызов. Если второй вызов i тот же самый результат, значит на самом деле оба вызова были уСпе.. ными; иной результат при втором вызове означает, что вызывающе- коду не повезло — предоставленный им буфер действительно оказав слишком маленьким. В Сообщение LBJSELITEMRANGEEX посылается окну-списку длявь бора или отмены выбора нескольких следующих друг за другом строк Определение параметров этого сообщения выглядит следуют^ образом: wParam = (WPARAM) wFirct, // первый элемент lParain = (LPARAM) wLast, // последний элемент Интересной деталью является то, что у программы нет способа явнс указать тип необходимой ей операции — выбора или отмены выбора Все, что можно использовать в сообщении для передачи дополнптель ной информации — параметры wParam и lParam — уже использовано И вместо передачи через lParain указателя на структуру со всеми не обходимыми полями или использования двух различных сообщали' для двух возможных onepamiii, Microsoft выбрала поистине занятный путь. В описании сообщения LB_SELITEMRANGEEX вы можете наи ти такое замечание: Если параметр wFirst меньше параметра wLast, заданный интерн элементов выбирается. Если параметр wFirst больше парами wLast, для заданного интервала элементов выбор отменяется Это интерфейсное решение очень неудачно не только потому, чт0° слишком уж мудреное, по и потому, что оно провоцирует вызываю^ программу на трудноуловимые ошибки (например, если гранив тервала вычисляются программой, они могут быть случайно переС" лены местами). Microsoft могла бы определить структуру, состоя из двух целочисленных индексов и булевской переменной (гран*111 , тервала и типа операции, соответственно), и при использовании ^ ного сообщения позволить передавать указатель на такую стрУкТ- Кроме того, при подобной схеме передачи параметров система * бы приспособиться и к такой вероятной ошибке, как случайная Iiel ( на местами границ интервала. (См. функции-зажимы из главЫ
^ccr& Абстрагирования 345 сТр. 122, которые демонстрируют именно такое поведение.) А еще лучше было бы сделать небольшую уступку и вообще разбить данную функциональную возможность на два отдельных сообщения (именно такой вариант я избрал при обертывании сообщения ТБЯВIIEMRANt¥hX в библиотеке Win32u). g При посылке сообщения LBSETCURSEL окно-список прекрасно воспримет значение -1 в качестве индекса элемента списка и проинтерпретирует его как указание отменить весь выбор. Сам по себе такой прием вполне нормален и являет собой хороший пример разумного смешивания семантики данных и управления в одном параметре. Однако, есть одна небольшая проблема, которая сильно портит всю картину: в данном случае Windows API возвращает значение LB_ERR, несмотря на то, что никакой ошибки не было. Помимо прочего, данный случай является классическим примером явления, о котором я говорил еще в рекомендации 3 на стр. 79 — благо- словления порочной практики путем ее документирования. Я не видел кода, отвечающего за обработку сообщения LB_SETCURSEL, но мне совсем не трудно представить, что на самом деле происходит там, за кулисами: скорее всего этот код использует некую общую подпрограмму проверки индекса, которая воспринимает значение -1 как ошибочное и устанавливает возвращаемое значение в LB_ERR; далее, эти действия игнорируются, а значение -1 просто воспринимается как особый случай и должным образом обрабатывается (без заботы о том, чтобы подправить возвращаемое значение). И даже если эта моя гипотеза абсолютно неверна, суть дела не меняется — поведение LB_SETCURSEL по-прежнему является ярким примером того, как нельзя себя вести. в Довольно странным представителем Windows API является функция GetSysColor() — она даже не пытается определить возможные ошибочные ситуации. Она принимает одну из пары дюжин констант, каждая из которых обозначает ту или иную часть пользовательского интерфейса Windows, и возвращает цвет соответствующего элемента интерфейса. Просто, до слез. Проблема в том, что стоит вам передать ей нелегальное значение, и функция возвратит ноль — значение, абсолютно неотличимое от RGB-кода черного цвета. Кто-то возразит мне — ну и что ж тут такого? Не надо делать дурацкую ошибку и передавать этой функции нелегальное значение. Передавайте ей только одну из предопределенных в системе констант, и вы будете в полной безопасности. Верно, но не совсем. Оказывается, с появлением Windows 95 усложнился сам вопрос о том, что считается легальным, а что нет. (Вы удивлены? Лично я — нисколечко.)
346 Абстрагирование в лействии: созлание своих виртуальных Проблема заключается в новых, специфичных для Window значениях параметра для GetSysColorO. Заголовочный файл WlMt ER.H определяет все допустимые значения следующим образом- «define COLOR.SCROLLBAR О «define C0L0R_BACKGR0UND 1 «define COLOR.ACTIVECAPTION * 2 «define COLOR.INACTIVECAPTION 3 «define COLOR.MENU 4 «define C0L0R_WIND0W 5 «define C0L0R_WIND0WFRAME 6 «define COLOR.MENUTEXT 7 «define C0L0R_WIND0WTEXT 8 «define COLOR.CAPTIONTEXT «define COLOR.ACTIVEBORDER «define COLOR_INACTIVEBORDER «define COLOR.APPWORKSPACE «define COLOR.HIGHLIGHT «define COLOR.HIGHLIGHTTEXT «define C0L0R_BTNFACE «define C0L0R_BTNSHAD0W «define COLOR.GRAYTEXT «define COLOR.BTNTEXT «define COLOR.INACTIVECAPTIONTEXT «define COLOR.BTNHIGHLIGHT #if(WINVER >= 0x0400) «define C0L0R_3DDKSHAD0W «define C0L0R_3DLIGHT «define C0L0R.INF0TEXT «define C0L0R_INF0BK «define C0L0R.DESKT0P «define C0L0R_3DFACE «define C0L0R_3DSHAD0W «define C0L0R_3DHIGHLIGHT «define C0L0R_3DHILIGHT «define C0L0R_BTNHILIGHT «endif /* WINVER >= 0x0400 */ Оставляя в стороне последние шесть определений, которые лишь Av 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 C0L0R_BACKGR0UND COLOR.BTNFACE C0L0R_BTNSHAD0W COLOR.BTNHIGHLIGHT COLOR.BTNHIGHLIGHT COLOR.BTNHIGHLIGHT лируют старые значения, мы имеем ровно четыре константы котор* уникальны для Windows 95. (Строго говоря, вышеприведенны*1 указывает, что они специфичны для версии 4.0 или старше, в то вР как описание самого API говорит лишь о Windows 95. Что ж, п°'Б „ мому, с появлением на рынке Windows NT 4.0 мы получим еШе сомнительное место в Win32 API.)
з0 Абстрагирования 347 9 lJto произойдет, если ваша 32-разрядная программа будет запущена под Windows NT 3.51 и попытается использовать одну из этих четырех констант? Здесь вас ждет приятный сюрприз — эти константы окажутся известными системе, и она даст вам корректные ответы. Но запустите эту же самую программу под Win32s 1.30а (build 166), pi ей вернут значение ноль (черный цвет) при всех специфичных для Windows 95 значениях параметра. Это замечательнейший пример того, как не следует смешивать данные и управление в семантике — контрольное значение, говорящее «я ума не приложу, как нужно интерпретировать переданный вами параметр», неотличимо от легального значения «черный цвет». Правильная обертка для данного API не является такой уж простой, как может показаться с первого взгляда. Вы могли бы без труда догадаться, что интерфейс этой обертки должен иметь примерно следующий вид: BOOL uGetSycColor( int nlndex, // элемент интерфейса LPDWORD lpColor// возвращаемое значение цвета ), и возвращать TRUE при получении легального значения nlndex и FALSE — в противном случае. Основная же хитрость, разумеется, в том, как с достаточной степенью достоверности (читайте: 100%) определить, какое значение nlndex является дегальным, а какое нет. Вряд ли новые допустимые значения будут когда-либо поддержаны в Win32s. Но как насчет еще более новых вариантов, которые могут появиться в Windows 97 или в Cairo? Если ваша обертка будет отбрасывать все значения, меньшие 0 и большие 24, то она может неожиданно привести к ошибочным сбоям при переходе пользователя к новой версии Windows. Вероятно, наиболее разумным оборонительным рубежом в данном случае является проверка не только платформы, но и номера версии тоже: при работе с версией старше 4.0 обертка могла бы просто передавать параметр в исходную функцию без изменений и таким же образом поступать с возвращаемым значением. Сообщение EM_GETLINECOUNT позволяет узнать количество строк текста в многострочном поле редактирования. Описание этого сообщения, в частности, включает в себя такие слова: Возвращаемое значение является целым числом, указывающим количество строк в многострочном поле редактирования. Если поле редактирования не содержит никакого текста, возвращаемое значение равно 1. При виде такого определения интерфейса слов для комментариев не остается, потому что теперь всякая программа, желающая выяснить ис-
348 Абстрагирование в лействии: созлание своих виртуальных л -___л^ тинное количество строк, вынуждена совершать абсолютно ду0. заклинания. Например, что-нибудь в таком духе: f DWORD uEMGetLmeCount(HWND editjiandle) { if (dines = SendMessagefeditJiandle.EMJaETLINECOUNT, 0,0)) > 1) return lines; else { int start_char = SendMessage(edit_handle, EM_LINEINDEX,0,0); if(SendMessage(edit_handle,EM_LINELENGTH, start_char,0) == 0) return 0 else return 1; } } Как вы уже догадались, это исходный код еще одной оберточной фут ции из моей библиотеки Win32u. К сожалению, этот вариант обери, не решает всех проблем, связанных с тем, как имею EM_GETLINECOUNT ведет подсчет строк. Например, пустых стро; не имеющих на конце даже символа возврата каретки. Если вы введе; в многострочное поле редактирования несколько символов, а затем к жмете Ctrl+Enter для ввода жесткого перевода строки, сообщен! EM_GETLINECOUNT будет свидетельствовать, что в поле реда! тирования имеются две строки текста. В данном случае количеств строк оценивается с точки зрения самого поля (сколько у него ее: строк, на которые можно было бы поставить курсор), и оно можетоь заться совсем не тем, что ожидает вызывающий код (сколько стр имеют текст или жесткий перевод строки). В Функция RegQueryValiieExO уверенно выигрывает приз под назван ем «Всегда Готов» за свой третий параметр — lpdwReserved " f торый должен быть равен NULL. Зачем вообще может вам понадо611 ся функция с таким параметром? Честное слово, я не могу приДУ*1' хорошего обоснования для такого интерфейса. (Хотя зарезервпроВ ные поля в файлах данных, действительно, могут оказаться оч^ь лезными при работе со специализированными форматами даянием, главу 9.) В FormatMessageO — чрезвычайно полезная функция: она умеет1? лировать расширенный код ошибки в некоторый содержат^ текст. Именно эта функция может служить вам ключом для преБР'
Абстрагирования 349 чф['"" — ^vccrge- а нпя значений, полученных при помощи GetLastErrorO, в осмысленный текст, понятный человеческим существам. Одно лишь режет глаз — первый параметр этой функции. Он имеет тип DWORD и мо- ^сет содержать традиционный букет битовых флагов — в этом нет ничего нового, а тем более чего-то неправильного. Но вот вам сюрприз: помимо битовых флагов, этот параметр служит еще и для указания длины форматированного сообщения. Конкретно, документация так описывает этот параметр: dwFlags — содержит набор битовых флагов, которые задают характеристики процесса форматирования, а также указывают, как следует интерпретировать параметр IpSource. Младший байт dwFlags указывает, как функция должна обращаться с переводами строк в выходном буфере. Кроме того, младший байт может задавать максимальную ширину форматированной выходной строки. В одной моей простенькой программе, которая использует эту функцию для пояснения кодов, возвращаемых GetLastErrorO, есть такая строчка: cMcgLen = FormatMes3age(F0RMAT_MESSAGE_FR0M_SYSTEM | F0RMAT_MESSAGE_ALL0CATE_BUFFER | 64, NULL, err_code, MAKELANGID(0, SUBLANG_ENGLISH_US), (LPTSTR) &msgBuf, 512, NULL); Обратите внимание на то, что первый параметр передается в виде комбинации двух флаговых констант (FORMAT_MESSAGE_FROM_SYSTEM и FORMAT_MESSAGE_ALLOCATE_BUFFER) и числа 64 (заказанной мною длины форматированного сообщения). Такое смешение семантики в одном параметре кажется мне ужасным. Я не люблю длинных списков параметров, но сказав А, нужно произнести и Б — раз уж Microsoft не побоялась снабдить эту функцию семью параметрами, то почему бы вместо странного винегрета в dwFlags не отвести для длины еще один специальный параметр? Я называю эту проблему «ложной: истиной». Некоторые булевские функции Windows 95 API в качестве отрицательного ответа возвращают 0 (равный константе FALSE — тут все в порядке), по в качестве положительного ответа возвращают все что угодно, но не 1 (для тех, кто не помнит — константа TRUE определена именно как целая единица). Безобидность такого небрежного поведения обманчива, 11 подобные выверты API могут приводить к очаровательным ошибкам. Например, к такой: lf(GetClientRect(the_handle,&the_rect) == TRUE) { /* Делаем что-нибудь с полученным прямоугольником */
350 Абстрагирование в лействии: созлание своих виртуальных else { /* Отказываемся продолжить работу, так как мы не можем ничего показать */ /* Сюрприз, под Windowo 95 мы ВСЕГДА будем бастовать1'' */ i Некоторые программисты будут возражать, что никакой проблемы нет — ведь «каждый» знает, что на самом деле общепринятое сог- шение в С утверждает: FALSE == 0, a TRUE != 0. Но я не согласи такой постановкой вопроса, поскольку речь в данном случае идет не каких-то общепринятых соглашениях, а о совершенно конкретномс глашении — об определении API, о его контракте с окружаю^. миром. И именно этот контракт бессовестно нарушается функцией, щ скольку она делает неработоспособным совершенно легальный (хотя не очень традиционно оформленный) вызывающий код, подобный вь шеприведенному примеру. В документации по Windows API есть несколько мест, в которыхН crosoft явно оговаривает, что данная функция возвращает неиулево значение в качестве истины, но все эти функции не являются новшш ми Windows 95 и всегда работали таким способом. Новые же случа возвращения «ложных истин» ^задокументированы, а это значит, hi никто за пределами Microsoft не знает, от каких функций нужно ждат подвоха. (Пожалуй, я даже пошел бы на авантюрное пари и заяви что и в самой Microsoft ни у кого не найдется полного списка нарушг телей.) Из этого примера вытекают два полезных урока для программист Во-первых, при встрече с подобными API, всегда страхуйтесь пома' симуму и будьте готовы обработать самые неожиданные варианты m ведения таких неопределенных интерфейсов. Во-вторых, не допуск те проникновения «ложных истин» в ваши программы. Именно э правила объясняют, почему во многих моих примерах вы неоднокр1 но заметите такие окончания булевских функций: return (SomeWin32API(/* parms */) '= FALSE); Выглядит такое заклинание глупо, однако оно принудив111 превращает результат вызова SomeWin32API() в приемлемое и сТР1 корректное значение. Тем самым ваша функция гарантирует вып° ние своего контракта (даже если используемые ею подпрограммы °' не делают). В Сообгцеппе EM_LINELENGTH становится лауреатом приза «IlP0L те, что вы сказали?» за свою документацию и за использо01 параметра wParam:
Абстрагирования <Jv> I о Указывает индекс символа в строке, длина которой должна быть получена при посылке EM_LINELENGTH многострочному полю редактирования. Если этот параметр равен -1, сообщение возвращает количество невыбранных символов в строках, содержащих выбранные символы. Например, если выбор простирается от четвертого символа одной строки до восьмого от конца символа следующей строки, будет возвращено значение 10 (три символа на первой строке и семь — на следующей). Все понятно. Не дай вам Бог спроектировать подобным образом пользовательский интерфейс вашей программы! Лучше даже не пробовать. 0 Если вам нужен наглядный пример того, насколько важным может быть хороший выбор имен, я не думаю, что вам удастся превзойти сладкую парочку GetDlgltemO и GetDlgltemlnti). Первая функция возвращает дескриптор элемента управления по заданному ресурсному идентификатору. Вторая функция используется для получения текста, ассоциированного с заданным элементом управления, и для преобразования этого текста в целое число. Делая совершенно разные вещи, эти функции имеют настолько схожие имена, что какой-нибудь новичок в Windows-программировании запросто может упустить из виду одну из них. (На самом деле, я лично столкнулся с одной ситуацией, когда именно это и случилось. Мой приятель только-только начал программировать для Windows и одно время тщетно разыскивал функцию, которая возвращала бы дескриптор элемента управления в диалоге. Когда он обратился за советом ко мне, я указал ему на GetDlgltemO. В ответ он заявил, что я вероятно его не понял, потому что ему нужна функция, возвращающая именно дескриптор, а не содержимое элемента управления. Мы потратили что- то около минуты на споры, пока он вдруг не понял, что мы говорим о разных вещах — он имел в виду именно GetDlgltemlntO. Мне почему- то кажется, что тот мой приятель не был единственным программистом, наступившим на эти грабли.) Для меня является загадкой, почему Microsoft не могла назвать функцию GetDlgltemO более адекватно; например GetChildHandleO или как-нибудь еще. И я даже не собираюсь всерьез дискутировать вопрос о возможном написании оберток для подобных интерфейсов — то есть об «аб- страгированнп» с единственной целью переименовать интерфейс. Я также не хотел бы обсуждать такие трюки, как использование пРеирсщесора для таких переименований. У всех у нас есть масса более интересных и полезных дел, чем эти. Если хотите, можете делать подобные трюки, но только не говорите мне об этом и, главное, не го- г у^ч^-—
352 Абстрагирование в лействии: созлание своих виртуальньн м ворите никому о том, что идею вы подчерпнули из моей книги т. идей в моей книге не было, договорились? f в Фунщия GetDlgltemlntO примечательна еще по одной причин синтаксис нарушает одно из важнейших правил абстрагирования тором я говорил ранее: данная функция высокомерно игнорируй более вероятные способы ее использования вызывающим кодом п этом нарушение усугубляется тем, что предлагаемый извращенный,, терфейс провоцирует вызывающий код на совершение ошибок. К-, уже упоминал в главе 2, эта функция написана «шивопг навыворот» — возвращает сконвертированиое значение вместо hh. катора успеха/провала (последний возвращается через параметр), з не дает вызывающему коду возможности использовать данную (V цию наиболее естественным, удобным и привычным способом: if(SomeAPIFunction(/* параметры */)) { /* Функция сработала успешно, обрабатываем ее результаты */ } else { /* Функция провалилась, жалуемся пользователю, завершаем работу и т д. */ V В той же главе 2 я говорил, что более осмысленным был бы такс вариант этой функции: BOOL uGetDlgItedmInt( HWND hDlg, // дескриптор диалога int nlDDlgltem, // идентификатор элемента управления UINT* lpValue, // указатель на переменную, которая должна получить // сконвертированиое значение BOOL bSigned // указывает, является ли значение знаковым/беззнаковь ); возвращающий TRUE или FALSE для индикации, успешно ли про111 извлечение текста и его конвертация. На самом деле, именно ^ вариант я включил в свою библиотеку Win32u. Несмотря на то, чт° кая обертка ничего не делает, кроме как видоизменяет синтакспс терфейса, результат оказывается значительно более удобным в ^ зовании, чем оригинал — одно это служит достаточным оправДа существования данной обертки. В Конечно же, самой большой и болезненной проблемой всего * API является документация. Недокументированные расшире коды ошибок (описание каждой функции, использующей их, Д0"' сопровождаться полным списком возможных кодов и условий 1Ь^ явления), отсутствие информации об особенностях версий (напр11'"
Unum 353 какие интерфейсы были добавлены в Windows NT 3.5 или 3.51?), драгоценные крохи информации о различиях между платформами (изменчивое поведение GetLastErrorO и всевозможные «ложные истины»), наконец, просто многочисленные откровенные ошибки — вот с чем приходится иметь дело. Чему тут можно доверять? На самом деле, все мы должны рассматривать нынешнее состояние документации по Win32 API как притчу. На протяжении всей этой книге я рассказываю о многочисленных проблемах программирования для Windows, в то время как многие из этих проблем были бы смягчены и даже полностью устранены просто за счет более высокого качества документации. И когда вы пишете вашу собственную библиотеку — будете ли вы единственным ее пользователем, или она станет инструментом многих тысяч других программистов — всегда стоит потратить несколько лишних минут на документирование деталей. Наверное, пора все-таки пояснить, откуда взялось такое странное название данного раздела. Есть один замечательный комический сериал — «Кельвин и Хоббс». Его главными героями являются остроумный, изобретательный мальчуган по имени Кельвин и его воображаемый приятель, тигр Хоббс. Они очень часто играют в одну интересную игру — кельвинбол, основная отличительная особенность которой — создание новых и самые неожиданные изменения старых правил игры прямо по ходу дела. И это, естественно, приводит к появлению самых неожиданных дурацких правил — например, требование ходить задом-наперед, петь песни, восхваляющие соперника. Вплоть до так называемых «обратных зон» — временного изменения правил на прямо противоположные. Конечно, даже шуточное сравнение Win32 API с кельвинболом все-таки было бы преувеличением. И тем не менее я должен признаться, что всяки раз, когда я открываю документацию для того или иного API, у меня то и дело возникают очень странные ощущения — то я чувствую на своей шее цепкие когти Хоббса, то мне кажется, что это Кельвин в очередной раз заставляет меня сделать какую-то глупость 1 Paribus Unum ' :)то не Программистская Латынь цля обозначения «#include <\vin- s ll>»1. Но :-)тн слова замечательно выражают общую философию, которой '1°лжны стремиться следовать при создании библиотеки: ваша цель — из * Л ""%ь,<( >lf4ibus Vmim -- можно перенести с латыни как «из множества — целое». Кстати, это |() 'Конце напечатано па всех американских долларовых банкнотах Наверное, поэтому оно 110 ч блц.жо сердцу каждого американца [Примечание переводчика.]
354 Абстрагирование в лействии: созлание своих виртуальных м более простых частей создать нечто новое и связное. Если вы это сделает получите новую, более высокую и более ровную платформу для дальней построения ваших программ и сможете извлечь максимально возможную в ^ ду от повторного использования кода. Иначе говоря, вы сможете водрузиТь Г код на его собственные плечи (наверное, только программисты смогут ц0 / тоинетву оценить эту рекурсивную метафору) и достичь больших высот это было возможно ранее.
SAlLUjJ ! I mmme ваши юр&й-ратмц тмкндтом Opportunity makes a thief. Фрэнсис Б<жоп Проблемы компьютерной безопасности сродни последовательностям Ман- дельброта: как бы глубоко вы ни погрузились в эти вопросы, как бы детально их ни представляли, перед вами всегда будут маячить едва различимые контуры следующего уровня сложности. Но это не должно вас беспокоить. Я не собираюсь заставлять вас нырять в мрачные глубины алгоритмов шифрования "классификаций безопасности, используемых министерством обороны США. Я с радостью затрону эту тему лишь поверхностно, а все таинственные детали °сгавлю специалистам и экспертам в этой области. В данной же главе хотелось бЬ1 представить вам пример реализации «легкой» защиты программы, который 1аодно демонстрирует не совсем обычный подход к Windows-приложениям. ^т°т прием я называю замкнутостью. Замкнутость программы — это особый режим ее работы, характеристика 41 поведения. Когда программа находится в замкнутом состоянии, пользова- 4,1 видит только ее значок на панели задач (или на поверхности стола, если ",С) происходит в Windows 3.1) и не может ничего сделать, кроме как ершить работу программы или вывести ее из замкнутого состояния, указав ' ,Ветствующнй пароль. Программа способна запомнить это состояние и будет Этически переходить в него при каждом следующем запуске до тех пор, пользователь не введет пароль. Выйдя из замкнутого состояния, Римма начинает работать как обычно. , ^ак вы уже наверное догадались, замкнутая программа - это еще один 1еР Минимизированного Windows-приложения. В главе 7 я уже говорил, -в Рог У су Р°ваиных программ, и вот перед вами еще один пример. , УЩсствует множество самых разнообразных облаете!! применения мини-
356 Слелайте вашу программу заМй( Зачем нуЛна минимальная защищу По сравнению с серьезными системами защиты программ и данных, обсу^ мый здесь метод является чем-то вроде «младшего брата». Причем это м0/ воспринимать буквально: защита, предназначенная всего лишь против слуц иого любопытного — например, против вашего младшего братишки, кот0" может оказаться за вашим компьютером в тот момент, когда вы на пару щ вышли из комнаты. Стоит ли вообще связываться с такой хрупкой защитой? Да. По той жес мой причине, но которой компании, производящие ставни из алюминия, строг рекомендуют устанавливать на них замки. Вы когда-нибудь пробовали открыт такие ставни, когда они заперты на замок? Лично я могу вас уверить, что of открываются без особого труда — достаточно один раз не очень сильно потя путь за ручку. Но тот факт, что эти стильные замочки могут быть так леи/ взломаны, ничего не значит: они вовсе не предназначены для отпугивание опытных и решительных воров, зато могут служить недвусмысленным сигм лом «Не входить!» и отличнотг защитой от шестилетнего, до жути любопытноа соседского ребенка. Аналогично, любой человек, работатающий на PC в офисе, знает цену и кой программы, как хранитель экрана с паролем. Реальную защиту он к обеспечивает - всякий знаток Windows «обманет» эту программу в считаные минуты. Но свою истинную задачу программа выполняет на все сто. Защищать или не защищать? Так почему бы не добавить такую возможность в свою программу9 Он. проста, она лишь немного увеличивает размер вашей программы и, в концекор jj,ob, некоторым пользователям вашей программы она несомненно придется п1 душе. Иными словами, ваша программа становится немного совершеннее удобнее за счет очень маленьких усилий с вашей стороны — такое соотношен^ всегда достаточно привлекательно. Некоторое время назад я реализовал так}г возможность в Stickiest, и мои последующие беседы с пользователями n°Ivl зывают, что многие из тех, кто работает в коллективной обстановке, II пользуют замкнутость моей программы изо дня в день. Разве не достаточно просто выгрузить программу, а потом вновь запу^1 ее после возвращения к компьютеру? Это зависит от природы и характера конкретной программы, и ответ может колебаться от безусловного «Да>> \ решительного «нет». Например, было бы бессмысленно делать заМ*^ , программой Notepad от Windows 95: Notepad открывает файл так быстро- ^ вынужденный перезапуск по возвращении с обеденного перерыва яв значительным неудобством. А как насчет RTF-файлов в Word for Wi^cl° (i Пробовали вы загрузить по-настоящему большой RTF-файл? Или ср^
аЯ молель, лубль первый 357 ко больших файлов? О, в подобных случаях этот маленький индикатор *° ecca внизу окна мгновенно теряет всю свою привлекательность! Я был бы ,г0 счастлив, если бы Word for Windows был замкнутой программой и умел 1 идИ1Дать загруженные в него файлы, избегая долгого процесса повторной |ГруЗКИ. Я м°гУ се"е представить ту бурную реакцию, которую может вызвать такое елание у некоторых блюстителей компьютерной производительности: как кН0 предлагать оставлять в памяти мегабайтные или еще большие RTF- , iiibi? Но на самом деле, тут не о чем беспокоиться. Во-первых, я и сам пнадлежу к тому типу пользователей, которые болезненно воспринимают юбые неэкономичные программы. А во-вторых, речь идет о виртуальной памяти, которой должно быть достаточно для подобных трюков даже на самой обычной пользовательской персоналке (при разумной настройке свопнинга в Windows). Если же говорить об остальных ресурсах системы, постоянное истощение которых было серьезнейшей ахиллесовой пятой Windows в течение многих лет, то с приходом Windows 95 ситуация поменялась нгютолько радикально, что вам ienepb придется изрядно постараться, чтобы исчерпать эти самые ресурсы. Я убежден, что это усовершенствование Windows будет не меньшим (если не большим) стимулом для перехода на Windows 95, чем остальные очевидные преимущества новой версии. Шовая модель, дубль первый Когда я реализовывал замкнутость в первый раз, это происходило с програм- м°н, написанной на Borland Pascal for Windows с использованием OWL. Всем программистам — пользователям этих инструментов хорошо знакома следующая последовательность кода, возникающая в главной подпрограмме прак- Г|1Чески каждого приложения: begin '"vApp Init(AppNaine), '1vApp Run, MvApp Done. cna 11не пришло в голову, что нет никакой причины, по которой было бы нель- I 1>1еть несколько объектов-приложений в одной программе или переключать- Г'МеЖду ними: begln ,ePeat ^VApp-i imt(AppNaine), ;vAppi Run, ^APP1 Done, n°t finished then
358 Слелайте вашу программу заМ1( begin МуАрр2 Imt(AppName); MyApp2.Run. МуАрр2 Done, end, until finished: end Значение глобальной булевской переменной finished в соответствую^ момент устанавливается главным окном объекта-приложения и указывает ствительно ли программа закончила работу, или она всего лишь перещг/ замкнутое состояние или обратно. (Из данного примера убрана масса детале но я надеюсь, что общая идея вам понятна.) При таком подходе, когда пользг ватель приказывает программе замкнуться или разомкнуться, соответствуют]!- объект-приложение устанавливает необходимые значения глобальных перемен ных и завершает свою работу, а управление передается другому объекту. Обратите внимание, что такая схема несколько усложняет проблем^ сохранности данных программы в нетронутом виде. Изначальная версия рас сматриваемой программы была написана так, что файлы данных закрывались по окончании работы. Поскольку при добавлении замкнутости в эту программ я беспокоился об экономии ресурсов системы во время пребывания программу в замкнутом состоянии, мне пришлось сделать так, чтобы файлы данных выгружались из памяти. Такое решение серьезно ограничивает применимость замкнутости, поскольку она становится оправданной только при достаточно вы сокой скорости выгрузки и перезагрузки файлов данных. В то же время, какя упоминал выше, при быстрой загрузке документов замкнутость программы, как правило, вообще не нужна. Это кажущееся противоречие на самом деле противоречием не является. Даже при быстрой загрузке документов добавление замкнутости в программу может оказаться очень осмысленным и полезным Например, для защиты документов программы от несанкционированного доступа: если данные хранятся в специализированном бинарном формате и могут обрабатываться лишь вашей программой, то переведя программу в замкнута состояние вы гарантируете недоступность ее файлов (без предварительного указания правильного пароля) не только во время работы программы, но и в промежутках между ее запусками. Когда я попытался перенести эту схему на Visual C++, я столкнулсяс неприятным сюрпризом. В качестве первоначального теста я написа' простейшую программу, которая в теле функции WinMainO пыталась два#Д подряд запустить одно и то же приложение. Данная проба, очевидно, ^ лишь первым приближением в тестировании всей схемы, но она оказалась редкость удачной - я сразу же понял, что мне не удастся так просто повтор1 старый прием: объект-приложение запускался только один раз. После 1 которого, довольно долгого расследования я выяснил, что причина проб*'1е> скрывалась где-то в недрах метода Run() объекта-приложения.
модель, лубль первый 359 г- ким образом, в этот момент я оказался на том самом распутье, которое яка хорошо знакомо многим Windows-программистам: мне предстояло .гъ между ковырянием исходного кода MFC с целью как-то обойти воз- л" препятствие и поиском другой физической реализации той же самой Дческой задачи. Г fciii бы у меня было достаточно времени на копание в исходных текстах щнис продвинуться так далеко от ствола по этой ветке, я наверняка смог Нити способ заставить MFC выполнить то, что мне нужно. Но мне не хва- ни времени, ни авантюризма, и поэтому я не пошел этим путем. Махинации со внутренностями такой библиотеки, как MFC или OWL, лишь очень редко являются оправданным методом решения проблем. И их следует причислить к Младшей Лиге Противоестественных Действий. При работе над Stickiest мне пришлось прибегнуть к этому средству, и плата за это была не такой уж малой: при выходе каждой новой версии OWL я был вынужден залезать в ее исходники и вносить необходимые изменения размером в несколько дюжин строк. Даже если вы уверены, что знаете внутреннее устройство каркасной библиотеки вдоль и поперек, подобные мероприятия всегда сопровождаются немалым риском. В подобных ситуациях, как всегда, не существует одного решения на все случаи жизни. И если изменение исходного кода библиотеки действительно является лучшим вариантом, вы можете делать это с чистой совестью. Главное - помните о потенциальной стоимости и риске такого решения и обязательно задокументируйте эти изменения так тщательно, как если бы от этого зависела ваша жизнь. Вазовая модель, ддВль второй Как это часто бывает в программировании, полезно было поразмышлять НаД этой проблемой с точки зрения пользователя — что, собственно, должен •*»деть пользователь при работе с такой программой. Когда программа нерево- 4,,т<-'я пользователем в замкнутое состояние, она закрывает свое главное окно, !1° продолжает присутствовать на панели задач (или на поверхности стола п°1ьзователя) в виде значка. Как только пользователь сообщает нужный аРоль, программа вновь принимает свой обычный облик, и пользователь снова о|Дит те же самые документы, которые были загружены при замыкании 10граммы (предполагается, что замыкание было произведено во время того же 1 1ого сеанса работы с программой). , Очевидно, все, что от меня требовалось — это завести глобальный флаг, . Пирующий состояние замкнутости программы, и использовать некоторые ! РЮков но написанию минимизированных программ, описанных в главе 7. ая Упрощенная схема реализации замкнутой программы на C++/MFC пред- ЛеНа в примере LOCKER. Поскольку основной целью этой программы-
360 Слелайте вашу программу заМк примера была лишь демонстрация сути предлагаемого решения, я не утрх, себя такими приземленными деталями, как реальный запрос пароля \ " проверка. Соответствующие диалоги в LOCKER всего лишь позволяют п0 ,' вателю нажать ту пли иную кнопку, чтобы имитировать ввод правильного' неправильного пароля Более полную реализацию работы с паролем вы щ.\ найти в программе MegaZero из приложения А. *z*m Построение примера. Ыщ Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chapn/ locker Платформа: Win32 Инструкции по сборке: откройте при помощи Visual C++ 2 прилагающийся МАК-файл и скомпилируйте, как обычно Листинг 11.1. LOCK.( ////////////////////////////////////////////////////////////////////////// // LOCK.CPP* Демонстрационная версия замкнутой программы // // Copyright (с) 1995 Lou Gnnzo «include "ctdafx h «include "locker h' «include "lock h static BOOL locked = FALSE, static char paccword[MAX_PW] = '**, ////////////////////////////////////////////////////////////////////////// BOOL RetneveLockedStatus(void) { // Реализация этой функции сильно зависит от внешних факторов, таких как // степень надежности замкнутого состояния и т п Пароль может храниться // в INI-файле, в реестре Windows или в специализированном бинарном файле // // Вне зависимости от способа хранения пароля и состояния, данная функция // должна оставлять программу в НЕЗАМКНУТОМ состоянии, если истинное значение // состояния не может быть прочитано' Например // // if(no какой-то причине не считать состояние из постоянного хранилища) // return (locked = FALSE), // locked = FALSE, return locked, )ll///ltII/III///IIIllllllllllIll/Ill IIIIIII///IIII/I//IIII//I//////IIIIII BOOL SaveLockedState(void) { // Реализация этой функции сильно зависит от внешних факторов, таких как
,. молель, лубль первый Pjd>^^- '- сТепень надежности замкнутого состояния и т п Пароль может храниться 6 iNI-файле. в реестре Windows или в специализированном бинарном файле .:оГп TRUE, , iillllllllll/llll/IJIIIIIIIIIIIItlll/ll/III//lll/lIll/Ill//I//I/III III -0L LockPWDlg(void) :сли мы уже находимся в замкнутом состоянии, эта функция не должна вызываться Однако вместо того, чтобы смущать пользователя показом неправильного диалога, мы просто возвращаем управление и говорим -, вызывающему коду, что все в порядке Это поможет вызывающему коду ,. синхронизировать свое представление о состоянии программы с истинным 'll положением дел if(locked) return locked, CDialog the_prompt(IDD_LOCKPW), if(the_ptoinpt DoModaK) == IDOK) xocked - TRUE return locked, )///1П1/I II/////////////////////I//////////////////////////////////////// BOOL UnlockPWDlg(void) // Если мы не находимся в замкнутом состоянии, эта функция не должна // вызываться Однако, вместо того, чтобы смущать пользователя показом // неправильного диалога, мы просто возвращаем управление и говорим // вызывающему коду, что все в порядке. Это поможет вызывающему коду // синхронизировать свое представление о состоянии программы с истинным // положением дел if(' locked) return locked, CDuloq the_proinpt(IDD_UNLOCKPW), if(the_prompt DoModaK) == IDOK) locked = FALSE, pasoword[0] = '\0', return locked, ,'';;' !'jl II fill/I ll/lll/llll/l/Ill/Ill l/ll/lllllllll/ll II/IIIII////IIII/III "v ^ IcLocked(void) '6tll|n locked. Листинг 11.2. MAINFRM.CPP из примера LOCK , ,nainfrm cpp реализация класса CMainFrame ?lnclude -otdafx h "delude locker h 361
Слелайте вашу программу зам^ «include lock h «include "mainfrin h #ifdef .DEBUG #undef THIS_FILE static char BASED_CODE THIS_FILE[] = __FILE__; tfendif ///////////////////////////////////////////////////////////////////////////// // CMainFrame IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd) BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) //{{AFX_MSG_MAP(CMainFrame) ON_WM_SYSCOMMAND() ON_WM_CREATE() //)!AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CMainFrame construction/destruction CMainFrame CMainFrameO // TODO: add member initialization code here CMainFrame::"CMainFrameO { ///////////////////////////////////////////////////////////////////////////// // CMainFrame diagnostics #ifdef .DEBUG void CMainFrame: AssertValid() const { CFrameWnd-: AssertValidO; void CMainFrame Dump(CDumpContext& dc) const { CFrameWnd •Dump(dc), i tfendif //_DEBUG ///////////////////////////////////////////////////////////////////////////// // CMainFrame message handlers void CMainFrame ■OnSysCommand(UINT nID, LPARAM lParam) UINT the_real_nID = nID & OxFFFO, if(nID == CM_L0CKUNL0CK) { OnLockUnlock(), return, switch (the_real_nID) { case SC_RESTORE' // Если мы замкнуты, показываем приглашение для ввода пароля case SC_MAXIMIZE: // В противном случае обрабатываем сообщения как обычно if(IsLocked()) OnLockUnlock(); else CFrameWnd :OnSysCoinmand(nID, lParam); break, default- 362
ла молель, лубль первый 363 9 GFrameWnd OnSycCommand(nID, lParam). .nt CMainFraine -OnCreate(LPCREATESTRUCT lpCreateStruct) ,'* (CFrameWnd OnCreate(lpCreateStruct) == -1) -eturn -1, 'Menu *сузшепи = GetSyctemMenu(FALSE): I 3inenu->AppendMenu(MF_STRING,CM_LOCKUNLOCK "Замкнуть/Разомкнуть. '), xf(RetneveLockedStatuG()) // Вызываем DoLockUnlockO только если мы замкнуты DoLockUnlockO, return 0, И CMainFraine замыкание/размыкание1 запуск диалога // // Мы вызываем DoLockUnlockO только после должной проверки состояния. void CMainFraine. :OnLockUnlock(void) if(IcLockedO) { TRACEC'OnLockUnlock. IsLockedO == TRUE\n"); lf('UnlockPWDlgO) // FALSE' Теперь мы не замкнуты DoLockUnlockO; else TRACE('OnLockUnlock: IcLockedO == FALSE\n'), lf(LockPWDlgO) // TRUE: Теперь мы замкнуты DoLockUnlockO; i ///////////////////////////////////////////////////////////////////////////// // CMainFraine замыкание/размыкание: изменения состояния // // Этот код предполагает, что он вызывается только тогда, когда это необходимо // (то есть когда состояние замкнутости программы действительно изменяется), void CMainFraine DoLockUnlock(void) { if(IsLockedO) { // Меняем значок - демонстрируем замкнутость DeGtroyIcon((HICON)GetClaGGLong(AfxGetApp()->in_pMainWnd->in_hWnd,GCL_HICON)), SetClaccLong(AfxGetAppO->in_pMainWnd->m_hWnd, GCL_HIC0N, (long)Afx6etApp()-^LoadIcon(IDR_LOCKEDMAINFRAME)); // Если в нормальном состоянии программа принимает сбрасываемые на нее файлы, 1! то тут нужно это запретить // DragAcceptFileG(FALSE); // Минимизируем себя PoGtMesoage(WM_SYSCOMMAND,8C.MINIMIZE.0), elce // Меняем значок на нормальный OeGtroyIcon((HICON)GetClaGGLong(AfxGetApp()->in_pMainWnd->in_hWnd,GCL_HICON));
364 Слелайте вашу программу замки SetClascLong(AfxGetApp()->m_pMainWnd->in_hWncl, GCL.HICON, (long)AfxGetApp()->LoadIcon(IDR_MAlNFRAME)), // Если в нормальном состоянии программа принимает сбрасываемые на нее файлы, // то тут нужно это вновь разрешить // DragAcceptFileG(TRUE), // Восстанавливаем свое окно PostMeGsage(WM_SYSCOMMAND,SC_RESTORE,0), Еще одной функцией-заглушкой является RetrieveLockedStateO. Онавь зывается при запуске, чтобы выяснить, находилась ли программа в замкнуто состоянии в момент последнего завершения ее работы. Эта же функция мо)к, загружать из постоянного хранилища пароль, необходимый для возвращен^ программы в норхмальное состояние. Именно эта функция является ключом обеспечивающим запоминание состояния замкнутости от одного запуск программы до другого. В реальной программе функция RetrieveLockedStateO могла бы опраши вать какой-нибудь JNI-файл, реестр Windows или файл данных специа лизироваиного формата. Независимо от избранного способа вы должны следо вать логике псевдокода, представленного в данном примере: если хранилище пароля и состояния потерялось или оказалось недоступным по какой-либо иной причине (а вы уже знаете, с какой легкостью пользователь или другая программа может вам это устроить), эта функция должна возвращать FALSE то есть указывать на нормальное состояние. В зависимости от того, что конкретно делает ваша программа, и в какой обстановке она используется, вы можете захотеть сделать ее состояние по умолчанию замкнутым Пускай даже ценой лишения пользователя дог тупа к собственным данным в некоторых редких случаях. Это превосход ный пример очень тонкого и непростого проектировочного решения которое следует принимать только в контексте конкретного проекта Поскольку в дайной демонстрационной версии ничего реального 1 паролями не делается, функция SaveLockedStateO также является заглушкой В зависимости от способа и места хранения пароля и состояния, вы можете сДе лать функции SaveLockedStateO и RetrieveLockedStateO более обобщенным11 Например, в качестве параметра им может передаваться дескриптор открыт01 файла, который они будут использовать для чтения-записи необходим01111, информации. Таким образом, ваша программа сможет хранить эти даннЫе своем специализированном файле, не заботясь о конкретных деталях, скрьГГ1) в черных ящиках SaveLockedStateO и RetrieveLockedStateO. Короче гов°?' абстрагируйтесь, абстрагируйтесь и снова абстрагируйтесь. Две эти фунК явно не будут вызываться слишком часто, поэтому вы можете смело обоб^'
йЯ модель, лубль первый 365 0 максимуму: потери в производительности будут незначительными, зато 4 -рым ПРП сопровождении программы в целом будет наверняка ощутимым. '' обратите внимание, что в представленном примере все функции и данные, а10щце за реализацию замкнутости, собраны в отдельную единицу компи- 1 иП и соответствующий заголовочный файл (LOCK.CPP и LOCK.H), а диа- и для работы с паролем доступны только через функции-шлюзы. Даже в де- сТрационной программе основной код не имеет доступа к паролю — это пактерная черта, которую вы должны стремиться сохранить и в ваших реаль- ' программах. Это позволяет возвести максимально высокую и непроии- 'аеМую стену между основным кодом и поддержкой замкнутости, что не только вдеется хорошим стилем программирования вообще, но и упрощает задачу превращения уже существующей программы в замкнутую. Как сделать замкнутой существующую программу Если ваша существующая программа вписывается в ту общую модель замкнутости, которая описана в начале главы, вы можете безо всякого ущерба для основных функций превратить ее в замкнутую программу, выполнив следующие пять шагов: 1. Добавьте какие-нибудь интерфейсные элементы, которые позволят пользователю замыкать/размыкать вашу программу. Это может быть реализовано как угодно — от простых дополнительных пунктов системного меню (как в приведенном выше примере) до сложных и изысканных графических прибамбасов (эй, любители шуток на тему Пояса Целомудрия — вот вам отличный повод заняться делом!). Главное — пользователь должен получить удобный, сподручный способ управлять замкнутостью программы (особенно при размыкании — именно для этого в демонстрационном примере для размыкания можно использовать не только соответствующий пункт системного меню, но и стандартные приемы восстановления окна из минимизированного состояния). 2- Решите, как и где вы будете хранить пароль и состояние замкнутости, а затем соответствующим образом модифицируйте функции Save- LockedStateO и RetrieveLockedStateO. Этот этап может занять от пары минут до нескольких часов — все зависит от того, как вы разделите эту работу между основной программой и модулем поддержки замкнутости. ^ Сделайте так, чтобы ваша программа перехватывала и должным образом обрабатывала сообщение WM_SYSCOMMAND со значениями wParam, равными SC_RESTORE и SC_MAXIMIZE. Примерный
366 Слелайте вашу программу залл^ вариант такой обработки, используемый в программе-примере Lev ER, находится прямо в модуле MAINFRM.CPP — см. листинг li •> общем-то, там не на что смотреть, и это неспроста — ваш обрати и не должен делать ничего особенного. (Однако стоит еще раз обра- У внимание на хитроумное использование параметра nlD — см. biw^ «Идиотские выкрутасы MFC» на стр. 260, где объяснены прищ этих манипуляций.) с 4. Добавьте в вашу программу функции OnLockUnlockO и DoLockC lock(). Модифицируйте функцию DoLockUnlockO так, чтобы она п пользовала правильные значки, проигрывала какие-нибудь звуки, м. няла заголовок программы, вытворяла бы еще какие-нибудь штучки интерфейсом пользователя, — короче, делала все, что вам покажете; уместным. Главное — помните, что все нетривиальные функционала ные возможности вашей программы должны быть заблокированы пока она находится в замкнутом состоянии. Это касается таких вещей как поддержка drag-and-drop, и вообще - всех функций, кроме размыкания и завершения работы. Демонстрационные примеры функций OnLockUnlockO и DoLockUnlockO, приведенные в листинге 11.2, показывают вам основную логику действий — вызов диалогов для ввода пароля, а затем переключение состояния программы. На первый взгляд, было бы здорово перенести эти функции в модуль LOCK.CPP и просто вызывать их из основной программы, используя возвращаемое значение типа BOOL для индикации состояния замкнутости. Однако, этот подход может оказаться совсем не таким удобным как кажется: в эти функции пришлось бы передавать массу дополнительной информации (ресурсные идентификаторы двух значков, параметры hlnstance и hWnd для загрузки значков и изменения внешнего вида окна и так далее — все, что необходимо для манипуляции интерфейсом). Если ваша программа использует какие-либо немодальные диалоги или другие элементы экрана, вам, вероятно, придется закрывать и11' прятать их, когда программа переходит в замкнутое состояние Оь с0 ответственно, вновь показывать их при обратной операции). Весь коД отвечающий за это, должен либо помещен в DoLockUnlockO, либо^ь зываться из этой функции. Наличие двух этих функций и показанное в примере разделение трУ 1 между ними обусловлено простой причиной: когда ваша программ11 ' пускается и проверяет свое сохраненное состояние, ей необходим иметь доступ к процедуре замыкания без предварительного заЯР пароля. Именно эта логика и реализована в методе OnCreateO в *' jic'MAINFRM.CPP.
ёАРмЫ_безопасности 367 При необходимости, видоизмените диалоги для замыкания и размыкания. На этом этапе вам могут пригодиться некоторые из моих замечаний и рассуждений, изложенных ниже в разделе «Проблемы безопасности». Разумеется, перечисленных выше пяти шагов может оказаться недостаточно для по-настоящему добротной реализации замкнутости. Например, как насчет справочной информации для WinHelp? Ведь это последнее дело - ввести новую, незнакомую для пользователя возможность и оставить ее без подсказок и объяснений. Кроме того, вы обязательно должны быть готовы к тому, что рано или поздно к вам начнут обращаться пользователи, забывшие свои пароли. Они будут приходить к вам - дрожащие, с пылающим взором и вставшими дыбом волосами — и будут умолять вас вернуть им их нечаянно навек заблокированные данные. Если ваша программа хранит свое состояние в каком-либо специальном файле данных, вам вероятно придется написать хотя бы простейшую утилиту, которая залезала бы в такой файл и должным образом «перевертывала в нем битики». Причем я настоятельно советую вам потратить несколько минут па написание такой утилиты загодя — еще до выхода в свет основной программы; я не думаю, что вам больше понравится писать ее потом, когда за вашей спиной будет стоять рассерженный начальник или расстроенный заказчик. Еще один существенный момент, влияющий на реализацию замкнутости, — это вопрос о том, оставлять или не оставлять открытыми (при переходе в замкнутое состояние) документы. Ответ на этот вопрос очень сильно зависит от предполагаемого стиля работы пользователей с вашей программой. Наиболее сложный вариант - это выгрузка документов и необходимое при эгом запоминание всего внутреннего и внешнего состояния программы (какие именно документы были загружены на момент замыкания? какой из них был активен? как были расположены перемещаемые дочерние окна? где находился иУрсор в активном документе? и т. д. и т. п.). Все это может вылиться в боль- Шие Дополнительные затраты (и наверняка в огромное количество вопросов и ,1Р°сьб со стороны пользователей), и именно поэтому гораздо более привлека- Те*1ьным является второй путь -- оставить документы загруженными и просто 1|1нимпзировать программу. ®6*обАемы безопасности р Нс111але этой главы я говорил, что не собирался погружаться в вопросы ком- ,.t °Герной безопасности. Я и не буду этого делать. Но сказать пару слов о Ролях все-таки стоит. , Работа с паролем - это одна из таких областей, в которой человеческий °Р должен быть учтен и предельно продуман в вашей программе. Очень
368 Слелайте вашу программу замки —^2j9^ легко написать программу так, что полезность данной функции будет све на нет. Недовольные пользователи попросту не будут ее использовать, а Л означает, что вы зря потратите свои драгоценные усилия. Это пробч^ критической массы: объем работы X фактически выполнится напрасно ' скольку приведет к слабому результату, которым никто не будет пользовать а более продуманная и качественная реализация, требующая объема раб0 2Х, окупится сполна, так как результат пользователям понравится. При обращении с паролями в конкретной замкнутой программе (обрати внимание на тщательность формулировки) вам следует учитывать следуют, рекомендации: 1. Не делайте пароль чувствительным к регистру букв. Это приводит лишь к ошибкам ввода со стороны пользователей и будет сбивать их с толку. Вы не хотите, чтобы ваша программа отказывалась размыкаться при вводе пароля «fred», если она была замкнута с паролем «Fred». 2. В целях проверки, просите пользователя задавать пароль дважды Вы считаете себя асом в машинописи? Я лично таковым не являюсь, и ваши пользователи тоже. Проверка пароля — простая и недорогая страховка. Я знаю это не только в теории, но и по своему собственнее печальному опыту: когда я забыл про такую проверку при первой реализации замкнутости в Stickiest, несколько моих пользователей тут же попали в ловушку (ввели пароль с опечаткой, а обнаружить это смогли только при следующей попытке разомкнуть программу) и обратились ко мне за помощью. Их опечатки были лишь непосредственными причинами проблемы, а корневая причина заключалась именно в моей ошибке проектирования. 3. Помимо максимально допустимой длины пароля (которая может определяться просто физическими деталями реализации) обязательно введите минимально допустимую длину пароля. Глупо использовать пароль из одной буквы? Конечно, да. Но стоит вам позволить это Ж' лать, и ваши пользователи, которые постоянно торопятся куда-то по своим делам, тут же приобретут дурную привычку задавать коротки пароли. Поэтому не надо это позволять. Я рекомендую минимальную длину пароля в пять символов. 4. Маскируйте ввод пароля, чтобы предотвратить «подсматривав через плечо». У стандартных полей редактирования в Windows &ь специальный атрибут для этого (ES_PASSWORD), который позв°лЯ ет автоматически заменять вводимые пользователем символы на зБ ' дочки. Не стесняйтесь использовать это хорошее, бесплатное среДсТ защиты. Ваши программы только выиграют от этого и будут выгЛЯД более профессионально.
6армы безопасности эЬУ Зашифровывайте пароль при хранении его на диске. При этом вовсе не нужно слишком усложнять себе задачу, взмывая к высотам мудреных алгоритмов шифрования, скрытых файлов и тому подобных вещей. Поскольку речь изначально идет о легкой, простой защите, будьте проще. Мой совет: возьмите любое 8-битовое значение и зашифруйте каждый байт пароля простой побитной логической операцией «исключающее или» с этим значением. Это превратит исходный пароль в «бинарный мусор» для глаз непрошенных любопытных, а дешифрование такого пароля осуществляется так же просто pi быстро, как шифрование — при помощи той же самой логической операции. (Всегда забавно наблюдать, какой эффект производит этот простой фокус на программистов-новичков. Если делать это в баре, то вы наверняка смогли бы заработать бесплатную выпивку за счет изумленной публики.) Это простой, эффективный и вполне подходящий (для данной задачи) способ хранения пароля в нечитабельном для человеческого глаза виде. 6. Отбрасывайте лидирующие и завершающие пробелы в пароле. (Это хороший повод для использования набора инструментов, представленного мной в главе 3.) Лишь очень немногие вещи приносят пользователям и программистам столько неприятностей, сколько эти пробелы. И когда речь идет о паролях, они могут быть просто смертоносными (разумется, я имею в виду пробелы, а не пользователей или пароли). Конечно же, вы должны задокументировать тот факт, что ваша программа отбрасывает эти символы, однако на практике это не имеет особого значения. Если даже пользователь не знает об этом фокусе, вам не о чем беспокоиться: пароль с умышленно введенным пробелом в начале или в конце будет так же корректно обработан, и пользователь вообще не заметит ваших манипуляций за кулисами. 7 В зависимости от ситуации, вам может понадобиться отвергать пароли, содержащие некоторые конкретные символы. Чаще всего эта проблема не встает вообще (особенно в таком вопросе, как замкнутость программы), но вы должны всегда помнить о возможных подводных камнях. Например, пользователь может задать пароль на одном компьютере, а затем попытаться воспроизвести его на другом, клавиатура которого не позволяет ввести тот или иной символ. "• Удостоверьтесь, что ваша программа предпринимает какие-либо серьезные действия только после ввода правильного пароля. Возможно, данное замечание не слишком актуально для такого простого примера, как использование пароля в замкнутой программе. Но уж коли речь зашла о паролях, я счел уместным упомянуть это простое правило именно здесь. Пока я писал эту книгу, я перепробовал множество инструментов разработки. Один из них (его имя я предпочитаю
370 Слелайте вашу программузам не оглашать) включал в себя две версии — для Windows 3 1 Windows NT — и поставлялся на одном диске CD-ROM. Для ус. " - ки каждой версии требовалось задать свой серийный номер ди H" свой пароль. Проблема оказалась в том, что человек, пославши? ' этот пакет программ, случайно перепутал эти пары кодов. Когдц пытался установить пакет в Windows 3.1 и ввел коды от д0., версии, инсталлятор покорно принял мой ввод, после чего на пару!" нут задумался — он использовал эти коды для дешифрования уста ливаемых файлов. И поскольку коды были неправильными, всечто^ сделал — это установил на диск покалеченные файлы. Попытавцц. запустить это бинарное чудовище, Windows моментально показалам, черный экран, а затем пожаловалась, что у нее недостаточно памя* для запуска данного приложения. Вначале я подумал, что сам допустил какую-то ошибку, поэтому я ец,- несколько раз повторил процедуру инсталляции - результаты был абсолютно такими же. В конце концов до меня дошло, что инсталля тору попросту не хватило ума обнаружить неправильно введенные коды и сообщить мне об этом. Я поменял местами присланные мне пары кодов, и все заработало так, как надо. Когда я обратился к фирме-разработчику и поинтересовался насче. этой и других проблем, замеченных мной в их продукте (вклкш демо-версию, которая то и дело вызывала GPF), мне ответили, чтоm фирма очень невелика, и что они изрядно торопились поскорее выбросить на рынок этот продукт. Что ж, они своего добились - продукт был действительно выброшен на рынок и имел так мнои проблем (вдобавок к описанному выше ляпу с серийными номерами паролями), что я даже не рассматриваю его как сколько-нибудь полез ный. Вы должны всегда рассуждать категориями функциональности удобства, помня при этом о законе критической массы: м°с' перекрывающий лишь 90 процентов пропасти, никому не нужен № разве что в воспитательных целях), а стоит он практически стольь же, сколько и полностью законченный мост. Что бы еще замкнуть сегодня? Метафора замкнутости программы таит в себе гораздо больше возможное для творчества, чем представлено в этой книге. На самом деле, в данной г> я привел лишь тот вариант реализации этой метафоры, который мне ся>1°\ нравится больше, и который я сам неоднократно использовал. Если вы $\ проводить эксперименты с этим приемом, я буду благодарен, если вы гюДс* тесь со мной своим опытом, новыми вариантами решения и проблей'" встреченными на этом пути.
Великий перевал
§3 WinlB в Win32 Чтобы переделать вашу программу в 32-разрядную, достаточно будет одной перекомпиляции! Четвертая Большая Про! раммистская Ложь, то и дело мелькающая в рекламных проспектах средств разработки Первые гри Большие Программистские Лжи формулируются гак 1. Эта ошибка исправлена в следующей версии драйвера. 2. Разумеется, он работает всего на четырех льегабай- тах памяти. 3. Дело здесь не в нашей програмлье (аппаратуре), а в вашей аппаратуре (программе). Припомните-ка, сколько раз вам приходилось видеть рекламу нового компиля- т°ра или оболочки разработки, в которой утверждалось, что задача переноса 'Фограммы с Wi.nl6 на Win32 решается теперь за пару щелчков мыши? Пари ДсРжу, немало. Мне за последний год таких реклам попалось не меньше °тни - но 1Ш одна из них не внушала слишком большого доверия. Просто я Липком хорошо знаю, что такое перенос программ с одной платформы на ^РУгую. Конечно, если вам захочется написать программу типа «Hello, World!» с ,°м°Щыо MFC, OWL или какой-нибудь другой библиотеки классов, а затем изменений перенести ее на другую версию Windows, то никакого труда это с°ставит; для подобного примера, притянутого за уши из учебника по Р^ммироваиию, это даже будет работать. Перенос программы на другую 1 Форму одним щелчком мыши — это хороший пример функции, неоцени- ' Для презентаций и рекламных шоу, но почти никогда не работающей на чГПке. Реальность, в которой нам вместе с нашими программами прпхо- 4 выдерживать натиск противоречивых требований пользователей,
374 из Winn* w бороться с неисчислимыми аппаратными несовместимостями и «незнач ными» расхождениями между Winl6 и разношерстными вариациями to/Л почти никогда не укладывается в рамки простой теории. (Я почти уверен ^ кто-нибудь из вас сейчас подскочит на стуле и засядет писать мне письмо ' сывая, как совсем недавно ему удалось перенести проект размером в 500 т строк кода с Windows 3.1 на Windows NT, не изменив в программе ни ед буквы. Я готов признать, что в реальной жизни случается и ие такое. Од ' чаще всего подобный успех — это результат тщательного планирован/ огромной подготовительной работы, и если учесть это, такой удачный пере уже никак нельзя будет отнести к «задачкам на один щелчок мыши».) С появлением Windows 95 задача переноса программ с одной платфот- на другую заявила о себе, как никогда раньше. В прошлом очень мНог, программисты, пишущие для Windows, решали задачу совместимости очеь просто -- они делали 16-разрядные программы и даже не пытались проверь как ведет себя их детище под Windows NT или OS/2. Самый серьезный вопрос который им приходилось решать до сих пор, - это предусматривать л, поддержку Windows 3.0 и включать ли на этот случай в дистрибутив свобода распространяемые файлы из 3.1, такие как COMMDLG.DLL и SHELL.DLL Тедерь нам с вами придется решать гораздо больше вопросов, среди которых; такие серьезные, как определение подмножества платформ Windows, к совмес тпмости с которыми есть смысл стремиться, и выбор функций API, которым, при этом можно пользоваться. Впрочем, здесь я уже забегаю немного вперед. В этой главе мы поговори1 о переносе существующих 16-разрядных программ на платформу Win32 и об судим возникающие при этом проблемы — часто довольно-таки неожиданные Обзору различий между существующими 32-разрядными платформами Win dows посвящена глава 14. Что такое Windows 95 с тачки зрения прикладного программиста Я не стану подробно описывать здесь архитектуру Windows 95, рисовать кар1- памяти и диаграммы с бесчисленными прямоугольниками и стрелочками, п°' зывающимн, как взаимодействуют друг с другом составные части опера*111 нон системы. Вообще-то я считаю такие сведения очень полезными п ^' просто интересными (вернейший симптом того, что я давно и неизлечим060' программированием), по в этой книге я решил обойтись без них по слеДУю двум причинам: В Этот материал можно найти во множестве книг и журналов, и еС,] я стал говорить об этом здесь, мне пришлось бы в основном щтФ других авторов. Вместо этого позвольте мне порекомендовать нес*0
кое Windows 95 с точки зрения приклалного программиста 375 ко источников, которые, на мои взгляд, отличаются широтой охвата и достоверностью сведений: Статья Мэтта Питрека «Understanding Windows 95 Memory Management: Paging, Address Spaces, and Contexts» в Microsoft Systems Journal за апрель 1995 года, помещенная также на Microsoft Developer Network CD-ROM. Его же статья «Stepping Up to 32 bits: Chicago's Process, Thread, and Memory Management» в номере того же журнала за август 1994 года (она также имеется на Microsoft Developer Network CD-ROM). Книга Эндрю Шульмана «Unauthorized Windows 95», (издательство IDG Books)1, в которой программисты найдут больше подробностей о Windows 95, чем большинству из них может когда-либо потребоваться. (Как в Microsoft Systems Journal, так и на упомянутом CD-ROM есть множество других статей о Windows 95. Однако я бы не советовал относиться к ним с безусловным доверием. Например, самый последний из имеющихся у меня выпусков MSDN CD-ROM все еще содержит статью Адриана Кинга, в которой он утверждает, что «с точки зрения программ MS-DOS, главная разница между Чикаго и Windows 3.1 состоит в том, что если вы будете работать только с приложениями Windows, код MS-DOS никогда не получит управления». Это утверждение блистательно опровергнут Эндрю Шульмаиом.) 8 Предметом этой книги является прикладное программирование для разных платформ Windows, по большей части для Windows 95. И хотя для прикладного программиста знание архитектуры операционной системы очень полезно (а иногда, к сожалению, даже необходимо), я думаю, что все же намного важнее познакомиться с новыми функциями и сервисом, которые Windows 95 предоставляет приложениям. (Разумеется, эти функции в конечном счете определяются именно архитектурой системы, но для программиста часто не так уж важно знать причину наличия или отсутствия той или иной функции, а нужно просто быстро выяснить, есть она или нет.) Итак, о чем же следует позаботиться в первую очередь? Переходя с Wind's 3.1 иа Windows 95, вы столкнетесь с огромным количеством нововведений 4Nibix разных областях, из которых, на мой взгляд, самыми важными яв- °Гся следующие: *■ Длинные имена файлов. Это очень важное новшество, и я уверен, что <жо одно вызовет больше ошибок, случаев несовместимости и проблем \^/ия конечных пользователей, чем любое другое нововведение в Win- Ц|уП1 >маи э Неофициальная Windows 95/Пср caiii.i — Киев Диалскшка, 1995
376 Из WinU в иг .——-—Ji!^, dows 95. На первый взгляд поддержка длинных имен кажет- смешного простой: нужно лишь помнить, что длинные имена (К- -' могут содержать в себе пробелы и что они «длинные», то ест ограничены старым форматом 8.3, к которому мы привыкли; Win]' предоставляет специальные функции API для перевода имен из о7 ' формата в другой. Казалось бы, что еще нужно? Увы, когда вы ца те работать с длинными именами, вы обязательно наткнетесь на сыпь весьма неприятных и труднообъяснимых особенностей (Лу даже сказать — капризов) операционной системы ПодробномуV смотрению проблем, связанных с длинными именами, посвяще глава 13. В Многопоточное выполнение программ. Как и длинные имена файле: мпогопоточность — это замечательное изобретение, с которым, однакг нужно обращаться очень осторожно. Прежде всего, поддержка мной поточности потребует тщательного планирования структуры и иове^ ния программы, что позволит выгодно и естественно раенределш работу между потоками и обеспечить их своевременное создание, уничтожение. (Каким бы очевидным это ни выглядело, я боюсь, ml вскоре увидим немало программ, которые будут вытворять странные; бесполезные вещи с потоками по той простой причине, что их автора' захотелось побаловаться с этой новой возможностью или похвастатьег кому-нибудь, что они умеют писать программы, использующие многопоточность.) В частности, программистам, которые давно пишут для Window придется расстаться с некоторыми из прежних представлений о w как должна вести себя программа. Мы все привыкли писать одной точные программы, которые совершенно не заботятся о какой-либ синхронизации. Обычное однопоточное приложение ничего не де^ по собственной инициативе, а ждет того момента, когда Windows ni хлопает его по плечу со словами «Послушай-ка, дружок... Тут кое-ч произошло Не хочешь ли как-нибудь отреагировать?» Осноь программирования, управляемого событиями, — это то, с чего начпн' ется обучение программиста для Windows. Но задумайтесь па мгновение что на самом деле происходит» к01- программа реагирует на системное событие? Практически пи1(аЬ' программа, прежде чем приступить к выполнению каких-то дейст^ не интересуется своим собственным состоянием. Как пр'11311 программа молчаливо предполагает, что все переменные и фап-*ь1' ходятся в известном и предсказуемом состоянии и что, кроме 0[\ последовательности операторов, в этот момент в програмхме Ш1 больше не происходит. В одиопоточной программе это так и ес1\ фрагмент, выполняемый в данный конкретный момент, полностЫ
oe Windows 95 с точки зрения приклалного программиста 377 вечает за поведение программы. Однако если вы не отучитесь мыслить подобным образом при создании многопоточных программ, ждите неприятных сюрпризов. Простейший пример — фоновая печать, которая очень часто приводится как пример оправданного применения многоиоточности. (Пожалуй, пример с фоновой печатью для концепции многоиоточности играет ту же роль, что комплексные числа для классов и объектов — вы вряд ли найдете книгу, в которой бы хоть что-то говорилось об объектно-ориен- шрованном программировании и не предлагалось бы в качестве упражнения написать свой собственный класс для комплексных чисел.) Конечно, удобно, когда пользователь может приказать программе начать печатать, после чего спокойно продолжить редактирование документа или какую-нибудь другую работу с программой, не дожидаясь, пока печатаемые данные будут отосланы в спулер. Но что произойдет, когда приложение, которому приказали начать печатать, создаст новый ноток для фоновой печати pi сразу же вернет управление в основной поток, позволяя продолжить редактирование? В результате вы можете столкнуться с крайне нежелательным эффектом: пользователь имеет шанс получить распечатку, содержащую некоторые из тех изменений, которые он внес в документ уже после отправки его на печать. Более того, в зависимостР1 от того, как именно программа хранит информацию документа, не исключено даже аварийное завершение программы. Представьте, что пользователь взялся редактировать именно тот фрагмент документа, который ровно в эту секунду счптывается параллельным потоком для отправки на печать. Столкновение па одном участке памяти двух параллельно работающих процедур, одна из которых читает данные, а другая изменяет (вполне вероятно, делая их временно некорректными), может привести к самым неожиданным результатам. Хватит ли у вас духу в такой ситуа- ции произнести, вслед за операционной системой, сакраментальное «программа выполнила недопустимую операцию»? Надеюсь, что нет. Придумать, как избежать этой проблемы, в общем-то нетрудно — нужно лишь, чтобы программа перед печатью создавала временную копию, «моментальный снимок» рабочего документа и чтобы поток фоновой печати работал с этой копией и, завершив печать, уничтожал ее. Увы, на практике реализация этой простой идеи может оказаться слишком сложной — настолько сложной, что применение фоновой Печати будет просто лишено смысла. К примеру, документ может содержать данные из множества источников, некоторые из которых к тому же расположены на других компьютерах сети; или объем документа может быть так велик, что создание его временной копии займет Почти столько же времени, сколько и сама печать (если для этой копии
378 из winie в ur >—-_^n^ вообще хватит свободной памяти). В таких случаях не стоит выл печать в отдельный поток, так как это приведет лишь к еще боть Я усложнению программы без какой-либо ощутимой выгоды для ц0т^ вателя. Помните, я недавно говорил о соотношении «стоимость/в ' да»? Вот вам еще один хороший пример, напоминающий к тому ! что не стоит осваивать 'новые возможности операционной сист < только для того, чтобы гордо поставить галочку в списке. (Кстати, вообще-то для фоновой печати поддержка операционной темы вовсе не обязательна; так, Word для Windows 6.0 прекра ■ справляется с этой задачей под Windows 3.1 -- да и не он один.) В 32-разрядная архитектура. (Наверное, вы уже начали недоумевав когда же я дойду до этого пункта.) Давайте поздравим себя: сверится исход в Священную Страну Тридцати Двух Битов, обетованную на столь давно, — и, не знаю как вы, но я этому несказанно рад. (Я уже слышу вопли протеста со стороны тех, кто всю жизнь пише- для Windows NT или OS/2. Пожалуйста, успокойтесь. Я говорю здес просто от имени большинства программистов для Windows, которы" вряд ли пришло бы в голову писать отдельные версии программ до OS/2 и даже для Windows NT.) Одно только избавление от возниt «близкими» и «далекими» указателями и от необходимости постояннс проверять, не вышли ли числа, размеры массивов и т. д. за преде 65 536, -- одно это стоит отпраздновать. Если же прибавить к этою реальную перспективу нов ышения производительности наш программ, то, я думаю, каждый из нас должен хотя бы разок пройда колесом от радости. Впрочем, давайте все-таки не будем терять голову. Как я не разеш- успею повторить в этой главе и в главе 14, переход на 32 бита -зь что угодно, но ни в коем случае не «бесплатный обед» (которого, & известно, и вообще не существует). Для начала вам, как миним}' придется столкнуться с изменениями в размерах типов данных. ^ более животрепещущим является вопрос, насколько 32-разрядной • ляетея та версия Windows, для которой вы пишете: ведь всем у>кеД" ио известно, что Windows 95 — далеко не стопроцентно 32-разряДн операционная система. Вы, вероятно, слышали об этом уже не ра3' иако истина не тускнеет от повторения. Компоненты GDI и Ub Windows 95 по-прежнему состоят из 16-разрядного кода. Это зн» в частности, что программы для Windows 95 по-прежнему выну# работать с 16-разрядной графической подсистемой, которая исП°' ет только 16-разрядные целые для экранных координат. (О6Р^ внимание, кстати, что я сказал «использует», а не «требует tfcfl '
379 акое Windows 95 с точки зрения приклалного программиста вания». Это не случайно, и подробнее об этом мы поговорим ниже. Впрочем, одно могу сказать заранее — хороших новостей не ждите.) g Новая оболочка. С точки зрения пользователя, самое очевидное изменение при переходе с Windows 3.1 на Windows 95 — это новая оболочка. Как и следовало ожидать, это изменение не является лишь косметическим, а влечет за собой появление новых функций API для работы с папками, списками свойств и ярлыками. Конечно, никто не мешает вам написать полнофункциональную программу для Windows 95, которая не будет пользоваться ничем из этих новых возможностей; однако очень часто обращение к ним позволяет, затратив совсем немного усилий, повысить привлекательность интерфейса программы с точки зрения пользователя. Q Интенсивное использование реестра для хранения настроечной информации. Реестр, как и OLE, — это одна из тех частей Windows, с которыми большинство программистов до сих пор предпочитали не связываться. Собственно, никто их и не заставлял — большинство задач можно было прекрасно решить, не приближаясь к реестру на пушечный выстрел. Теперь Windows 95 изменила правила игры, и многие из очень полезных функций этой системы доступны только через реестр. Один пример такой функции я уже приводил — это строка Арр Paths, которую программа может записать в реестр и которая будет для этой программы добавлена к значению переменной окружения PATH, позволяя программе использовать для .хранения своих DLL-, VBX-файлов и прочих исполняемых модулей любой каталог. Эта возможность позволяет упросгать установку и удаление программы с компьютера, но за все приходится платить — пользуясь реестром и функциями его поддержки в Windows 95, вы будете вынуждены отказаться от поддержки Win32s и Windows NT (по крайней мере до тех пор, пока эти платформы не будут предоставлять такой же сервис). Пойдете ли вы на то, чтобы писать несколько процедур установки и конфигурации для разных систем, или вы просто запретите программе вставать на Windows NT версии менее 4.0? Я подозреваю, что в ближайшее время большинство программистов не станут усложнять себе жизнь и будут обходится простой процедурой инсталляции, не использующей никаких новых возможностей Windows 95. Vjini6lock Ано [[3 свойств Windows 95, которое я, честно говоря, не могу оценивать ^ ебеспр11СТрастН0) - это печально знаменитый Winl61ock. Речь идет о ме- Xj' °т°рый избрала Microsoft, чтобы решить проблему, вызванную еще од- ^нителыюй разумности решением, а именно — решением оставить не-
380 Из WinJ6 в ы. Wit которые нз компонентов системы 16-разрядными (в частности, USER и Одно из следствий 16-разрядной архитектуры этих компонентов — то GD|, 4Tof не являются реентерабельными. Это ставит более чем интересные проб J перед операционной системой, которая теперь обеспечивает вытесняющую ° гозадачность и миогопоточность. 1! Задача эта является примером тяжелого, но неизбежного компромцс.. которому рано пли поздно приходится прибегать почти любому программ/ У Microsoft было несколько вариантов подхода к этой проблеме, и ни 0ди них нельзя назвать идеальным. То, что они в конце концов выбрали, вероят' действительно представляет собой оптимальный баланс между устойчивое и трудоемкостью реализации. Решение это сводится к созданию особого мех низма, называемого Win16lock или Winl6mutex (слово «mutex» расщифрош вается как «mutual exclusion semaphore», «семафор взаимного исключения» или, попросту говоря, флаг «занято»). Windows 95 использует Winl61ock, чтобы не допустить ситуации, ког два или больше 16-разрядных потока пользуются одним и тем i нереентерабельным модулем одновременно. Флаг этот устанавливается Wir clows сразу, как только 16-разрядный поток запускается планировщиком п когда 32-разрядный поток обращается за сервисом к 16-разрядному коду. По скольку GDI и USER состоят из 16-разрядного кода, 32-разрядным програм мам приходится обращаться к 16-разрядным функциям очень часто, и семафор Winl61ock постоянно находится в работе. Как только какой-нибудь потоки тановил Winl61ock, он тем самым не дает никаким другим потокам ни сбросить его, ни вызвать какой-нибудь из 16-разрядных компонентов. Это значит, что 16-разрядная программа, в принципе, может застопорив все остальные приложения в системе, забрав и не отпуская Winl61ock Рм или поздно любое активное 32-разрядное приложение захочет обратиться к 1& разрядной поддержке и вынуждено будет бездействовать, ожидая, пока неoi вободится WinlGlock. Задолго до выхода окончательной версии Windows 95 программисты * пали о существовании Winl61ock - и, разумеется, принялись дружно воз* щаться. Некоторые из них подняли такой крик, что можно было подУ^,аТ будто Microsoft решила сделать всю Windows 95 восьмиразрядно11 II ограничить ее монохромным пользовательским интерфейсом. (ДоЛ'1 признаться, впрочем, что когда я сам впервые узнал о Winl61ock и о су^1 вовании 16-разрядных компонентов в системе, я был более чем обескур^ ведь я, как и многие другие, надеялся, что Windows 95 наконец-то 6уДеТ стоящей 32-разрядной системой.) .. Вас, конечно же, интересует, насколько эта особенность архитектура dows повлияет на жизнь обычного программиста. Если вы пишете 32-ра J ные программы или же 16-разрядные программы, которые ведут себя <<f(' ложено» (т. е. достаточно часто возвращают управление Windows/-
пкое Windows 95 с точки зрения приклалного программиста 381 ivit, ничего больше вам делать не придется. Боюсь, вы не сможете обой- 6е:з обращений к компонентам USER и GDI, даже если очень захотите. J rxivi'i, здесь я не совсем прав - можно представить себе Windows- .vrMVf которая вообще ничего не выводит на экран — ни собственного ji P яп диалоговой панели, ни единого сообщения, ничего. Однако, как вы \маете, полезность такой программы весьма сомнительна, так что я могу по- П[ть се^)е пе рассматривать здесь этот крайний случай.) Пожалуй, все, что можете сделать в этой ситуации, — это просто помнить о существовании та- } проблемы и быть готовым к звонку возмущенного пользователя, который ,lBllr вам, что ваша программа зависла на его компьютере, - тогда как ис- - иной виновницей происшествия окажется какая-нибудь дурно написанная ])0грамма, застопорившая всю систему. Чего я хочу избежать в этой книге — так это бесконечных и безрезультатных споров о том, «чья операционка лучше» Похоже, только эти «священные войны» и скрашивают жизнь яростным фанатикам Windows и OS/2. Однако здесь уместно будет сделать одно замечание относительно OS/2, а именно - относительно поддержки многозадачности в этой системе. Мне постоянно приходится работать с 16- и 32-разрядными программами под Windows for Workgroups 3.11, Windows 95, Win32s 1.30, Windows NT 3 51 и OS/2 Warp 3.0, и на каждом из моих компьютеров установлено как минимум три системы из этого списка Даже на таком представительном фоне OS/2 неизменно производит на меня сильное впечатление тем, насколько четко и безошибочно в ней работает многозадачность Возьмем для примера один из моих компьютеров - Pentium-75 с 24 мегабайтами памяти Windows 95 на этой машине работает очень прилично (да и странно было бы ожидать от нее иного при таком количестве памяти и мощи процессора), особенно с 32-разрядными программами Однако OS/2 все-таки работает очевидно быстрее — это особенно заметно, когда я работаю с мощными, тяжелыми приложениями Я заметил, что после запуска на компиляцию и компоновку большого проекта в Visual C++ 2 2 под Windows 95 я уже не могу по-прежнему быстро и гладко переключаться между задачами, редактировать текст, работать с панелью задач и т и - все на экране начинает двигаться рывками, как будто засыпая на ходу Добиться тех же симптомов от OS/2 практически невозможно - сколько бы вы пи назапуекали программ, даже самых ресурсоемких, замедления вы практически не почувствуете. Каждый раз, когда я сталкиваюсь с этим, я начинаю думать, что Windows 95 не слишком хорошо приспособлена для работы в экстремальных ситуациях. изменение типов данных большей своей части Windows 95 — это все же 32-разрядная операцион- 1 система, и следовательно, нам с вами придется писать для нее 32-разрядные
382 из Winn в ur -—-ijjjfc программы. (Впрочем, в разделе «Перенос 16-разрядных программ ца и,. dows 95» мы обсудим проблему выбора между 16 и 32 битами более подр0/- ' Какие же изменения в нашей жизни влечет этот переход — помимо необх ^ мости осваивать новые инструменты или до сих пор не использовавшиеся 4 можности старых инструментов? Поскольку моя личная точка зрения, уже известная вам, состоит в том в программировании главное — это обработка данных, я начну обзор новщ'е Win32 с изменения размеров типов данных. Конечно, это далеко не единств ная проблема, но, в то же время, очень важная. Прежде всего, необходимо разграничить изменения в том, как типы да. ных реализуются в вашем компиляторе, и изменения во внутренней архитектуре Windows (хотя эти изменения и связаны между собой). Вы, вероятно, знаете, что при переходе с 16-разрядного компилятора н; 32-разрядный размеры некоторых типов данных изменяются. Это изменение по сути, не имеет никакого отношения к DOS, Windows, OS/2 или любой другой операционной системе. Собственно говоря, и сам язык C/C++ тоже не требует этих изменений — вполне можно было бы оставить тип int 16-разрядным и заставить программистов использовать в 32-разрядных программах для целых чисел исключительно тип long. Этим я хочу сказать, что ответственность за изменение типов данных целиком и полностью лежит на разработчиках ком пиляторов. Обзор 16- и 32-битовых типов данных в C/C++ приведен в табл. 12.1. Три последние строки табл. 12.1 содержат нестандартные расширена языка, поддерживаемые компилятором фирмы Microsoft. В документацш1 на этот компилятор сказано, что в данной версии int8 является синоии мом для типа char, int 16 — для типа short, а int32 - для типа int Эти типы с фиксированной длиной позволяют писать программы которые будут защищены от неприятных последствий изменения размер0" типов (имеется в виду, что во всех будущих версиях компилятор размеры этих типов останутся прежними - к примеру, int32, в отлич1' от типа int, не будет снова и снова увеличиваться в два раза). На >" взгляд, именно так и должны себя вести базовые типы как в С/С^<ъ и в Паскале и во всех других языках программирования. При пере*10' кода с одной платформы на другую чаще приходится иметь Де1 увеличением длины цсчого, что не так уж часто вызывает пежелатель эффекты в вычислениях (если лее работало с меньшим количеством то будет работать и с большим), однако вполне может породить серье' проблемы с изменением размеров (и, соответственно, проблемы с вь1' пиванием при размещении в памяти) сложных типов данных. Кроме того, Visual C++ 2.x поддерживает (хоть и с некоТ°1 ограничениями) тип int64, который позволяет использовать №п знаком в диапазоне приблизительно +/-9Е18 или беззнаковые в Д1Ь , не от 0 до 1 8Е19 Рекомендую статью Майка Поттера «64-bits ni
акое Windows 95 с точки зрения приклалного программиста 383 Huge Files and Volumes» в октябрьском номере Windows/DOS Developer's Journal за 1995 год — там вы узнаете об этом типе данных во всех подробностях и найдете специальные функции для перевода 64-разрядных целых со знаком и без знака в ASCII-эквивалент (возможность, которую сам компилятор не поддерживает) Таблица 12.1. Типы данных C/C++ в 16- и 32-разрядных программах Тип данных C/C++ rull isigned char ,u jsigned inl 4)it insignecl short 'ong "Kigned long тип oat •uble ^ double Jnt8* ОЩб* -U32* Размер в 16- разрядном компиляторе 8 8 16 16 16 16 32 32 16 32 64 80 N/A N/A N/A Диапазон в 16- разрядном компиляторе от-128 до 127 от 0 до 255 от -32768 до 32767 от 0 до 65535 от -32768 до 32767 от 0 до 65535 от-2147483648 до 2147483647 от 0 до 4294967295 от -32768 до 32767 3.4Е±38 1.7Е±308 1.2Е±4932 N/A N/A N/A Размер в 32- разрядном компиляторе не изменился не изменился 32 32 не изменился не изменился не изменился не изменился не изменился не изменился не изменился не изменился 8 16 32 Диапазон в 32- разрядном компиляторе не изменился не изменился от-2147483648 до 2147483647 от 0 до 4294967295 не изменился не изменился не изменился не изменился не изменился не изменился не изменился не изменился от -128 до 127 от -32768 до 32767 от-2147483648 до 2147483647 Примечание: нестандартное расширение Microsoft в Visual C++ 2 х.
384 Таблица 12.2. Типы данных Windows в 16- и 32-разп Тип данных Windows ~Всё дескрипторы (HWND.HPEN и т. д.) UINT BOOL WORD BYTE DWORD WPARAM (==UINT) LPARAM (==long) Размер в 16- разрядном компиляторе 16 16 16 16 8 32 16 32 Диапазон в 16- разрядном ' компиляторе от 0 до 65535 от 0 до 65535 от -32768 до 32767 (допустимы ТОЛЬКО 0 И 1) от 0 до 65535 от 0 до 255 от 0 до 4294967295 от 0 до 65535 от-2147483648 до 2147483647 Размер в 32- разрядном компиляторе 32 32 32 не изменился не изменился не изменился 32 не изменился Диапазон^ РазРядн0м ' компиляторе отТдГ^ 4294967295 отО до 4294967295 от-2147483648 До 2147483647 (допустимы ТОЛЬКО 0 И 1) не изменился не изменился не изменился от 0 до 4294967295 не изменился __^ Очевидно, оборотная сторона медали - это изменения в искусственных я пах данных, используемых в Windows. Как следует из табл. 12.2, изменен!' этих вполне достаточно для того, чтобы превратить вашу жизнь в опасн1 приключение. Конечно, очень хорошо, что типы WORD, BYTE, DWOKL> LPARAM остались такими же, как и раньше. Однако мне до сих пор не поня по, зачем нужно было изменять размеры типа UINT и тем более BOOL. Следует помнить, впрочем, что все эти типы не относятся к самому язЫ а созданы Microsoft. В частности, встроенного типа BOOL в C/C++ нет ^1 готовящийся сейчас к принятию стандарт C/C++ наконец-то предусматр11' встроенный булевский тип, который, насколько я понимаю, позволит пере' ным иметь только значения 0 и 1). А такие типы как WPARAM и LPA^' тем более не имеют отношения к языку, поскольку предназначены для оор1 ния к функциям Windows.
KOe Windows 95 с точки зрения приклалного программиста цщо значит изменение размеров типов программистов 385 Последствия описанного выше изменения размеров типов данных для ва- |0пкретнон программы, конечно, зависят от самой программы. Некоторые ч)Жсапя, которым не приходится создавать или записывать на диск данные ,андартных форматах, вообще никак не будут затронуты .-ггим пзмене- п их можно будет переносить с 16 на 32 бита, не уделяя этой проблеме ')(Мо внимания. Все сведется к тому, что некоторые переменные станут bine, другие останутся прежних размеров, программа перекомпилируется и е, работать, как и раньше. (Конечно, здесь я пока игнорирую остальные „фосы, связанные с переносом, -- о них мы еще успеем поговорить.) Из таблицы 12.2 следует, что к иЗхЧенпвшпмся типам данных относится \PARAM. Л это означает, что оба параметра с дополнительной информацией, ,горые сопровождают каждое сообщение Windows, имеют длину в четыре hi га Формально WPARAM по-прежнему является беззнаковым целым, а JHRAM — целым со знаком (что и отмечено в табтице), однако для програм- ,1'ста эти значения чаще всего являются просто набором битов, которым он мо- vci приписать любые значения (например, мне приходилось передавать в uipdMcrpe LPARAM упакованные четырехбайтовые структуры). Вероятно, меньше всего последствий из перечисленных в таблице 12.2 из- шетшй будет иметь увеличение размера дескрипторов с двух до четырех бай- <н; Нам с вами никогда не прихо/иггея записывать значения дескрипторов в ntновый файл (по крайней мере, мы не должны этого делать), п единственная "|ш, которую можно извлечь из дескриптора, это возможность вернуть () системе в качестве магического заклинания для доступа к тому или иному %скту (Некоторые любители копаться во внутренностях Windows, впрочем, ,|1(пьзуют дескрипторы с другими целями, поскольку в Windows 3.1 любой 'С1фшгтор в действительности является указателем. Однако практику такого ^'пользования никак не назовешь общепринятой.) Поэтому, строго говоря, 'л)грш\1мисту должно быть безразлично, сколько байт занимает дескриптор -- а четыре пли семнадцать. Ац нет. Как вы знаете, во многих случаях Windows передает дескрипторы "аРамеграх сообщения, и в одном из этих случаев увеличение размера (1М(-Лгрл WPARAM оказалось недостаточным, чтобы вместить в два <1Х|сфи ту же информацию, которая передавалась с их помощью в Win 16. ' llA^i о сообщении WM_CTLCOLOR, которое посылают информационные впадающие списки, поля редактирования текста, списки, кнопки, ' 4епще органы управления и панели прокрутки своему окну-родителю, тем ,' [ Давая ему возможность изменить принятые гго умотчашпо цвета а)Кенця этих органов управления Кроме того, это сообщение посылается 1 °иому окну (а не его родителю), давая ему шанс изменить его собствен-
386 Из Winie в Ur В Windows 3.1 интерфейс сообщения WM_CTLCOLOR был опр0 следующим образом: hdcChild = (HDC) wParam. /* дисплейный контекст дочернего окна */ nwndChild = (HWND) LOWORD(lParain). /* дескриптор дочернего окна */ nCtlType = (mO HIWORD(lParain), /* элементы управления */ Чтобы правильно отреагировать на сообщение WM_CTLCOLOR, 0I, родителю нужны все эти сведения, которые в Witil6 едва-едва помещала двухбайтовый wParam и четырехбайтовый lParam. Однако, поскольку в \\V . оба передаваемых дескриптора (hdcChild и hwndChild) увеличились каж./" на два байта, то одного лишь увеличенного WPARAM недостаточно, 4T(y уместить оба эти дескриптора, да еще и двухбайтовый параметр nCtlType этом случае Microsoft нашла довольно смелый выход из положения, отброа сообщение WM_CTLCOLOR и введя вместо пего несколько новых сообщен^ а именно' Ф WM_CTLCOLORBTN • WM_CTLCOLORDIG * WM_CTLCOLOREDIT * WM_CTLCOLORLlSTBOX # WM_CTLCOLORMSGBOX © WM_CTLCOLORSCROLLBAR • WM_CTLCOLORSTATIC Каждое из этих новых сообщений сопровождается следующим наборо параметров: ndcObject = (HDC) wParam, /* дескриптор дисплейного контекста объекта */ hwndObject = (HWND) lParam, /* дескриптор объекта */ Теперь, когда параметр nCtlType больше не нужен, не составляет тр\- разместить два четырехбайтовых дескриптора в двух четырехбайтов1 параметрах сообщения. Поэтому любая 16-бптовая программа, ожидающе1 общения WM_CTLCOLOR, потребует внесения изменений, прежде чем можно будет перенести на Win32. Это один из тех случаев, когда исноль3° ипе среды разработки заметно облегчает работу по переносу программ с оД' платформы на другую: в 16-разрядной среде MFC сообШ1 WM_CTLCOLOR было зашито в метод CWnd::OnCtlColor(), которые "Р1, мал три вышеописанных информационных объекта (два дескри1ГГ°Р1 параметр nCtlType) в виде отдельных параметров. 32-разрядная npoiP' использующая данную среду разработки, по-прежнему имеет дело с эти*1 дом, набор параметров которого остался прежним, - так что программ принципе, даже не обязательно знать, что исходное сообщение подвер1 Win32 API раздроблению на семь отдельных сообщений.
16-битового кола на Windows 95 387 Метод CWncl .OnCUColorO, определенный в MFC — это превосходный пример того, какую пользу могчи бы принести среды разработки программистам, которым приходится переносить приложения с одной платформы на другую Именно об этом я говорил в 1лаве 10, подчеркивая, что абстрагирование - это единственный способ создать новую, более высокою порядка платформу для создания устойчивых п независимых программ К сожалению, ни MFC, ни OWL, ни другие среды разработки, с ко- горыми мне приходилось иметь дело, даже близко не подходят к идеалу в этом отношении Они действительно сглаживают некоторые второстепенные архитектурные расхождения (вроде ситуации с сообщением WM_CTLCOLOR), но оставляют без внимания многочисленные отличия в поведении между функциями API на разных платформах Win32 А ведь это гораздо более серьезная проблема, гак как одна и та же 32-разрядная программа, вполне вероятно, будет .запускаться без изменений на всех ipex платформах Win32, гот да как программа, использующая WM_CTLCOLOR, обязательно будет перекомпилирована, прежде чем ее можно будет запускать как 32-разрядную Подробнее мы поговорим об этом в главе 14 Еще одна, неочевидная на первый взгляд ловушка, связанная с изменившимися размерами типов данных, — это тот факт, что соотношения размеров, ^ которым вы, вероятно, уже привыкли, тоже изменились. В 16-разрядных \iiido\vs-nporpaMMax на С типы WORD, WPARAM, IJINT, short и inl были того и того же размера два байта. Теперь в С для Win32 размер типов 'OR!) ц short остался прежним - два байта, а типы WPARAM, UINT и int значили длину до четырех байт. В зависимости от того, как вы привыкли 'ьив;1ять переменные и к каким преобразованиям типов вам приходилось рчбегать, вы, вполне возможно, столкнетесь с некоторыми неприятными 'Призами, причину которых будет довольно трудно обнаружить Едннетвеп- >!п совет, который можно дать в этой ситуации, - задействовать все Упреждения компилятора и аккуратно проглядеть весь код на предмет пе- 1,1 ильного смешения типов и размеров переменных. та&с 1&~8итовёвг& Мода х ^ всего я хочу оговориться: под переносом Побитового кода на Windows Уразумеваю не одну лишь перекомпиляцию с другими опциями, а весь ^ вязанных с этим проблем. Я считаю, что каждый программист должен )тот перенос с того, что запустить свою 16-разрядную программу и за- U4)e вопрос «Какие изменения имеет смысл внести в эту программу, что- 11ГсНособить ее к Windows 95? И вообще, нужны ли какие-нибудь
388 из winи изменения?» Как известно, большинство 16-разрядных программ работу Windows 95 без каких-либо изменений, поэтому, на мой взгляд, в некср ' случаях вполне приемлемым вариантом является полный отказ от каких, * изменений — если выгоды создания 32-разрядной версии не оправдываю занных с :>тим трудозатрат. Конечно, во многих случаях выгоды есть { ' весьма значительны, тогда как .усилий для переноса требуется затратней- уж много. Но бывают случаи, когда соотношение прямо противоположное Я уже много раз говорил в этой книге, что для того, чтобы праВи . репшть даже самые незначительные проблемы, встающие при лланцронагц/, реализации программных проектов, главное - это умение оценивать псрсг тивы и предвидеть отдаленные последствия своих решений. Мне кажется i вопросы переноса программ с одной платформы на другую относятся к гем которых особенно легко принять неправильное решение под влиянием сшом путных побуждений. Именно поэтому я хочу подробно рассмотреть здесь щ возможные стратегии переноса программ и представить обзор всех сообра^ ний, которые следует принять во внимание для каждой переносимо программы. Выбор стратегии переноса Итак, представьте, что у вас есть готовая полнофункциональная 16-разряд ная программа, прошедшая тестирование и имеющая известное количество пользователей, - и что вы, как и большинство Windows-программистов, Д1 сих нор полностью игнорировали существование Win32. Но теперь, с появтс нием Windows 95, пришло время выбрать наилучшую стратегию переноса д^ вашей программы. Первое, что вы должны осознать, — это то, что величайшей ошибкой было бы принять «двоичное мышление», которое делит все программы на два непересекающихся класса: старые, дребезжащие и разв" лпвающиеся на ходу 16-разрядные и новые, сверкающие и бесшумные о- разрядные Такой слишком упрощенный взгляд на вещи чре11'' стратегическими ошибками. На самом деле существует, во-первых, множе<-',ь различий между разными платформами Win32, и во-вторых, мпоя<еС1' различных уровней, на каждом из которых ваша программа может либо №1 и использовать новые свойства Win32, либо полностью игнорировать Поэтому решение, которое вам придется принять, на самом деле сост°11Г множества мелких решений, касающихся отдельных компонентов програ>1, В качестве первого приближения ниже приведен список, яв/шюшппся ' пыткой очертить самые важные вопросы, встающие перед программ^'14" этой ситуации. Вам придется оценить важность каждого пункта этого с*11 ^ применительно к своей программе, обязательно учитывая не только сш°,м пые выгоды, по и долгосрочную перспективу. К примеру, в данный мо^[е1 можете решить, что неразумно пользоваться новыми функциями, прел°с1'
^ 16-битового кола на Windows 95 389 \Vmdows 95, поскольку многие из ваших пользователей работают на I NXb NT 3.51 или далее на Win32s. Однако более чем вероятно, что еитуа- оро изменится, поэтому имеет смысл начать разработки в этом направление сейчас. 1 Какие платформы Win32 имеет смысл поддерживать? Если вы привыкли ориентироваться только на Windows 3.1, то вопрос для вас может оказаться одним из самых трудных. Самый естественный ответ - попытаться поддерживать все возможные платформы, чтобы расширить круг пользователей программы. Однако этот вариант наталкивается на неизбежность огромных трудозатрат па тестирование, осложняемое множеством мелких различий между разными платформами (подробнее об этих различиях я расскажу в главе 14). 2 Обеспечивать ли поддержку длинных имей файлов? Как ни странно, этот вопрос тоже не из простых. Возможно, ваша программа очень мало работает с файлами, и поэтому поддержка длинных имен для нее не столь актуальна. Однако я подозреваю, что очень скоро поддержка длинных имен будет главным признаком, по которому пользователи будут проводить границу между «старыми плохими» и «новыми хорошими» программами. Поэтому поддержка длинных имен становится в некотором смысле вопросом престижа. Л что самое интересное, - оказывается, поддержка длинных имен вполне возможна в 16-разрядных программах, о чем подробнее говорится во врезке на стр. 395. 3 Следует ли использовать в программе миогопоточиость? Если вы отвечаете на этот вопрос положительно, то тем самым сразу же ограничиваете программу поддержкой только Windows 95 и NT, поскольку в Win32s многопоточность отсутствует. 4 Следует ли программе обращать внимание на 16-разрядные ограничения, существующие в Windows 95? Как я уже говорил, компоненты GDI и USER в новой версии Windows по-прежнему 16- разрядные, — а это значит, что все графические координаты и индексы элементов списков в диалоговых окнах обрезаются до 16-разрядных значений. Для большинства программ эти ограничения несущественны, особенно если вы берете за основу старую 16-разрядную программу, которая всю свою жизнь работала с теми же ограничениями. Тем не менее, этот вопрос следует иметь в виду, так как если вы решили резко улучшить поддержку графики и добавить новые возможности за счет использования 32-разрядных координат, вам придется либо отказаться ori этой мысли, либо ограничить свою программу только одной операционной системой — Windows NT. (Кроме того, в поддержке графического интерфейса Windows 95 есть и другие ограничения, та-
390 из winiee H,. кие как отсутствие функций для создания контуров; опять- подробности вы найдете в главе 14.) а 5. Насколько интенсивно ваша программа будет пользоваться (и циями, поддерживаемыми только Windoles 95? Пока что такие цJ ные вещи, как контекстное меню в Проводнике, переменная ре/ Арр Paths и расширения наборов свойств (property sheets), jIMe только в Windows 95. Вероятно, Windows NT будет иметь те ж0, ства в версии 4.0, по от Win32s, скорее всего, не приходится ожи ничего похожего. 6. Вопросы маркетинга. Конечно, если ваша программа нредназнач* только для использования внутри фирмы или пишется по проц/ пары друзей, то эти вопросы вы можете со спокойной совестью > норировать. Но во всех тех случаях, когда программа требует хоты кого-нибудь маркетинга - будь это условиобесплатное приложена массовая и универсальная утилита или дорогая специализирован, программа, - вы и ваша фирма должны проделать серьезное марке тинговое исследование. Как я упоминал в пункте 2 (о длинных имен; файлов), иетехиические вопросы часто становятся критическими,!, как в конце концов вам придется иметь дело с людьми, которые плат? деньги, и учитывать их взгляд на то, что является важным, а что- нет. 7. Прочие, самые разнообразные вопросы, не относящиеся к техническо стороне дела. Сюда входит все то, с чем программисты не очень-т любят сталкиваться, но чего им редко удается полностью избежат Скажем, нетрудно представить себе ситуацию, когда программа меш пока прекрасно обойтись без переноса на Win32. Но ваш начальник восторге от новой игрушки Microsoft, он думает иначе, pi по инстанш ям спускается безоговорочный приказ: разрабатывать новые програ' мы только для Windows 95 и перенести на эту же платфор!^ в старые 16-разрядные программы. Пример мой, конечно, слегка пр"1 нут за уши, но на самом деле я видел, как похожие решения npi'11 маются н по менее значительным поводам. В таких ситуациях вам предстоит принять нелегкое решение — под1*111*1 ся приказу и заняться переделкой и без того прекрасно чувствуюши* lt программ либо вступить в борьбу с начальством, пытаясь доказать ему» чТ кие-то программы абсолютно ничего ие выиграют от переноса. Решение зД как вы понимаете, нужно принимать как можно скорее, чтобы избавИ'гЬ самого и всю команду программистов от сил, впустую потраченных на 6есс' ленную работу либо на бесплодную борьбу. Только после тщательного рассмотрения всех перечисленных в()1Т' „ можно приступить к выбору стратегии переноса программ. Сам ,1^
„г 16-битового кола на Windows 95 391 0ев<л^—_ _ )са можно уложить в одну стадию и закончить его с минимальными ■ ими, хотя чаще он распадается иа несколько этапов и в некоторых ' qX может занять год или даже два Конечно же, ни я, ни Microsoft, пи /)Удь еще пе сможет дать вам универсальный совет на все случаи жизни; >[ИЯ для каждой конкретной программы должна выбираться с учетом всех 1'\цпостей ее разработки и использования, и принять это решение можете 1,1,0 ВЫ Ца мой взгляд, если попытаться найти достаточно разумный компромисс lV обобщением и детализацией, то мириады частных случаев и задач, ко- ,,г приходится решать при переносе, сводятся к четырем основным вариан- сгратегии переноса, перечисленным ниже (предполагая, что исходной ,|()ц служит готовая программа для Windows 3.1/Winl6): Q Оставишь программу 16-разрядной и ничего пс менять. Вы удивлены, что я говорю об этом в книге с таким заглавием? Напрасно. Конечно, Microsoft спит и видит, как все программисты на свете словно но мановению волшебной палочки переделывают все существующие программы в 32-разрядные. По-видимому, в значительной мере это объясняется тем, что такая массовая миграция на Win32 выбьет почву из-под йог у OS/2. Компания IBM официально заявила, что не собирается в ближайшее время вводить в OS/2 поддержку специфических функции Win32. (Боюсь, это было бы совсем непросто, даже если бы они захотели это сделать.) Почему вы должны ломать себе голову над эти,м? Разве это имеет какое-либо отношение к программированию? Увы, как бы нам ни хотелось делать выбор на основании аргументов чисто технического порядка, очередная война между Microsoft и IBM имеет к нам, программистам, самое непосредственное отношение, так как от воли этих двух могущественных корпораций очень сильно зависит погода в компьютерном мире. Однако, если Microsoft столь активно проталкивает идею всеобщего перехода на Win32, вы должны по крайней мере понимать причину этого. Вряд ли здесь имеет место альтруистическое желание дать в руки пользователям более мощные п стабильные приложения; очевидно, что Microsoft преследует в первую очередь своп собственные интересы. Если бы руководство Microsoft почему- либо вдруг уверилось в том, чгю прибыли компании резко возрастут, если убедить людей перейти на компьютеры с одномегагерцовым процессором Z80 и 16 килобайтами оперативной памяти, то, смею вас уверить, эфир был бы тут же заполнен рекламными роликами, расписывающими достоинства этих компьютеров — ах, они такие недорогие, Миниатюрные и очаровательно старомодные! Я вовсе не хочу этим сказать, что Microsoft поступает дурно; именно такого поведения и следу- ст ожидать от любой крупной фирмы — ведь это лежит в самой
392 Из Win16 в иг природе капитализма. Та же IBM с чистой совестью поступц.,. точно так же. Уч Вы как программист обязаны осознавать подобные факторы и вать их в своей деятельности. А это значит, что вы не должны'vn, ляться крысам из старинной легенды и, забыв все на свете, идти hi дудочки крысолова. Но* вряд ли разумно будет и вывешивать ло-J типа «Microsoft — империя зла», отказываясь переносить програ\* только для того, чтобы досадить этой вездесущей корпорации фа, тизм любой из расцветок -- это самый простой и верный путь к пт1ь, гию неправильных решений. Даже если вы решите ничего не менять в своей 16-разрядной uporna. ме, вы должны по крайней мере протестировать ее под Windows 95 Windows NT. Да, вероятность того, что никаких проблем при этом of наружено не будет, весьма высока, однако далеко не равна ста процеь там. Незадолго до выпуска Windows 95 Microsoft опубликовав результаты своих собственных тестов на совместимость. Согласно эти- тестам очень многие известные приложения набивали себе синяки v шишки в новой для них обстановке. Причем речь идет не о каких-т< утилитах низкого уровня, которые по природе своей непереносимыi одной системы на другую. Да что там говорить - я и сам наткнули па несколько неприятных неожиданностей при переносе свое; программы Stickles! (см. врезку «Stickies! и Windows 95: как у переносил свою программу»). В Оставить программу 16-разрядной, но ввести в нее минимально поддержку возможностей Windows 95. У меня такое впечатление, что никто сейчас не рассматривает эту возможность всерьез. Не знающем1- это приписать - успешности пропагандистской кампании Micros01 или просто увлечению цифрами, столь свойственному американи»' (ого-го, 32 — это не каких-нибудь там 16!). Так или иначе, похо*1 никто не отдает себе отчет в том, что 16-разрядную программу m°;KF довольно успешно приспособить к Windows 95, не переделывая ее 32-разрядную. Такая программа вполне может использовать дли*1*1 имена файлов, пропорциональные движки панелей прокрутки, ТР мерный дизайн органов управления, личные каталоги для xp*uie DLL-файлов и т п. С другой стороны, если вы пойдете по этому пути, вам придется затратить немало усилий, чтобы заставить программу приличп0 Р' тать на других платформах, либо ограничить ее только Wind°^'\ Подробнее о 16-разрядных программах иод Windows 95 вы узн^е врезки на стр. 395.
,nr 16-битового кола на Windows 95 393 ,ре№^ — 9 g Переделать программу в 32-разрядную, но не вносить в нее никаких изменений, специфических для Windows 95. При этом вы получите программу, которая сможет работать под Windows 95 и Windows NT 3.51. Вероятно, в долгосрочной перспективе это не слишком выгодное решение, так как, по всей вероятности, Windows NT 4.0 практически уничтожит разрыв между Windows 95 и Windows NT по набору поддерживаемых функций (по крайней мере, теория предсказывает именно такое развитие событий; лично я к такой перспективе отношусь пока скептически - и надеюсь, что, прочитав все главы этой части, вы поймете, почему). Здесь я отношу новые органы управления, введенные в Windows 95 (списки с изображениями в качестве элементов, древовидные структуры, усовершенствованные поля редактирования, диалоговые окна с вкладками и т. п.), к элементам, специфическим для Windows 95, хотя, строго говоря, ими можно пользоваться и под Windows NT 3.51 с помощью свободно распространяемого DLL-файла (COMCTRL32.DLL). Если вы собираетесь пользоваться каким-либо из этих усовершенствований, вы должны отнести свою программу к следующей категории моего списка. Полезно также провести границу между теми функциями операционной системы, которыми ваша программа лишь иногда пользуется, и теми, которые необходимы ей для успешного старта. Например, представьте, что вы пишете программу, которая должна успешно работать под Windows NT 3.51 и Windows 95, используя свои собственные DLL-файлы. При этом вы все равно можете завести специальный каталог для хранения этих файлов под Windows 95, добавляя его к пути поиска для исполняемых файлов с помощью переменной реестра Арр Paths, хотя Windows NT 3.51 и не поддерживает эту возмоленость. Стоит ли создавать две разные процедуры инсталляции для двух операционных систем — решать вам, но во всяком случае рассмотреть такую возможность стоит. *■ Переделать программу в 32-разрядпую и добавить поддержку функций Windows 95. При этом ваша программа сможет работать тотько па Windows 95 п Windows NT версии 4.0 п более (обеспечить функциональность программы под Windows NT 3.51 можно только в том случае, если функции, которыми вы будете пользоваться, ограничены только новыми органами управления). Учтите, однако, что в зависимости от набора этих функций и от того, что именно Microsoft решит включить в Windows NT 4.0, дело может кончиться тем, что ваша программа будет работать только под Windows 95.
394 Из WinU B Эта последняя категория наиболее богата возможностями дальне', выбора - каждое из новшеств Windows 95 (скажем, новую обо^ / можно использовать, а можно и воздержаться. Я объединил -jw возможные варианты, гак как они обладают одним 0^ свойством - ограничивают применимость вашей программы То Windows 95 и Windows NT Не забыл ли я чего-нибудь'^ Win32s, говорите'-* Нет, я ее не забы отбросил совершенно сознательно. Win32s - это определенно «паршивая он в стаде» Почитайте хотя бы статьи на Microsoft Developer Network CD-Roy посвященные Win32s, и вы увидите, что из-за многочисленных ошибок nh, задии и врожденных ограничений Win32s представляет собой настоящую по к су препятствий, па которую может отважиться вступить только очен уверенный в своих силах разработчик (хотя, конечно, можно представить ссб ешуацпю, в которой ориентировка на Win32s может быть оправдана например, когда по всем признакам возможность программы работать иод\Уц, dows 3.1 резко увеличит число ее пользователей). Если вы думаете, чго я слишком суров к бедной Win32s, познакомыеи хотя бы со следующими статьями на Developer Network CD-ROM Q121906 «Win32s 1 2 bug List (at the Time of its Release)» QГ30139 «Win32s 1 25 Fix List» Q130138 «Win32s 1 25 Bug List» Q133027 «Win32s I 3 Fix List» Q133026 «Win32s 1 3 Bug List» Там вы найдете просто потрясающую коллекцию ошибок, буквальной1 нескольку десятков в ка;кдой статье Так, самый последний из этих сип сков, Q133026, содержит свыше пятидесяти ошибок, включая, наири>1еГ такие «Функции geklcwdO и getcwcK) из библиотеки времени вы1Ю,,н ния С не работают» (бе? объяснений), «Функция C3etFu 11 PathNaiiHi возвращает корневой каталог всегда, когда получает в качестве парами диск, отличный oi текущего», «Элемент biSizclnuige структуры Bill'- lNFOHEADFR всегда равен нулю», «Функция CreateFileO с некоторы'■ неверными длинными именами файлов приводит к закрытию Wiin'^4 «Функция FindTextO приводит к утечке памяти», «Затычка вместо о14 ствующей функции APT FindFirstFileWO не возвращает -1», и нако^ моя любимая «Вызов функции GetShortPathNameO с неверным арОм том не прпводич к ошибке, как зго происходит на Windows NT» ^ * гочисленных странностях Win32s, касающихся функции ^ct PathNameO, я буду подробно говорить в главе 14, пока скажу дни-11'' это признание со стороны Microsoft лишь одна страничка из n()13L которую я вам собираюсь рассказать )
395 пГ 16-битового кола на Windows 95 ,$НО^^-—■ Ожидается, что Win32s версии 1.30а будет скоро выпущена в свободное обращение (я пишу эти строки в середине октября 1995 года) и, конечно, там будут исправлены многие из ошибок версии 1 30, перечисленные в Q133026 Microsoft уже поместила эту обновленную версию в файловую библиотеку CompuServe, но список ошибок, исправленных в ней, еще не обнародован Вероятно, самый ценный совет, касающийся переноса, который я могу вам , (кроме очевидного «тестируйте до посинения»), это то, что перенос на aj2 нужно стара]вся по-возможпоетп разбить па несколько этапов. Лучше, ,п бы отстрелите себе пару пальцев на ноге неудачным выстрелом из писто- 1а чем если вам оторвет всю ногу взрывом противотанковой гранаты. Если отбросить это слишком мрачное и не очень-то идеологически выдержанное ыснснпе, то вернее всего будет сказать, что перенос программ, как никакой ругой элемент программистского ремесла, представляет собой контакт с непо- .шшым в чистом виде. Как известно, вероятность неудачи проекта находится ; не пшенной зависимости от уровня неопределенности, поэтому намного безо- [лснее пересекать тонкое место, строя маленькие мосты от одной устойчивой чочкн к другой, чем пытаться перебежать или перелететь его с разбегу. еЭ еэ £Э Использование длинных имен файлов в IB- разрядных программах: да здравствует алхимия! Просто удивительно, до какой степени программисты зациклились на 32-разрядности. Очень многие, из них даже не подозревают, что самое очевидное для пользователя (и, вероятно, самое популярное) нововведение Windows 95 - длинные имена файлов - вовсе не является привилегией одних только 32-разрядных программ. Программисты Microsoft, вопреки своим лее принципам, предусмотрели полный набор функций для работы с длинными именами, которые доступны 16-разрядным программам через прерывание 21Ь. (Более того, шесть из этих функций, среди которых есть очень полезная функция GelLongPalhNameO, доступны только через вызов прерывания 2lh и не имеют эквивалента в Win32 API.) Поскольку мало кто приходит к восторг от перспективы писания ассемблерных вставок для доступа к прерываниям, я написал набор функций-оболочек для этих ассемблерных фрагментов п собрал их в файле под названием LFNRTNS.CPP, который составляет часть программы LFNSTUFF, рассматриваемой ниже в этой главе. Для примера ниже приведен функция lfn- DeleteFileO из LFNRTNS.CPP: //////////////////////////////////////////////////////////У///////////////// I/ lfnDeleteFileO LFN-capahle file delete function, using the new W95/int
396 Из WinU B Ur // 21h file ojppott ^^ /II! ! 4////ill il/ili lll/l/////////!/////lli/l I/III//1/П/П11/////П1 if( lfnHaol rNS'inoortO) retu"i JALSE _asiii puon oi _ac.m irov ал 7l41h _ar-n mov cn,FILE_ATrRIBUTE_MORMAL @P?^ -aciri !nov cl,FTLE_ATThIBUTE_ANY_FILE ""1i;j*^ _acm las ax dvord ptr fn _acn mov si 0 _acm int 21h _asm pop dc _acm jс enor return TRUE G3fi) error ^^^^^ ieCu,n FALSE, Пользоваться этими функциями-оболочками очень просто. Открыв фай и длинным именем, вы можете затем либо использовать функцию _fdopen() чтобы связать дескриптор файла с потоком, либо воспользоваться функции InfCrcateTexlFileStreamO из LFNRTNS.CPP, которая позволяет оеущеавшк oiкрыл не файла п обращение jc _fdopcn() за один прием. Какой бы из .тшхспи собой вы пи выбрали, .-пи функции позволят вам осуществлять обычны буферизованный, гексювый пли двоичный ввод-вывод с длинными именам, файлов томно так же, как и с обычными. И хотя программа LFNSTUFF но'1, зустся именно этими функциями для создания и открытия файлов, вы на само* деле можете обойтись даже и без них. Хоть я и Fie нашел об этом упоминаши нигде в документации, по, оказывается, старые добрые функции _lcreat() • _1ореп() умеют под Windows 95 работать с длинными именами файлов, Да}1и если их вызывает 16-разрядная программа, предназначенная для Windows.^ (Под Windows NT 3.51 эти функции ire столь предупредительны и без M^]l{iv слов обрезают длинные имена до восьми символов.) Этот факт <к мопсфпруется в другой небольшой программе, LFNDEMO Инструкции к примерам: LFNSTUFF и (ggSgf^ Местоположение: п http://www.symboI.ru/russian/library/prof_prog/source/chap " Ifnstuff http://www.symbol.ru/russian/library/prof prog/source/chap * Ifndemo
,пГ 16-битового кола на Windows 95 397 Платформа: Winl6 Инструкции но компиляции: Используйте прилагаемые МАК- файлы с компилятором Visual C++ 1.52 и компилируйте как обычно. Обратите внимание, что программа LFNDEMO не имеет никакого интерфейса пользователя; все, что она делает - это создает на вашем компьютере файл с названием c:\really really long file name.txt, после чего завершает работу. еэ Имейте также в виду, что новые функции прерывания 21 h с равным успе- ,м работают не только с длинными именами, но и с соответствующими им .дикими, - 1ак что, по сути, ничего, кроме этих функций, вам не ионадо- '„пся (это справедливо только для программ, предназначенных для Windows Ч), поскольку на Windows NT эти функции не поддерживаются).footnote!} Если вы собираетесь пользоваться длинными именами файлов в своей 16- шрядной программе, логичным следующим шагом будет попытка пепользо- ;aib новые стандартные диалоговые окна в стиле Windows 95. Я жшериментировал с этой возможностью с помощью простых переходников (generic Uiunks), но мне никак не удавалось заставить их работать — тестовая программа работала безупречно, за тем исключением, что всегда вызывала го 1ько старые диалоговые окна в стиле Windows 3.1. Я долго не мог понять, в чем же тут дело, пока не наткнулся на следующее место в статье «Thunking Benefits and Drawbacks», входящей в Win32 SDK: 16-разрядный процесс не может создавать новые потоки. Некоторые элементы Win32 API, такие как функции поддержки новых стандартных диалоговых окон или консольных приложений, создают новые потоки от wvienu вызвавшего их приложения. Эти функции, таким образом, не могут исполъзоватъс$1 в 16-разрядных программах. Хотя я не изучал этот вопрос во всех деталях, но у меня создалось впечатано, что упомянутые здесь функции Windows проверяют, кто вызвал их, и в '}мае 16-разрядной программы обращаются к старым стандартным диалого- "1М окнам. Если кому-нибудь из вас пришло в голову, как можно было бы ,(,нгц это затруднение (кроме очевидного, но крайне неизящного способа - ti-»естц специальную 32-разрядную программу, которая будет действовать как (1)еДщпс и вызывать нужные функции по приказу 16-разрядной), пожалуй- '' ^Шнщите мне об этом. r/'^^iioM нот явчяется к), что даже некоторые из особенно законопослушных ирофлмм i ^- пользующихся для рабош с файлами функциями .леяо прерывания, умудряю 1ся , 1,и,Ныи> работать с длинными именами файлов, будучи запущенными в окне DOS под Wm- s -^ [Примечание переводчика]
398 из winibR Теперь поговорим еще об одной хитрости, не имеющей отношения у > ным именам файлов, но также позволяющей сделать программу более цу" соблениой для работы под Window's 95 без переделки ее в 32-разрядную ъ идет об изменении номера «ожидаемой версии Windows», который заццг. заголовке программы. Чтобы изменить этот номер на 4.0, можно воегго ваться той версией программы RC.EXE, которая поставляется с Win32 $г (она расположена в каталоге \WIN32SDK\MSTOOLS\BINW16), добавив Bf! мандную строку вызова этой программы параметр «-40», который n3l\le, номер ожидаемой версии операционной системы для обрабатываемого исп пяемого файла. Зачем это нужно? Главная выгода заключается в том, что если в ваше программе используются новые функции прерывания 21h, из ее кода м0;к. убрать проверку версии Windows, так как Windows 3 1 сама откажется запи кать такую программу. К сожалению, тем самым вы запретите своей программ работать и па Windows NT 3.51, что довольно странно - ведь ц0 Windows NT 3.51 без труда работают 32-разрядные программы, помечена для версии 4.0. Однако поскольку в этой версии Windows NT новые функции прерывания 21h не поддерживаются, в этой странности есть своя логика Кроме того, пометив 16-разрядную программу версией 4.0, вы заставите Windows 95 относиться к ней иначе, чем к обычной 16-разрядной программе причем сразу по нескольким параметрам. Вот некоторые из этих отличий, ко торые Microsoft удосужилась задокументировать: В Ваша программа больше не сможет пользоваться функцией SetWin dowLongO для установки или сброса стиля окна WS_FX_TOPM0ST Вместо этого она должна будет использовать функцию Set Windows PosO. В Все диалоговые окна вашей программы будут 'теперь по умолчанш1 иметь стиль DS_3DLOOK. Этот стиль подразумевает выпуклые рамь. вокруг всех дочерних органов управления и, по умолчанию, свепьь (а не полужирный) шрифт надписей Программа, помеченная верс|1е Windows 3.x, может воспользоваться этим стилем, указав для сБ°' диалоговых окон DS_3DLOOK явным образом. В Windows 95 откажется создать диалоговое окно с неверно указан^ стилем DS_*. В то же время программа для Windows 3.x, указав'1 лее самый несуществующий стиль, все равно сможет создать затре° ванное диалоговое окно. В Перед рисованием на экране кнопки родительское окно получШ с°( щенпе WM_CTLCOLORSTATIC, а не WM_CTLCOLORBTN, ко^!1 посылается программам для Windows 3.x. В Перед выводом на экран поля редактирования родительское оК1{0г]-ь лучист сообщение WM CTLCOLOREDIT или WM CIbCOLOR5TA
•$»«*- 16-битового кола на Windows 95 399 Первое из них посылается для обычных полей редактирования, а второе — для тех, которые изначально отключены или имеют атрибут «только для чтения». g Аналогично, перед выводом на экран комбинированных списков родительское окно получает сообщение WM_CTLCOLOREDlT (для нормального списка) или WM_CTLCOLORSTATlC (для изначально отключенного списка), тогда как программы для Windows 3.x получают в этой ситуации сообщение WM_CTLCOLORLISTBOX. ti Поля редактирования из нескольких строк будут иметь пропорциональные движки панели прокрутки. g Вызов функции RcgistcrClassO будет невозможен, если либо компонент cbWndExtra, либо компонент cbClassExtra в передаваемой структуре WNDCLASS превышает но своему значению 40 (см. пример с WNDEXT16, описанный в разделе «Ужастики»). Однако это еще не самое интересное, что молено сказать о 16-разрядных программах, помеченных четвертой версией Windows. Если у вас есть набор разработчика драйверов устройств для Windows 95 (Windows 95 Device Driver Kit), загляните в файл \DDK\INC16\WINDOWS.H. Там вы найдете множество фрагментов, которые активизируются при условии, что переменная WIN- VER равна 4.0 или больше. Эти разделы описывают десятки констант, структур и функций API Windows 95 из таких областей, как доступ к реестру, доступ к INI-файлам, функции файловой системы и дополнительные функции обработки кодов ошибок (например, GetLastErrorO pi SetLastErrorO). Странно •мже не то, что Microsoft сделала тем самым эти функции доступными 16- ;)афядным программам, а то, что oini не поддерживаются под Windows NT (no 'Ранней мере в версии 3.51). Ыо самое невероятное то, что эти новые функции '!11гле ке документированы, кроме как в этом заголовочном файле, и они ^красно работают даже в программахг помеченных 3.x (по крайней мере те Только функций, которые я проверил). Целая россыпь этих жемчужных 1еРен, которые так любят программисты, — недокументированных функций ipJ Их там достаточно для того, чтобы написать о них целую книгу. Если вы хотите пользоваться этими новыми функциями API, вы должны включить именно эту версию файла WINDOWS.H (или скопировать из него /Кные фрагменты в свой проект), а также слинковать объектный файл с биб- J'OTeKoii LIBW.LIB в каталоге DDK\LIB на CD-ROM диске с набором '^Р^ботчика. При этом имейте в виду, что если вы используете эти функции, **°лжны запретить с коей программе работать под теми версиями Windows, '' г°рых они отсутствуют, вставив в код проверку версии или, еще лучше, по- ^ в исполняемый файл версией 4.0. Без этой предосторожности ваша .. Р^мма, вероятно, сможет даже запуститься на Windows 3.1 и Windows NT 11 будет безупречно работать до тех пор, пока не вызовет одну из этих от-
400 Из Win 16 • ч сутствующих функцпИ API. Такой вызов приведет к появлению обобщающего о том, что программа произвела вызов по «неопределепнг •• намической ссылке» (call an undefined dynalink). Остается лишь надеятьс поддержка этих новых (16-разрядных!) функций API появнгс Windows NT 4.0, хотя сейчас, когда я пишу эти строки, ничего нельзя с наверняка. кал Переходник «шестнадцать на тридцать дВд Использование механизма «переходников» (thunks) позво ет производить вызов программ иной разрядности, чем вьр,' вающая программа. Например, 16-разрядная программа м0,К( вызывать функцию из 32-разрядной динамической библиотек- (Откуда взялся такой странный термин — thunk — мне точноь известно. Я слышал по крайней мере три разных теории его прок, хождения.) В разных версиях Windows существует общим счею три различающихся между собой типа переходников: простые (ge neric), универсальные (universal) и плоские (flat), — но ни оди из этих типов не поддерживается всеми тремя 32-разрядным, платформами. В статье Microsoft Q125710 «Types of Thimbu Available in Win32 Platform» приведена таблица, в которог собраны сведения о доступности этих трех видов переходников (здесь эта таблица слегка модифицирована): Таблица 123 Тип переходников Простые Универсальные Плоские Win32s поддерживается Windows 95 поддерживается поддерживается* поддерживается Windows NT поддерживаете; * Примечание: Windows 95 поддерживает универсальные перехоДН11Ь| «но только для приложений, помеченных версией 3.1», если верить1 erosofl Когда кто-то задал об этом вопрос в одном из форум013 puServe, представитель поддержки разработчиков из Microsoft отве" буквально следующее «Вообще говоря, универсальные переходи111*11 поддерживаются Windows 95 Возможность пользоваться ими бы i<1 бавлена в состав системы по просьбе одной компании, и эта мера н*1Ь1 не рассматривалась иначе как временная» (Только не спрашивайте } о какой компании идет здесь речь; лучше, если вы сами знаете оо • что-нибудь, напишиге мне )
16-битового кола на Windows 95 401 9 Простые переходники позволяют 16-разрядным программам вы- ^% зывать функции из 32-разрядных DLL-файлов. Простые переходни- ^"^ :си не зря называются простыми — для их использования не требуется ничего, кроме вызова четырех специальных функций ^ч (LoadLibraryEx32W(), FreeLibrary32W(), GetProcAddress32W() и S^*^ CallProc32W()) и явной загрузки 32-разрядного DLL-файла точно гак же, как вы загружаете 16-разрядную динамическую библиотеку. ^^^ Ситуация несколько осложняется тем, что при этом вам придется &J) обращать особое внимание на указатели, которые в 16-разрядной программе представляют собой объединение 16-разрядного адреса сегмента и 16-разрядного смещения, а в 32-разрядной — линейный 32- f^*5) разрядный адрес памяти. В некоторых случаях вам может понадобиться преобразовать указатель типа 16:16 в указатель тина 0:32, для чего служат функции GetVDMPointer32W(), WOWGetVDMPoint- fS^ er() и WOWGetVDMPointerFixO. ^^ Универсальные переходники существуют только в Win32s. Их цель — позволить 32-разрядным программам вызывать функции ^^ из 16-разрядных DLL-файлов. В статье Microsoft Q125710 мы Е2/ читаем, что универсальные переходники можно также использовать и для вызова из 16-разрядной программы функций 32-разрядной библиотеки, «хотя официально эта возможность не поддержи- pSjl) вается». (Интересно, зачем тогда было упоминать об этой возможности в официальной публикации? Еще одна загадочная страничка мировой майкрософтианы.) су% Плоские переходники позволяют вызывать как 16-разрядный ^ код из 32-разрядного, так и наоборот, — но область их действия ограничена только Windows 95. (Таким образом, Windows NT oc- ^^ тается без средств вызова 16-разрядных DLL-файлов из 32- ^3) разрядных программ — любопытно, не правда ли?) Этот метод также требует больших затрат труда — вам придется пользоваться специальным компилятором, поддерживающим переходники, и (^3) поставлять вместе со своей программой особый «переходный» DLL-файл. Более подробные сведения о переходниках вместе с fe-«^ примерами программ вы найдете на Microsoft Developer Network ^^ CD-ROM или на диске Win32 SDK поиском но ключевому слову «thunk». ^^^ Таким образом, мы видим, что единственный способ вызвать ^>3) 16-разрядную функцию из 32-разрядной программы под Windows 95 - это использование плоских переходников, что вряд ли можно назвать особо элегантным решением. Но задумайтесь-ка1 ^3l разве сама Windows 95 не пользуется постоянно переходниками
402 Из WinU B U/1 __jo^ между 16- и 32-разрядным кодом — например, когда GDI32 h и USER32.DLL пользуются 16-разрядной поддержкой GDI EXE и USER.EXE? Похоже, что у Microsoft есть в Зап" гораздо более простой и эффективный способ организовать г взаимодействие. В действительности так оно и есть, хотя этот соб не документирован. Мэтт Питрек упоминает его в своей сг , в PC Magazine от 26 сентября 1995 года. Оказывается, и вы впо можете осуществлять «переходы без переходников» иод Wind0', 95 с помощью недокументированных функций Load Library щ, FreeLibraryl6(), GetProcAddress 160 и QT_Thunk(), которыеnd положены в файле KERNEL32.DLL. По сути своей этот мето мало чем отличается от использования простых переходников разве что действует он в обратную сторону, да еще заставляв программистов поволноваться от сознания недокументированное!, этих функций. (Подробнее вы можете прочесть о функци- QT__Thunk() в упомянутой статье Мэтта и в его книге «Секреть системного программирования для Windows 95».) Я уже говорил, что не одобряю использование неестественна средств в программировании, а вызов недокументированш функций — это, без сомнения, в высшей степени неестественна? акция. Тем не менее, в данном случае мне остается лишь выразил свое недоумение — почему программисты Microsoft, реализовав столь простой и удобный механизм, оставили его недокумен тированным? Великие неизвестные Следует отдавать себе отчет в том, что мир Windows не сможет пересечь вен кий перевал из долины Winl6 в долину Win32 как единое целое. Если уч#' архитектурные различия между Windows 95, Windows NT и Win32s, то flpIlM' дится лишь удивляться, что эти три платформы обладают хотя бы таким УРоЕ нем совместимости. Сейчас, когда я пишу это, семейство Wind°v приспособилось к новым условиям, если так можно выразиться, на две треТ' Windows 95 и осовремененная Win32s появились сравнительно недавно, т°г' как новая версия Windows NT, как ожидается, появится не раньше чем че^ несколько месяцев. По-видимому, в Windows NT 4.0, которая должна иояб11 ся в начале 1996 года, Microsoft реализует оболочку, аналогичную Windows и значительно изменит интерфейс API, сближая его с интерфе111' Windows 95. К сожалению, все это остается пока в области догадок. Например, в°т большой список очень важных вопросов, на которые я так и не смог поЛУ1 ответ:
неизвестные 3 Будут ли в Windows NT 4.0 поддерживаться новые функции прерывания 2 lh для 16-разрядных программ? g Будет ли в Windows NT 4.0 введена поддержка полного набора функций API оболочки? g Смогут ли под Windows NT 4.0 работать 16-разрядные программы, помеченные версией 4.0? g Можно ли будет под Windows NT 4.0 пользоваться длинными именами файлов с помощью функций _lcreat() и _1ореп()? g Останутся ли Windows NT и Windows 95 отдельными и независимыми продуктами на протяжении ближайших версий? Как я уже писал, если верить журналу Info World за сентябрь 1995 года, Джим Оллчин из компании Microsoft заявил, что Windows 95 и Windows NT не будут объединены в одном продукте, положив тем самым конец довольно долгому периоду, когда все были почти уверены в обратном. Этот вопрос очень важен для разработчиков, поскольку от него очень многое зависит. До тех пор пока эти операционные системы будут существовать отдельно друг от друга, я уверен, различия в их реализации никогда не исчезнут. Вполне очевидно, что каждая из двух отдельных групп разработки Microsoft будет не в восторге от предложений внести в свой API изменения, которые в перспективе могут доставить неприятности «своим» независимым разработчикам. С другой стороны, если Microsoft решит свести воедино Windows 95 и Windows NT, мы с вами будем свидетелями более чем захватывающей эквилибристики в попытках разрешить многочисленные противоречия и несовместимости между двумя системами. Хорошенький получается выбор — тем более что, я уверен, нам с вамп вряд ли дадут право голоса в этом вопросе. Stickles! u Windows 95: как я переносил свою программу Первое, что я сделал, когда присоединился к бета-тестерам Windows 95, — это, разумеется, протестировал все свои старые программы. Зная, каким количеством хитростей и недокументированных особенностей пользуется моя программа, я был готов к тому, что при первом запуске она разлетится на миллион кусков. И был приятно удивлен тем, что этого не произошло. На самом деле она разлетелась всего на несколько кусков, которые было не так уж сложно снова собрать в работающую программу. В этой врезке я перечислю главные из проблем, с которыми я столкнулся, и решения, которые мне пришлось принять.
404 Из WinU в и,- — ——5^ Первое же, на что я наткнулся (впрочем, не сразу, а т после получения какой-то из очередных бета-версий Wmi 95), — это то, что я не мог теперь перетаскивать и бросать ф- на главный значок программы Stickles! (роль которого играг перь кнопка на панели задач). При попытке сделать это Wind 95 выводила на экран сообщение о том, что «перетаскивать o6i ты па одну из кнопок панели задач нельзя» (я уже писал об Эг, в главе 7). Поскольку Stickles! вообще не имеет обычного 0К1 маневр «зависающей мыши», к которому пас хочет приучить ,\i crosoft, тоже был неприменим. Эту проблему нельзя, конечно 0 нести к слишком серьезным, поскольку Stickles! имеет множесц других способов для импорта файлов. Тем не менее, симптому вызвал во мне серьезное беспокойство, так как на моих глазах бы разрушен весьма простой, удобный и (если мне позволено буде употребить это не слишком популярное среди программистов сю во) элегантный элемент интерфейса пользователя. Другая проблема был связана с представлением большого ко личества наклеек кнопками на панели задач. Моя программа вое станавливает при запуске все наклейки (небольшие окна), заве денные пользователем, в том же месте на экране и в том же состоя нии (открытом либо мииимизировапном). Под Windows 95 каждое окно, включая п каждую из моих наклеек, получает соответст вующую кнопку на панели задач. Само по себе это еще не является проблемой -- хотя, конечно же, желательно было бы пред\ смотреть опцию, по которой добавление наклеек не будет загромо ждать панель задач. Однако, как выяснилось, когда Stickiest за пускается с большим количеством наклеек (приблизительно -1 или больше), Windows не удается разместить на панели зала требуемое количество кнопок, и некоторые из кнопок пропади ' (причем иногда пропадает даже кнопка главной программы Sti^ les!). Это уже куда серьезнее, поскольку тем самым пользовав' лишается доступа к некоторым из опций программы, которые Д°1 тупны только через главное системное меню Stickles! ^с" программа не представлена ни собственным окном, ни даже кн° кой на панели задач, пользователь в принципе не может добра11'1 до ее системного меню. Помимо необходимости что-то делать с этими неприятие сюрпризами, я также встал перед необходимостью принять ряд, ветствениых решений о том, переделывать ли Stickier! в/' разрядную программу, поддерживать ли длинные имена фа11 и т. п., — о которых я расскажу ниже.
е$ лР неизвестные 405 Решения Проблема перетаскивания файлов на значок программы была решена сравнительно просто - я переделал Stickles! в минимизированную программу типа III (которая, как вы помните, обычно представлена только кнопкой на панели задач, но при необходимости открывает специальное окно, единственная цель которого - принять брошенный файл). Собственно говоря, куда больше времени у меня заняло рисование приличного «бычьего глаза» -- растровой картинки, изображающей мишень в окне-приемнике файлов. Проблема отсутствующих кнопок на панели задач оказалась серьезнее. Когда я задал об этом вопрос представителям Microsoft, кто-то из группы поддержки бета-тестеров .заявил мне, что данное ограничение (касающееся только 16-разрядных программ) вряд ли когда-нибудь будет снято. Как выяснилось, суть проблемы заключается в том, что Stickles! создавала слишком много новых окоп в слишком короткий промежуток времени, в результате чего система не успевала произвести все необходимые действия для создания каждой из кнопок. Сложно сказать, как именно это происходит - то ли система, занятая созданием очередной кнопки, пропускает мимо ушей запрос па создание следующей, то ли она сознательно решает отказать каким-то из стоящих в очереди запросам - короче говоря, суть в том, что Stickles! требует от системы слишком быстрой реакции и в результате не получает никакой. Поразмыслив над этой проблемой, я решил, что стоит попробовать просто замедлить процесс загрузки и вывода на экран наклеек, - и сделанная па скорую руку тестовая версия показала, что такой подход действительно работает. Все, что мне пришлось сделать, - это завести особый таймер и загружать каждую наклейку по истечении очередного небольшого промежутка времени. Как только я сделал это, проблема исчезла - каждое окно получило свою кнопку, и кнопка главной программы больше никуда не пропадала. По-видимому, эта история - - хороший пример того, к каким странным и подчас неестественным трюкам приходится прибегать при переносе программ с одной платформы Windows па другую. Конечно, то, что мне пришлось сделать со своей программой, не вызвало у меня никакого восторга, - далее несмотря па то, что с точки зрения пользователя ничего не изменилось (наклейки по- прежнему загружаются и отображаются слишком быстро, чтобы можно было заметить хоть какую-то задержку). И все-таки это слишком похоже на «хатсерский» метод решения задач, к которому ^Э
406 Из Win16 в и,. __^VV^ я испытываю самые неприветливые чувства. К сожалению, *f го выхода Microsoft мне не оставила... Стратегические вопр0. Разобравшись с описанными выше иесовместимостямц вплотную подошел к необходимости решить два других, бп- стратегических, чем тактических, вопроса: переделывать программу в 32-разрядную и поддерживать ли длинные имс файлов. К сожалению, создание 32-разрядной версии Sticky потребовало бы от меня огромных затрат труда. Дело в том, ЧТг программа эта написана с помощью Borland Pascal для Window* а это значит, что ее сорок три с лишним тысячи строк кода тесно переплетены с 16-разрядным OWL. (Как известно, 32-разрядноги OWL для Pascal разрабатываться не будет, a Delphi не позволяет легко и быстро переделать код, написанный с помощью 16-разряд кого OWL, переключив его на 16- или 32-разрядный WCL.) По мимо этого, следует признать, что Stickles! не очень-то выиграет от переделки в 32-разрядную программу — во всяком случае, небольшое повышение производительности вряд ли будет замечено ее пользователем. Поэтому я решил отложить эту огромную работу по крайней мере до версии 5.0. Тем самым у меня будет время освоить 32-разрядную версию Delphi и следующее поколение ком пиляторов C/C++ и, в конечном итоге, принять более вззешенное решение о конкретной тактике переноса. Кроме того, мне известно, что многие из моих пользователей работают в офисах, где зачастую еще никто и не думает о переходе на Windows 95, поэто му решение не переносить Stickiest вызвано отчасти и соображениями удобства этих пользователей. При принятии решения о поддержке длинных имен фапЛ°Б приходится также учитывать то, что Stickiest в этом смысле - ^ вольно необычная программа, пользователю которой почти н приходится работать с файлами как таковыми. Пожалуй, тольк* экспорт и импорт требуют указания имени файла, тогДа |ч' обычная повседневная работа с наклейками - их созД<1Н1 перемещение, уничтожение и т. п. - позволяет обходиться без vi ких бы то ни было файлов. А поскольку перед этим я уже реШ ' что не буду делать 32-разрядной версию Stickiest 4.0, я с ^ разумным также и не добавлять поддержку длинных имен Фа1Ь в 16-разрядную версию. Это решение принять было уже сло# поскольку я более чем уверен, что по крайней мере нек°т0'
0^L 407 еэ ^Э ^> пользователи будут этим разочарованы (более того, несколько человек уже спрашивали меня о том, существует ли 32-разрядная версия Stickies!). С другой стороны, отказавшись от поддержки длинных имен файлов в 16-разрядной версии, я тем самым избавлю своих пользователей от возможности столкнуться с довольно неприятной ошибкой в Norton Navigator 1.0 (см. об этом на стр. 421). Единственное, что я ввел в новую версию своей программы в ответ на многочисленные соблазны Windows 95 — это контекстное меню, вызываемое правой кнопкой мыши или специальной клавишей «Application» на клавиатуре Microsoft. Ничего сложного в этом нет — достаточно было ввести перехват сообщения WM_CONTEXTMENU, которое посылается в ответ на нажатие правой кнопки мыши или упомянутой выше клавиши всем окнам, в том числе принадлежащим 16-разрядным программам (даже если они помечены версией меньшей, чем 4.0). Однако, если бы я обрабатывал только это сообщение, я бы лишил возможности вызывать контекстное меню пользователей Windows 3.1, поэтому сейчас Stickies! выводит контекстное меню как в ответ на WM_CONTEXTMENU, так и просто по нажатию правой кнопки мыши. Кроме того, мне пришлось как-то отреагировать на ошибку в Symantec Norton Navigator 1.0, из-за которой команда Close в системном меню программы может стать недоступной, если программа внесла изменения в это меню. Поскольку эта неприятность затрагивает Stickies! самым непосредственным образом, мне пришлось предусмотреть на этот случаи обработку не SC__CLOSE, а непосредственно кода команды из системного меню. Конечно, это не слишком изящно, по вполне решает проблему (подробнее об этом ниже). Помимо всего прочего, мне нужно было решить, использовать ли стиль DS_3DLOOK для диалоговых окон, делать ли версию файла помощи в формате WinHelp 4.0, пользоваться ли новыми стандартными диалоговыми окнами Windows 95 и многое другое. Пока что я решил подождать некоторое время после выпуска Stickies! 4.0 и посмотреть, насколько быстро мои пользователи будут переходить на Windows 95 и чего именно они захотят от моей программы. ^ФясяшАмг» 1оР1Щ. собранные в этом разделе, относятся рс разряду таких, которые впол- ' °гУт остаться для вас только историями. Но если вы, не дай бог, когда-
408 Из WinU в и нибудь сами наткнетесь на эти подводные камни, то, скорее всего, это "POlljr дет в час-два ночи, когда до сдачи программы заказчику остались считц часы и когда крепкий кофе является единственным способом поддержания ни в организме Такое случалось и со мной. Поэтому я назвал эти ист «ужастиками». Если говорить серьезно, то здесь я опишу все то, что мне удалось BocnD вести и проверить собственноручно и что, на мой взгляд, достаточно вц^ притом не является очевидным Я не пишу здесь о том, какие заголовок файлы нужно подключать, чтобы скомпилировать программу как > разрядную — вам, я думаю, читать об этом уже неинтересно. Нет здесь* исчерпывающего списка ошибок, которых в Windows 95, как и в любой дрУи большой программе, более чем достаточно. Короче говоря, здесь собраны д( ствительно неожиданные и неочевидные особенности поведения системы которых вы, вполне вероятно, нигде никогда не услышите, пока не столкнем с ними сами. Даже если все, о чем здесь говорится, лежит в стороне от вашг интересов как программиста, знакомство с этими проблемами должно пост жить вам напоминанием об опасностях, таящихся за каждым углом. Должен сказать, что этот список не претендует на полноту. В подготовь тельных записях к этой главе перечислено гораздо больше потенциальных по; водных камней, которые были отброшены либо потому, что я счел их слишко1 незначительными, либо потому, что мне не удалось их воспроизвесгп Например, тот факт, что функция IsGDlObjectO есть в 16-разрядном API, но отсутствует в 32-разрядном, не был включен в список просто потому, что эь функция чрезвычайно редко используется (ее нет даже в заголовочных фаг лах; попробуйте вставить ее в свою программу, и вы получите ошибку при ком пиляции). И разумеется, охватить все случаи несовместимости межд1 платформами Windows можно только, если затратить на это огромные усиш (и написать целую книгу только об одних этих песовместпмостях). Впро1^ принимая решение о том, что важно, а что нет, мне пришлось принять не которые априорные посылки о том, что именно интересует вас как программ ста. Тем не менее я надеюсь, что составленный список будет нредставля' интерес для большинства моих читателей. И наконец, последнее: я буду рад получить от вас любую ипформ^1110 всех проблемах и неожиданностях, встретившихся вам при переносе програМ Мой электронный адрес указан во Введении. Проверка номера версии: не все так просто Строго говоря, этот пункт нельзя отнести к числу настоя «ужастиков», - по вы просто обязаны иметь представление об этой пр0^1,. Как известно, Windows 95 па самом деле является Windows вере1111 Поэтому, когда 32-разрядная программа спрашивает Windows о номере вер
409 г она получает 4.0. Однако когда этот лее вопрос задает 16-разрядная ямма, в ответ она получает довольно странное значение — а именно 3.95. м здесь Дело? Оказывается, за эту странность в поведении Windows мы мо- рпнпть только самих себя. Дело в том, что очень многие 16-разрядные ;,аммь1 до сих пор осуществляли проверку версии неправильно. Чтобы ,г1Ггъ выполнение на любой версии Windows, меньшей 3.1, очень многими „, )1ьзовался примерно следующее выраженгге: t((inajor„version <, 3) || (Minor_version < 10)) сообщить о невозможности работы на этой версии */ . и завершить программу */ Очевидно, что из-за этой проверки программа не будет работать в любом л чае, когда число после точки в номере версии меньше десяти — что и имеет ее к) в версии 4.0. У Microsoft просто не было другого выхода, кроме как несть эту ошибку (а программ с этой ошибкой, вероятно, довольно много) и извращать 16-разрядным программам такой «хитрый» номер версии - ;ающий, тем не менее, возможность определить, что на самом деле программа дополняется под Windows 95. Интересно, что 16-разрядная программа, помеченная в заголовке версией 10 (и, таким образом, удостоверяющая, что она готова работать под версией 10), все равно получит в ответ на вопрос о номере версии значение 3.95. Стоит ■акже упомянуть, что 16-разрядная программа под Windows NT 3.51 получит зответ на вопрос о номере версии 3.10, что вполне логично — эта версия Windows NT не обеспечивает 16-разрядным программам ничего сверх услуг йычиой Windows 3.1. ОШ обрезает координаты до двдх байт Многие программисты по-прежнему не отдают себе полного отчета в том, 10 подсистема GDI в Windows 95 осталась 16-разрядной, хотя ее интерфейс ' ' и сделан 32-разрядным. Это значит, что когда вы, к примеру, вызываете ПкШпо RectangleO и передаете ей четверку сверкающих суперсовременных -Разрядных координат, GDI бесцеремонно отрезает и выбрасывает у каждого нчх верхние два байта, не считая себя обязанной хоть как-то уведомить об /м вызывающую программу. Что еще более интересно, при этом GDI не }'иЦпет абсолютно никакого внимания на знак исходного значения. В резуль- . ' если одна из ваших 32-разрядных координат положительна, но после f 3аШгя ее старший (крайний левый) бит будет установлен, в результате Рината изменит не только величину, но и знак! ((( л Уверен, что множество программистов, занимающихся графикой и устав- '.р0\ °т ограничений, налагаемых 16-разрядными координатами, сразу же Гся писать программы для Windows 95 в надежде избавиться от этих
410 Из WinU в ur __^ ограничений. И хотя 16-разрядность подсистемы GDI ни для кого не секп боюсь, что некоторые из них могут потратить слишком много времени, в, няя, что лее на самом деле происходит с координатами в их программах ' ' Один из способов хоть как-то решить эту проблему -- использование г „ л потеки функций-оболочек. Одна из таких библиотек под названием Wm'j приведена в приложении С. Ограничения значений cbWndExtra и cbClsExtra Как я уже говорил выше, 16-разрядные программы, помеченные версщ. Windows 4.0, ведут себя под Windows 95 совсем иначе, хотя Microsoft так тоi ком и не документировала эти различия (подробнее об этом см. врезку ниже) По большей части эти изменения можно только приветствовать, так как они по зволяют 16-разрядным программам шире использовать преимуществами Win dows 95 без необходимости перекомпиляции их в 32-разрядные. По-видимому, самое странное нововведение, с которым столкнутся как программы 16/4.0, так и 32/4.0, имеет отношение к полям cbWndExtra neb ClsExtra структуры WNDCLASS, которая передается в качестве параметры вы зова функции RegisterClassO. С помощью этих полей можно заказать нужное количество байтов, которые будут зарезервированы дополнительно для каж дого окна этого класса (cbWndExtra) и для всего класса в целом (cbClsExtra) Запись и считывание данных из этих зарезервированных участков памяти осу ществляется с помощью функций GetWindowLong(), SetWindowLongO, Get ClassLongO и SetClassLongO. Если значение cbWndExtra или cbClsExtra превышает 40, функция Regb terClassO не сработает и возвратит NULL. Все 32-разрядные программы помеченные версией 4.0, подвержены этому ограничению под Windows 95 - но не под Windows NT 3.51. Об этом я еще буду говорить в главе 14 (в разде|с с таким же заглавием). Сорок байт — это, конечно, очень мало, особенно если учесть, что пр' регистрации класса диалоговых окон еще тридцать байт из cbWndExtra 1K пользуются самой Windows для своих нужд, оставляя нам, тем самым, БСГ десять байт. В статье Q131288 Microsoft советует тем из нас, кому нУж' больше сорока байт памяти на такие цели, установить значения cbWndExti' cbClsExtra равными четырем и хранить в них указатели на динамически вы ляемые фрагменты памяти с нужными данными. Я пытался выяснить, чТ°' заставило Microsoft установить настолько скудный (и настолько проПзБ° иый) лимит на величину этих областей памяти. Единственное объясни ' которое я слышал от программистов, заключается в том, что некие програ'' забывали инициализировать значения cbWndExtra и cbClsExtra, из-за ^ Windows приходилось выделять огромные и никому не нужные облает!
^cr^L 411 Инструкции к примеру: WNDEXT16 Местоположение : http://www.symbol.ru/russian/library/prof_prog/source/chap12/ wndext16 Платформа: Win 16 Инструкции по компиляции: См. комментарии в файле WNDEX- Т16.С, в котором описаны возможные способы компиляции программы. Используйте прилагаемый МАК-файл и Visual C++ 1.52 Формат результата BetOpenFUeName() Когда готовилась к выпуску Windows 3.1, вероятно, наибольший энтузиазм у программистов вызывало одно из новшеств — а именно стандартные кйлоговыс окна (common dialogs). С тех пор мало кто из программистов, пишущих для Windows, мог обойтись без этого элемента интерфейса. Разумеется, стандартные диалоговые окна есть и в Windows 95. Однако есгь одна неожиданная деталь поведения диалогового окна Open File, о №'юрой вам следует знать заранее. Оказывается, функция GetOpenFileNameO иногда (но не всегда) возвращает 32-разрядной программе несколько выбранных имен файлов в формате, который слегка отличается от того, к которому пРнвыкли 16-разрядные программы. Это изменение вызвано необходимостью Сильно обрабатывать длинные имена файлов, которые могут содержать 'Робелы. 16-разрядная программа, даже если она пользуется флагом ^LONGFILENAMES при вызове GetOpenFileNameO, получит в резуль- ате несколько длинных имен файлов в старом формате, в котором они отрется Друг от друга пробелами. Например, если ваша программа вызвала ^OpenFileNameO, и пользователь выбрал файлы MARCH.DOC и ^IL.DOC из каталога C:\REPORTS, то в ответ на свой вызов программа ,°IVlI"T строку, содержащую «C:\REPORTS MARCH.DOC APRIL.DOC» л Нечно, без кавычек). После этого программа должна разобрать строку, J)e3UB ее на составные части там, где стоят пробелы, и собрать из этих частей Pq полных имени файла C:\REPORTS\MARCH.DOC и C:\RE- 1S\APRIL.DOC. (На мой взгляд, перепоручение этого разбора самой Рамме ничем не оправдана. Лично я не вижу причин, почему бы Microsoft
412 из winu в u, ___П^ не предусмотреть специальный флаг, установка которого позволит пр0[,. получать сразу готовые полные имена файлов.) Как видите, даже если 16-разрядная программа явным образом заказы,, длинные имена файлов, формат результата молчаливо предполагает, чюс"'' имена файлов не содержат пробелов. Оказывается, функция Getf) FileNameO специально проверяет каждое длинное имя на содержание пробе и возвращает длинное имя файла только в том случае, если пробелов в нем В обратном случае строка результата будет содержать соответствующее коп кое имя данного файла. (Интересный пример того, как это соглашение бы • нарушено независимой программой, вы найдете в разделе про N0rt Navigator 1.0 па стр. 421.) Даже если ваша 16-разрядная программа помечена версией 4.0, Windows будет по-прежнему следовать этому правилу. Конечно, это не слншк0. логично, поскольку программа открытым текстом потребовала длинные имен, файлов и ее номер версии говорит о том, что она знает, как разбираться to строкой результатов даже в новом формате. Впрочем, особо ругать Microsoli по этому поводу не стоит, так как здесь мы имеем пример разумного подходам обеспечению совместимости с предыдущими версиями. Если же ваша программа 32-разрядная и пользуется новыми стандартными диалоговыми окнами в стиле программы «Проводник», то возвращаемая строк, будет иметь иной формат, в котором вместо пробела для отделения имен фай 'юв друг от друга используется символ с нулевым кодом, а в конце строкиош символ повторен дважды. Таким образом, в нашем примере диалоговое окно возвратит следующую строку: "С \REP0RTS~MARCH D0C~APRIL DOC""' (нулевые символы, обозначаемые в С с помощью сочетания «\0», здесь пред ставлены символом «~»). Здесь следует обратить особое внимание на то, что для того, чтобь. получить строку в таком формате, ваша программа не просто должна быть^- разрядной, но еще и использовать диалоговые окна в стиле Проводи1115,1 Проделав некоторое количество тестов, я обнаружил, что если вызвать Get peiiFileNameO, не установив ни флаг OFN_ALLOWMULTISELECT, ппФ1'1 OFN_EXPLORER, программа (как и ожидалось) по-прежнему вызовет дДО|l говые окна в стиле Проводника. Однако если я разрешал выбор нескольЬ' файлов с помощью флага OFN_ALLOWMULTISELECT, но не устанавл^'1 флаг OFN_EXPLORER, происходил вызов старых диалоговых окон со стар11 форматом возвращаемой строки. И только если оба эти флага установи результатом будет новое диалоговое окно, возвращающее результат в i10 формате. Я говорю здесь об этом так подробно потому, что очень далее вероят*1'1 , туация, когда, разрешив по какой-то причине в одном из своих окон ОреП м выбор нескольких файлов, вы столкнетесь с тем, что 32-разрядная проП*'
./£2££ 413 fge искать ассоциации? £сЛп вашей программе требуется получить доступ к ассоциациям — сис- ibiM установкам, связывающим расширение имен файлов с программами, 0рые эти файлы обрабатывают, - то вам предстоит узнать неприятную но- ib Оказывается, универсального способа сделать это, годящегося для всех цгформ, не существует. Это значит, что вы можете пользоваться ассоциации запуская внешние программы, по если вы просто хотите выяснить, ка- 1Я командная строка соответствует файлам *.ТХТ, то вы не найдете никакой лекции API, которая годилась бы для решения этой задачи. Есть, конечно, пнкцпя fileExecutableO, но она делает совсем не то, что нужно. Она си- :\ шрует выполнение команды, учитывая текущий каталог, который вы „ошны задавать при ее вызове, а кроме того, требует указания имени какого- нибудь существующего документа. Это значит, что если вам нужно просто выяснить, с чем ассоциировано расширение *.ТХТ, вы должны передать этой функции полное имя файла, что-нибудь вроде «c:\mydocs\summary.txt», причем этот файл должен действительно существовать. К сожалению, никакого другого способа получить информацию об ассоциациях, кроме как копаясь вручную в реестре, по-видимому, не существует (я говорю «по-видимому», по- гому что спрашивал об этом знакомых программистов и даже саму Microsoft, ю никто не дал вразумительного ответа. В поисках подходящей функции я несколько раз пропахал весь API, но ничего не нашел). Ну что ж, давайте засучим рукава и запустим руку поглубже в реестр — k гам-то требуемая информация точно есть. В конце концов, программиста та- v,,e предприятие пугать не должно. Однако, к сожалению, дело сильно ослож- "Яе1'ся тем, что на разных версиях Windows ассоциации хранятся в разном Армате п даже в разных местах, так что вашей программе придется реализо- <11Ь Довольно сложный алгоритм их поиска в зависимости от версии Windows, которой все происходит. В старых версиях Windows ассоциации хранятся в файле WIN.INI в раз- Че I Extensions]. Например, на одной из моих машин с установленной Win- VlS tor Workgroups 3.11 этот файл содержит следующее: ^tencionc] c,d=cardfile exe ~ crd DCx=:PbruGh exe ~ pcx 'ec=recorder exe ~ rec 'JP=WINHELP EXE - hip '^=c \WINW0RD6\WINW0RD EXE ~ doc ^ET::c \WINW0RD6\WINW0RD EXE ~ LET r0t=c \WINW0RD6\WINW0RD EXE ~ dot и так далее ]
414 Из Winie в Ur ___и^ В этом случае для получения информации об ассоциациях доста-г применить функцию GetProfileStringO и выудить нужное значение возвращаемой ею строки. Однако на более новых версиях Windows (и даже на многих систем-, Windows 3.1) ассоциации уже хранятся в реестре. А это значит, что нам с,, в любом случае придется предусмотреть поддержку как нового, так и стцп формата хранения ассоциаций. Старый формат используется преимуществе!, на Windows 3.11 и на некоторых системах с Windows NT, однако его м0/К встретить и на некоторых компьютерах с Windows 95, примером чему слу/К„ такой элемент реестра на одном из моих компьютеров: HKEY_CLASSES_ROOT\.lGt\Ghell\open\command = С \MSON\PPTVIEW ЕХЕ %Г Ассоциации, хранящиеся в новом формате, разбиваются на две записи Первая запись соотносит расширение имени файла с идентификатором прцЛо жения: HKEY..CLASSES_ROOT\ так = "Visual C++ а вторая -- идентификатор приложения с набором команд и конфпгураци оппых опций (включая значок по умолчанию). Среди этих команд содержите [| та, которую мы ищем - команда запуска приложения: HKEY_CLASSES_ROOT\ViGual C++\shell\open\command = "D \MSVC20\BIN\Mcvc exe %1 Необходимость обрабатывать все три перечисленные ситуации, конечно, не упрощает жизнь программиста. Учтите, кстати, что (хотя это и маловероятно) иногда в реестре можно найти нечто среднее между этими двумя форматами например: HKEY_CLASSES_ROOT\ так = Visual C++ HKEY_CLASSES_ROOT\ inak\Ghell\open\coinmand = 'D \MSVC20\BIN\Mgvc exe %1 Если ваша программа предназначена для широкой аудитории и способна работать на разных версиях Windows, вы должны продумать порядок, в ко тором программа будет делать попытки поиска ассоциаций. Этот порядок имее' смысл сделать зависящим от версии Windows, под которой выполняете программа. Я решил рассказать об этой проблеме здесь, так как, вероятно, вы ст°п' нетесь с ней именно при переносе 16-разрядных программ на Win32 (илиДа'Ь1 просто при запуске 16-разрядной программы под Windows 95). Проблемы с перетаскиванием Представьте себе, что вы находитесь в Windows 3.1. Захватив мышьЮ0^' или несколько файлов из Диспетчера файлов, вы перетаскиваете их на краеП х окна какой-то программы, высовывающийся из-под других окоп, и 6росас'Ге там. Что при этом происходит? Окно-приемник получает фокус и всплыв^1
415 ^т zlz jfi план, после чего программа, создавшая это окно, обрабатывает '^ценные файлы. Привычная последовательность действий для многих поль- мтслей Windows. К сожалению, в Windows 95 это происходит слегка по-другому. По какой- (непзвестной мне) причине очень часто перетаскивание файлов из окна водника и бросание их на частично видимые окна 16- или 32-разрядных ' 1рамм приводит к тому, что эти окна становятся активными, но не пьшают на передний план. В результате после приема файлов программа с1ко выводит диалоговое окно пли окно сообщения — которое, как и глав- wjkiio программы, остается невидимым под другими окнами. Разумеется, та- ,е поведение краппе неудобно. К счастью, выход из положения найти не так ,к трудно. Самый удобный метод борьбы с этой неприятностью, который я нашел для ^.разрядных программ, — это вызов функции SetForeGroundWindowO с передачей ей дескриптора вашего собственного окна. Этот вызов следует разместить в самом начале фрагмента кода, обрабатывающего сообщение \VM_DROPFILES. Для решения проблемы этого вполне достаточно. Для 16-разрядных программ, однако, способ этот не годится, так как в Winl6 API пет функции SetForeGroundWindowO. (Этой функции нет даже среди расширений API для 16-разрядных программ с ожидаемой версией Windows 4.0, которые входят в состав Windows 95 DDK.) К сожалению, даже самое лучшее решение, которое мне удалось найти, нельзя назвать иначе как притянутым за уши: SetWinaowPoc(m_nWnd,SWP_T0PM0ST,0,0,0.0,SWP_NOSIZE | SWPJI0M0VE), SetWindowPoc(in_nWnd,SWP_N0T0PM0ST,0,0,0,0,SWP_N0SIZE | SWP_N0M0VE), Эти две строчки кода нужно поместить в начало того фрагмента, который обрабатывает сообщение WM_DROPFILES. (Кроме того, советую вам помес- 111ть рядом с этим местом обширный комментарий, в котором бы объяснялась ^ь и обосновывалась необходимость столь странного программистского вы- верта.) Мне неизвестно, является ли описанное здесь поведение Windows 95 °е,}Ультатом сознательного решения создателей системы или же это просто ,Ц1ибка. Как бы то ни было, приятной неожиданностью эту особенность не на- °Ве,иь, так как из-за нее программы, прекрасно работающие под Windows 3.1 Windows NT 3.51, могут производить впечатление «поломавшихся» под endows 95. Кроме того, я заметил, что в Windows 95 вообще очень слаба связь между 11ГаДлежностыо фокуса одному из окоп и Z-порядком окон, присутствующих |! кРане (т.е. порядком, в котором окна располагаются одно поверх другого). ,с 'РПмер, для панели задач у меня установлена опция «Автоматически Рагь с экрана» (Aulohide). Проделав что-нибудь с панелью задач и позво- Ч 611 псрыться, я довольно часто оказываюсь в ситуации, когда на экране
416 Из WinU в иг видно одно максимизированное окно какой-то программы — но оно не при этом фокуса. Это происходит буквально по несколько раз в день и дов ь меня до белого каления. Нередко, видя перед собой нужное окно и не у 'l! жившись проверить, имеет ли оно фокус, я начинаю печатать и далеко не ci - замечаю, что клавиатурный ввод передается какой-то совсем другой прогрц^ окна которой вообще не видно за другими окнами. (Однажды я нажал так к вишу Delete, в ответ на что Windows спросила меня, действительно ли я xil уничтожить некий ярлык на рабочем столе. Очевидно, рабочий стол и бы этой ситуации тем «окном», которое имело фокус и получало клавиатуру, ввод.) Чтобы перевести фокус на нужное мне максимизированное окно, Мн приходится щелкать по нему мышью. Я считаю, что здесь мы имеем дело с яр ной недоработкой в пользовательском интерфейсе Windows 95, которую Micro soft должна исправить как можно скорее. Арр Paths срабатывает для 16-разрядных программ лишь наполовину Эта странность воистину является необъяснимой для меня. Как известно вы можете завести в реестре специальную запись, которая позволит вашем\ приложению задать свой собственный приватный каталог для DLL-файлов VBX-файлов и прочих исполняемых файлов. Когда ваша программа будет за пускаться, Windows 95 добавит этот каталог в начало переменной окружения PATH, передаваемой вашей программе. Поскольку все каталоги, перечислен пые в PATH, просматриваются в процессе поиска загружаемых динамических библиотек, ваша программа может засунуть свои DLL-файлы в любой уголок файловой системы и по-прежнему успешно находить их, когда нужно. (Я все такп считаю, что более разумным и взаимовыгодным — и для программы, и для ее пользователей — было бы размещение специализированных Д" намических библиотек прямо в том же каталоге, где находится главный ncnoi няемый файл программы. Однако, я не хотел бы вдаваться в этот вопрос именно здесь.) Неприятность заключается в том, что эта возможность прекрасно работа?1 для 32-разрядных приложений, а для 16-разрядных приложений она срабаты вает только тогда, когда они явно загружают DLL при помощи функции 1°[]{ Libra]у() Если же библиотеки загружаются неявно, то Windows не находи1" (подразумевается, что их местоположение указано только в Арр Path), я п° зо вате ль получает соответствующее, абсолютно ничего не объясняющее соо^'1 пне. Я был порядком удивлен, когда обнаружил эту проблему. Казалось бы, может быть проще — прицепить содержимое Арр Paths к переменной Р^ передаваемой программе, и вперед - стандартная схема поиска должна Ф тать. Но удивлялся я не долго -- ровно до того момента, как вновь пер^411'
и^^ 417 v Мэтта Питрека «Windows Internals»; точнее — ту ее часть, в которой АХ о описывает обращение с динамическими библиотеками в 16-разрядной \ a0ws. После того прочтения мне кажется, что никакие новости, связанные oLL, больше не смогут меня удивить. # даже написал тривиальную тестовую программу, которая просто выво- значение переданной ей переменной окружения PATH — разумеется, до- ,ка из Арр Paths всегда была на месте. Вывод: при запуске 16-разрядной "оГраммы, либо неявная загрузка происходит до модификации PATH, либо ,время этой загрузки используется нетронутое значение PATH. Но что бы там -происходило на самом деле, ясно одно: обсуждаемая функциональная воз- оясность неисправна, и лучшим способом обойти эту проблему является явная лгрузка DLL. функция GetFreeSystemResourcesO пропала в Win32 Одна из самых дурацких проблем, с которыми вы можете столкнуться при переносе программы с Win 16 на Win32, — это необъяснимое исчезновение функции GetFreeSystemResourcesO из Windows API. Лично мне эта функция очень симпатична, потому что она предоставляет программе относительно простой способ выдать пользователю весьма ценную информацию о состоянии системы. И несмотря на то, что эта информация остается не менее интересной ii в Windows 95, такой функции больше нет. (Хочу пояснить, что я считаю это изменение API именно дурацким потому, что сведения об использовании трех основных типов ресурсов Windows по-прежнему имеют немалую ценность в Windows 95. В конце концов, ведь даже в поставку этой операционной системы Microsoft включила специальную утилиту Resource Meter, которая больше чичего и не делает, кроме как отслеживает эти три показателя. Я почти не сомневаюсь, что яростные сторонники Windows NT тут же возразят мне, что на "Их платформе» поддержка такой функции действительно бессмысленна. Ну и То^ В чем проблема? Почему бы именно тут не применить очень подходящий, 1[Рфективный и безопасный для данного случая прием написания функции-за- :'Шки, которая продолжала бы работать как прежде под Windows 95 и in32s, и возвращала бы сигнал об ошибке под Windows NT?) Документация по Win32 SDK гласит, что данная функция была «заме- На>> новой функцией GlobalMemoryStatusO, которая теперь возвращает /КтУРУ, полную самой разнообразной информации об использовании па- u В частности, в этой структуре имеется элемент dwMemoryLoad, который ,еДелен следующим образом:
418 Из Win16 в иг Число в диапазоне от 0 до 100, дающее общее представление кущем использовании памяти, где 0 обозначает, что палшпгь в ! ще не используется, а 100 — что память используется полного ° Мне непонятно, имеется ли в виду виртуальная намять, реальная нам\ комбинированное среднее ресурсов пли что-нибудь еще. (Кстати, с этой chv цией связан один из забав нейти их ляпов в документации по Win32 SDK: в 0 санпи структуры MEMORYSTATUS подсказка «Quicklnfo» содержит щ>, различных нолей - «Windows NT», «Win95» и т. д. — но не прив0л! значения ни для одного из них.) Разумеется, 16-разрядная версия функции GetFreeSystemResources() П( прежнему является частью Windows 95, и 16-разрядные программы все так^. могут ею пользоваться Но 32-разрядным программам она напрямую недос ту una. Если же иы достаточно иастырны и действительно хотите вызвать эг, функцию из 32-разрядной программы, то вы можете выбрать один ц; следующих способов (ни одни из которых не выиграет никаких призов за элегантность): 1. Воспользоваться недокументированными функциями QT_Thimk() LoadLibrarytGO и пр., которые я упоминал во врезке «Переходник "шестнадцать па тридцать два"» на стр. 400. Конечно, этот способ за ставит вас изрядно повозиться -- и это только лишь для того, чтобы выяснить кое-какие статистические данные о системе. Кроме того, на этом пути вам придется решать для себя всегда мучительную дилемму - использовать или ие использовать недокументированный API. Вдобавок, этот метод непереносим на Windows NT 3.51, а о его работоспособности в Windows NT 4.0 можно лишь гадать. 2. Воспользоваться плоскими переходниками. В этом случае вас также ожидает масса возни и проблема с переносимостью на Windows №'' Win32s, которые не поддерживают плоские переходники. (На саМСГ деле, именно этот способ был предложен кем-то из сотрудников грУ11 пы поддержки разработчиков в Microsoft, когда оп отвечал в Con puServe на чей-то вопрос о вызове некоторого API из 32-разряД*10 программы.) 3 Создать вспомогательную 16-разрядную программу и восполъзов^1 ся ею в качестве посредника. Вы можете паппсать очень малельку1( простую самостоятельную 16-разрядную программку, которая Д0^ ет требуемые вам числа старым добрым способом, а затем iiotfp0 забор1 5) д передает пх вашей основной программе «через дыру в (например, через сообщения пли свое возвращаемое значение «решение» (кавычки призваны спасти репутацию этого иевшШоГО , ва) будет работать па всех платформах Win32, но потребует с°3^' отдельной 16-разрядпоп программы лишь для имитации весьма PL'
^£TW 419 Мне почти стыдно признаваться в этом, но я действительно написал пример программы-посредника, упомянутой выше Я надеюсь, никому из вас не придет в голову использовать этот трюк в ваших публичных программах. Моей единственной целью при написании этого примера была лишь попытка убедиться, что такой прием может успешно работать. К сожалению, это действительно так. Все кровавые подробности, а также исходный код вы можете найти в WWW-библиотеке, в каталоге PROXY. Ay, кто стянул мои значок? При переносе вашей 16-разрядной программьйжа Win32, и даже просто при запуске старой 16-разрядной программы под Windows 95 вы можете столкнуться со множеством странностей, связанных со значками. Подробнее эти вопросы будут освещены в одноименном разделе главы 14. Аиалоги масштабируются по-разному Во врезке «Использование длинных имен файлов в 16-разрядных ирограм- w да здравствует алхимия!» на стр. 395 я уже упоминал, что Win32 по-осо- °ому обращается с диалогами, принадлежащими 16-разрядным программам с кидаемой версией Windows, равной 4.0. На самом деле, этот новый внешний ВЦД диалогов используется для всех программ для Windows 4.0 — как для 16- Р^рядных, так и для 32-разрядных. А это значит, что вы столкнетесь с данной Проблемой и при переносе 16-разрядной программы на Win32, и даже при внешний незначительного изменения в старую Win32-nporpaMMy. и последующей с перссборке последним компоновщиком от Visual C++ (который но 10лчащцо изменит ожидаемую версию Windows с 3.1 на 4.0). ^ результате может измениться (отнюдь не в лучшую сторону) '^^тированне текста в диалоге, а также могут произойти другие нежелатель- °° визуальные эффекты. Больше подробностей на эту грустную тему вы смо- 1е найти в одноименном ужастике из следующей главы.
420 из Winn в иг Ааступ h INI-файлам Помните наших старых добрых друзей — INI-файлы? Те самые (W,, которые мы столько лет успешно использовали, пока в городе не воцарился вый шериф — реестр Windows. И что бы там ни говорили некоторые о пыщн блеске реестра и дряхлой нищете INI-файлов, последние все еще могут сое жить хорошую службу. Я вовсе'не являюсь большим фанатом INI-файлов Т нако очень хорошо знаю, на что они способны. Именно они могут бь хорошим вариантом для хранения принадлежащих вашей программе данны И на самом деле, зачастую они даже предпочтительнее реестра, в зависимости от того, насколько переносимым вы хотите сделать ваш код. Любителей INI-файлов ожидает одна маленькая, но приятная новость одна из самых ужасных и неудобных проблем с INI-файлами в Winlg ограничение в 64К на размер INI-файла — наконец-то устранена в Window^ NT. Но есть и плохая новость: она заключается в том, что я вовсе не оговорился, сказав только что «в Windows NT», а не «в Win32». Да, вы правильно догадались — это ограничение все еще в силе, если ваша программа работает под Windows 95 или Win32s. Но это еще не самое худшее: вместо того чтобы сообщать об ошибке при попытке обратиться к слишком большому INI- файлу, Windows API на этих платформах просто вернет вашей программе неправильные данные и даже не удосужится намекнуть ей, что не все в порядке. Вы можете воспроизвести это явление при помощи программы-примера Ж- 1TEST. Эта программа использует функцию GetPrivateProfileStringO для извлечения некоторой строки из файла IN1TEST.INI, показывает полученную строку и сравнивает возвращаемое значение с длиной этой строки, чтобы проверить успешность вызова. Если вы запустите эту программу под Windows 95 и Win32s, то вы увидите, что GetPrivateProfileStringO вернет только часть строки и при этом заявит, что она сработала успешно. В зависимости oi конкретного размера INI-файла, данная функция может срабатывать и более достойным образом — ничего не возвращать и рапортовать об ошибке, но это отнюдь не спасает ее репутацию. Как ни крути, а при работе в Windows 95п-1,! Win32s ваша программа никак не может быть на 100% уверена в успехе вызо^ GetPrivateProfileStringO, даже если функция пытается вас в этом убедить Построение примера: Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chap1 ' initest Платформа: Win32
n Navigator 1.0 и вы 421 Инструкции по сборке: откройте при помощи Visual C++ 2.2 прилагающийся МАК-файл и скомпилируйте проект как обычно. Перед запуском программы не забудьте поместить в главный каталог Windows (например, C:\WINDOWS) копию тестового файла INITEST.INI. Norton Navigator 1.0 и вы осеМ вам известны многочисленные шутки и циничные комментарии по поводу иго, что не следует запускать версию 1.0 (или даже х.0) любого программного продукта. Когда вышла в свет версия 1.0 Norton Navigator от Symantec, это еще ,а3 напомнило мне о том, что в подобных высказываниях есть немалая доля истины. Накануне выхода в свет Windows 95, я установил финальную версию Norton Navigator 1.0 на одну из моих тестовых систем и очень быстро обнаружил сразу два варианта «вредительства» этого продукта по отношению к другим приложениям. Я сообщил об этих проблемах сотрудникам Symantec, затем еще немного поизучал их, написал несколько демонстрационных программ и, отправив все это разработчикам Symantec, поинтересовался, когда же можно ожидать исправления всех этих ошибок: В Если ваша программа (16-разрядная или 32-разрядная) модифицирует свое системное меню, компонент Norton Taskbar из Norton Navigator 1.0 может заблокировать один или несколько пунктов системного меню вашей программы (включая команду «Close»). В Если ваша 16-разрядная программа вызывает функцию GetOpen- FileNameO и при этом использует любой из флагов OFNJLONGNAMES и OFN_ALLOWMULTISELECT, Norton Navigator может привести к тому, что при выборе пользователем файлов ваша программа получит совершенно непредсказуемый текст. В Если ваша 16-разрядная программа вызывает функцию GetOpen- FileNameO и при этом использует флаг OW_ALLOWMULTlSELECT, Norton Navigator может не позволить пользователю ввести несколько имен файлов. . Первая проблема относительно проста. Я не экспериментировал с ней дос- 'Т01Шо долго, чтобы полностью определить те условия, при которых Norton , *^аг беззастенчиво блокирует пункты системного меню другой программы. NlHe удалось найти вариант, при котором проблема уверенно воспроизводи- ь Достаточно было добавить один лишний пункт к системному меню. Для т °Нстрации этого эффекта я написал тестовую программу, которая прилага- к данной книге под именем CLOSE. Комментарии в файле MAIN-
i22 из Mnuj^ FRM.CPP этого проекта подскажут вам, как можно включить и отклкк проявление ошибки. Вы можете просто скомпилировать эту программу ц За, '' тить ее на машине, где уже работает Norton Taskbar от Norton Navigator 1 п Л вы тут же увидите заблокированные пункты системного меню. «gsagjSi Построение примера; сщ Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chapi2/ close Платформа: Win32 Инструкции по сборке: откройте при помощи Visual C++ 2) прилагающийся МЛК-файл и скомпилируйте ироеЕст как обычно. Решение этой проблемы также не является сложным, но оно требует некоторого изменения вашей программы - не очень-то приятная перспектива, особенно если ваша программа уже широко распространена. (Поскольку в первый раз я обнаружил эту проблему на своей собственной программе Stickiest, я надеюсь, вы простите мой не слишком веселый тон при обсуждении «проделок» Norton Navigator. Когда мои пользователи начнут жаловаться мне на эту проблему, мне будет очень трудно разговаривать с ними и при этом пытаться не выглядеть так, как будто бы я сваливаю свои ошибки на чужие плечи. И это при том, что па самом деле я никаких ошибок и не совершал. Но я, кажется отвлекся.) Я выяснил, что проблема исчезает, если исходный пункт системного меню «Close» заменить на свой собственный пункт меню с тем же названием. но с идентификатором, отличным от SC_CLOSE. По-видимому, когда Norton Navigator совершает какие-то своп, одному ему известные махинации с вашим системным меню, пункт с идентификатором SC_CLOSE служит своеобразно!! точкой отсчета. Заменив этот пункт меню на свой, вам, разумеется, нриДеТСЯ чуть-чуть подправить обработку сообщения WM_SYSCOMMAND — если°НР приходит с wParam, равным вашему новому идентификатору, вы должны сы* митировать приход команды SC__CLOSE (то есть инициировать соответ^ вующую обработку этой команды). Конечно, можно пойти и другим путем - как-нибудь изменить норяД01' пунктов в системном меню. Однако я считаю подобный вариант неприемлем1*1 почему мои пользователи должны увидеть пускай даже незначительное изМе | иие внешнего вида моей программы из-за какой-то чужой ошибки? №0'' того, абсолютно неизвестно, что случится, когда Symantec починит даниУ10 ^ исправность пли выпустит новую версию; очевидно, полный отказ от испо-^ вания SC_CLOSE и исходного пункта меню является лучшей страхов^11 возможного повторения проблемы в новом виде.)
Navigator 1.0 и вы 423 Проведенные мной тесты показывают, что трюк с заменой исходной ко- ль1 «Close» надежно срабатывает во всех случаях. Если вы будете 'ерцмситировать с программой-примером CLOSE и обнаружите, что это не пожалуйста, сообщите мне об этом. *' Вторая проблема уже не так проста, и я так и не смог найти более-менее .цемлемого решения для нее. Windows 95 позволяет 16-разрядным програм- м указать, что они могут обрабатывать длинные имена файлов, выбранных стандартном диалоге Open File. Для этого программа должна использовать маг OFN_LONGNAMES при вызове функции QeLOpenFileNameO. Это очень юбезно со стороны Windows 95 — разрешать подготовленным для этого 16- ъифядным программам пользоваться длинными именами, пускай даже стиль амого стандартного диалога остается старым. Разумеется, основной трудностью при этом является обработка этих дое- ивучих пробелов в длинных именах. И в данном случае следует отдать должное Microsoft: реализация этой детали оказалась на редкость удачной и правильной. Если пользователь выбирает несколько файлов, Windows строго педует документированному формату при возврате имен этих файлов: одна прока с именем каталога в начале и с последующими именами файлов через пробел. И если имя каталога или какого-нибудь файла содержит пробелы, Windows просто подставляет вместо этого имени соответствующий псевдоним в формате 8.3. Если же какое-то имя просто является длинным (длиннее восьми символов), но не содержит пробелов, то Windows по-прежнему возвращает само длинное имя. Ничего не скажешь, в данном случае Windows просто сама на себя непохожа - так ловко согласованы довольно специфичный запрос п старый формат возвращаемой строки. Дайте Microsoft большую золотую звезду за такое внимательное отношение к деталям в данной ситуации! Истинным виновником проблемы является Norton Navigator с его функци- ei1 «Enable Long File Names», которая позволяет пользователю 16-разрядной программы работать с длинными именами файлов в стандартных диалогах. Без Мнения, фирма Symantec знала о семантике флага OFN_LONGNAMES, по- Г°МУ что в осутствие этого флага Navigator, как и положено, возвращает ''Р°грамме короткие имена файлов. Но если программа использует этот флаг, 1е)кдут серьезные неприятности: хотя возвращаемые имена файлов по-прежне- п Не будут содержать пробелов, для каталога будет всегда возвращаться длиное имя — даже если в нем есть пробелы. Например, если при включенной 'Ункции «Enable Long File Names» пользователь выберет в каталоге «c:\temp * х^ b.txt» файлы с именами «c.txt» и «d.txt», программа получит от стан- Ч1Ртного диалога Open File следующую строку: с \temp a txt b txt с txt d txt £сли же функция «Enable Long File Names» была бы выключена, програм- с получила бы что-нибудь вроде
424 из го&Чб^ц^ с \temp~1 с txt d txt где «c:\temp~l» — псевдоним для длинного имени каталога «c:\teirm - b.txt». Согласно описанию API, у программы нет никакого способа проан- зировать эту строку, кроме как использовать пробел в качестве разделите Поэтому она попросту не сможет, корректно расшифровать результат — По вершенно понятной и законной причине она будет искать в каталоге c:\teJ четыре файла с именами a.txt, b.txt, с.txt и d.txt, тогда как и сам каталог такие файлы в нем могут не существовать. И даже если программа окажет о достаточно подготовленной к такому сюрпризу, как возврат из стандартной диалога имен несуществующих файлов, она все равно не сможет корректнг обработать эту ситуацию и в глазах пользователя будет выглядеть бракованной (В конце концов, пользователь выбирает реально существующие файлы а программа вдруг начинает жаловаться, что какие-то совсем другие файлы отсутствуют! Не знаю, как вы, а я определенно воспринял бы такое поведение программы как признак ее серьезной недоделанности.) Данная проблема является неразрешимой только в том случае, когда ваша программа использует флаг OFN_ALLOWMULTISELECT. Если же этот флаг не используется, то ваша программа будет ожидать в возвращаемой строке только одно имя файла. Поскольку теперь никакого синтаксического разбора строки не понадобится, наличие или отсутствие пробелов в ней не имеет ника кого значения (если ваша программа использует флаг OFN_LONGNAMES, то она так и так должна уметь обрабатывать любые длинные имена файлов). Третья проблема, также связанная с функцией «Enable Long File Names», касается 16-разрядных программ, использующих флаг QN^IDWVlUnHlHI У нее тоже нет хорошего решения. Суть проблемы проста: если пользователь вручную введет через пробел несколько имен файлов, стандартный диалог (который под влиянием Norton Navigator становится не таким уж стандартным) интерпретирует этот ввод как одно длинное имя файла с пробелами внутри сконвертирует это в псевдоним и в результате вернет программе одно единственное имя файла, который к тому же почти наверняка не существует в сШ" теме. И даже если программа пожалуется пользователю об этом (например указав флаг OFN_FILENUSTEXIST), то только лишь окончательно собьет его с толку — ведь имя файла, которая она покажет, будет совсем не тем, что Поль зователь только что собственноручно ввел. Эта ошибка затрагивает все разрядные программы, которые разрешают множественный выбор файлов > стандартных диалогах, вне зависимости от желания или нежелания прогр^м>' работать с длинными именами. Вы можете поэкспериментировать с этой проблемой, используя пре^ гаемую для этих целей прорамму-пример LFNCD. При запуске разля4** вариантов этой программы не забудьте запустить Norton Navigator ' включить в нем функцию «Enable Long File Names» (если она выключен3, j под Windows 95 все варианты этой программы работают у меня как поло#е
n Navigator 1.0 и вы 425 Проблема со склеиванием двух введенных имен в одно интересна еще одной деталью. В одном из тестов я ввел имена двух существующих файлов как «d.txt c.txt» (без кавычек, разумеется), а в ответ получил жалобу об отсутствии в системе файла «dtxte~l.txt». Откуда взялось такое имя? То ли Norton Navigator попросил Windows дать ему псевдоним для «d.txt с txt», и Windows наколдовала ему «dtxte~l.txt»; то ли Norton Navigator сам занимался колдовством (что пугает меня еще сильнее). В любом случае, кто-то в системе явно что-то химичит, потому что невозможно странслировать в псевдоним имя несуществующего файла. Одно дело — честные ошибки, но когда программы начинают гадать на кофейной гуще, я начинаю ощущать реальное беспокойство за безопасность своей системы. е® Построение примера: LFNCD Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chap12/ Ifncd Платформа: Win 16 Инструкции по сборке: ознакомьтесь с комментариями в файле MAINFRM.CPP, которые поясняют различные способы построения программы; затем откройте при помощи Visual C++ 1.52 прилагающийся МАК-файл и скомпилируйте проект как обычно. В данном каталоге также находятся в готовом виде все четыре варианта программы: LFN_M.EXE: требует длинные имена, разрешает множественный выбор; LFN_S.EXE: требует длинные имена, не разрешает множественный выбор; SFN_M.EXE: не требует длинных имен, разрешает множественный выбор; SFN_S.EXE: не требует длинных имен, не разрешает множественный выбор. Я нахожу всю эту ситуацию очень и очень неприятной, потому что выше- '''111с^нные ошибки Symantec создают такие условия, в которых совершенно ;Г11е, абсолютно корректные программы могут выглядеть бракованными. Ко- 1 пРограммисты огорчают своих пользователей своими собственными ошиб- 'и ~- это уже достаточно плохо, но когда эти ошибки начинают портить ,'"'ТаЦию других, ни в чем не повинных разработчиков — это уже проблема власти профессиональной этики.
426 Из Win16 в и,- ___И^? Какой же урок следует нам извлечь из всего этого? Да, у меня есть который соблазн пуститься в долгие злобные рассуждения по поводу того ^ тщательно, должно быть, тестировался Norton Navigator 1.0, если он выще '' свет с такими ошибками. Но я не буду делать этого, потому что ругань в ал '' Symantec или какой-либо другой компании вовсе ие является моей целью (л С протокола: я видел программные продукты, которые делали вещи и поху^ "* поэтому будьте уверены - если бы моей единственной целью было швырЯн камней, я мог бы представить вам намного более крупную и соблазнительцу. мишень, чем Norton Navigator.) На самом деле, я посвятил так много мест этим ошибкам по двум основным причинам. Во-первых, данная ситуация (ц, особенности факт влияния ошибок одной программы на другие) служит всем нам превосходной притчей о том, как осторожно нужно подходить к вопросу 0 противоестественных действиях в своих программах (таких действиях, как махинации с меню и диалогами неизвестных программ). Во-вторых, описанные мной ошибки все еще остаются неисправленными (на момент написания этих строк — конец октября 1995 года), и значит каждая публичная программа для Windows 95 с определенными характеристиками по-прежнему находится под домок ловым мечом производства Symantec. Я думаю, что вам следует об этом знать. В начале октября Symantec выпустила и опубликовала в CompuServe заплату для Norton Navigator, который превращал версию 1.0 в версию 95.0 а Когда я приложил эту заплату, выяснилось, что первая ошибка (блокировка пунктов системного меню) уже исправлена, а другие две - еще нет. Когда я обратился в Symantec с вопросом об этих все еще неисправленных ошибках, я получил следующий ответ: Мы обнаружили, что функция «Enable Long File Names» несовмес- тилш с некоторыми продуктами. Прилагаю к данному письм) программу, которая позволит Вам настраивать File Assist и Ш на индивидуальные программы. Эта программа также общедоступна через оперативные службы. Что касается Вашего вопроси о том, когда эта проблема будет решена, я не знаю. А пока вы можете использовать эту программу для отключения «Enable ШЪ File Names» для тех или иных программ. К письму прилагалась программа FACFG.EXE - некая утилита, п°' згзоляющая приказать Norton Navigator не применять «Enable Long File Nafltfs* и другие услуги к конкретным программам. Я поэкспериментировал с этой У11 литой и убедился, что она действительно работает так, как было обещано. ^ вы, вероятно^ догадываетесь, я не считаю эту утилиту нормальным решен111,1 проблемы. Вместо того/ чтобы исправить ошибки (а приведенные >IIi0 примеры должны быть достаточно легко исправимы), Symantec предло^11' лишь некоторый способ отключения неисправной функциональности в св° '
оП Navigator 1.0 и вы 427 лукте (причем далеко не самый удобный и весьма хлопотливый для поль- ателя). Я искренне надеюсь, что к моменту выхода этой книги в свет Syman- \БЬтуст1ГТ версию 95.1 (или 95.0.Ь, пли как там они ее назовут) своего Nor- Njyvigator. Но далее в этом случае, благодаря весьма успешным стартовым пажам версии 1.0, еще многие п многие пользователи будут запускать на иХ машинах именно эту версию Norton Navigator. А значит нам, нашим *оГраммам и нашим пoлbзoвaтeля^r придется как-то уживаться с описаннымрг ' 0()темами еще в течение какого-то времени.
щ® слишком краткая лекция § длинных именах файлов What's in a name? that which we call a rose By any other name would smell as sweet. Уильям Шекспир When you notice a cat in profound meditation, The reason I tell you is always the same; His mind is engaged in a rapt contemplation Of the thought, of the thought, of the thought of his name: His ineffable effable Effanineffable Deep and inscrutable singular Name. T С Элиот Я Убежден, что длинные имена файлов, даже не будучи «глубокими и непостижимыми» или «исключительными», обязательно станут одним из самых попу- 1ЯРных свойств Windows 95. Сбросив наконец кандалы под названием 8.3, 10'1ьзователи возрадуются и станут присваивать своим файлам имена, подобные «Рецепт очень-очень острого харчо от тети Жанны.doc». Ничего удиви- ltIbHoro в этом нет. В конце концов, программы придуманы для того, чтобы ^Ужнть нам, а не для чего-то другого. Значит они обязаны приспособиться к Jlee осмысленным, разумным и дружественным человеку именам файлов. К сожалению, та реализация длинных имен, которую предложила нам Mi- '0s°ft, полна дефектов и сюрпризов. И я даже нашел некоторые ошибки, доенные при использовсшии длинных имен самими разработчиками Windows "о к этому вопросу я вернусь чуть позже. На что в первую очередь обращают внимание пользователи, глядя на длин- Имена, подобные вышеприведенному примеру с рецептом? Конечно, на их **У и на тот факт, что в именах теперь сохраняется регистр букв. Ура!
430 Не слишком краткая лекция о ллинных именах </, >. Больше не нужно будет придумывать такие уродливые и трудночптц, имена, как MYRECS.XLS, к которым нас приучали столько лет! lL А что прежде всего бросается в глаза бывалым программистам (це шим опыта работы с NT)? Разумеется, эти досадные пробелы в именах фа^ ' Они морщатся (программисты, не пробелы) и, насупив брови, начицЛ греволшым голосом задавать вопросы: «Гм, иу и как нам теперь делать разс этих имен? Как они будут представлены в командной строке? А как нас/ drag-and-drop?» И именно подобные вопросы послужили поводом для нап пня этом главы Должен признаться, я достаточно равнодушно относился к длинным им нам файлов в Windows 95. Я очень мало работал с NT и при этом почти ц0ь постыо игнорировал длинные имена. На всех моих компьютерах я используй широкоизвестную файловую систему FAT без какой-либо компрессии диска iaic как мне необходимо делить дисковое пространство между DOS различными разновидностями Windows и OS/2. В результате длинные имена были для меня скорее помехой, чем благом. По крайней мере, до того момента как я начал запускать Windows 95. ЙВбзор основных свойств длинных имен и псевдонимов... Но достаточно болтать о рецептах тети Жанны и античной истории. Пора перейти к делу: 1. Длинные имена файлов -- это те, которые мы видим в Проводнике и вообще почти везде в Windows 95, по крайней мере в 32-разрядных программах. (Последнее замечание, строго говоря, не очень корректно — см. врезку «Использование длинных имен файлов в 16- разрядных программах: да здравствует алхимия!» па стр. 395.) Наши старые друзья, файловые имена вида 8.3, само собой, никуда н~е пРопа ли, но теперь Microsoft называет их «псевдонимами» — не такое у* слабое терминологическое понижение в звании. Документация п° Win32 SDK разжаловала эти короткие имена еще сильнее — в onnCt нпи структуры WIN32_FTND_DATA Microsoft упоминает их всей] лишь как «альтернативные имена файлов, выраженные в формате ° (filename.ext)». Согласитесь, после всего, что сделали эти коро?^ имена файлов для Microsoft и ее клиентов, можно было бы относив к ним с немного большим уважением. 2. Локальное имя файла (полное длинное имя за вычетом пути) *м0/Ч иметь длину до 256 символов, включая завершающий нуль-сиМр Путь может иметь длину до 246 символов, включая букву диска, ДБ( точие и первую обратную косую черту. Таким образом, 246 и 14 ^и
основных свойств ллинных имен и псевлонимов... еимальная длина короткого локального имени вида 8.3, плюс завершающий нуль п возможную обратную косую черту) в сумме дают максимально возможную длину пути - 260. Именно это число скрывается в STDL1B.11 под именем МЛХ_РАТН. (Обратите внимание, что 246 не согласуется с константой MAX_DIR из STDLIB.H, которая равна 256. Интересно, не это ли является первопричиной тех ошибок, которые я обнаружил в Windows 95, и о которых я подробнее расскажу позже? Стоит также упомянуть, что, когда я спросил об этом Microsoft, мне ответили примерно так: конкретное ограничение на длину пути без имени файла ие устанавливается, так как оно «не имеет смысла» и фактически равно МЛХ__РЛТЫ минус длина имени файла. Предполагая, что минимальная длина имени файла равна двум -- один символ плюс завершающий нуль, -- мы получим, что максимальная длина пути без имени файла равна 258 символов. Поразительно, как много конфликтующих деталей можно найти в таком элементарном вопросе!) j Windows 95 автоматически создает псевдоним файла, когда это необходимо (а 15 некоторых случаях когда никакой нужды в этом пет). Если длинное имя создаваемого файла синтаксически допустимо для псевдонима, оно и будет использовано в качестве последнего. В противном случае будет сгенерировано у*|икальдое и*мя,'В котором обязательно есть хотя бы одна тильда^ (например, B/^RN^Y-^.DOC). Скоро я покажу вам, насколько важнотТ п интересной' деталью является эта практика использования 'тильд и цифр для генерации «хвостов» у псевдонимов. 4 В длинных именах допустимы те же символы, что и в псевдонимах, плюс: + , ; = М <пробел> 5 Длинные имена могут содержать более одной точки, и могут даже начинаться с точки. При этом расширением считается только то, что находится справа от самой правой 'точки; все остальное считается именем файла. ^ Расширения теперь могут быть длиннее трех символов. Документация по Win32 SDK (а точнее статья «Filename Aliases») утверждает, что в файловых ассоциациях учитываются только первые три символа расширения, по на самом деле это не так. В своих тестах я создавал файлы с расширениями .text и .lext2 и при этом имел возможность ассоциировать с ними разные программы (которые потом успешно запускались двойным щелчком мыши по файлам в Проводнике). Длинные имена и псевдонимы используют одно и то же пространство имей. Это означает, что в одном каталоге не может существовать файл,
432 Не слишком краткая лекция о длинных именах длинное имя которого совпадает с псевдонимом другого файла, g казалось бы, безобидная деталь имеет весьма интересные следствие ' (Все вышеперечисленные факты позаимствованы из документации Щ» soft по Win32 SDK, в частности — из статьи «Long Filenames and the Protect/ Mode FAT File System.».) ...л что это означает Я не хочу углубляться в низкоуровневые детали того, как Windows 95 м нипулирует длинными именами и псевдонимами в файловой системе FAT (j оставлю эти вопросы более авторитетным знатокам данного предмет например, я могу порекомендовать вам статью Роберта Хаммела под название». «Short Shrift on Windows 95 Long File Names», которая была опубликована в номере PC Techniques за июнь/июль 1995 года.). Но есть несколько весьма интересных тонкостей, связанных с длинными именами, о которых вам и вашим пользователям следует знать: 1. Windows 95 сохраняет строчные буквы в длинных именах, но всегда сравнивает эти имена без учета регистра символов. Это очень хорошо поскольку дает всем возможность ссылаться на файлы без какого-либо беспокойства о регистре каждой отдельной буквы. Кроме того, это предохраняет программы (и их пользователей) от замусоривания ката логов файлами, имена которых отличаются друг от друга всего лишь регистром одной или нескольких букв. 2. Есть один побочный эффект совместного использования пространства имен: при переименовании, перемещении и копировании файла Win dows 95 может изменить псевдоним другого файла при необходимости избежать конфликта имен. Например, если вы возьмете Проводники попробуете перименовать файл «My January report.doc» во что-нибудь более приземленное типа «REPORT.DOC», то вполне вероятно, что в том же самом каталоге уже есть другой файл с псевдонимом «№ PORT.DOC», но с длинным именем «My February report.doc». Ясно что в такой ситуации у Microsoft было всего два реальных вариант решения: либо запретить операцию переименования и сообщить по1Ь зователю о конфликте нового имени с псевдонимом другого фай1, либо сделать то, что и делается сейчас — втихаря изменить псевДОн1 другого файла, никак не афишируя это. Мы имеем дело с к>1 сическим примером безвыигрышной ситуации: каждое из двух возМ0'^ ных решений несомненно привело бы к недовольству или смуШе11 значительной части пользователей. (К вопросу о смущении: тот Фа что при подобной операции один из файлов вдруг покажется исчез# шим — до следующего обновления окна Проводника, явно не пр*1' ляет очков этой программе. Конечно же, файл на самом деле н^-'
основных свойств ллинных имен и псевлонимов... 433 не девается. Вот вам pi еще одна из тех причин, по которым серьезные пользователи могут выбросить Проводник в помойку при первой же возможности заменить его на что-то более толковое.) Да самом деле, выбранное Microsoft решение совсем не до конца снимает проблему, из-за которой был отвергнут альтернативный вариант. Пред- -чвьте себе, что тот файл, чей псевдоним должен быть втихомолку изменен, гкрыт в этот момент какой-нибудь программой. Тогда операция переименовали провалится и пользователь получит следующее сообщение: «Cannot rename <filename>. A file with the name specified already exists. Specify a different «•leiiame.» («Невозможно переименовать <filename>. Файл с заданным именем /Ке существует. Укажите другое имя.»). Такое объяснение ситуации трудно назвать адекватным: про псевдонимы — ни слова, и пользователю остается ю'шко догадываться, какого лешего ему рассказывают про какой-то суще- С1вующий файл с тем же именем, если такого файла нигде не видно в Проводнике Обратите внимание на то, что при изменении пользователем длинного имени файла Windows 95 также изменит его псевдоним, даже если никакого конфликта имен не происходит. Например, если у вас есть файл с длинным именем «January summary.xls» и псевдонимом «JANUAR~1.XLS», то при переименовании этого файла в «January.xls» или «February summary.xls» Windows 95 автоматически поменяет псевдоним на «JANUARY.XLS» или «FEB- RUA~1.XLS» соответственно. Аналогично, когда вы перемещаете каталог таким образом, что наступает конфликт псевдонимов, Windows 95 опять прибегает к скрытому изменению псевдонима, но в этом случае будет изменен тот каталог, который перемещается (а не тот, который остается на месте). Этот подход правилен, поскольку из Двух затрагиваемых каталогов — перемещаемого и остающегося на месте — только ссылка на имя последнего (которая, возможно, где-то хранится) должна статься неизменной, в то время как подобная ссылка на первый каталог все Р^вно будет нарушена из-за перемещения. Хранение длинных имен Iакои агрессивный подход к изменению псевдопимов, весьма вероятно, мо- vtI Сдавать определенные проблемы для тех программ, которые запоминают евДонимы файлов от одного своего запуска до другого. Наверное, именно тому в документации по SDK, в статье Storing Filenames Microsoft рекомен- ет Программам хранить именно длинные имена файлов, а не псевдонимы, по- v Льку последние так подвержены неожиданным изменениям (как мы уже ви- .j ц ~~ не только при умышленном переименовании файла пользователем). ^ т совет мало помогает тем тысячам 16-разрядных программ, которые были Дс1Ны и широко распространены еще до того, как длинные имена файлов за-
434 Не слишком краткая лекция о ллинных именах фа* нялп такое важное положение в мире Windows. Кроме того, если вспоми всю ту неразбериху с максимально допустимой длиной полного имени фац{. /' писал об этом в начале главы), данная рекомендация Microsoft несомнен спровоцирует массу 32-разрядных программ па ошибки, которые порой бу. приводить к самым катастрофическим последствиям. Еще более интересен гот факт, что та же самая статья советует программ стам использовать результаты функции GetShortPathNameO для выяснение «канонического представления пути» и хранить именно это представление если требуется запомнить абсолютный путь. Очевидно, что эта рекомендацц, находится в серьезном противоречии с предыдущей: во-первых, функция Gct- ShortPathNameO возвращает псевдонимы для всех компонент имени файла а во-вторых, псевдонимы каталогов подвержены неожиданным изменениям вне меньшей степени, чем псевдонимы файлов. Возможно, это консервативный взгляд, но я все-таки предпочел бы оставлять старые псевдонимы по-возможпостп нетронутыми -- просто для того, чтобы обеспечить на 1% большую безопасность. (На самом деле, это етце один пример анализа соотношения затраты/выигрыш в действии: выигрыш от сохранения псевдонимов, скорее всего, невелик, но затраты на это еще меньше Более того, в зависимости от структуры кода, затраты могли бы бьпь практически нулевыми, если бы Microsoft просто отказалась от одного шага изменения псевдонима. И чтобы не упустить шанс вставить пару слов об абстрагировании, я хотел бы отметить, что разбиение кода на маленькие логические куски могло бы серьезно облегчить жизнь в подобной ситуации Если бы код был организован так, что изменение псевдонима осуществлялось бы отдельной самостоятельной- функцией, то отказ от смены псевдонима был бы так же дешев, как удаление из кода одной строки — вызова этой самой функции Я не знаю, как Microsoft реализовала этот код па самом деле, но обсуждаемая проблема несомненно является хорошим примером того, как грамотная модульность кода может окупаться в перспективе.) Честно говоря, мне не но себе от такой практики переименования дрУг11Х файлов без разрешения пользователя и даже без его ведома. Разумеется, использование тильд при генерации псевдонимов в какой-то степени уменьши вероятность закулисных переименований, поскольку немногие пользоватси' выдумывают для своих файлов странным образом разукрашенные имена поД°} ного вида. И тем не менее конфликты неизбежно будут происходить -- веро>п нее всего при операциях копирования и переноса файлов с длинными имел^" Например, если в двух разных каталогах создать файлы с разными, но ol{L\ похожими длинными именами (скажем, «Report from Dave.doc» n «Report и0' Susan.doc»), то скорее всего у них будет один if тот же псевдоним (<<l '- PORT~1.DOC») — ведь для генерации уникального псевдонима Window • использует несколько первых символов длинного имени, к которым добатз-^ тильду и некоторое число. Л теперь перенес fire или скопируйте эти два Фа1
р основных свойств ллинных имен и псевлонимов... 435 1Н каталог — и Windows 95 придется менять один из псевдонимов. Если просто переместите один из этих файлов в каталог другого и обратно, псев- пм этого файла уже окажется измененным, хотя его длинное имя останется В заключение, если всего вышесказанного вам покажется мало, вот вам аб- 110ТНо серьезная цитата из статьи Filename Aliases, входящей в документно по SDK: Когда приложение делает системный вызов для удаления или переименования псевдонима, система вначале получает и сохраняет пакет информации об этом файле, а затем выполняет операцию удаления или переименования. Сохраняемая информация включает в себя длинное имя файла, дату и время его создания, дату и время его последней модификации и дату последнего обращения к оригинальному файлу. После выполнения операции удаления или переименования система в течение некоторого короткого промежутка времени (по умолчанию 15 секунд) отслеживает, не поступит ли требование создать или переименовать файл с тем Dice именем. Если система обнаруживает операцию создания или переименования недавно удаленного псевдонима, она прилагает к новому файлу сохраненный пакет информации и таким образом сохраняет длинное имя файла. И когда в следующий раз кто-нибудь попробует утверждать, что обратная совместимость несложна и недорога, покажите этому человеку сей перл Windows 95. «Алинные имена файлов как параметры командной cmpoHu.EXE» Есть много способов, которыми пользователь может сказать Windows: Открой этот документ при помощи такой-то программы». И в зависимости от бранного пользователем метода, ваша программа получит удивительно раз- 1Ь1е Данные. В то же время, у 32-разрядной программы, написанной на языке C/C++, ь как минимум два способа определения аргументов командной строки. В 'в°м используются наши старые друзья argc и argv (эти параметры, переда- ' мЫе функции main любой программы, содержат количество аргументов в ко- 1 4ноп строке и указатели на сами аргументы), которые существуют, образно касаясь, еще со времен Мелового Периода программирования. Второй спо- j "" Функция GetCommandLineO из Win32 API. Эта функция возвращает ' атель на всю командную строку целиком — неразобранную, необрабо- У10, со всеми пробелами между аргументами и так далее. Своеобразный
436 Не слишком краткая лекция о ллинных именах ih - —S^Moe вариант набора «Сделай сам!». На первый взгляд, программа полу информации больше, чем достаточно для осмысленного анализа коман ^ строки — в конце концов, она имеет доступ как к оригиналу, так и ' разобранной версии. Но, как мы увидим далее, эта проблема намного иНте' ' нее, чем кажется. В действительности, это самый лучший пример гибельной ^ вушки, который я смог найти в' Windows 95. Поэтому данная проблема зас < живает тщательнейшего ее изучения под микроскопом. Давайте начнем с безумно простого консольного приложения для Win3) Вся программа, CMDLINE.CPP, имеет вот такой исходный код: #mclude <windows h> «include <stdio h> mt main(int argc, char *argv[], char *envp[]) { int l, for(i = 0, l < argc, i++) printf( 'Аргумент %d [%c]\n',i,argv[i]), printf( \nGCL() [%G]\n\n".GetCommandLine())f pnntf("(Ha>KMMTe ENTER) ), getchar(), return 0, } Соберите эту программу в виде исполняемого файла CMDLINE.EXE, за тем создайте файл с именем «LFN test file.zzz». Содержимое этого файла значения не имеет, и далее я буду ссылаться на него как на документ. (В WWW-библиотеке также имеется 16-разрядная версия данной программы - в каталоге CLINE13. Эта версия не использовалась для тестов, описанных в данной главе, однако, поскольку она была мною написана, я счел уместным включить и ее. Мало ли, вдруг кому-то она пригодится.) Построение примера: CMDLINE и CUNEM Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chap13/ cmdline http://www.symbol.ru/russian/library/prof_prog/source/chap13' cline16 Платформа: консольное Win32-пpилoжeниe (CMDLINE) ' ^итЛб-приложение (CLINE16). Инструкции по сборке: откройте при помощи Visual C++ 1". Visual C++ 1.52 прилагающиеся МАК-файлы CMDLINE MA и CLINE16.MAK, соответственно, и скомпилируйте эти ПР° ты как обычно.
п основных свойств алииных имен и псевлонимов... **э7 j^gfcL—■ — ЗаМеЧУ, что во всех последующих примерах приведенный мною вывод граммы CMDLINE.EXE был непосредственно скопирован с экрана, так что /удете видеть в точности то, что я получал.1 \ Для установления «точки отсчета», щелкните дважды по CMDLINE.EXE в Проводнике. Результат будет таким: Аргумент О [С \S0URCE\CHAP13\cmdline\WinRel\cmdlme ехе] GCLO ['С \S0URCE\CHAP13\cindline\WinRel\cmdline ехе ] Обратите внимание на тот странный завершающий пробел, который выдает GetCommandLineO, и которого вовсе нет в argv[0]. Вот еще один случай, когда вам могут пригодиться те подпрограммы для обрезания пробелов из строк, которые я описывал в главе 2, и рекомендовал использовать при работе с ненадежными данными. В этом случае источником данных неизвестного происхождения является сама Windows. Запуск CMDLINE.EXE при помощи двойного щелчка по ярлыку на рабочем столе приводит к точно такому же результату. Перенесите исполняемый файл в каталог, имя которого содержит пробел. Тогда и при запуске из Проводника, и при запуске с рабочего стола вы получите такой результат: Аргумент О [С \temp dir\cmdline ехе] GCL() [ С \temp dir\cmdline.exe' ] 2. Поместите файлы CMDLINE.EXE и «LFN test file.ZZZ» в каталог «C:\temp dir», затем создайте при помощи Проводника ассоциацию между файлами с расширением .ZZZ и программой CMDLINE.EXE. Вы должны заключить имя исполняемого файла в кавычки, иначе Windows 95 не примет его из-за наличия в нем пробелов. Теперь дважды щелкните мышью по документу в Проводнике. Как выясняется, программа подумает, что ей было передано пять аргументов, а вовсе не два: Аргумент 0 [с \teinp dir\cmdline ехе] Аргумент 1 [С \teinp] Аргумент 2 [dir\LFN] Аргумент 3 [test] Аргумент 4 [file ZZZ] GCL() [ с \teinp dir\cindline ехе С \temp dir\LFN test file ZZZ] Обратите внимание на то, что путь к документу оказывается разломанным на куски в тех местах, где в нем имелись пробелы, а путь к самой Рсфразировка известной англоязычном аббревиатуры WYSIWYG What You See Is What 11 Get - вы впдпге то, чго вы получите [Примечание переводчика ]
438 Не слишком краткая лекция о ллинных именах сопрограмме остается в целости и сохранности. В то же время, фуи, (JetCommandLineO .заключает путь к программе в кавычки л гг/ *' документу - нет. Из-за этого задача добывания имени документ'. командной строки становится предельно трудной: у программы • возможности определить, какие элементы являются составными ца> ми первого исходного аргумента, а какие могут принадлежать возм0Г пьгм следующим аргументам (например, какому-нибудь ключу имени второго файла) Также обратите внимание на то, что таинство' иый пробел в конце результата GetCommandLineO теперь исчез, ц 4 буква устройства в одном месте оказывается строчной, а в другом заглавной. 3. Схватите документ мышкой, иеретатците его на ярлык CMDLINE.EXF на рабочем столе и сбросьте. Мы получаем первый большой сюрприз Аргумент О [С \temp dir\cmdline exe] Аргумент 1 [С \rEMPDI~1\LFNTEG~1 ZZZ] GCL() [ С \temp dir\cmdline.exe С \TEMPDF1\LFNTES~1 ZZZ] Вместо длинного имени документа мы вдруг получаем его псевдоним - ура! Теперь извлечение этого имени не представляет особого груда - нам больше не нужно мучиться с пробелами (но крайней мере, при данном способе запуска программы). Заметьте, что имя самой программы по-прежнему передается в длинном формате 4. Измените ассоциацию между ZZZ и CMDLINE.EXE так, чтобы она выглядела следующим образом. с \temp dit\cline1 exe "%1 Да, именно так - с кавычками вокруг %\. Дважды щелкните по документу. Ого! Кажется, IQ пашей программы резко вырос: теперь она знает, как разобрать имя документа, содержащее пробелы: Аргумент 0 [с \teinp dir\cmdline exe] Аргумент 1 [С \temp dir\LFN test file ZZZ] GCL() ['с \temp dn\cindline exe" С \teinp dir\LFN tect file ZZZ ] Разумеется, секрет не в % 1, а в окружающих его кавычках Если131 уберете эти кавычки, оставив %1 на месте, результат будет такими как в эксперименте 1. Это не просто стерильный лабораторный эксперимент. По;1Р^1 образом вы можете сбить с голку реальные программы. Например- меипте ассоциацию между BMP-файлами и MSPA1NT.EXE, уДа параметр °о1 (пли удалив хотя бы кавычки вокруг пего). Затем 1це , ните дважды по «Carved stone.BMP» (одной пз картинок, входя^11'
основных свойств алииных имен и псевлонимов... поставку Windows 95), и MSPAINT.EXE пожалуется, что не может найти файл «Carved.BMP». (Что интересно: если после этого закрыть MSPAINT и повторить эксперимент, все будет в порядке. По-видимому, MSPAINT просто перерегистрирует связанные с ним ассоциации, но делает это только для «своих» типов файлов. Если вы ассоциируете MSPAINT с расширением .ВВВ, то для таких файлов он не будет перерегистрировать ассоциацию pi обеспечивать наличие параметра «%1».) Другой пример касается Word for Windows 6.0. Ассоциируйте с этой программой расширение .ZIG, указав в командной строке «%1», и Word не будет открывать соответствующие файлы Вместо этого он будет жаловаться, что «The document name or path is not valid», и приводить полное имя файла вместе с кавычками. Похоже, Word разбирает строку, возвращаемую функцией GetCommandLineO. А теперь уберите из ассоциации параметр «%1», и все заработает, как часы. Даже с документами, имеющими длинные файловые имена с пробелами внутри — ведь эта версия Word является 16-разрядным приложением и, следовательно, всегда получает псевдонимы в командной строке. Тот факт, что «правильным» способом передачи имен файлов программе Word является DDE, не имеет значения. Подавляющее большинство пользователей не поймет и не примет этого, предпочитая вручную задавать новые ассоциации при помощи параметров командной строки. Проблема в том, что предыдущий пример с MSPAINT может заставить пользователя уверовать в необходимость наличия параметра %1 в кавычках (для успешного распознавания программой длинных файловых имен с пробелами). Но это правило абсолютно не подходит к другим программам, например, Word for Windows 6.0, а значит, масса конфузов и головная боль пользователям гарантирована. Такая ситуация наводит на самые грустные мысли Ые дело, когда такая нетривиальная деталь, как способ общения системы с вашей программой, зависит от какой-то внешней настройки, свободно доступной для пользователей и других программ. И это лишь усугубляется тем, что при создании новых ассоциаций пользователям приходится выбирать между несколькими вариантами и форматами (DDE или командная строка, отсутствие или присутствие параметра %1 и т д.), не имея при этом возможности получить какую-либо справочную информацию или подсказку. Самым ужасным во всем этом деле является отсутствие какого-либо минимального механизма защиты. Если пользователь забыл или не знает, как правильно задать ассоциацию для вашей программы, Windows 95 все равно будет запускать ее. И ваша программа будет выглядеть в глазах пользователя бракованной, хотя в действительности она делает все, что возможно в данных условиях.
440 Не слишком краткая лекция о длинных именах ю-»- 5. Создайте на рабочем столе ярлык документа и дважды щелкните нему мышкой — результат будет таким же, как в эксперименте 3 ( '' длинных имени в качестве аргументов, строка с двумя длинными и ° нами, заключенными в кавычки). 6. Возьмите документ из окна Проводника, перетащите его и сбросьте CMDLINE.EXE (но только не на ее ярлык на рабочем столе) - ' получите ту же картину, что и в случае 2 (первый аргумент — длинн имя программы, второй — псевдоним документа, GetCommandLined заключает имя программы в кавычки, но не делает этого с именем Л( кумента). 7. Откройте окно командной строки, перейдите в каталог «C:\temp оль и введите командную строку «cmdline LFN test file.ZZZ». (Помните эту, одну из самых долгожданных и сильно запоздалых, возможность Windows 95 — запуск Windows-программ из командной строки?) Как и следовало ожидать, аргументы опять разлетелись в пух и прах: Аргумент О [C:\TEMPDI~1\CMDLINE ЕХЕ] Аргумент 1 [LFN] Аргумент 2 [test] Аргумент 3 [file ZZZ] GCL() [С \TEMPDI~1\CMDLINE EXE LFN test file ZZZ] А теперь, не закрывая окно, попробуйте такую командную строку: cmdline 'LFN test file zzz" и наша программа вновь окажется в рядах разумных существ: Аргумент О [С \TEMPDI~1\CMDLINE ЕХЕ] Аргумент 1 [LFN test file ZZZ] GCL() [С \TEMPDI~1\CMDLINE EXE "LFN test file ZZZ'] Такое обращение с именами файлов, указанных в командной строке вполне осмысленно, поскольку это пользователь должен использовать кавычки для разъяснения системе, что является длинным именем фаП ла с пробелами, а что — несколькими отдельными аргументами Н° обратите внимание, что мы в итоге получили и такой вариант, когДа качестве первого аргумента передается псевдоним программы (а не с длинное имя), а функция GetCommandLineO больше не заклю^' имя программы в кавычки. Последняя деталь является весЬ" спорной: хотя кавычки в данном случае и не нужны (то есть со6лЮ>' ется разумный сам по себе принцип «не сообщать никому ничего У него»), жизнь программы, анализирующей результат Line(), значительно усложняется — она все-равно должна быть го ко всем возможным вариантам. Воистину, иногда более простое оК«- вается более сложным.
основных свойств ллинных имен и псевлонимов... 441 g Находясь все в том же окне командной строки, попробуйте выполнить следующие команды (вводить их нужно точно так, как указано здесь; з некоторых местах я использовал символы подчеркивания вместо пробелов — вы обязательно должны ввести в этих местах пробелы): cmdlme ' _hello, _world' Аргумент О [С \TEMPDI~1\CMDLINE ЕХЕ] Аргумент 1 [] Аргумент 2 [hello,] Аргумент 3 [world] GCL() [С \TEMPDI~1\CMDLINE EXE " hello, world] cmdlme " '_hello,_world' Аргумент 0. [С \TEMPDI~1\CMDLINE EXE] Аргумент 1 [ ] Аргумент 2 [hello, ] Аргумент 3. [world] GCL() [С \TEMPDI~1\CMDLINE EXE hello, world] Что же такое argv[l] — строка нулевой длины или несколько пробелов? Как вы думаете, сколько программ смогут разумно обработать такие ситуации? Да, кстати, 16-разрядные программы поведут себя в этих случаях точно так же. Вы можете повторить этот эксперимент и на тестовой программе CLINE16 — увидите те же самые результаты. Попробуйте запустить подобной командной строкой Word, и он попытается интерпретировать каждую кавычку как отдельное имя файла, или пару кавычек (не разделенных хотя бы одним пробелом) как отдельное имя файла. Вы можете до посинений спорить о том, что должна и что не должна делать программа с подобными кривыми входными данными — все это ерунда и легко преодолимо: нужно просто убирать окружающие параметры пары кавычек и игнорировать все аргументы, имеющие нулевую длину или состоящие только из пробелов. Ну что, достаточно экспериментов? Пожалуйста, вот еще один. Поместите :ац1 ZZZ-файл на сетевой диск, не связанный с какой-либо буквой устройства. атем дважды щелкните по нему мышкой в Проводнике, и пробелы в UNC- Мени файла посеют хаос в тех же самых местах, где это ранее делали пробелы ,11Мени документа. Этот вариант касается и 16-разрядных программ тоже. Я dI°Дарен Дону Элдеру за этот пример. Становится только xyHte и хуЛе ^ак показывают вышеописанные тесты, достоверной и надежной информа- ,, ° командной строке — кот наплакал. Вы можете предполагать, что полное в^Шей собственной программы будет передано через argv[0], и что оно бу- ,^ еРвьщ по счету в строке, возвращенной функцией GetCommandLineO, и ^Но заключенным в кавычки. При этом оно может быть как псевдонимом,
442 Не слишком краткая лекция о ллинных именах так и длинным именем, поэтому вам все-равно придется побеспокоить ч конвертации псевдонима в длинное имя, если вы собираетесь показывать вашей программы пользователю (для этой цели вы можете воспользовцт функцией AliasToLFNO, описанной мною в главе 3 на стр. 132). Что же касается имени документа в командной строке, то тут вас озкщ настоящая безвыигрышная лотерея. Практически вы должны быть готовь чему угодно* псевдониму или длинному имени, наличию или отсутстви,, кавычек, пустой строке или строке нулевой длины и так далее. Поскольку Е, не гарантированы ни от неправильно заданных пользователем ассоциаций от неоднозначно оформленных командных строк, ситуация выглядит безнаде/ ной. Даже самая примитивная командная строка, состоящая только из имеН1, программы и имени одного документа, может поставить перед вашей програм мой удивительно сложную проблему идентификации — достаточно лишь «за быть» указать параметр «%1» в соответствующей ассоциации. Предположим что вы и в самом деле хотите сделать вашу программу достаточно разумной умеющей во что бы то ни стало идентифицировать имя документа в командно]: строке. Что делать? Очевидно, в первую очередь можно было бы попробовать использовать в качестве имени документа то, на что указывает argv[l] Ecu1 это имя выглядит правильным (все символы и расширение допустимы, или по крайней мере не являются явно недопустимыми), значит дело сделано и выод нозначно идентифицировали документ, так? Нет, потому что есть одна убийст венная деталь: возьмите Проводник и переименуйте наш достаточно простои тестовый документ «LFN test file.ZZZ» в «LFN test file 2.holy cow!.ZZZ». Пом ните, как в начале главы я упомянул, что длинное имя может иметь несколько точек? В сочетании с допустимостью пробелов это напрочь рушит ваш наивны!, план. После такого переименования эксперимент 2 из предыдущего раздев приводит к такому результату: Аргумент 0 [с \teinp dir\cmdline exe] Аргумент 1 [С \temp] Аргумент 2 [dir\LFN] Аргумент 3 [test] Аргумент 4 [file] Аргумент 5 [2 holy] Аргумент 6 [cow1 ZZZ] GCL() [ с \temp dir\cmdline exe' С \teinp dir\LFN test file 2 holy cow1 ZZZ] «Нет проблем!» — слышу я от наиболее настойчивых читателей. — <<l можем постепенно объединять все слова, найденные в строке, полученное GetCommandLineO, и проверять существование соответствующего файла;ll гда мы "попадем" в существующий файл — дело сделано, правильно?» ™ Вернитесь в Проводник, скопируйте в тот же каталог какой-нибудь ФаП' переименуйте его в «file 2.holy». Вы помните о том, что теперь расширен*1* гут быть длиннее трех символов? (Радуйтесь, что Microsoft не прибегли
, р основных свойств алииных имен и псевлонимов... 443 иЫМ извращениям и не разрешила использовать в именах двоеточия и сим- , обратной косой черты!). Создайте в том же каталоге еще несколько 1ЮБ с такими именами, как «cowl.ZZZ», «file 2.holy» и «test file 2. holy». икпиьтс, какие параметры можеч получить ваша программа, и сразу станет -ijiblHO, что данный метод «последовательной проверки существования фай- слишком подвержен так называемым ложным позитивам и не способен неосмысленно разобрать командную строку во всех возможных случаях. Не 11Те - попробуйте. Как говорится, флаг вам в руки. Если вам нужен конкретный пример из жизни, возьмите программу ISPAJNT, уберите из соответствующей ассоциации параметр «%1» (или хотя -Ь1 кавычки вокруг %1), затем создайте в одном каталоге два файла с „игровыми картинками и именами «a.BMP» и «a.BMP Not!». Теперь дважды ,е пените мышкой по второму файлу, и MS PAINT откроет первый. Что делать? Как разработчик, вы не можете предотвратить все вышеописанные каверзы ю стороны пользователей и самой Windows. Ваши пользователи будут любить д питые имена, пробелы в них и все прочее, и будут орудовать всем этим с таким же упоением, какое испытывает пятилетний сорванец, которому за обеденным столом в руки попала полная бутылка кетчупа. И если не прибегать к крайним мерам (о них я расскажу ниже, только рассказ этот не для слабонервных), то почти все, что вы реально можете сделать, — это должным образом оформить ассоциации для вашей программы во время инсталляции. Например, если ваша программа 32-разрядная, вы обязательно должны указать зассоциации параметр «%1» (в кавычках). Вы должны быть очень осторожны и осмотрительны при перерегистрации ваших файловых ассоциаций Чтобы понять, почему я специально подчеркиваю этот момент, попробуйте ассоциировать BMP-файлы с какой-нибудь посторонней программой и затем один раз запустите MS PAINT поздравляю, BMP-файлы опять будут ассоциированы с MS PAINT! Правильно, данная программа, не обращая внимания ни на что, «присваивает себе» BMP-файлы И это не единственный пример такого самонадеянного поведения, которое я не могу классифицировать иначе, как вопиющим неуважением к конфигурации пользователя Не знаю как для вас, а для меня это как раз один из тех случаев, когда я удаляю программу после двух минут ее использования. •ам ГР следует позаботиться о том, чтобы ваша 16- пли 32-разрядная Р^мма проверяла наличие параметров, целиком состоящих из пробелов или 01Дих нулевую длину. Такое случается редко, но случается; кроме того, та- гРоверка довольно проста и практически ничего не стоит.
444 Не слишком краткая лекция о ллинных именах Одд^ Ваша 32-разрядная программа может и должна предпринять некоторы полнительные действия в том случае, когда она не может найти файл ^ которого было передано в командной строке. Она должна выдать пользоват сообщение (например, как это делает MS PAINT) и обеспечить достато внятную и подробную дополнительную справочную информацию (ч ' MSPAINT не делает). Как минимум, ваша программа должна подсказать если пользователь уверен в существовании «неоткрываемого» файла, то может быть в неправильной настройке ассоциации. Сообщение следует так сопроводить кнопкой, открывающей страничку справочной документации данная ситуация и ее возможные причины освещены более подробно и вс сторонне. (Мой друг Ричард Олверсон любит сетовать на то, что он так частг получает от Windows или Windows-приложений какие-либо сообщения г, неприятностях, сопровождаемые одной единственной кнопкой «ОК». Рич все время повторяет: «Как же так? Скорее на этих кнопках должно быть написано "Not OK", потому что что-то явно не в порядке!». Я не думаю, что пошел бь на такое изменение интерфейса в моих публичных программах. Однако, я абсолютно уверен, что если в подобных ситуациях вы будете предоставлять ва шим пользователям оперативный доступ к соответствующей справочной информации, вы хотя бы немного уменьшите их раздражение и недовольство) Крайние меры В продолжение занудной темы об анализе соотношения затраты/выгода при принятии решений, я хочу представить вашему вниманию классический пример такого трюка, который может быть оправдан только весьма экстремаль ными обстоятельствами. И хотя в большинстве случаев его применение не будет оправданным, я все-равно опишу его — как пример того, что вы можете еде лать, когда вас приперли к стенке. При изучении всех этих проблем с командными строками, ассоциациям!' файлов и прочая, мне пришло в голову, что спасательным кругом в такой си туации мог бы стать 16-разрядный посредник для запуска 32-разрядна программ. Иначе говоря, можно поместить основную 32-разрядную программ в исполняемый файл (например, MyProg.PRG) и поставлять его вместе с не большой 16-разрядной программой (например, MyProg.EXE), которая в<#1 лишь делает некоторую предварительную обработку аргументов команд110, строки, затем запускает MyProg.PRG при помощи функции WinMainw ' завершает свою работу. Это позволяет довольно ловко обойти описанные ра 1 конфликты и неоднозначности (за исключением, пожалуй, синтаксйЧеС неправильного «ручного» ввода), так как основной программе будет пе? ваться определенным образом оформленная командная строка. Основная идея этого трюка — тот факт, что 16-разрядная программ3 гда получает псевдоним файла, вне зависимости от того, каким из вышеоП110'
„л основных свойств ллинных имен и псевлонимов... 445 1дзО^- — способов она была запущена. (Разумеется, это'еще один пример факта, рЬ1Й не всегда является фактом. Например, 16-разрядная программа с ^лаемой версией Windows», равной 4.0, при запуске под Windows 95 будет вергнута тем же издевательствам, что и любая 32-разрядная программа, в с1е передачи ей аргументов командной строки.) Если пользователь введет !маНДУ: •oyprog 'с \long dir name\my data file txt T0, благодаря расставленным пользователем кавычкам, программа-посред- ,к получит имя документа в одном параметре argv[]. Ей останется только lUJb проверить каждый параметр на наличие в нем пробелов и, если таковые Ждутся, передать этот параметр в кавычках, так чтобы основная программа фантированно получила его также в одном параметре argv[]. Единственный 1Учай, в котором эта схема не срабатывает, — это когда пользователь вводит неправильно сформированную командную строку типа: тур год с \1опд dir name\iny data file txt В такой ситуации программа-посредник получит шесть параметров вместо двух («<p<r/^>\myprog», «c:\long», «dir», «name\my», «data» и «file.txt»), и ей ничего не останется сделать, кроме как передать их дальше основной программе. То есть мы опять сталкиваемся с проблемой идентификации. Но по фсяйней мере правильно оформленные командные строки будут теперь обработаны корректно. Кроме того, программа-посредник может легко решить все проблемы с обнаружением и отсеиванием аргументов, целиком состоящих из пробелов или имеющих нулевую длину. На WWW-сервере вы найдете простой демонстрационный вариант подобной программы-посредника — файл FRONTEND.CPP в каталоге http:// www.symbol.ru/russian/library/prof_prog/source/chap13/frontend (это 16-разряд- 1Ь1й проект, который вы можете собрать при помощи Visual C++ 1.5). В том *е каталоге вы найдете копию программы CMDLINE.EXE, но с другим Менем — CMDLINE.PRG. Вы можете поэкспериментировать с программами CONTEND.EXE и CMDLINE.PRG, повторив те же самые опыты, которые я "^ывал ранее. Данный трюк следует рассматривать действительно как крайнее средство, 111чно я не стал бы использовать его в своей публичной программе без каких- J Серьезных на то оснований. Включать в поставку еще один отдельный ис- ^яемый файл, который занимается всего лишь предварительным разбором 1с1нДНой строки? Уж если этот прием не заслужит толстого, большого нуля v°Hltypce на самый элегантный дизайн, то я не знаю, что тогда может его забить. ^ *^к я уже не раз подчеркивал в этой книге, подобные решения нельзя ' Имать в абстрактном виде. Обязательно следует учитывать те условия, в РЫх будет использоваться ваша конкретная программа. Например, если вы
446 Не слишком краткая лекция о ллинных именах заранее знаете, что она будет работать в такой среде, где пользователи б всерьез увлекаться различными манипуляциями с конфигурационн ^ настройками (создавать и редактировать ассоциации и тому подобное), и М; по политическим или финансовым соображениям вам необходимо д0б1г ' максимально корректного поведения программы в ответ на приказания полг вателей (которые не обязательно совпадают с тем, чего пользователи хогяг самом деле), то создание программы-посредника будет осмысленным и 0п " данным. Вы можете отказаться от использования «ненормального» расширения л исполняемого файла основной программы. Например, резонно предположив что файл с расширением PRG вместо ЕХЕ будет более подвержен риску бьц искалеченным или даже удаленным особенно авантюрными пользователями с другой стороны, оставив этому исполняемому файлу традиционное расширение ЕХЕ, вы даете пользователям больше шансов догадаться, что вашу главную программу можно запускать и напрямую, без посредника. И тогда пользователи могут соответствующим образом подправить свои системные настройки, и все ваши усилия по созданию программы-посредника окажутся напрасными Однако, у этой проблемы есть довольно простое и элегантное решение* сделайте так, чтобы основная программа ожидала специальный недокументированный параметр в командной строке (своего рода пароль), а программа-посредник передавала бы ей этот пароль после предварительной обработки параметров. Тогда при попытке запустить вашу программ! напрямую она вежливо отказывалась бы работать и предлагала бы использо вать программу-посредника. Хорошие новости На самом деле все не так уж плохо. В конце пути у вас будет передышка: npi' работе с drag-and-drop вам не придется бороться с подобными трудностями ведь имена сброшенных файлов передаются по очереди, а не будучи сваленны ми в один большой текстовый буфер, как это делается с именами файлов в ко манд ной строке. Ради Бога, не забывайте о человеческом факторе! Учитывая все вышесказанное, я почти на все сто процентов уверен, чт° ' ним из небольших, но весьма навязчивых источников раздражения пользой1 лей Windows 95 будут 32-разрядные программы, использующие псевД<^1р файлов (вместо длинных имен) в своих сообщениях, диалоговых oKtfaN прочих элементах пользовательского интерфейса. Представьте себе: вы Я°*
ооШ*е_ новости 447 -и1 на рабочий стол ярлык какой-нибудь программы, затем выбрали в ,''роднике некоторый фаг!л с симпатичным, длинным, дружественным вам oficM, потащили его и сбросили на данный ярлык. Но вы нечаянно сделали [С11ькую ошибку - например, попытались открыть при помощи, "кЬ|1ческого редактора простой текстовый файл. Программа, получившая имя , уМепта в виде псевдонима, безуспешно пытается открыть ваш файл н выдает общение об ошибке, утверждая, что формат файла <<С\jE4PDhl\IJNfES^1252>> не 1[{е1 быть ею распознан. Я могу представить ваш внутренний монолог в такой ivaunn* «Какого черта программа попыталась открыть mom файл? Это со- см не го, что я только что сбросил на значок! Более того, если верить Провод- ilvV) такого файла вообще нет в том каталоге!» Мораль* пожалуйста, не пожалейте пары липших минут, потраченных )llU того, чтобы ваша публичная программа транслировала псевдонимы фай- оС в длинные имена перед показом их пользователю. В этом деле вам могут весьма пригодиться функции AliasToLFNO и LFNToAliasO, исходный код .оюрых я приводил в главе 3 па стр 132. Только учтите, что эти функции извращают только локальное имя файла. Поэтому, если вам нужно [конвертировать весь путь, включая имена всех вложенных каталогов и само кжалыюе имя, то у вас есть только два пути использовать вышеупомянутые функции итеративно, транслируя один компонент имени за другим, либо обратиться к встроенному ассемблеру. Поразительно, в Windows 95 есть целых тлсчть новых функций для работы с файловой системой, которые недоступны через Win32 API и могут использоваться только кодом на ассемблере через прерывание 2lb- Отсутствие одной из них воистину необъяснимо для меня - я имею в виду функцию Get Long Path Name, которая транслирует полный псевдоним фай на в его полное длинное имя (Только, пожалуйста, не путайте jtot системны}! вы.юи с функцией GetPiiliPatliNameO из Win32 API, которая всего лишь прицепляет к заданному локальному имени файла букву текущего устройства и имя текущего каталога, не проверяя при этом ни существование такого файла, ни правильность заданного имени ) Остальные пять недостающих функций называются так: Generate Short Name, Seiver Create or Open File, Create Subst, Terminate Snbst и Query Subst titgume ошибок, tkgume ошибок... ':jHe приступая к написанию этой книге, я обещал и себе, и издателю, что \ *УДУ слишком увлекаться критикой кого-либо за ошибки. VI я сдержу :>то ^нцс Однако, сейчас я хотел бы поговорить о некоторых весьма 1( 1атПчных проблемах, возникающих при работе Проводника и Windows 95 Гниымп файловыми именами. Я не могу утверждать наверняка, по мне ка-
448 Не слишком краткая лекция о ллинных именах (h^ —^^ое жется, что все эти ошибки являются внешними по отношению к самой реа» ции длинных имен в Windows 95. А значит, ошибки именно такого типа n, u всего будут возникать в других программах, в том числе и в ваших. При этом я сразу хочу подчеркнуть два момента. Во-первых проблемы, описанные ниже, были проверены мной на первой общедосту^ финальной версии Windows 95 от 24 августа 1995 года, и вполне возможно ч к моменту вашего прочтения этих строк какие-то из этих проблем (если не р они) уже устранены в более новых версиях Windows 95. По крайней мере искренне надеюсь на это. Во-вторых, если вы собираетесь воспроизвести ъл проблемы на своей системе, пожалуйста, будьте предусмотрительны сохраните ваши данные, выгрузите все ненужные программы, проверьте, нет ли под кроватью каких-нибудь монстров или пользователей Мае, — короЧе примите все разумные меры предосторожности. Для демонстрации обсуждаемых здесь проблем можно снова воспользоваться программой-примером CMDLINE.EXE (что лишний раз показывает как много пользы можно порой извлечь из какой-то пары дюжин строк кода) На этот раз создайте в каталоге «C:\Temp dir» еще один тестовый каталоге очень длинным именем — например, «очень очень очень длинное имя каталога для тестирования». Затем перейдите в него и создайте в нем еще один каталог, также с довольно длинным именем. Наконец, переместите или скопируйте в самый вложенный каталог один из ваших ZZZ-файлов, и вы готовы к экспериментам. Вначале попробуйте постепенно увеличивать длину имени одного из тестовых каталогов и после каждого наращивания его на один-два символа заходите в самый вложенный каталог и пытайтесь запустить CMDLINE.EXE двойным щелчком мыши по ZZZ-файлу. В какой-то момент либо Проводник, либо Windows 95 откажется запускать нашу программу и выдаст при этом сообщение о невозможности найти файл C:\WINDOWS\SYSTEM\CONAGENT.EXE (Странно. Не найти? А ведь именно в этом месте этот файл и лежит. Можете проверить.) Как только это произойдет, вы можете укоротить имя одного из каталогов на один-два символа и добиться того, что Проводник запустит-таки CMDLINE.EXE. Но при этом программе вообще не будет передано имя док}' мента в командной строке. Иногда в командной строке может встречаться неправильный путь (вовсе не соответствующий только что сделанной операи111, переименования). Если продолжать эти манипуляции достаточно долго, то ^ часть системы, у которой первоначально съехала крыша, явно начинает ИДТ11 разнос. А тем временем ваша программа будет получать такие данные, когорт и рядом не стоят с реальными существующими путями и файлами в систем^ невинному пользователю будет трудновато догадаться об истинной при411 ' проблем и не обвинить во всех бедах вашу программу. Попробуйте пернменовать ваш ZZZ-файл в подобных условиях, и вЫ '\, жете получить от Windows 95 сообщение о том, что имя вашего файла содер*
лшие новости 449 Ш цустимые символы, чего в действительности нет. И это уже не двусмыслен- 1 соо6щетше, как в приведенном выше примере, - это уже совершенно од- значное и откровенно ошибочное сообщение. Один раз я даже наблюдал, как Windows 95 изменяет длинное имя файла ц копировании этого файла из одного каталога в другой, и даже заменяет его 1 соответствующий псевдоним. При этом я перемещал файл вверх по дереву налогов (то есть так, что его имя могло только укорачиваться), и следова- .|ьно причина была вовсе не в нехватке места для нового имени файла, а ,менно в каких-то действиях над его исходным именем. Отдадим Microsoft должное — некоторые попытки предотвратить подобней хаос все-таки делаются. Например, если у вас есть достаточно глубоко вло- кениый файл с именем JOE.TXT, п если вы попытаетесь переименованием cicnca удлинишь его локальное ттмя так, что результирующее полное имя вый- iei за допустимый верхний предел длины полного пути, Проводник остановит вас Это круто, это замечательный пример разумного поведения программы, — дайте Microsoft медаль за эту маленькую деталь. Но даже в этой лолске меда есть своя капля дегтя: дело в том, что сообщение, появляющееся в такой ситуации, не только не позволяет оперативно получить дополнительную справочную информацию, по и само по себе звучит двусмысленно - Проводник заявляет, что имя, которое вы пытаетесь использовать, является либо «слишком длинным», либо «неправильным». (Иногда, встречая подобные сообщения, я испы- 1ываю сильнейшее желание выкрикнуть прямо в экран: «Ребята, ну хотя бы намекните, в чем же именно проблема!?» Вот уж точно, прав был Рич, говоря, что такие сообщения должны сопровождаться кнопкой «Not OK». В конце концов, я уверен, что где-то в недрах программного кода есть место, где известна совершенно конкретная причина проблемы — так почему же они хоть чуть-чуть не помогут мне выяснить, что же именно не в порядке?) Кроме того, ясно, что гакая защита способна оградить пользователей далеко не от все возможных вариантов создания слишком длинного полного пути: вы можете перемещать Файлы из одного каталога в другой, переименовывать промежуточные каталоги 11' Д Попытка заставить Проводник отслеживать все подобные ситуации была ()Ь1 но меньшей мере безрассудной и непрактичной, так как сразу же привела %( к серьезному ухудшению производительности. Простые манипуляции с Лиловой системой (такие как переименование каталога), от которых нользо- ,,1|слц ожидают мгновенного выполнения, стали бы неоправданно длптель- Ь|ми -- ведь программе Проводник пришлось бы сопровождать каждую такую 11еРацц1о рекурсивной проверкой всех подкаталогов на предмет появления |Ццпсом длинных полных имен. И никакая оптимизация тут не помогла бы. Ьсли имя какого-либо каталога оказывается достаточно длинным, Провод- , 1к Даже не даст вам возможности просмотреть списки файлов в «нпзлежа- 'о каталогах - он пожалуется на слишком большую длину пути. (Что ж, Ранней мере, в этом случае формулировку сообщения можно по достоин-
450 Не слишком краткая лекция о ллинных именах тй^ ству назвать точной. И далее очень точной, если сравнить его с упоминавще- выше абсолютно бестолковой жалобой о невозможности запустить CQNi Я GENT.EXE.) " 'А" Не имея доступа к исходному коду Проводника или Windows 95, я цс М( » представить каких-либо доказательств, но данная ситуация выглядит так у будто какая-то часть системы явно не делает проверку переполнения буфе .' при формировании командной строки или полного пути к документу. (как ' упоминал ранее, определенную роль в этом наверняка играет то скудно', ограничение длины пути в 260 символов, фактически унаследованное от Windows 3.1; вспомните также о настораживающей несогласованности документации и кода в вопросе о максимально допустимой длине полного имени каталога.) Можно также предположить, что эти проблемы связаны с извечной бедой C/C++ - несовершенством базовых (да и не только базовых) функций для работы со строками и смертельными побочными эффектами появления незавершенных нулем строк. Даже если какие-то проверки переполнения буферов делаются, они могут содержать ошибки (самая типичная из которых — отсутствие поправки длины на один служебный символ, завершающий нуль), и тогда в один прекрасный момент происходит нечаянное слияние правильных строковых данных с совершенно непредсказуемым бинарным мусором, по случаю расположенным в окружающей памяти. Кстати говоря, аналогичный базовый тест -- попытка открыть документ со слишком длинным полным именем - приводит к краху File Manager от Windows NT 3.51 (к попытке записи в недопустимую область памяти). Это наводит меня на серьезные подозрения, что коренная причина этой ошибки одинакова и в Windows 95, и в Windows NT. Ну что ika умники, и как Я§ея по-вашему, Microsoft долйкна была реализовать длинные имена файлов? Это своевременный и естественный вопрос. Я думаю, попытка ответить на него была бы хорошим упражнением в мысленном проектировании, а заоди° лишний раз продемонстрировала бы, в какой угол была загнана Microsoft (главным образом, своей же собственной метлой). Я не рассматриваю Microsoft (или любую другую большую компанию) #1К монолитную во времени организацию. Вероятно, группа разработчик00' отвечавшая за Windows 95, имела незначительное (но наверняка не нулев°е пересечение с проектировщиками поддержки длинных файловых имен. ^ этом смысле команда, выпускавшая Windows 95, сделала максимум из т()1С что могла, если принять во внимание начальные условия этого проекта и обШ) обстановку в компьютерном мире.
о0шие новости 451 Начнем с того, что Microsoft никак не могла выпустить в свет Windows 95 / поДДеРжки Длинных имен. Такая поддержка уже есть в Windows NT, не воря Уж °^ известном творении Большой Голубой Империи Зла — операцией системе OS/2. Поэтому в вопросе делать/не делать выбора у Microsoft чКТ11чески не было — если бы такая поддержка отсутствовала в Windows 95, недовольства pi насмешек со стороны пользователей, писателей и журнали- \иВ был бы оглушительным. Поскольку было необходимо соблюсти совместимость с Windows NT, Microsoft некуда было уйти от допустимости пробелов в -•донных именах. И раз уж механизм передачи аргументов через командную троку не мог быть полиостью ликвидирован, не оставалось ничего другого, р0ме как смириться с теми случаями, когда весь ввод пользователя передается .фограмме в нетронутом виде в надежде на лучшее. (Лично я искренне желал оы, чтобы Microsoft нашла способ избавиться в Windows 95 от этого устаревшего механизма и перешла бы к новой модели передачи параметров программе — например, помещению их в какую-нибудь структуру и затем передачу программе именно этой структуры. Так или иначе, это решило бы все проблемы неоднозначности разбора командной строки, а сами командные строки заслуженно стали бы достоянием книг по истории программирования. Но пока это все мечты — голубые мечты об элегантности. На самом деле, подобный шаг означал бы серьезный переворот в мире программного обеспечения, и не был возможен по чисто практическим соображениям. Разработчики не часто бунтуют, но, когда такое случается, это становится ужасным катаклизмом. И он наверняка случился бы, если бы Microsoft вдруг выбросила на свалку механизм командных строк.) Учитывая все эти факторы, что же могли разработчики Microsoft изменить "улучшить по сравнению с тем, что мы имеем на сегодняшний день? Прежде Всего, они могли бы сделать передачу имен файлов через командную строку бо- ]ее последовательной как минимум в одном вопросе: всегда передавать псевдонимы, а не длинные имена (даже 32-разрядным программам). Как я показал Millie, в нынешней ситуации никакая программа не может заранее предвидеть, р;цкого типа ссылку на файл она может получить, поэтому программы так и так д°1Жны быть готовы к принятию псевдонима и, при необходимости, к по- 1еДУющей конвертации его в длинное имя. Или, в качестве альтернативного 1е1нещщ, они могли бы всегда заключать длинные имена с пробелами в авЬ1чки, так чтобы гарантировать успешный последующий разбор командной Рокн. Лично я издал бы вдобавок декрет о запрещении пустых параметров и •ц Необходимости должным образом «ремонтировал» бы командные строки 'п°средственно перед передачей их программам. Иногда совсем не вредно, и Ч(е Полезно установить тот или иной закон, и именно в этом случае я только 11Ветствовал бы подобное законотворчество со стороны Microsoft. Иными словами, находясь в идеальном мире, Microsoft наверняка была бы Стоянии устранить обсуждаемые проблемы путем должного продуманного
452 Не слишком краткая лекиия о ллинных именах фа^ проектирования. Но мы живем в реальном мире, где для этого Microsoft п добилась бы либо способность необычайного предвидения еще в начале к()^ (хотя, как все мы знаем, первая версия MS-DOS была написана вовсе не к * панией Microsoft), либо готовность спровоцировать серьезную революцию мире разработки программного обеспечения и потом пережить и преодолеть р возможные последствия такого шага. Но теперь уже поздно, и нам придет смириться с той реализацией длинных файловых имен и командных стрп которую мы сегодня имеем. И чтобы не заканчивать эту главу в минорной тональности, позвольте н- помнить вам старую поговорку: если жизнь преподносит вам только лимоны не падайте духом и превращайте их в лимонад. Даже стоя на краю этого мИн! иого поля с длинными файловыми именами, 1Ш знаете некоторые обходные пути. Поэтому не переставайте думать и изобретать, не переставайте искать лучшие решения, и, если вы будете достаточно осторожны, это поле может стать той ареной, па которой ваши программы будут значительно выделяться на общем фоне.
JLUJ. № Функции, сообщения и структуры Win32 образуют последовательный и единообразный программный интерфейс для всех 32-разрядных платформ Microsoft- Windows 95, Microsoft Windows NT u Windows 3.1 с Win32s. Microsoft Coiporation. Pi ogi a miner's Guide to Miciosofi Windows 95, страница 14 Remember, there's just one Win32 API. Мэгт П игре к Только в одной из этих двух цитат заложена умышленная ирония. К сожалению для всех Windows-разработчиков, это относится к словам Мэтта Питрека. И следовательно, Microsoft просто хочет заставить нас поверить в то, что, на мой взгляд, можно справедливо назвать весьма спорным и сомнительным утверждением. Иными словами, Win32 API вовсе не является тем, чем ее предъявляет Microsoft. Эндрю Шульман, Дэвид Мэкси и Мэтт Питрек назвали первую главу своей книги «Undocumented Windows» так: «Этого не должно было случиться», где 11оД «этим» подразумевались огромные объемы либо очень плохо документированной, либо вообще недокументированной информации о 'Фофаммнровании для Windows — то есть ситуация, которая так осложнила К|1знь многим программистам. И практически всю эту главу они посвятили Р«иъ51сыеншо того, как это случилось и что это означало. Во время своей работы Жданной главой и над главой 12 я все время вспоминал этот заголовок, по- Щу что я ТОже пишу о такой ситуации, которая не должна была случиться. Ш32 API находился и до сих пор находится под полным контролем одной v°Nuiaain] -- Microsoft, а значит у них должна была быть возможность осуще- ГнИть этот переход от 16 к 32 битам (и тем более — переход от одной вариации е 1ц32 к другой) с минимальными издержками, суетой и хлопотами. Ведь не 1л° никакого комитета, которому надо было бы угождать. Не было никакой
454 Разновидности w- ——— —г^ш рабочей группы ANSI по стандартизации, вечно медлительные члены кот месяцами или годами подавляли ли бы Microsoft своей волей. Это была с г венная песочница Microsoft, и они могли бы сделать новомодную реализа * Win32 API — Windows 95 - намного более совместимой с уже существуют *° серьезной его реализацией по имени Windows NT. Но этого не случилось 7 именно подробному разговору об основных отличиях между Win?; платформами я и посвящаю данную главу. Прежде чем погружаться в эту проблему, я хочу сделать пару важнь замечаний по поводу версий. Во-первых, в данной главе более чем в остальны важно учитывать конкретные номера версий Windows, о которых идет речь Если явно не оговорено обратное, под «Windows 95» я подразумеваю «золотую» версию Windows 95, которая вышла в свет 24 августа 1995 года, пол «Win32s» — версию 1.30.159 этого продукта (то есть также «золотую» версию выпущенную почти сразу после того, как Windows 95 стала общедоступной), а под «Windows NT» — версию 3.51. Во-вторых, в тексте вы встретите описания многих проблем, связанных с Win32s 1.30. При этом, по крайней мере, часть из них была устранена в версии 1.30а, которая была выпущена так близко к плановому сроку окончания моей книга, что я физически не мог успеть внести соответствующие коррективы. В конце сентября Microsoft опубликовала эту версию Win32s в CompuServe, но соответствующий официальный список исправленных ошибок не публиковался до конца октября, поэтому я не могу его цитировать. Почемм Win32 /= Win32 f= Win32 В нормальных условиях, если вы сталкиваетесь с несколькими операционными системами, на которые можно нацелить свой проект, вы идете совершенно очевидным путем: вы изучаете эти платформы, вычисляете ожидаемые выгоды от поддержки каждой из них, а также затраты на такую поддержку, затем взвешиваете все это, положив на другую чашу весов ресурсы, которыми вы располагаете, и, наконец, выбираете одну или несколько целевых платформ- ^а следующем шаге, когда вы занимаетесь непосредственно задачей создания ис' полняемого файла, в нормальных условиях дело сводится к подбору соответсг вующих опций компилятора и/или компоновщика, к подключение соответствующих библиотек и прочих ресурсов, к пометке программы неК° торым «ярлыком», который не позволял бы запустить ее во враждеб*10' окружении (в таком, как, например, операционная система неправилЫ*^ версии). И все! Voila! Дело сделано, и у вас на руках одна программ*1» ' мечательно сориентированная на несколько платформ. ^ Вы правильно догадались, что я неспроста несколько раз повторял фР1, } «в нормальных условиях». Потому что теперь - добро пожаловать в * программирования для Windows 90-х годов!
оче^У. Win32 != Win32 != Win32 455 Домните, как просто выглядела проблема поддержки нескольких Win- vs-платформ во времена Windows 3.0 и Windows 3.1? Фактически, чуть ли ", самым сложным тогда был вопрос, включать или не включать в поставку \\цю динамической библиотеки COMMDLG.DLL для удобства тех пользова-' \ 1ей, которые все еще работают с Windows 3.0. Вы правильно меня поняли: ддержка нескольких Windows-платформ (даже если речь идет только о 32- 13рЯдных версиях Windows) больше не является такой простой проблемой. Я ^,ду говорить на эту тему более детально чуть позже, но суть ее в том, что Mi- "rosoft предложила всему Windows-сообществу весьма своеобразный, полный юрпризов ПУТЬ перехода на 32-разрядную платформу и при этом предоставила .{рограммистам гораздо меньше помощи, чем хотелось бы. В качестве предварительного примера, задумайтесь на тем, как различные версии Windows сами относятся к программам, разработанным для других платформ: Windows NT 3.5 и 3.51, а также Win32s (подсистема, предназначеншш для запуска 32-разрядных программ под Windows 3.1) запросто позволяют запускать программы с «ожидаемой версией Windows» 4.0, несмотря на то, что они не поддерживают все возможности Windows 95 (которая, собственно, и имеет номер версии 4.0); в то же время Windows NT 3.51 откажется запускать 16- разрядную программу, помеченную версией 4.0, в то время как Windows 95 запустит такую программу и будет обращаться с ней, как с «приложением, осведомленным о Windows 95» (см. врезку «Использование длинных имен файлов в 16-разрядных программах: да здравствует алхимия!» в главе 12 на стр. 395). Если вам интересно знать, есть ли серьезные различия между реализациями Win32, взгляните на обзорные таблицы 14.1 и 14.2. Первая из них является просто сводкой, описывающей поддержку тех возможностей Win32, которые, на мой взгляд, наиболее интересны для программистов. Я думаю, она Даст вам почувствовать разницу в архитектурах трех 32-разрядных платформ. Вторая таблица представляет собой статистическое резюме той информации, которую предоставляет Microsoft в файле WIN32API.CSV — мне кажется, что °на замечательно показывает, до какой степени реализован Win32 API (функ- ции, сообщения, константы и пр.) в каждой из трех 32-разрядных вариаций Windows. Таблииа 14.1. Win32: взгляд с высоты 30 000 футов Категория ЬазОБая поддержка 32 бит 1 ^огопоточность К°нсольный API Windows 95 Shell Windows 95 Да Да Да Да Windows NT 3.51 Да Да Да Нет Win32s 1.30 Да Нет Нет Нет
456 Разновилности Ц/- Категория 32-разрядный модуль USER 32-разрядный модуль GDI Именованные каналы Файлы, проецируемые в память Unicode OpenGL Новые стандартные элементы управления Средства безопасности Структурная обработка исключительных ситуаций Windows 95 Нет Нет Да (только клиентская сторона) Да Нет Нет Да Нет Да Windows NT 3.51 Да" Да Да Да Да Да Да Да Да Win32s 1^(Г^ Нет~^^^ Нет Нет Да Нет Нет Да Нет Да Таблица 14.2 требует небольшого пояснения. Я загрузил файл WIN32- API.CSV в базу данных и затем делал запросы, выяснявшие, сколько элементов попадают в каждую из семи «категорий совместимости». Прежде всего, я выяснил, что только для 3882 из 5167 элементов «Да» или «Нет» указано во всех трех позициях. Вместо того, чтобы придумывать, как поступить с оставшимися 1285 не до конца документированными элементами, я просто их проигнорировал. (Сделав выборочный просмотр нескольких таких «белых пятен» в WIN32 API. CSV, я понял, что большинство из них очевидно должны быть заполнены образцом «Нет», а значит их дальнейший учет лишь не значительно увеличил бы «счетчики» во всех категориях, кроме Да/Да/Да>] в целом незначительно повлиял бы па картину в целом). Кроме того, исключил из расмотрения 155 элементов, попавших в категорию Нет/Нет/^ (это те элементы API, которые были выброшены при переходе от Win*0 Win32, и нет особого смысла включать их в интересующий нас анализ). №° я получил 3727 элементов, которые были полностью документированы поддерживались хотя бы одной из трех Wii^-ibiaTc^opM.
№ чему Win32 != Win32 != Win32 457 Таблица 14.2. Win32: взгляд с высоты 10 000 футов (согласно WIN32API.CSV) Windows NT ^~Да Да Да Нет Да Нет Нет . Windows 95 Да Нет Да Да Нет Да Нет Win32s Да Нет Нет Нет Да Да Да Количество элементов 1779 847 692 391 15 2 1 % 47.7 22.7 18.6 10.5 0.4 0.05 0.02 И что же мы видим из этой таблицы? Оказывается, менее 48% элементов попали в категорию Да/Да/Да - то есть только они поддерживаются всеми тремя версиями Win32 (а если бы я все-таки учел вышеупомянутые «белые пятна», данная процентовка оказалась бы значительно меньшей — примерно 35.5%). Согласитесь - это удивительно мало! Конечно, данная сводка использует невзвешенные числа, то есть никак не учитывает «значимость», частоту использования каждого отдельного элемента. Например, какая-нибудь безвестная функция, используемая одной программой из тысячи, вносит в результат точно такой же вклад, что и вездесущие RegisterClassO или WM_CREATE. Но с Другой стороны, данная статистика также не учитывает различий между реализациями одного и того же API на разных платформах. Вспомните все тот же, наверное, уже набивший оскомину пример с обрезанием 32-разрядных GD1- к°ординат в Windows 95, которое делает такие координаты по сути бесполезными (иными словами, если ваша программа нуждается в поддержке 32- 1)азрядных графических координат, то вышеприведенную таблицу можно во- °бще выбросить в помойку — для такой программы реальный «процент совместности» Win32 API между тремя платформами вообще падает с 48% до нуля). Ч- Документация, документация и снова документация Те из вас, кто прочитал большую часть книги до этого места, вероятно, уже Удивятся тому, что документация является для меня Очень Важным ГРУментом. Да, это именно так. И я надеюсь, что и для вас она имеет такое ва>кное значение, если вы всерьез заняты разработкой Windows-при ложе-
я 458 Разновилности W: „ — —zz[032 Увы, документация по Win32 API оставляет желать много лучшего. Р говорить о различных вариациях Win32, то наиболее естественным воппгл И при изучении этой документации будет вопрос о том, какие функции доступ на каждой из трех рассматриваемых платформ. Лучшим отдельным источ '* ком такой информации является файл WIN32 API. CSV, на базе которого строил таблицу 14.2. Вы можете найти этот файл в катаю WIN32SDK\MSTOOLS\LIB\I386 на Win32 SDK CD-ROM. (Содержимое этого файла гораздо удобнее изучать после загрузки его в какую-нибудь баз< данных или электронную таблицу. Я рекомендую вам сделать это, б^г WIN32API.CSV является чисто текстовым файлом, специально отформатированным в виде таблицы с запятыми в качестве разделителя полей.) Как я уже говорил выше, этот файл содержит перечень более 5100 структур, констант и функций и разъясняет вам, с какими Win32- платформами, подсистемой, заголовочным файлом и библиотекой импорта ассоциируется каждый элемент API. Звучит здорово, правда? Да, и я очень рад, что такой документ существует. Однако, в реальной работе он оказыватеся полезным лишь до некоторой степени. Когда я гляжу на него, я всякий раз поражаюсь тому, что в него не включили много другой полезной информации (например такой, как поддержка Win 16 или ее отсутствие; точный номер версии, начиная с которой поддерживается данный элемент API; и так далее) Почти все функции, сообщения, структуры и пр. также описаны в SDK и в API32.HLP (справочном файле, поставляемом вместе с Visual C++ 2 х) В этих источниках информация о переносимости тех или иных элементов API может быть получена при помощи всплывающего окошка «Quicklnfo», в котором также указан соответстующий заголовочный файл. Однако, как и WIN32 API. CSV, эти источники тоже не могут похвастать полнотой - в них тоже нет информации, касающейся Win 16 и точных номеров версий. Что еще хуже, в окошках «Quicklnfo» можно встретить порядочное число различных ошибок вплоть до откровенной несогласованности с основным содержанием (например, основной справочный текст говорит, что данное сообщение является новым для Windows NT, а в окошке «Quicklnfo» отмечено, что это сообщение вообще Tie поддерживается на Windows NT). На мой взгляд, самым серьезным недостатком документации по Win32 является практически полное отсутствие информации о кодах ошибок, используе мых механизмом GetLastErroK )/SetLastError(). Это очень затрудни программистам задачу написания хорошего (разумного и услужливого) ко#1> к этой проблеме я еще вернусь несколько позже. В целом, документация по Win32 имеет все классические признаки т°г • что она была написана и проверена людьми, которые ее реально используют — по подробности и корректности изложения она даже близко стоит к тому, что требуется разработчикам. (Я не знаю, да и не хочу знать,1ч и как писал и проверял это творение Microsoft. Но именно такое ощуЩе***
^ему Win32 != Win32 != Win32 459 9 ,к разработчик испытываю всякий раз, когда сталкиваюсь с очередной ' аКтической проблемой и пытаюсь обратиться за помощью к документации.) Цтобы немного конкретизировать эту мою невнятную общую критику, повольте мне привести один реальный пример. Когда я работал над одним из ужастиков» для данной главы, я столкнулся с интересными выкрутасами со- , бщения LB_D1R. Вы можете посылать это сообщение окну-списку для добав- еиия в него имен файлов, соответствующих заданному набору аттрибутов и не- 10торому требованию на имя. На первый взгляд все достаточно просто. Вот как оП(1Сап в документации по Win32 SDK параметр, отвечающий за указание 1ре6ования, предъявляемого к имени файла: IpszFileSpec — Значение lParam. Указывает на завершенную нулем строку, содержащую имя файла, которое следует добавить в список Если эта строка содержи! метасимволы (например, *.*), то в список будут добавлены все файлы, имена которых согласуются с этими метасимволами, и атрибуты которых соответствуют значению параметра uAttrs. Ну и как должен быть задан этот параметр, если вы, например, хотите показать в окне-списке все файлы из каталога C:\MYDOCS? Как выясняется, при задании строки «C:\MYDOCS» (или даже «C:\MYDOCS» ) в качестве IpszFileSpec окно-список откажется что-либо показывать. Вы должны задать строку «C:\MYDOCS\*.*». Это явно противоречит общепринятой практике различения имен каталогов и файлов: откройте в Windows 95 окно командной строки, введите команду «dir c:\mydocs», и вы получите список файлов в указанном каталоге. По какой-то непонятной мне причине, окна-списки обрабатывают сообщение LB_D1R более аляповатым и, на мой взгляд, неинтуитивным способом. И уж если так случилось, то, возвращаясь к нашей основной теме, именно документация призвана помочь разработчику быстро разобраться в подобной неочевидной ситуации. В данном случае буквально пара дополнительных предложений или конкретный пример о том, как именно окна-списки ин- 1српретируют параметры сообщения LB_D1R, избавили бы нас, 'Программистов, от напрасной траты времени на простые эксперименты. Кончпо же, API является движущейся мишенью - просто потому, что он Разрастается и изменяется во времени. Это не более, чем естественный п°бочньш эффект развития успешного программного продукта. Иными сло- Вами, любой прогресс имеет свою цену, и мы должны быть готовы как к изме- И(?1Шям API, так п к появлению несообразностей в определениях интерфейса, в 11х^нах [I пр. (И Win32 имеет порядочное число таких неприятных моментов.) *° ас это является предметом обсуждения в данной книге. Сверхсложность Проблемы изучения и отслеживания Win32 API обусловлена не только его )в°люцией, но и одновременным наличием трех его реализаций (а вскоре, блатаря IBM, их вообще будет четыре), никакие две из которых не согласуются Юностью. И когда в таких условиях вы сталкиваетесь с неаккуратной,
460 Разновилности ty; противоречивой и недоделанной документагщей, как вы можете опредеч точное поведение того или иного API на различных платформах (или даже Ь одной, отдельно взятой платформе)? Выбора нет -- вам придете экспериментировать. *я Aaike Microsoft иногда пьет из этого колодца Хотите познакомиться с несколькими реальными жизненными примерам, пз области несовместимости Win32 API на различных платформах? Пожалуй- ста, я с удовольствием предложу вашему вниманию целых три — все они позаимствованы непосредственно из набора программ-примеров, поставляемых самой Microsoft па CD-ROM с Visual C++ 2.2. Первый пример — программа MAZE LORD, которую вы найдете в каталоге \MSVC20\SAMPLES\WIN32\MAZELORD. Это игровая программа лабиринтного типа, которую Microsoft в течение некоторого времени поставляет в качестве демонстрационного примера для работы с графикой. Скомпилируйте эту программу, и под Windows NT 3.51 она будет работать безукоризненно. Мо попробуйте запустить тот же исполняемый файл под Windows 95, и результат будет менее чем удовлетворительным — часть графического вывода попросту отсутствует. (Для детального сравнения получающихсяя картинок взгляните на рисунок 14.1.) В чем же тут дело? Оказывается, функция PolyDrawQ, которую использует MAZELORD, не поддерживается в Windows 95. В данном случае примечателен тот факт, что в оперативной справочной информации от Visual C++ 2.2 (т. е. в самой последней версии этого документа, которую я видел) Microsoft все еще уверяет, что функция PolyDrawQ поддерживается в Windows 95. (Кстати, в WIN32 API. CSV этой досадной ошибки уже нет.) Второй пример - это версия MnltiPad, написанная без помощи MFC и расположенная в каталоге \MSVC20\SAMPLES\WIN32\MULTIPAD (пожалуйста, не перепутайте с \MSVC20\SAMPLES\MFCWIN32\MULTIPAD). Эта программа также имеет проблемы при работе иод Windows 95 - взгляните на рис. 14.2, на котором изображен результат попытки загрузить в MultiPad небольшой, чисто текстовый файл. В заключение, попробуйте скомпилировать программу Word Pad из тех исходников, которые предложены в каталоге MSVO(XSAMPfJES\MFC\WOKDPAD." она не будет работать в Win32s 1.30. При попытке запустить ее на этой систем вы получите сообщение о том, что невозможно загрузить RICIIED32.DLL, п° тому что эта библиотека «возможно отсутствует или некорректна».
wjgsm Win32 1= Win32 != Win32 461 штшт .j№'..fii*oft &» t HHUnul4Uii4riNyiiiHhllniri^U>i^Nllnsil}tll<iLri*iUniH»iUnu1*iUnuiNHnl II L2 ZEOS@MachineName 0 ! 1 I File Options About 1 Рис. 14.1 MAZELORD работает. Отчасти... Я знаю, какой реакции на эти примеры следует ожидать от определенных рдеюсь, небольших и большей частью иерепрезентативных) групп людей, '^йкрософтофобы тут же вырвут эти страницы, отольют их копии в бронзе и, с'Делав небольшую рамку вокруг них, вывесят на фасадах своих домов. '^айкрософтофилы, напротив, вообще не станут разговаривать и посчитают эти Фимеры дурацкими — просто потому, что это всего лишь примеры, не предназначенные для доказательства или опровержения чего-либо. У меня один и 1'0Т Же ответ обеим этим группам: вы неправы.
462 РаЗНОВИАНОСТИ\Л/; £ife £1* &*$rch Ш*гф* Жй '.. .; Gar* open th* W& Й SiTEMPwqjrt <&V^rfWftUcp-"tW 1ВЩ f M>: Рис. 14.2 Работа MultiPad пол Windows 95, Разумеется, эти примеры ни в коем случае не доказывают, что программисты Microsoft являются идиотами, злодеями или еще кем-либо в этом духе. И в то лее время, эти примеры довольно кратко и убедительно демонстрируют, что даже такая простая вещь, как обеспечение работоспособности несложной программы на более чем одной платформе Win32, может оказаться отнюдь не простой задачей. В конце концов, что мы наблюдаем? Одна программа жизнерадостно работает, предоставляя пользователю совершенно бесполезный интерфейс, другая по совершенно неочевидной причине отказывается загрузить простой документ и при этом всего лишь констатирует этот факт, а третья приводит к появлению абсолютно ложного сообщения об ошибке, которое может запросто заставить пользователя поверить в «потерю или порчу» упомянутой системной динамической библиотеки (которая, на самом деле, находится на месте и пребывает в полном порядке). И я предвижу очевидный находчивый ответ моих оппонентов: «ЕрунДа Все эти нелепости можно легко устранить, сделав так, чтобы программ проверяла версию Windows, под которой она запущена». На что я тут #е скажу: «Вы попали в самое яблочко! Именно в этом и состоит проблема! И спа сибо за подсказку плавной логической связки для перехода к следующей тем разговора...» Это незначительная деталь, но я полагаю, что ее следовало упомянуть i^c нибудь в этой книге В течение некоторого времени, общеизвестным "ь с предположение, что системный файл RICHED32.DLL (динами^, библиотека, обеспечивающая работу форматированных п°' q редактирования под Win32) рано или поздно прекратит свое сам°с 'tf тельное существование, а все его функции войдут в с°с'
пппверкз версии и платформы 463 х- 9 COMCTL32.DLL. (Например, посмотрите статью Дэйва Эдсона под заголовком "Chicago's Interface Gadgets, Part II- Toolbars, Status Bars, and the RichEdit Control", которая публиковалась в августовском выпуске Microsoft Systems Journal за 1994 год.) Но, оказывается, теперь этот план изменился, и RICHED32.DLL останется отдельным файлом (согласно тому, что я узнал из недавнего разговора с кем-то из сотрудников Microsoft). Проверка версии и платформы Итак, на одной стороне уравнения мы имеем все эти различия между реализациями Win32. На другой — механизмы, доступные системе и вашей программе, позволяющие предохраниться от нежелательных последствий этого разнообразия. Казалось бы, задача вашей программы в этом плане ненамного сложнее того тривиального метода, который предлагается самой системой: например, вы указываете, что «ожидаемая вашей программой версия Windows» равна 4.0, и тогда ваша программа будет запускаться только теми разновидностями Windows, которые имеют номер версии, больший pi ли равный 4.0. К сожалению, это только так кажется. С легкой руки Microsoft, Windows NT 3.5 и 3.51 преспокойно запускают программы, помеченные версией 4.0, и, следовательно, мы не всегда можем положиться на данный механизм. (Кстати, Win32s еще менее разборчива в этом вопросе — она позволит работать и программам с версиями 4.1 и 5.0.) Кроме того, мы, на самом деле, имеем три вариации Win32 API, которые вообще неразличимы по номеру версии. А это значит, что у нас нет единого, централизованного способа определения, на какой из этих трех платформ рабо- мет программа. Было бы здорово, если бы Microsoft обеспечила какой-то способ идентификации программ, предназначенных для работы на этих платформах, и соответствующий механизм для разработчиков. Например, в РЕ-формат исполняемого файла можно было бы добавить поле или битовые Флаги, которые указывали бы на те платформы, которые должны запускать Данную программу. Далее, разработчикам можно было бы предоставить отельные битовые значения для Windows NT, Windows 95 и Win32s, а уста- новку любой их комбинации можно было бы производить при помощи соответ- СГ13Ующих ключей компоновщика. И тогда при попытке запустить программу, 8 которой отсутствует нужный бит, система могла бы запрещать этот запуск и ВЬ1Давать пользователю адекватное и вразумительное сообщение. (На самом ^еле, в РЕ-формате есть некое поле под названием «Subsystem», но оно пред- Пцзначено совсем не для того, о чем я говорю — оно указывает такие ' с1Рактерпстики программы, как использование/неиспользование ею Windows J^I, текстового режима OS/2 и пр.)
464 Разновидности/^ Но это все не более чем мечты н теории. В условиях суровой действите носи, все, что есть в нашем распоряжении - это довольно грубый и ненадс^ ный механизм проверки версии программы. Никого нет дома Что происходит, когда программа неявно ссылается на функцию в DU если эта функция на самом деле отсутствует? Под 16-разрядной Windows, такая ссылка будет разрешена в момент возникновения соответствующего вызова (т.е. когда программа уже запущена) при помощи специальной подпрограммы в KRNL386.EXE (UNDEFDYNLINK), которая просто напросто прекратит работу программы и покажет пользователю сответствующее сообщение об ошибке. Если рассмотреть все возможные варианты обработки описанной ситуации, то данный вариант наверное является самым худшим. 32-разрядная Windows поступает иначе -- если в неявно используемой DLL отсутствует требуемая программой функция, система откажется запускать такую программу. При этом Win32s рапортует о некоей «Error 21», Windows NT скажет пользователю, что «The ordinal <number> could not be located in the dynamic link library <name>» («Функция с номером <number> не найдена в динамической библиотеке <name>») и «The application failed to initialize properly (0xc0000138)» («Приложение не смогло нормально инициализироваться (0хс0000138)»), a Windows 95, вероятно для того, чтобы выиграть приз «Кто кого больше собьет с толку», выдаст целых два сообщения подряд - см. рис. 14.3. (Что там за ерунда происходит с Windows 95, и откуда вообще берется это сообщение о «device attached to the system is not functioning» («подключенное к системе устройство не функционирует»)? Я видел его неоднократно, но ни разу оно не оказывалось хоть сколько-нибудь осмысленным в контексте произошедшей ошибки! С тем лее успехом это сообщение могло бы звучать как «Опаныш!».) Несмотря на определенную придурковатость некоторых из вышеупомянутых сообщений, я все-таки предпочитаю подход, избранный в Win32 (в отличие от Win 16, где аварийный выход может произойти в самый неожиданный и самый нежелательный для пользователя м°" мент — например, когда работа с программой уже в самом разгаре, или когДа в памяти имеются несохраиенные данные пользователя). ШШШЁГй] Л Tbe!ibEM.L32E>'E6fefc- lirteU to-missing &фог£ Ш!Э2 OLh.2 Ж С 'C:enTestWdll32\WiriP«l\U5edB:2.<xe \ device attached to the *-v lent« net f Jt Kiiontng { !*Ffc Рис. 14.3 Как умеет рапортовать Windows"
пяерка версии и платформы 465 Вы спросите, к чему весь этот долгий разговор о реакции системы на от- тВпе неявно используемой функции в DLL? Причина проста: Win32 API „ждется для вашей программы набором именно таких функций. И поскольку >все из них поддерживаются различными разновидностями Win32, перед Mi- ()SOft стояла непростая задача: как обращаться с программами, намеревающи- 11>я использовать те элементы API, которые не поддерживаются данной ,1СТемой? Существуют два подхода к этой проблеме, которые я опишу ниже в 0рядке уменьшения их предпочтительности (с точки зрения разработчика). 3 Условная компиляция. При таком подходе разработчик должен заранее решить, какие версии операционной системы будут поддерживаться. Затем он может использовать различные переключатели, опции, библиотеки для построения отдельных версий своей программы, ориентированных на одну или более операционных систем. При этом сам инструментарий разработки не должен позволять оставить в программе обращения к тем элементам API, которые не поддерживаются целевой платформой. ( В причудливом царстве Win32 такой подход потребовал бы от Microsoft некоторых дополнительных усилит!. Например, только стандартные заголовочные файлы пришлось бы вдоль и поперек разукрасить секциями условной компиляции примерно такого вида: #ifdef WINNT BOOL PolyDraw(HDC hdc,CONST POINT *lppt,CONST BYTE *lpbTypec,int cCount); #endif (вышеприведенный пример, разумеется, выдуман). Или другой, еще более хлопотный, вариант -несколько версий заголовочных файлов (по одной на каждую возможную платформу). В любом случае, поддержка и сопровождение такого механизма не сулило бы Microsoft ничего кроме лишних забот и кошмаров. Поэтому я думаю, что не ошибусь, если скажу, что мы вряд ли дождемся от них чего-нибудь подобного в ближайшее время. Тем пе менее, данный механизм стоит упомянуть, потому что этот разумный подход в сочетании с описанной выше моделью работы с неразрешимыми неявными ссылками обеспечил бы нам достаточно безопасную и надежную инфраструктуру для разработки программ, относительно элегантно ведущих себя на разных Win32- платформах. Кстати, та поддержка распознавания версий и создания совместимых программ, которая существовала во времена Windows 3.0 pi Windows 3.1, по сути является разновидностью условной компиляции. 14 хотя метод обработки неразрешенных ссылок был тогда далеко не самым совершенным, проверка версий системой была достаточно хорошей для того, чтобы удовлетворить большинство наших нужд (по
466 Разновилности w -—■——Х!^ш крайней мере, при работе с системными DLL. У нас даже была можпость умышленно пометить программу версией 3.0, при иеобх °* мости включить в поставку такие файлы, как COMMDLG.DLL ^ результате относительно малой ценой создать программу, прекр- .• В работающую как под Windows 3.1, так и иод Windows 3.0. ° В Условное выполнение. Этот подход, выбранный Microsoft и дополн ный странной моделью проверки версий системой (при котоп - 3.51==4.0), целиком возлагает бремя разборок с несовместимость Win32-iLiarK{)opM на программиста. В этом случае заголовочные файлы содержат все и вся, без каких бь то ни было условных компиляций, соответствующих различиям Win32 API. (Да, включаемые файлы действительно содержат многочисленные секции условной компиляции, связанные с поддержкой Unicode, но это относительно самостоятельная проблема, а ее более или менее грамотное и разумное решение скорее является исключением из правил Microsoft.) Co стороны самой системы, «отсутствующие* функции просто реализованы в виде заглушек. Если ваша программа, работая под Windows 95, попытается вызвать функцию PolyDrawO, как это делает MAZELORD, то система ничего не сделает в ответ и вернет значение FALSE. (Большинство «заглушённых» функций индицируют провал вызова и устанавливают расширенный код ошибки, равный 120, то есть ERROR_CALL_NOTJMPLEMENTED.) Если задуматься, то данный метод реализации «отсутствующих» API достаточно курьезен. Просмотрите файл WIN32 API. CSV или другую справочную документацию по Win32, и вы увидите целое множество таких «заглушённых» функций. И в то же время у вашей программы есть только два реальных способа избежать проблем, связанных с такими «заглушками». Первый способ — попросту игнорировать их. Но этот только кажущаяся простота: при таком качестве документации" в отсутствие какой-либо инструментальной поддержки на этапе компиляции и компоновки вам будет на так уж легко выяснить точный я n°v ный набор функций, который следует выбросить в канаву. Второй способ заключается в том, что теоретически вы можете сопровождать ш пользование каждого «заглушённого» API примерно такп-м заклинаниями: з f (• SomeAPIThatMightBeNTOnlyK/* параметры */)) { if(GetLactError() == ERROR_CALL_NOT_IMPLEMENTED) { // попытаемся повторить попытку, используя аналогичные // функциональные возможности Windows 95 и/или Win32s
пверкв версии и платформы 467 9 else ErrorHandler(/* параметры */), i Я никогда не видел, чтобы кто-то делал такое, я знаю, что сам никогда не внесу такой мерзкий код в реальную программу, и я искренне надеюсь, что вам тоже не придется проходить через нечто подобное при написании ваших публичных программ. Было бы нелепо предлагать программистам писать такого сорта код, и я хотел бы верить, что это не входило в истинные намерения Microsoft. Но тогда зачем же с самого начала утруждать «заглушечные» функции установкой такого расширенного кода ошибки? Единственное разумное объяснение, которое я нахожу, — это некоторая подмога при отладке программ. Например, если ваша программа не делает то, что должна делать, и если вам не удается найти никакую другую причину такого ее поведения, вы можете прибегнуть к пошаговой проверке соответствующего участка кода на предмет нечаянного включения в него одной или нескольких функций, которые не поддерживаются текущей Win32-плaт- формой. Лично я еще не наступал на эти грабли, но я уверен, что рано или поздно наступлю. И если учесть многочисленные ошибки в документации (такие как, например, явное утверждение о работоспособности функции Poly Draw () под Windows 95), то я также не сомневаюсь, что на это обречен не только я один. По сути, модель условного исполнения вынуждает программиста делать «вручную» ту работу, которую в модели условной компиляции за него автоматически выполнял бы компилятор. Такая ситуация не является справедливым компромиссом, и я считаю ее огромным шагом назад. (В конце концов, разве не было изначальной целью создания компьютеров перекладывание на их плечи всей черной, рутинной работы и высвобождение человека для творческого труда?) На мой взгляд, самый неприятный аспект данной проблемы — это те неоправданные трудности, которые могут стоять на пути поиска неработающих API в реальных программах. Многие программы вызывают по нескольку системных функций подряд, и если одна из них, находящаяся в самой середине, оказывается неработоспособной на данной платформе, то выслеживание ее может оказаться ужасно дорогим и утомительным развлечением. Сложность такой задачи только усиливается тем фактом, что практически никто не проверяет успешность каждого отдельного вызова функции. Лично я точно не делаю этого, хотя, уверяю вас, я далеко не самый последний параноик в рядах программистской братии. Загляните в файл DRAW.С программы- примера MAZELORD, и вы увидите, что она никогда не проверяет значение, возвращаемое функцией PolyDrawQ. И это
468 РазновиАности\л/;п неудивительно — ведь вызовы этой функции стоят в конце процеду рисования. И даже если бы код обнаруживал в этом месте провал -'* что же ему следовало бы делать дальше? Сообщить пользователю \ ° продолжить работу невозможно, и после этого завершить программ г Конечно, это было бы все же лучше, чем то, что делает нынещн версия MAZELORD — истинный солдат в смысле полного безра личия к собственной гибели. Но такая честность со стороны програ,\ мы по удобству даже близко не стоит к более практичному и разумНо_ му сценарию — проверке текущей версии и платформы сразу после запуска и вежливому отказу от работы, если дальнейшая полноценная работа невозможна. Такие дела. И как же теперь нам быть? К сожалению, хорошего ответа на сей риторический вопрос я найти не могу. В отличие от менее радикальных «шероховатостей» в реализации API на разных платформах, которые зачастую можно легко «заасфальтировать» при помощи оберточных функций, заглу- шечные функции являются гораздо более крепким орешком. Вы могли бы «переоборудовать» заголовочные файлы и потребовать, чтобы ваши собственные программы всегда пользовались этими скорректированными заголовками н какими-нибудь макроконстантами типа WINNT, WIN95 и WIN32S для того, чтобы еще на этапе компиляции исключить возможность использования элементов API, не поддерживаемых целевой платформой. Но этот метод требовал бы таких больших трудозатрат и был бы столь подвержен ошибкам, что мог бы оправдать себя, вероятно, только в очень необычных условиях. Если вы оказались в достаточно отчаянном положении, вы также можете прибегнуть к написанию оберточных функций, проверяющих текущую платформу и имеющих примерно следующий вид: BOOL platfоrmSomeAPI(/* параметры */) { #ifdef _DEBUG if(! ((WINNT | WIN95) & current_platfonn)) MessageBox(0, "Попытка вызвать SomeAPIO1''* , "Нарушение совместимости1', MB_ICONEXCLAMATION | MB.SYSTEMMODAL), #endif return SomeAPI(/* параметры */), i где платформы, поддерживающие данный API, представлены битовыми фл»га ми (в приведенном примере — это WINNT и WIN95), a current_platform — эТ глобальная переменная, которая установлена один раз при запуске програ^1,. с использованием тех же самых битовых флагов. Помимо того, что даннь трюк является довольно трудоемким, он требует от вас осторожности хорошей осведомленности о том, какие элементы API какими платформ поддерживаются. (То есть этот «оберточный» метод совсем не снимает оД**У серьезнейших исходных проблем. Однако, если немного подумать, т°
«ппве£ка версии и платформы 469 крывает перед нами одну интересную возможность: задавая различные эоизвольные значения переменной current_platform, вы сможете тестировать uly программу па совместимость с различными платформами, не переключая lMv вашу рабочую машину на эти платформы. Кроме того, эта схема может %1ть легко расширена для решения проблем, связанных с поддержкой раз- мчных версий Windows. Хмм.) Если перейти от «доморощенных» средств к уже готовым инструментам а3ра6отки, то радужных перспектив также не видно. Например, выясняется, чГ0 вам не следует рассчитывать па помощь отладочной версии Windows. Я не проводил исчерпывающих тестов, но, например, использование специфичных юлько для Windows NT функций не вызывало никакой реакции со стороны от- юдочной версии Windows 95. Есть, правда, один замечательный инструмент под названием Bounds Checker Pro от NuMega Technologies, Inc. Возможности этого продукта весьма впечатляют (см. мои рекомендации в главе 15), и, в частности, он позволяет огслеживать используемые вашей программой функции и сообщения и классифицировать их в соответствии с текущей платформой. Эта функциональная возможность действительно может облегчить ваш труд, но, к сожалению, она не помогает решать проблемы, связанные с поддержкой различных версий. Если вы думаете, что я преждевременно ставлю вопрос о версиях, то задумайтесь над тем, чего можно ожидать от грядущего появления Nashville и Memphis - двух последующих версий Windows 95, от Windows NT 4.0 и Cairo, не говоря уж о nape-тройке новых версий Win32s. Я почти не сомневаюсь, что спустя некоторое время написание программы может оказаться достаточно рискованным предприятием, как только вы захотите сделать ее совместимой с такими «старыми» реализациями Win32, как нынешняя Windows 95 или (о Ужас!) Windows NT 3.51. Было бы несправедливым не вспомнить в этот момент о Win32c Помните это название, которое Microsoft дала подмножеству Win32 API, реализованному в Windows 95? (Предположительно, суффикс «с» соответствовал первой букве первоначального рабочего названия этой версии Windows — «Chicago».) Это название упоминалось во многих местах и в какой-го мере служило неявным признанием существования трех версий Win32 API. Win32 (Windows NT), Win32c (Windows 95) и Win32s (Win32-on-Winl6) Даже сейчас вы еще можете встретить ссылки на Win32c в текущей редакции Microsoft Development Network CD-ROM. Однако, мы уже довольно давно не слышим это название из уст Microsoft и ее сотрудников -- видимо, сами они уже окончательно уверовали в существование «последовательного п единообразного Win32 API»
470 Разновидности w- Занесите этот диск в Красную ...иад Если вы уже отдали ваш CD-ROM с Visual C++ 2.0 своей баке в качестве игрушки для развития челюстей, или уже при '° пяете его в качестве подставки или мобиле, сейчас же отчим, книгу в сторону и достаньте этот диск! Поверьте мне, это оче важно. Этот старый диск с Visual C++ 2.0 может еще не раз в. пригодиться, благодаря тому интригующему способу, которьг избрала Microsoft для построения 32-разрядных Windows-при%. жений и пометки их для различных версий ОС. (Возможно слишком простодушен, но эта проделка Microsoft действительно очень удивила меня.) Дело в том, что в поставку Visual C++ 2 1 п 2.2 включена одна и та же версия компоновщика LINK.EXE, которая по умолчанию помечает исполняемые файлы версией 4.0 д LINK.EXE от Visual C++ 2.0 ио умолчанию использует номер версии 3.1. Почему это должно вас волновать? Хотите верьте, хотите нет, ио LINK.EXE от Visual C++ 2.1 и 2.2 ие желает признавать настройку на версию Windows, меньшую 4.0. Если вы попробуете использовать значения 3.0, 3.1 или 3.5 (например, указав в интегрированной оболочке Visual C++ опцию компоновщика «/sul)system:windows,3.5»), то они будут отвергнуты и компоновщик использует значение по умолчанию — то есть 4.0. Поэтому, если вы захотите собрать исполняемый файл для более старой версии Windows, чем 4.0, вам попросту ие обойтись без старой версии компоновщика LINK.EXE. Так что, увы, вашей собачке придется расстаться со своей новой игрушкой. Кстати, компоновщик от Visual C++ 2.0 иногда тоже ведет себя странным образом. Если вы просто попросите его построить исполняемый файл вашей программы, он радостно выполнит ваДО просьбу и пометит созданный файл для Windows 3.1. Ио если вы попробуете явно указать ему, что желаете построить программ для версии 3.1, компоновщик почему-то выругается — пожалуй па то, что значение 3.1 является «неправильным» — после чеГ0 скажет вам, что он будет использовать значение по умолчанию. к° торое равно (ах!) 3.1. Еще более странным является тот факт, что все эти комп°н0 тики от Visual C++ прекрасно воспримут значения версии W11 dows, равные 4.1, 5.0 и т. д. Почему я считаю это странным? Ра3 \ гуго не является свидетельством заботы Microsoft о буду^1 ' результатом перспективного планирования, о котором я нам уши прожужжал в этой книге? Нет, я так ие думаю, потому ч если вы попробуете запустить 5.0-программу иод Windows 95»
<]ф&£™- версии и платформы 471 станете свидетелем спектакля, изображенного на рисунке 14.4 (обратите внимание на очередной выход на сцену популярного и вездесущего сообщения о том, что «device attached to the system is not functioning»). Windows NT 3.51 также не имеет иммунитета против этой глупости — на том же рисунке 14.4 показано сообщение этой операционной системы в ответ на запуск того же самого 5.0-приложения (сообщение, немедленно вызывающее у пользователя реакцию типа «Господи, ну хоть один намек — почему?»). Но самое большое развлечение доставит вам Win32s 1.30, всегда славившаяся своей раскрепощенностью: она попросту запустит программу без каких-либо разговоров. Интересно, а что же будет, когда действительно появятся реальные программы для версий Windows, более новых, чем 4.0, и пользователи Win32s начнут эти программы запускать? Вот вам и еще одна причина задуматься над «ручной» проверкой версий pi платформ в ваших программах — на механизм, предоставляемый операционной системой, явно нельзя полагаться. Для желающих повторить все эти эксперименты, в WWW- библиотеке имеются специальные версии одной из программ- примеров, помеченные для Windows 4.1 и 5.0 (см. файлы WV41.EXE и WV50.EXE в каталоге WINREL). Windows 95 Windows NT :f he GAZEftl NCHSh?WIN32&E R \WI NREt^WVSftEfeae tirade уШ Vf, idu^? Vision... OK I Cannot Run Program Cannot Run Program i№£ Рис. 14.4 Попытки запустить программу, предназначенную для Windows 5.O.
472 Разновилности W: _ _ --~11^/Ш «Yikacmuku» OK, теперь пора перейти к более подробному разговору о тех конкреп проблемах совместимости Win32 API, которые, на мой взгляд, являются более важными, и которые я имел возможность изучить самостоятельно п обсуждаемые ниже примеры собраны из многих источников — из моего соб венного практического опыта, опыта моих друзей, а также из документации М- crosoft, форумов CompuServe и настенных надписей в мужских туалет- программистских компаний и баров. Что касается самого названия «ужастик», то его происхождение подроби разъяснено в предисловии к одноименному разделу главы 12. Если же попытаться сформулировать причину кратко, то я бы сказал так: когда вы столкнетесь с любой из этих причуд Windows API в самый неподходящий для этого момент (обычно, это так и происходит, как назло), ощущения окружающего вас таинственного кошмара вам гарантированы. Сразу оговорюсь, что при оценке важности для вас каждого примера, я в первую очередь ориентировался на Windows 95 и Windows NT 3.51. Я также хотел поподробнее охватить и такие разновидности Windows, как Windows NT 3.1 и 3.5 (поскольку они все еще актуальны и уместны в свете обсуждаемой темы), но у меня просто не хватило времени на это. Аналогично, я вовсе не отрицаю важности Win32s как таковой, но я определенно считаю эту платформу менее интересной, чем Windows 95 и Windows NT; поэтому вы найдете относительно мало примеров «из жизни Win32s», и мое тестирование ограничивалось лишь версией 1.30 — той, которая появилась на свет вскоре за выходом Windows 95. Перечислив все то, чего вы не увидите в последующих примерах, я должен высказать пару предварительных замечаний и о том, что включено в эти рассказы. Если вспомнить все официальные утверждения Microsoft о «единообразии и последовательности» Win32 API для всех трех 32-разрядных платформ, то нижеприведенный перечень проблем, а также признанные самой Microsoft несовместимости и странности со всей очевидностью показывают, насколько скептически вы должны относиться к всем этим претенциозным заявлениям. В пестром букете Win32 API встречаются экземпляры, которые в Де11' ствительности реализованы только на одной или двух платформах, есть образцы, которые немного по-разному работают иа разных платформах, и, на' конец, есть даже такие константы, которые определены только для одн01 платформы, но при этом безропотно принимаются другой разновидность* Win32 и приводят к возникновению откровенно ошибочных данных. Иначе говоря, мы имеем дело с поразительной путаницей, чреватой У^ас ными неприятностями. И благодаря многим проектировочным решениям crosoft (таким, как неработоспособный механизм проверки версий пспоЛ# мых файлов, наличие 16-разрядных системных компонентов, «заглушек
^астики» 473 GDI обрезает значения координат 9 о двух байт Сегодня каждый знает, что Windows 95 не является стопроцентно 32- операционной системой. Помните, какая шумиха, какие бес-
474 Разновидности w .——^ZX1!}32 конечные дискуссии поднялись в прессе и информационных сетях, Ко впервые был обнародован тот факт, что в брюхе этого чудовища еще порЯд0ч а 16-разрядного кода? Сегодня это уже почти история. Но если учесть е щепризнанный ныне неслабый аппетит Windows 95 в отношении оперативн * памяти, то я не могу удержаться и не обратить ваше внимание на ту жестоку' иронию, которая скрывалась в не столь уж давних заявлениях о том, что одно" из основных причин сохранения 16-разрядных компонентов являлось стремv ние сделать операционную систему максимально экономичной. (Например, -п гляните в майский номер Microsoft Systems Journal за 1994 год, где в стать Адриана Кинга под названием «Memory Management, the Win32 Subsystem and Internal Synchronization in Chicago» было написано буквально следующее' «Поскольку целью являлось обеспечение нормальной работы Chicago на систе мах с 4-8 Мбайт ОЗУ, данное увеличение [размера базовой рабочей системы вызванное конвертацией USER и GDI в 32-разрядный код] было неприемлемо».) Но я, кажется, отклонился от темы. Самым серьезным побочным эффектом этого архитектурного решения Microsoft является уже неоднократно упоминавшаяся проблема с обрезанием графических координат: все функции GDI, работающие с координатами, принимают 32-разрядные знаковые целые (в строгом соответствии с документацией по Win32 API), но при этом отбрасывают старшие 16 бит. Про эту «особенность» в документации по MFC не сказано ни слова, и упоминается она лишь в API32.HLP (в справочном файле от Win32 SDK). Наихудшая деталь во всей этой причудливой картине — полное отсутствие какого-либо оповещения вызывающей программы о том, что что-то не в порядке. GDI-функция возвращает значение, которое индицирует нормальное завершение работы, в то время как на самом деле функция либо ничего не сделала (если обрезанное значение оказалось недопустимым), либо сделала нечто экзотическое и неожиданное (если обрезанное значение оказалось допустимым). Поскольку 16-битные графические координаты (те, с которыми призван работать 16-разрядный модуль GDI) по определению также являются знаковыми, обрезанное положительное 32-разрядне число, имеющее единицу в 1э*м разряде, будет интерпретировано как отрицательное. В результате, передавав мые вашей программой координаты могут не только радикально менять св01 абсолютные значения, но и менять знак. То есть графический вывод ваш? программы может быть буквально поставлен с ног на голову. На рис. 14.5 вы можете увидеть, как выглядят результаты Ра°° о программы-примера GDICLIP под Windows 95 и Windows NT 3.51. Картин^ возникающая под Windows 95, выглядит весьма симпатично. Но лишь Д° пор, пока вы не посмотрите в исходный код и не заметите, что в действите' ности правые и нижние границы прямоугольников задаются числа» превышающими 65536. И истинно правильный вариант этих картинок — к°
ужастики» 475 Windows 95 ЩЩЩ^ШШ Windows NT Рис. 14.5. Работа программы GDICLIP под Windows 95 и Windows NT. В исходном коде программы GDICLIP нет ничего особенного, кроме одной йаЖной детали: он проверяет значение, возвращаемое функцией Rectan^leO, и J( чучае возникновения ошибки должен выдавать пользователю сообщение. Зачтите эту программу под Windows NT n Windows 95, п она даже не пикнет. Листинг 14.1. Рисование в GDICLIP //////У////////////////////////////////////////////////////////////////////// Ч CGdiclipView drawing v°id CGdiclipView::0nDraw(CDO pDC) CGdiclipDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc)t // При таких огромных координатах, правый нижний угол каждого
476 Разновилности \л/: . _^/л // прямоугольника должен оказаться далеко за пределами нашего // окна. Под Windows NT именно так и происходит Но под Windows 95 // эти координаты обрезаются, и мы получаем совершенно другой // результат (и не получаем никакого намека со стороны Windows о // том что что-то неладно) for(mt 1 = 1 1 ^ 11, i++) if('pDC->Rectangie(i * 5,i * 5 65636 + i * 5,65636 + i * 5)) MessageBox( Вызов Rectangle провалился1 , GDIClip \MB_0K),, Построение примера: COf cup Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chapl4/ gdiclip Платформа: Win32 Инструкции по сборке: откройте при помощи Visual C++ 2.2 прилагающийся МАК-файл и скомпилируйте как обычно. Запустите результирующий исполняемый файл под Windows 95 и Windows NT, и вы увидите разное поведение. Если вы можете гюзволргть себе такую роскошь, как уверенность в достаточно малых значениях всех используемых вашей программой координат, особой проблемы тут нет. Вперед, используйте ваши 16-разрядные значения, но постарайтесь не забывать, что в 15-м разряде вас поджидает засада. Что делать, если вы написали программу, которая вовсю использует 32- разрядное пространство координат под Windows NT? Увы, в этом случае вам серьезно не повезло, и ваша программа никак не сможет нормально работать под Windows 95 (по крайней мере, без значительной модификации кода). Если мы стремимся придерживаться той философской точки зрения, что у программы есть только один допустимый вариант работы — корректный, и что все возможные отклонения от этого пути должны быть заблаговременно обнаружены и предотвращены, то как же быть в данной конкретной ситуации с обрезанием координат? Одно из решений — создать и использовать оберточные подпрограммы для всех «порочных» GDI-функций из Win32 А" Именно такой подход к данной проблеме использован в моей библиотеЬ Win32u, подробно представленной в приложении С. И в последний раз- обращение Windows 95 с GDI-координатами являе громаднейшим извращением. Она не только совершает самый плохо* всех возможных проступков (принимает, но не использует 32-разр^1 значения координат), она еще калечит их и втихую использует пол) ные останки, не предоставляя вызывающему коду ни малейшего на^,еч<1,| том, что случилось. На самом деле, вкупе с аналогичной «обработкой дексов в окнах-списках (см. ниже), данная проблема настолько У#' ;а^
ужастики» 477 что, ей Богу, пора, наконец, учредить Орден Нулевого Указателя за плохой дизайн и тут же торжественно вручить первую награду Microsoft. Окна-cnucku обрезают значения строковых индексов Двоюродной сестрой проблемы с GDI-координатами является аналогичная проблема с индексами, которые передаются программами окнам-спискам. Род- гвенность этих проблем можно понимать буквально: у них одно и то же происхождение. Как вам вероятно известно, в Win32 API есть несколько сообщений с префиксом LB_, принимающих одно или два 32-разрядных целых числа, которые, в свою очередь, указывают на те элементы списка, над которыми будут производиться те или иные действия. Вы правильно догадались: Windows 95 тайком выбрасывает из этих чисел старшие 16 бит и индицирует провал юлько в том случае, если останки ваших индексов случайно оказались негодными для дальнейшей работы. При таком «подходе» система легко может превратить неправильный параметр в правильный (например, обрезать 65537 до 1), а затем использовать те данные, которые программист никогда не намеревался передавать (и на самом деле не передавал). И все это без какой- табо индикации об ошибке. Если бы Windows 95 была достаточно смышленой и вместо обрезания индексов просто возвращала LB_ERR, тогда у программ был бы хоть какой-то шанс в борьбе за корректность своей работы. Было бы еще лучше, если бы Windows 95 могла возвращать специальный код ошибки (например, LBJPARMERR) в случае передачи окнам-спискам недопустимых значений индексов. В справочном файле API32.HLP, в тех местах, где описываются LB-сооб- 'Дшия, неоднократно встречаются замечания следующего вида: Только для Windows 95: параметр wParam ограничен только 16- разрядными значениями. Это означает, что окно-список не может содержать более, чем 32,767 элементов. Хотя количество элементов ограничено, суммарный размер самих элементов ограничивается только количеством свободной памяти. Это утверждение абсолютно корректно в отношении ограничения на ко- !11{ество элементов списка. Запустите программу-пример LBCLIP, продолжай- Убавлять в список новые строки при помощи кнопки «Add», и в какой-то °х1ент (как раз когда количество строк приблизится к 32767) вы получите солнце о нехватке памяти. Однако, это утверждение очень обманчиво, когда еМ говорится о том, что «параметр wParam ограничен только 16-разрядными Ненпями». Если бы Windows 95 отказывалась принимать строковые индек- ' Сходящиеся за пределами интервала от -32768 до 32767, тогда это за-
478 Разновилности щ мечание было бы обоснованным. Но в действительности такие значения не от. ведение, отнюдь очевидное, после прочтения вышеупомянутого описания. вергаются, а калечатся и используются дальше — поведение, отнюдь Построение примера: LBciip Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/chapi4/ Ibclip Платформа: Win32 Инструкции по сборке: откройте при помощи Visual C++ 2 2 прилагающийся МАК-файл и скомпилируйте как обычно. Запустите результирующий исполняемый файл под Windows 95 и Windows NT, поэкспериментируйте с кнопками, и вы увидите разное поведение окон-списков. Мне трудно понять причину такого подхода Microsoft к решению проблемы с ограничением на количество строк. Особенно, если учесть, что правильное решение лежит на поверхности и является таким простым и недорогим. Все, что им нужно было сделать, это вставить в места обработки 32-разрядных LB-co- общений примерно такой код: if((wParam > 32767) || (wParam < -32768)) return LB_PARMERR; else { /* Вызов 16-разрядной реализации */ } И я категорически не соглашусь с теми, кто, возможно, попытается возразить — мол, добавление такой проверки в обработку каждого сообщения увеличило бы накладные расходы. Те несколько дополнительных инструкции, которые нужны для реализации такого простого pi быстрого теста (на самом Де" ле, их число вероятно не превышает 10), — пренебрежимо малая величина по сравнению с теми издержками, которые и без того несет это семейство функши1 Win32 API (маршрутизация сообщений, управление внешним видом окна другие проверки pi т.д.) К сожалению, в данном случае мы имеем дело не с функциями, а с соо® щениями. Поэтому применение оберток здесь более проблематично, чеП случае с GDI-координатами. Разумеется, написать сами оберточны; функции — проще простого, зато дальнейшее использование их м°* потребовать немного больших усилий. Если использование GDI-оберток с дится к тривиальной замене имен исходных функций на имена соотв ствующих оберточных функций, то теперь в большинстве случаев приДеТсЯ менять вызовы SendMessageO на вызовы соответствующих оберток (кот°Р
црстЦКЦ. 479 10т сильно отличающийся синтаксис). Даже такие небольшие дополнитель- е упражнения с клавиатурой для некоторых могут послулсить достаточным поЦОУ- Для отказа от использовання оберток. Цо я всегда был оптимистом, поэтому я, несмотря ни на что, включил в '.«биютеку Win32u полный набор LB-оберток. Я хотел бы вновь обратиться к одному вопросу, который является скорее философским, и, наверное, никто не пострадает от его повторения: какими могут быть приемлемые, разумные отклики на вызов функции или АРГ вообще? По моему глубокому убеждению, у вызываемого кода есть только два законных варианта действий: (1) выполнить требуемую операцию в строгом соответствии с документацией или (2) отказаться от ее выполнения, недвусмысленно указав вызывающему причину отказа Очевидно, разнообразные ошибочные ситуации (в том числе передача недопустимых параметров) попадают во вторую категорию В свете такой постановки вопроса, нынешнее поведение окоп-списков в отношении 32- разрядных индексов безусловно является грубым нарушением контракта с вызывающим кодом. Я знаю, что некоторые программисты не считают подобные вопросы существенными — «Вызовите функцию, передав ей неправильные данные, и в ответ вы получите то, что заслуживаете», — говорят они. Я думаю, такая позиция является свидетельством невероятной наивности и лени. Она прямиком ведет к ошибкам в коде, потому что программисты оправдывают этой байкой свое нежелание тщательно работать над своим кодом и документацией. Я даже слышал такое мнение, что все API, функциональные библиотеки, но определению, «предназначены для использования лишь теми, кто знает, что делает». Мол, поскольку они являются программным, а не пользовательским интерфейсом, то приемлема любая реакция на вызов с неправильными параметрами или данными, вплоть до краха всей системы. Я искренне надеюсь, что ни в одной из программ, которыми я пользуюсь для дела, нет ни одной строчки кода, написанной такими людьми. Окна-списки не могут обрабатывать аяинные имена файлов в Windows 95 Что? Опять что-то про длинные файловые имена? Увы, да. Длинные имена аи1ов в Windows 95 можно смело уподобить сказочному дракону — стоит вам ^Умать, что вы уже отрубили ему последнюю голову, как появляется новая, че более уродливая и страшная голова. А то и несколько... На этот раз виновником проблем является сообщение LB_D1R, которое по- ( '1етея окну-списку для заполнения его списком файлов, отвечающих задан- а Критериям. Оказывается, что под Windows 95 обработка этого сообщения цсходиТ в 16-разрядном модуле USER и, следовательно, не может
480 Разновилност\л/;п ч правильно обращаться с длинными файловыми именами (даже если обращен делается из 32-разрядной программы). Сам по себе этот факт никак радует - вам придется всегда помнить о необходимости передавать имя кат- лога в виде псевдонима. Ыо есть по крайней мере одно отрадное обстоите г ство: окно-список возвратит вам LB_ERR при передаче ему длинного фай* вого имени (разумеется, как индикацию неправильности заданного критерия вовсе не обнаружения длинного имени файла как такового). В любом случае такое поведение дает вашей программе хоть какой-то шанс быть осведомленно]' о возникновении проблемы (в отличие от случаев с обрезанием GDI-координат и индексов в окнах списков, когда система молча калечит ваши параметры). Простое и очевидное решение, которое первым приходит в голову - добавить в свой программистский арсенал оберточную функцию примерно такого вида: int DoLBDir(HWND lb, UINT attrs, char *dir) { char buffer[MAX_PATH]. if(,GetShortPathName(dir,buffer,Gizeof(Duffer))) return LB_ERR. return SendMe3sage(lb,LB_DIR,attrc,(LPARAM)dir). j. и затем все время использовать ее (даже если вы уверены, что ваша программа работает иод Windows NT). К сожалению, все не так просто, и приведенный выше пример будет работать далеко не всегда. Дело в том, что в качестве критерия для имени файлов сообщение LB_DIR ожидает не просто имя каталога, а полное имя файла вместе с расширением. И в общем случае этот параметр может содержать метасимволы (причем на практике метасимволы используются почти всегда). Это значит, что вы не можете достаточно надежным и общим способом сконвертпровать исходное имя в «чисто псевдонимный» его вариант (например, функция GetShortPathNameO не допускает наличия метасимволов во входных данных). Имея вначале самые серьезные намерения написать ДеН" ствительно работоспособную, надежную и «всепогодную» обертку для сообщения LB_DIR, я провел множество экспериментов и с функцией _fsplitpath(/.' с многочисленными легальными и нелегальными конструкциями имен. 1 ° тщетно — всякий раз, когда я думал, что я, наконец, учел и корректно обра° тал все возможные патологические случаи, обнаруживался еще один к°н™ пример, в котором подпрограмма не справлялась с конвертацией длинно имени в псевдоним. Раз уж речь зашла о сообщении LB_DIR, я должен отметить еще оДИУ' таль: даже когда мы передадите окну-списку имя каталога в виде псевдо1111;.^ под Windows 95 пользователь увидит в списке псевдонимы файлов, а поД dows NT - длинные имена. И с этим вам ничего не поделать, кроме как в°° ) отказаться от использования LB_D1R и реализовать своп собственный с
„Ужастики» 481 оГцчный механизм. Единообразие пользовательского интерфейса вашей р0граммы для Windows 95 и Windows NT — это отдельный вопрос, который ' j должны решать сообразуясь с конкретными условиями вашего проекта. Од- аКо, очень досадным является сам факт возникновения данного вопроса. Microsoft задокументировала обсуждаемую проблему с LB_DIR в своей pSS-статье Q131286. К сожалению, пример кода, показанный в этой статье, вырядит так, как будто он был покалечен в процессе публикации: char czLong [256], szShort [256], DWORD dwResult, LONG IResult. lsticpy (szLong С \\ThiG Ig A Teot Subdirectory"), dwReGult = GetShortPathName (GzLong czShort, 256), if (•dwResult) dwResult = GetLactError (). lGticat czShort, \lReGult = SendDlgltemMesoage (hdlg, IDC_LIST1. LB_DIR, (WPARAM)(DDL_READWRITE), (LPARAM)(LPSTR)czShort), if (LB_ERR == lReoult) // an error occurred Я планировал закончить сей «ужастик» в этом месте, но буквально в один из самых последних дней моей работы над книгой, когда я повторно проводил некоторые эксперименты, произошло пренеириятиепшее событие: я вдруг окончательно открыл для себя, насколько некорректной является в действи- гелыгости обработка сообщения LB_DIR. Сначала душе показалось, что я просто наткнулся па отдельный частный случай, в котором все было наоборот — сообщение LB_DIR срабатывало правильно вопреки заявлениям Microsoft. A именно: при задании в качестве критерия для имени файла строки <Л1трД\**>> ■жно-сиисок успешно заполнялось именами файлов из каталога «c:\temp dir». Я выругался про себя п решил разобраться, что лее тут происходит. Разве на- ,,,1|нс пробела в пути (допустимое только в длинных файловых именах) не явится как раз тем условием, при котором сообщение LB_DIR не должно сраба- |,1в^ть? Если присмотреться, то, несмотря на этот пробел, локальное имя Залога в данном случае имеет длину всего 8 символов. И когда я заменил имя "Залога на «c:\terap dir with a really long name» (и соответствующим образом 1Рнщеновал сам каталог), я облегченно вздохнул — сообщение не сработало, 1Ь и следовало ожидать, согласно документации Microsoft Но, как оказалось, я расслабился несколько преждевременно. Задумайтесь 1 Секуиду - что же означал этот случайно обнаружений аномальный случай 4Atemp dir\*.*>>? Если этот критерий был успешно сопоставлен с реально ^П'вующим каталогом «c:\lcmp dir», то не следует ли из этого вывод, что в М1с>твптельностп обработчик LB__DIR производит поиск файлов по всему '°Странству имел файловой системы, включая длинные файловые имена?
482 Разновилностии/; Ведь псевдонимом данного каталога была строка «TEMPDI-1», Кото очевидно никак не подходит под заданный критерий. У меня сразу появилось нехорошее предчувствие, и я решил продолжи эксперимент. Я создал каталог с именем «c:\temp dir\temp clir», поместит него несколько произвольных файлов и запустил тестовую программу, указ- в качестве критерия «c:\temp.dir with a really long nameemp dir with anoth really long name\*.*>> - все сработало правильно. Я попробовал заменить м тасимволы на конкретное имя файла - все сработало правильно. Наконец заменил «*.*» на «holy cow really long file name.txt», и тест успешно показа- мне в окне-списке файл «holy cow.txt» (разумеется, показан был псевдоним но - обратите внимание - этот «подходящий под критерий» файл явно бьп «найден», благодаря именно своему длинному имени). Что любопытно, когда я заменил расширение «.txt» на «.*», тест провалился и не показал тот же самый файл. Как видите, ситуация оказалась весьма и весьма неприятной: сообщение LB_DIR принимает синтаксически корректный критерий, содержащий длинные файловые имена, и подбирает по нему совершенно неправильный набор файлов. Если бы LB_DIR просто отказывалось работать с такими «длинно- именными» критериями, это было бы пусть неудобно, но как минимум не так рискованно для качества программы (так как быстро обнаруживалось бы при тестировании). Но от такого откровенного извращения, которое имеет место на самом деле, меня просто оторопь берет. Я так долго и подробно все это описывал потому, что, на мой взгляд, данный пример служит очень убедительным и полезным уроком на тему, как следует работать с Win32 API и тестировать его. Если бы кто-то другой, еще не зная о неспособности LB_DIR работать с длинными файловыми именами, провел бы всего один тест, по случайности совпадающий с моим исходным примером с «c:\temp dir\*-*», и увидел бы, что все в этом случае работаег хороню, он мог бы очень легко прийти к ложному и радостному выводу о допустимости использования длинных имен с LB__DIR. Лично я почти наверняка попался бы на эту удочку в таких условиях. Как видите, поведение сообщения LB_DIR оказывается еще более странным, чем это описано в документации № crosoft. Я вас предупредил, делайте выводы. Функция BetLastCrror() возвращает непригодные для работы значения Это еще одна пренеприятнейшая проблема, которая не кажется таковоп первый взгляд -- до тех пор, пока вы не задумаетесь о всевозможных ^ следствиях. Когда же вы представите их, вы захотите плакать. ^ киберпространстве никто слезам не верит.) В Programmer's Guide to Microsoft Windows 95 Microsoft пишет:
мастики» 483 Что касается расширенных кодов ошибок, генерируемых функцией GetLastError, то их одинаковость для Windows 95 и Windows NT не гарантируется. Различия затрагивают функции для работы с графикой, функции управления окнами, системные сервисные функции. Если же называть вещи своими именами, данный кристально чистый флмер недоговоренности следует понимать так: один и тот же Win32 API будет 10-разному интерпретировать одни и те же условия в зависимости от того, на ,аКой платформе работает программа, и эти различия в интерпретациях будут сражены выбором тех или иных расширенных кодов ошибки. Например, функция GetShortPathNameO транслирует длинное имя файла в псевдоним. Что произойдет, если переданное ей длинное имя не соответствует реально существующему файлу? Windows 95 склонна интерпретировать эту ошибочную ситуацию как ERROR_PATH_NOT_EOUND, в то время как Windows NT 3.51 предпочитает рассматривать ее как ERROR_FILE_NOT_FOUND. (Многочис- 1енным причудам этой функции я посвятил отдельный ужастик.) Несогласованность Л¥т32-платформ в отношении расширенных кодов ошибок означает, что вы ничего не можете делать с результатом функции Get- LastErrorO, кроме как проверять его на равенство нулю (все в порядке) или на равенство ERROR_CALL_NOT_IMPLEMENTED (вызов заглушечного API). Конечно, вы можете передать это возвращаемое значение в функцию For- matMessageO, чтобы получить соответствующее текстовое описание ошибки и показать его пользователю. Flo в этом случае пользовательский интерфейс вашей программы перестанет быть единообразным - на разных платформах пользователь будет получать различные диагностические сообщения в одних и тех же условиях. (Кстати, в той копни PSS-статьи Q83520, которая включена в Win32 SDK gold, даже константа ERROR_CALL_NOT_IMPLEMENTED ошибочно упомянута как ERROR_NOT_IMPLEMENTED.) Но все вышеперечисленное — это только полбеды. Самым неприятным ас- Пектом данной проблемы является то, как Microsoft задокументировала исполь- Н)ванпе расширенных кодов ошибок каждой функцией: вообще никак. Просмотрите документацию no SDK, и вы найдете многочисленные вариации °Дпого и того же замечания: при возникновении ошибочной ситуации данная Функция или сообщение будет сигнализировать об этом при помощи возвращае- 10г° значения, а также установит соответствующий код ошибки, который выдающая программа может получить при помощи GetLastErrorO. И при этом ^Какого намека на полный перечень возможных ошибок и соответствующих "'Расширенных кодов. При таком состоянии документации, функция GetLas- lv°r() фактически превращается в простой инструмент отладки, а не в реалъ- е средство обработки ошибочных ситуаций — когда что-то не в порядке, и 1 "°льще всего нуждаетесь в помощи со стороны системы, вы получаете лишь IVl° минимальную подмогу. Если ваш код имеет примерно такой вид:
484 Разновилности W: -Л[{{Ш if (' SoineWin32API(/* параметры */)) Gwitch(GetLactErгог()) { сасе ERROR_PATH_NOT_FOUND /* Обрабатываем ситуацию когда не найден путь */ Dteak default /* Сообщаем о неизвестной ошибке,' завершаем работу */ то ваша программа рискует завершить свою работу без резонных на то оснований - при возникновении нефатальной ошибки, которую легко обработать ц1и которую можно вообще игнорировать. Предположим, ваши тесты под Windows 95 показали, что в отсутствие нужного файла некоторая функция приводит к появлению кода ошибки ERRORPATHNOTFOUND, поэтому отслеживая этот код, вы можете элегантно обработать данную ошибочную ситуацию - например, скопировать заданное имя файла в выходной буфер и далее проигнорировать ошибку. Но вот беда — вы заранее не знали, что в тех же самых условиях ваша программа может получить код ошибки ERROR_FILE_NOTJFOUND, и тогда при работе под Windows NT тот же самый, абсолютно безвредный случай будет обработан иначе — ваша программа закончит работу, предположив, что возникла непредвиденная и непреодолимая ошибка И как же региить подобную проблему? Никак. Microsoft взяла на вооружение простой и практичный механизм индикации ошибок, недостаточно задокументировала его, непоследовательно реализовала на разных платформах п в итоге превратила его в генератор сбоев и ошибок. Вот теперь вы можете начать плакать. У вас есть только два пути: 1) вообще ничего Ere делать с результатом Get- LasiErrorO, кроме тех случаев, когда вам необходимо отслеживать значение ERROR_CALL_NOT_IMPLEMENTED; пли 2) написать оберточные функции, которые будут принудительно устанавливать заранее известные расширенные коды ошибок. По крайней мере, какой-то выбор есть. В первом случае получается код, который либо вообще пе проверяет успешность вызовов, либо проверяет, но катапультируется при любых неполадках. Во втором случае вы взваливаете на себя массу дополнительной работы па системном уровне - т0 есть такой работы, которую мы, прикладные программисты, делать не должны Что должна была сделать Microsoft в дайной области? Для начала - 3tl ставить разработчиков Windows 95 и Windows NT согласовать расширен111^ коды ошибок И точка. Несоблюдение этого простого и очевидного требован11^ не приводит ни к чему, кроме хаоса. Затем следовало бы для каждой функШ1' или сообщения недвусмысленно и подробно задокументировать, какие иМеИН коды ошибок при каких именно условиях возвращаются (причем в оиеративй справочной системе такая информация должна быть помещена прямо в глав*1Ь страницы, а не во вспомогательные). Например, вот каким я хотел бы впДе
ухэстики» 485 ведение функции GetShortPathNameO, и как, на мой взгляд, она должна ' ть задокументирована: Условие Расширенный код ошибки "^правильно задан файл, включая казатель и строку нулевой длины Злдашюе устройство, каталог или файл ije существуют ERROR BAD PATHNAME ERROR PATH NOT FOUND (На самом деле, именно таким образом работает функция uGetShort- paihNameO - моя оберточная функция для GetShortPathNameO из библиотек Win32u.) Но, как говорится, история ие признает сослагательного наклонения. На сегодняшний день Microsoft поставила себя и нас в очень трудное положение. Если они попробуют унифицировать использование расширенных кодов ошибок, они без сомнения нарушат работу множества существующих приложений. В то же время текущая ситуация настолько запутана, что, если бы они попытаюсь задокументировать все ныне используемые коды (представьте себе таблицу, подобную приведенной выше, но с дополнительными колонками для различных платформ Win32), они наверняка не смогли бы сделать это на сто процентов корректно и вызвали бы еще большие беды. (Я ие хотел бы, чтобы зш мои слова прозвучали так, будто я вообще не верю в способность Microsoft писать пригодную для работы документацию. Справедливости ради, я должен заметить, что как раз Microsoft делает это лучше большинства других производителей программного обеспечения, особенно - производителей инструментария разработки. Однако, в документации по Win32 так много ошибок и белых пятен, что я и в самом деле пессимистично отношусь к возможно- сти исправить ситуацию и довести корректность этой документации до приемного уровня. Разумеется, я был бы просто счастлив, если бы Microsoft с^огла доказать необоснованность моего пессимизма.) Кроме того, нельзя заушать и про грядущие новые версии Windows 95 и Windows NT. Как вы ду- Маете, ждут ли нас недокументированные изменения в недокументированном Пользовании расширенных кодов ошибок? Мои способности в предсказаниях 11е лучше ваших, но, я думаю, мы можем смело сказать, что некоторые «нерачительные» пеанонспрованные изменения в этой области вряд ли кого-ни- 5v Дь шокируют.
486 РазновиАности\\/;п ? Windows 95: проблема с 5UB5T и GetShortPathName() Наша старая добрая знакомая (еще со времен DOS), команда SUBST конечно же, никуда не пропала и ио-прежнему доступна и работает под Win dows 95 и Windows NT. Но теперь с ней связана одна неприятная проблем- эта команда может сбить с толку функцию GetShortPathNameO. (На т0 случай, если вы не знаете команду SUBST — а это сегодня свойственно многт пользователям Windows и даже программистам — я вкратце поясню: эта к0. манда позволяет вам создать новое логическое устройство на жестком диске, ассоциировав его с существующим каталогом. Например, если в вашей системе нет устройства Q:, вы можете приказать операционной системе — DOS и/или Windows - интерпретировать все ссылки на устройство Q: как ссылки на каталог C:\MYDOCS\SUMMARY. Тогда вы сможете вместо имени файла C:\MYDOCS\SUMMARY\JANUARY.DOC использовать имя Q:\JANU- ARY.DOC.) Чтобы увидеть проявление вышеупомянутой ошибки, откройте в Windows 95 окно командной строки и введите команду: SUBST G С \TEMP которая заставит Windows 95 интерпретировать все ссылки на логическое устройство G: как ссылки на каталог C:\TEMP. Затем запустите программу- пример SUBSTBUG, которую вы найдете в каталоге http://www.symbol.ru/rus- sian/library/prof_prog/source/chap14/substbug. Эта программа несколько раз вызывает функцию GetShortPathNameO и показывает вам результаты этих вызовов. Всякий раз, когда программа передает этой функции путь, ссылающийся па устройство С:, все работает нормально. Но как только передается путь со ссылкой на устройство G:, дела плохи: «g:» транслируется в «g:\TEMP», а «g:» транслируется в «g:\TEMP» — в обоих случаях результат трансляции соответствует пути, несуществующему в файловой системе. Исключением является ситуация, когда каталог TEMP оказывается текущим для устройства G' " тогда относительный путь вида «g:\fred.txt» успешно транслируется в такой #е относительный, но теперь уже корректный путь — в «g:\FRED.TXT». Меня нисколько не удивит, если существуют другие примеры «сумасшествия» функции GetShortPathNameO в сочетании с командой SUBST — более колоритные, чем ге, которые я обнаружил и описал. Но это уже не так важно, поскольку весь драматизм ситуации заключается в том, что у вашей программ1 все равно пет способа определить, используется ли команда SUBST. Следов*1* тельно, если эта команда задействована, и если ваша программа вызывает Get' ShortPathNameO, вашим пользователям придется столкнуться с eaMbtf111 неприятными сюрпризами, когда программа попытается использовать резу-1Ь таты работы этой функции и будет выдавать странные диагностические coov щения.
ужастики» 487 Построение примера: SUB5TBUG Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/ chap14/substbug Платформа: Win32 Инструкции по сборке: откроите при помощи Visual C++ 2.2 прилагающийся МАК-файл и скомпилируйте как обычно. <5*5) Для проведения тестов следуйте инструкциям, приведенным выше Странности функции EetShor£PathName() Функция GetShoriPathNameO проста настолько, насколько простой может быть функция вообще. Вы передаете ей путь, который может содержать или не содержать длинные файловые имена, а в ответ получаете версию того же пути, состоящую только из псевдонимов. Кроме этого, функция выдает вам код возврата и при необходимости использует функцию SetLastErrorO для установления расширенного кода ошибки. Что может быть проще такого весьма практичного и, казалось бы, хорошо задокументированного интерфейса? Хмм, но что должна сделать функция GetShortPathNameO, если вы, например, передадите ей путь, ссылающийся на логическое устройство, не существующее в вашей файловой системе? Скажем, путь «z:\autoexec.bat», когда v вас нет диска Z:? Должна ли она считать такую ситуацию ошибочной? Я думаю, что должна. В конце концов, ведь эта функция предназначена для трансляции имени существующего файла - она вовсе не использует какой-то алгоритм генерации псевдонима но длинному имени, а именно осуществляет соответствующий поиск по файловой системе. И если заданный файл не существует, то единственной логически правильной реакцией может быть только возврат ошибки. К сожалению, в такой ситуации (и в других подобных случаях) вы должны ожидать от функции GetShortPathNameO самых разнообразных откликов в зависимости от того, на какой платформе - Win32s 1.30, Windows 95 или Windows NT 3.51 — работает ваша программа. (Кстати, документация по этой Функции — и API32.HLP, и Win32 SDK - ни слова не говорит об этом.) В таблице 14.3 приведено резюме результатов моих собственных тестов, вторые вы можете повторить, используя программу-пример GSPN для данной 'л^вы. Обратите внимание на то, как безумно отличается поведение GetShort- athNameO на разных платформах, и какое влияние на него оказывает даже <*кая вещь, как наличие одной или двух файловых компонент в заданном пути. ■^Пример, если в корневом каталоге диска С: нет подкаталога с именем «temp 11 *' функция GetShortPathNameO будет интерпретировать строки «c:\temp
488 Разновилносги Win32 dir» и «c:\temp dir\file.txt» по-разному. Первая строка успешно обработав иод Windows NT и будет считаться ошибочной иод Windows 95, в то время у '* вторая приведет к ошибке на обеих платформах (но с разными расширенны\ кодами ошибки) Win32s 1.30 — совершенно другой зверь, что в общем-то не должно никог удивлять. На этой платформе вызов функции GetShortPathNameO вообще щ когда не приводит к ошибке, за исключением самого криминального случая передачи ей нулевого указателя, что приводит к возникновению исключительной ситуации. (На мой взгляд, гораздо логичнее было бы сделать так, чтобы под Win32s функция GetShortPathNameO всегда возвращала расширенный код ошибки ERROR_CALL_NOT_IMPLEMENTED, поскольку на этой платформе она по определению не может делать что-либо осмысленное с длинными файловыми именами.) Еще одна интересная деталь заключается в том, что при провале вызов GetShortPathNameO не всегда устанавливает расширенный код ошибки при помощи SetLastErrorO. Например, замените в программе-примере строку SetLas- tError(O) на SetLastError(300) (или на вызов SetLastErrorO с любым другим кодом, необъявленным в W1NERROR.H), а затем перекомпилируйте программу [I запустите. Вы увидите, что под Windows NT 3.51 функция GetShortPathNameO при передаче ей строки нулевой длины вернет значение, индицирующее провал (как и должно быть), но при этом не удосужится подправить должным образом расширенный код ошибки. Таблица 14.3. Повесть о GetShortPathNameO Тест Нулевой указатель Строка пулевой длины Несуществующее устройство Существующее устройство, несуществующий каталог, имя файла не задано Win32s 1.30 Необработанная исключительная ситуация в W32COMB.DLL Успех, оставляет выходную строку пустой Успех, входная строка копируется в выходную Успех, входная строка копируется в выходную Windows 95 Провал, код ошибки 87 Провал, код ошибки 161 Провал, код ошибки 3 Успех, входная строка копируется в выходную Windows NT Провал, код ошибки 87 Провал, код ошибки не устанавливается Успех, входная строка копируется в выходную Провал, код ошибки 2 ^
Ужастики» 489 Тест '^шествующее uтройство, несушествующии каталог, имя файла задано Существующее устройство, несуществующий кагалог, несуществующий файл Win32s 1.30 Успех, входная строка копируется в выходную Успех, входная строка копируется в выходную I г Windows 95 Провал, код ошибки 3 Провал, код ошибки 3 Windows NT Провал, код ошибки 2 Провал, код ошибки 2 Пояснение: константы для упомянутых кодов ошибок, согласно WINERR- 0R.H: 2 = ERR0R_FILE_N0T_F0UND 3 = ERR0R_PATH_N0T_F0UND 87 = ERROR_INVALID_PARAMETER 161 = ERROR._BAD_PATHNAME Программу-пример, демонстрирующую обсуждаемую проблему, вы можете найти в WWW-бнблиотеке в каталоге GSPN. Главная программа просто вызывает несколько раз rtPathNameO с разнообразными вариантами входных параметров, перебирая тем самым несколько общих патологических случаев, и показывает результаты этих вызовов. Если вы будете экспериментировать с этой программой, вам придется изменить некоторые строки, передаваемые функции DoGSPNO, так, чтобы учесть отличия вашего тестового фрагмента файловой системы от моего. (Исходный код GSPN.C полностью приведен в листинге 14.2.) Листинг 14.2. GSPN.CPP #include <windowG h> tfinclude <ctdio h> //ttdefme do_wrapped 1 BOOL DirExictc(const char *dn) BOOL FileExicts(const char *fn), DWORD uGetShortPathName(LPCTSTR long_path,LPTSTR buffer, DWORD buffer_len), void DoGSPN(char *old_path), mt main() { DoGSPN( *c \\temp dir\\tect txt ). // Каталог и файл существуют DoGSPN( *c \\temp dir\\file not theie txt ), // Каталог существует, файл DoGSPN( с \\teinp dir2 ), // Каталог не существует DoGSPN( с \\teinp dir2\\file not there txt ), // Каталог не существует DoGSPN( z \\readme txt ), // Устройство не существует DoGSPN( z \\ ), // Устройство не существует DoGSPN( ), // Пустой ввод DoGSPN(NULL), // Патологический случай
РазновиАности\\/ц printf( мтНажмите Enter ) getchari) return 0, i /////////////////////////////////////////////////////////////////////////// void DoGSPN(char *old_path) { char buffer[MAX_PATH] = DWORD re = 0, DWORD gle = 0 , SetLactError(O), #ifdef do_wrapped re = uGetShortPathName(old_path buffer,Gizeof(buffer)), #else re = GetShortPathName(old_path.buffer,Gizeof(buffer)), #endif gle = GetLactEr rorO #ifdef ao_wrapped printf( Функция uGetShortPathNaine( ), #elee printf( Функция GetShortPathName( ), #endif it(old_path == NULL) pnntfC NULL ) eice prmtf( \ %g\ old_path) printf( ) говорит, что она ) if(rc '= 0) printf('сработала успешно\п ), else printf( потерпела неудачу\п"), printf( выходной буфер = \"%s\', re = %d, GLE() = %d\n ',buffer,re,gle), i /////////////////////////////////////////////////////////////////////////// DWORD uGetShortPathName(LPCTSTR long_path,LPTSTR buffer,DWORD buffer_len) { #ifdef _DEBUG if((long_path == NULL) || (buffer == NULLM { OutputDebugString( \nuGetShortPathName Обнаружен неправильный параметр1\n\n ), return TRUE tfendif // Эта проверка заодно отвергает случаи передачи нулевого указателя // или строки нулевой длины if(FileExiGtc(long_path) || DirExiotG(long_path)) { SetLactEiior(0), return GetShortPathName(long_path,buffer,buffer_len), elGe > if((long_path == NULL) || (ctrlen(long_path) == 0)) SetLaGtError(ERROR_BAD_PATHNAME), elGe SetLaGtErrot(ERROR_PATH_NOT_FOUND), return 0, 490
мастики» 491 lllllIII III IIIIIIII III I III I/III II III/III/I III/II/II/I//III/11//I//III/I II11 BOOL FileExiGtc(conot char *fn) ifrfn == NULL || (Gtrlen(fn)) == 0) return FALSE DWORD dwFA = GetFileAttnbutec(fn;. if(dwFA == OxFFFFFFFF) return FALSE else return ((dwFA & FILE_ATTRIBUTE_DIRECTORY) ' = FILE_ATTRIBUTE_DIRECTORY). )lIII IIIII III IIIIIIIII III III II III 11IIIIIIIIII/IIIIIIII/II I/1 III IIIII/II III I BOOL DirExists(conct char *dn) if(dn == NULL || (strlen(dn)) == 0) return FALSE, DWORD dwFA = GetFileAttnbuteo(dn), if(dwFA =■= OxFFFFFFFF) return FALSE, elce return ((dwFA & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY), i Как всегда, мы приходим к вопросу, что же делать в сложившейся ситуации. И снова, если у вашей публичной программы есть хоть малейший шанс быть запущенной и под Windows 95, к под Windows NT (и возможно даже под Win32s), наилучшим, на мой взгляд, решением является написание оберточной функции, которая сглаживает «шероховатости» поведения исходного API. Например, оберточной функции uGetShortPathNameO, код которой приведен втом же листинге 14.2. Построение примера: BSPN Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/ chap14/gspn Платформа: Win32 Инструкции по сборке: используйте комментарии в функции mainO в GSPN.CPP для правильной корректировки строк, передаваемых в DoGSPNO; при помощи константы do_wrapped вы можете переключать программу с использования GelShortPathNameO на использование оберточной функции uGetShortPathNameO; откройте при помощи Visual C++ 2.2 прилагающийся МЛК-файл и скомпилируйте как обычно.
492 Разновилности W; ^ — —-—j_zsl}32 Написание подобных «унифицирующих оберточных функций» может п заться довольно мудреной задачей, поскольку осповиоп целью создания так оберток является устранение всех различит! в поведении на разных Win3o платформах. (В сущности, вы решаете задачу унификации отдельного эдемён- < Win32 API, которая нам так необходима, и которая должна быть обеспечена самого начала самой Microsoft.) При написании uGetShortPathNameQ я и пользовал следующий подход: В Все, очевидно неправильные входные данные должны своевременно обнаруживаться и рационально обрабатываться. Так, функция uGei- ShorlPalhNameO проверяет, не передан ли ей нулевой указатель ца строку или строка нулевой длины. И в обеих этих ошибочных ситуациях она не только возвращает нулевое значение, но и устанавливает расширенный код ошибки в ERROR_BAD_PAiTINAME. В Если заданный путь соответствует существующему файлу или существующему каталогу, обертка позволяет исходной функции обработать ею и затем просто переправляет результат этой обработки вызывающему коду. Во имя законченности и «чистоты» такой процедуры, оберточная функция вызывает SetLastError(O) непосредственно перед вызовом исходной функции (хотя, возможно, это и необязательно) В Если заданное длинное имя не соответствует реально существующему каталогу или файлу, обертка просто возвращает ноль и устанавливает расширенный код ошибки ERROR_PATH_NOT_EOUND. Это, в каком-то смысле, обобщенное решение, так как не делается никаких попыток различить ситуации, когда не найдено имя файла, и когда не найден путь к файлу. Ио простого ответа на вопрос, как осуществить такое распознавание, пет, поскольку нет способа определить, каковы истинные намерения вызывающего кода, и какой смысл он прпдаег входным и выходным данным. Обратите внимание на то, как изначально несогласоваиы Windows 95 и Windows NT в этом вопросе. В любом случае, лучше выбрать одно из возможных значении (ERROR_PATII_NOT_FOUND и ERRORJFILEJNIOT_FOUND) и затем последовательно его использовать, чем использовать то одно, то другое без каких-то резонных оснований. Жнпы данных в реестре Если вы взглянете на ключ SharcdDLLs в реестре Windows 95, то ^ обнаружите, что ои должен служить тем местом, где хранятся счетчики псПОт1Г зования общесистемных, совместно используемых динамических библиот^ Идея заключается в том, что при инсталляции счетчик использования той li;l1 иной DLL должен увеличиваться на единицу, а при деинсталляции — yMefIb
ужастики» чуэ чТься. (Насколько хороша или плоха эта идея сама но себе - отдельный г1р0с, который я уже обсуждал ранее в этой книге. Смотрите врезку ""Стандарты для слабаков?» в главе 6 на стр. 234.) К сожалению, если ваша программа собирается использовать этот меха-' 13М> она должна быть готова встретить значения неправильного типа. И вот 1еСь вас могут поджидать серьезные неприятности, обусловленные тем, как ,а6отает функция RegQueryValueExO. У вас нет возможности указать этой дикции, какого типа данные вы ожидаете; она прочитает значение любого j'lina, вернет вам сами данные и укажет тот тип, которые они имеют на самом ,еле. И ваша программа обязательно должна принять во внимание эти сведения 0 типе прежде, чем будет что-то делать с извлеченным из реестра значением кЛюча. Например, если ваша инсталляционная программа прочитывает значение ключа ShareciDLLs, намереваясь увеличить его на единицу без проверки тина и потом записать результат обратно (ожидая, что получит данные типа REG_DWORD), то она может успешно получить строковое значение и в итоге безнадежно покалечить значение ключа. Подобную ошибку очень дегко совершить Более того, можно даже сказать, что эта ошибка провоцируется самим синтаксисом функции RegQueryValueExO, поскольку совершенно законным значением параметра lpdwType является NULL, при котором функция вообще не будет возвращать вам фактический тип прочитанных данных. Последствия же этой ошибки могут быть катастрофическими не только для вашей программы, но и для других программ, и для всей системы (в зависимости от того, какие именно ключи реестра окажутся испорченными). Чтобы вы не посчитали меня параноиком, взгляните на пример кода, приведенный ниже, и скажите — вызовет ли он у вас хоть какие-то подозрения Щ беглом просмотре? Этот код замечательно работает и старательно проверяет все возможные коды ошибок - все выглядит превосходно, не так in? Нет, не так Проблема в том, что он наивно полагает, что функция RegQueryValueExO вернет ему значение именно того типа, который он ожида- ет Доверчиво используя возвращенное значение и его фактический тип, при ^следующей записи в реестр этот код может запросто изувечить значение !^юча до неузнаваемости. (Обратите внимание, что этого примера кода нет в ^WW-библиотеке. Я поступил так по одной простой причине — я не хочу, Нтобы вы компилировали и запускали такой код. Он приведен исключительно качестве отрицательного примера.) // Отрицательный пример1'' Не использовать1'' BOOL IncSharedDLL.3Counter(char *name) { char *key = Software\\Microcoft\\Windows\\CurrentVerGion\\$haredDLLc , HKEY the_key, DWORD the_type DWORD buffer, BOOL return_value = FALSE, if(RegOpenKeyEx(HKEY_LOCAL_MACHINE,key,0,KEY_ALL_ACCESS,&the_key) == _
494 РазновиАностиум . ERROR.SUCCESS) { unsigned long buffer_len = sizeof(Duffer), if(RegQueryValueEx(the_key,name,0,&the_type, (unsigned char * )buffer,&buffer_len) == ERR0R_SUCCESS) { buffer++, if(RenSetValueEx(the_key.name,0 the_type,(unsigned char *)buffer, sizeof(DWORD)) == ERR0R_3UCCESS) -eturn_value = TRUE, else Coinplam( Запись значения в реестр провалилась1 ), else Coinplain( "Чтение значения из реестра провалилось1 ). RegCloseKey(the_key) else Coinplain( Невозможно открыть ключ1 '), return return_value Разумеется, нельзя не признать, что этот ужастик сильно отличается от большинства остальных, приведенных в данной главе: он, очевидно, не относится ни к проблемам совместимости, ни к примерам ошибок в реализации Win32 API на той или иной платформе Windows. Он скорее демонстрирует весьма неудачно спроектированный программный интерфейс, чреватый серьезными последствиями. И поскольку частота использования этой функции, вероятно, будет большой, я посчитал необходимым обратить ваше внимание на те потенциальные ловушки, которые могут вас ожидать в этой области. Пожалуйста, будьте максимально осторожны и аккуратны всякий раз, когда ваша программа считывает данные из реестра или что-то записывает в него. Функция WNetBetVniversalName( ) проваливается в Windows 9S Универсальные имена - это имена файлов и других системных устройств или ресурсов, не зависящие от буквенных названий логических дисков. Общий формат универсального имени таков: \\server\shnrename\path\file где server — это символическое имя сервера, shnrename — это, кгис правило, имя совместно используемого диска, a path и file — обычные имена каталога*1 файла. Например, универсальное имя файла, находящегося на диске С: одного из моих тестовых компьютеров, может иметь такой вид: \\Midwect p75\mwp75_c\temp\barney txt
мастики» 495 е «\\Midwest р75» - сетевое имя компьютера, «mwp75_c» — имя, под ко- рЪ1М совместно используется корневой каталог диска С: этого компьютера, а telT1pbarney.txt» — просто имя каталога и файла. До появления Windows 95 большинство пользователей редко сталкивалось . универсальными именами, поскольку совместно используемым в сети дискам /каталогам обычно ставили в соответствие однобуквенные имена устройств (например, вместо вышеприведенного универсального имени пользователи ви- 1ели что-то типа «H-\temp\barney.txt»). Теперь же, когда базовые сетевые возможности больше не требуют предварительного проецирования совместно ис- юльзуемых дисков и каталогов на однобуквенные имена устройств, универсальные файловые имена будут встречаться гораздо чаще. И в этом яв- 1ении, как таковом, ничего сложного (а тем более страшного) пет. Неприятность ждет пас, программистов, чуть глубже — при решении вопроса о получении нашими программами универсального имени файла по заданному обычному полному имени. Как и следовало ожидать, Win32 API предоставляет для этой цели специальную функцию — WNetGetUniversal- NameO. Но беда в том, что эта функция никогда не работает под Windows 95 -- к сожалению, она всегда индицирует провал и устанавливает расширенный код ошибки 1200 (ERROR_BAD_DEVICE). Microsoft официально признала эту ошибку в своей PSS-статье Q131416 и даже предложила некоторый способ имитации данной функции — функцию GetUniversalNamcO, код которой показан ниже. Обратите внимание, что это решение еще не готово для использования его в реальной публичной программе, так как функция GetUniversalNameO предполагает, что переданный ей буфер достаточно велик для помещения в пего результирующего универсального имени. И тем не менее, этот код вполне четко и полно демонстрирует вам ту идею, которую вы можете смело использовать до тех пор, пока Microsoft не исправит обсуждаемую ошибку з Windows 95. (Само собой, Даже после того, как это исправление будет сделано, вашей программе все равно придется быть готовой к данной проблеме — далее в XXI веке найдется немало пользователей, которые все еще будут работать на «золотой» версии Windows 95 от 24 августа 1995 года.) Я также предлагаю вашему вниманию небольшую программу пример, GE- TUNC. которая демонстрирует обсуждаемую проблему, вызывая обе Функции — и GetUniversalNameO, и WNetGetUniversalNameO. Построение примера: GETUNC Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/ chap14/getunc Платформа: Win32
496 Разновилности iv; .—_-__j^//m Инструкции по сборке: посмотрите комментарии в GETUNfC Съ на предмет возможного изменения переменной «|'ц» , правильной работы программы на вашей системе; открой при помощи Visual C++ 2.2 прилагающийся МАК-фай» скомпилируйте как обычно. // Function Name GetUmversalName //' // Parameters szUniv - contains the UNC equivalent of szDrive // upon completion /7 // szDrive - contains a drive based path // // Return value TRUE if successful, otherwise FALSE // //' Comments This function assumes that szDrive contains a // valid drive based path // // For simplicity this code assumes szUniv points // to a buffer large enough to accomodate the UNC // equivalent of szDrive BOOL GetUniversalName( char szUmv[], char szDrive[] ) { // get the local drive letter char chLocal = toupper( szDrive[0] ), // cursory validation if ( chLocal < 'A' j| chLocal > '2' ) return FALSE; if ( szDnve[1] '= ' ' I! szDnve[2] '= '\\' ) return FALSE, HANDLE hEnum, DWORD dwResult = WNetOpenEnum( RESOURCE_CONNECTED, RESOURCETYPE_DISK, 0, NULL, &hEnum ), if ( dwResult '= N0_ERR0R ) return FALSE, // request all available entries const int c_cEntries = OxFFFFFFFF, // start with a reasonable buffer size DWORD cbBuffer = 50 * sizeof( NETRESOURCE ), NETRESOURCE *pNetResource = (NETRESOURCE*) malloc( cbBuffer ), BOOL fResult = FALSE, while ( TRUE ) { DWORD dwSize = cbBuffer, cEntries = c_cEntries, dwResult = WNetEnumResource( hEnum, &cEntries, pNetResource, &dwSize ), if ( dwResult == ERR0R_M0RE_DATA ) < // the buffer was too small, enlarge cbBuffer = dwSize, pNetResource = (NETRESOURCE*) realloc( pNetResource, cbBuffer ), continue,
мастики» if { dwRecult '= NO.ERROR ) goto done // search fot the specified drive letter for ( int i = 0, l ' (mt) cEntnec, i++ ) if ( pNetReoourceti] lpLocalName && chLocal == toupper(pNetRecource[i] lpLocalName[0]) ) { // match fResult = TRUE, // build a UNC name ctrcpy( Gzliniv, pNetResource[i] lpReinoteName ), strcat( czUniv, czDrive + 2 ), _stnipr( ozUniv ), goto done, done // cleanup WNetCloceEniim( hEnum ), free( pNetRecource ), return fResult. Ограничения для cbWndExtra и cbdsExtra Нет, пожалуйста, не удивляйтесь, это не дежа вю. Это в основном тот же самый ужастик, что я описывал в главе 12. Я упоминаю его повторно лишь по гой причине, что он также является примером несовместимости Win32 API между различными платформами Windows. Если значение любого из полей cbWndExtra и cbClsExtra превышает 40, вызов функции RegisterClassO провалится под Windows 95, по успешно сработает под Windows NT 3.51 и Win32s 1 30. (Да, это один из тех немногих случаев, когда Win32s оказывается «на том же берегу», что и Windows NT, а не Windows 95.) Более подробную информацию об этой проблеме вы можете почерпнуть из PSS-статьи Q131288 °т Microsoft. Для экспериментов с этим явлением вы можете использовать 32-разрядную ВеРсию программы WNDEXT16. Построение примера: WNDEXT32 Местоположение : http://www.symbol.ru/russian/library/prof_prog/source/ chap14/wndext32 Платформа: Win32 Инструкции по сборке: посмотрите комментарии в WNDEX- Т32.СРР о разнообразных возможных вариантах построения программы; откройте при помощи Visual C++ 2.2 прилагающийся МАК-файл и скомпилируйте как обычно. ^
498 Разновилности W; ^ —_ц//т Аналоги масштабируются по-разному В данном случае речь пойдет не только о различиях между 32-разрядцЬ1х платформами, по также и о различиях в обращении одной и той же операц11о ' ной системы с программами разных версий. Создайте 32-разрядНу программу, указав 3.1 в качестве, номера версии системы, и диалоги это" программы по умолчанию будут иметь привычный для Windows 3.1 виещцИ" вид: отсутствие трехмерных эффектов, белый фон, жирный шрифт. Но стоит вам изменить помер версии системы этой же программы на 4.0, как под Windows 95 ее диалоги будут иметь другой, характерный для Windows 95 вид- трехмерные эффекты, светло-серый фон, нежирный шрифт. Под Windows NT 3.51 вы также увидите светло-серый фон и нежирный шрифт, а под Win32s внешний вид диалогов останется неизменным. Если оставить в стороне чисто эстетический аспект проблемы, то в чем же причина для беспокойства? А причина очень проста: нежирный шрифт диалогов, характерный для Windows 95, немного уже того жирного шрифта, который но умолчанию используется в Windows 3.1. А это означает, что один и тот же диалог будет иметь разные размеры (в пикселах) в зависимости от номера версии системы, присвоенного программе. Например, я написал тривиальную 32-разрядную программу, которая в качестве своего главного интерфейса использует простое диалоговое окно и при его открытии показывает физический размер этого диалога (получаемый при помощи функции GetClientRcctQ). Будучи построенной с номером версии системы 3.1, программа показала размеры 324 на 150. Если же собрать ее, указав в качестве номера версии системы 4.0, то размеры диалога окажутся другими — 278 на 150. Поскольку Windows 95 позволяет пользователю менять внешний вид практически любого стандартного элемента интерфейса, как никогда важно, чтобы ваша программа ие делала никаких предположений о таких вещах, как высота заголовка окна, ширина полос прокрутки и т. п. Также ваша программа не до л лена использовать «зашитые» в код координаты и размеры динамически создаваемых пли перемещаемых элементов интерфейса, рисуемых растровых картинок и прочая. Подобные приемы всегда считались дурным тоном в программировании для Windows, а теперь они практически гарантирУк)Г публичной программе потенциальное обезображивание ее внешнего вида. Когда ваша программа перешагнет через «черту версии 4.0», она также может столкнуться с непредсказуемыми изменениями форматирования текста д статических элементах управления. Ситуация такого рода изображена и' рис. 14.6.
ужастики» 499 я&мш This i; aH on line 1 this is all on line 2 and this is on нпе 3 ^T^J^^^^I^D^H Tf-f tt aH'ori- urn fa-- aj «. rt and К ml • 3; ^"^■^■e„,<] Рис. 14.6. Интерпретация 3.1- и 4.0-диалогов. Единственный способ избежать подобных проблем — еще на этапе тес- -ирования вашей программы проверять внешний вид каждого диалога на всех разновидностях Windows, которые вы собираетесь поддержать, и которые мо- :\т видоизменять облик пользовательского интерфейса. Поскольку в достаточно большом приложении трудно вручную перебрать все условия, приводящие к появлению на экране каждого диалога, я рекомендую вам рассмотреть возможность написания специальной вспомогательной программы для этих целей: она могла бы использовать те же ресурсы, что и сама основная программа, и только тем и заниматься, что по очереди показывать все диалоги. Таким образом вы могли бы значительно облегчить себе проверку внешнего вида программы. В некоторых случаях, в зависимости от природы основной программы, может оказаться целесообразным модернизировать эту вспомога- 1ельную программу так, чтобы она давала возможность проверить не только внешний вид, но и поведение диалогов (то есть код, обслуживающий их). Ау, кто стянул мой значок? Перед вами - еще одна из многих маленьких тайн программирования для Windows 95. Создайте при помощи Visual C++ 2.2 совершенно новую 32- ^зрядную MFC-программу, которая использует диалоговое окно в качестве своего главного интерфейса. Сделайте это при помощи AppWizard и позвольте Visual C++ создать и настроить все автоматически, без каких-либо модификации с вашей стороны. (Иными словами, постройте новое, ничего не делающее Приложение в точности тем же способом, который так любят применять поставки программного инструментария при демонстрациях своих интегрирован- Ь1^ оболочек разработки.) Затем скомпилируйте и запустите программу под ' lndows 95 -- вы увидите, что у программы не будет никакого значка ни на '1,1с:щ задач, ни в заголовке главного окна. В то же время, если вы запустите ,л Же программу под Windows NT 3.51, то в минимизированном состоянии она ^ет отображаться нормальным значком «AFX», используемым по 10лчанию. Чтобы понаблюдать этот эффект, вы можете воспользоваться гото- ' u Программой-примером DROP2 из главы 7 на стр. 255 или специальной ,р°гРаммой-прнмером NOICON, написанной для данной главы.
500 Разновилности \л/: —-~-^[ХЦ}32 Как я уже упоминал в главе 7, эта ситуация публично обсуждалась еще выхода в свет Windows 95, и в конечном итоге свелась к вопросу наличия ( ° товых флагов WS_CAPTION и WS_SYSMENU в стиле рассматриваемого 0v Если оба этих флага присутствуют, окно будет иметь значок на соотв ' ствующей кнопке панели задач. Если не присутствуют, то не будет. По крайне мере, именно так описывал положение дел кто-то из сотрудников гру11г поддержки разработчиков в Microsoft. Но, оказывается, все не так просто Если ваш диалог имеет тонкую рамку, то значок в его заголовке и на па нели задач действительно появится, если в стиле диалога присутствуют фдап WS_CAPTION и WS_SYSMENU. При этом у диалога будет системное меню (диалогового типа), включающее только два пункта — Переместить (Move) ц Закрыть (Close) (Кстати, именно такой вид имеет программа-пример NOICON, если вы скомпилируете ее без изменения исходного кода.) Если ваш диалог имеет «растяжимую» рамку (стиль WS_THICKFRAME) плюс одновременно флаги WS_CAPTION и WS_SYSMENU, у него будет значок на панели задач и обычное (оконного типа) системное меню с пунктами Восстановить (Restore), Переместить (Move), Размер (Size), Свернуть (Minimize), Развернуть (Maximize) и Закрыть (Close). Если же ваш диалог имеет типичную для модальных диалогов рамку (DS_MODALFRAME), используемую в Visual C++ 2.2 по умолчанию, то даже в присутствии флагов WS_CAPTION и WS_SYSMENU у него не будет значка на панели задач. Все вышесказанное распространяется на любые приложения под Windows 95, включая 16-разрядные программы и программы с номером версии системы, меньшим чем 4.0. Если ваш диалог имеет должный стиль и если версия вашей программы меньше 4.0, то будет показан тот значок, который содержится в ваших ресурсах. Если же версия равна 4.0, то по умолчанию будет использован знакомый всем значок Windows — четырехцветный флаг, развевающийся на черном фоне. В этом случае вашей программе придется использовать новое появившееся в Windows 95 сообщение WM_SETICON для того, чтобы похлопать Windows 95 по плечу и приказать ему, какой именно значок вы хотите показывать. В программе-примере NOICON это делается при помошн однострочного вызова в методе OnlnitDialogO: B00L CNoiconDlg OnlnitDialogO { CDialog OnlnitDialogO, CenterWindowO, // T0D0 Add extra initialization here Po3tMessage(WM_SETIC0N,(WPARAM)TRUE,(LPARAM)m_hIcon), return TRUE, // return TRUE unleoc you set the focus to a control
ужастики» 501 Все эти дурацкие заморочки с пропадающими и подставными значками явится относительно незначительной проблемой, но они запросто могут 'лдать вашей программе странный облик в глазах пользователя, причем без 11Сих бы то ни было веских причин. Стоит отметить, что при всей той путанице проверками версий и платформ в Win32, в данном случае Microsoft сделала 0 южительнос исключение: все «новые» программы (помеченные версией 4.0) одчиняются всем новым правилам (включая условия, при которых значок по- .В1яется на панели задач и в заголовке окна), а «старые» программы — старым- (На самом д^лс, и здесь Microsoft сделала только полшага — Win32/ } 1-программы получат правильный значок только при условии, что они поучат хоть какой-то.) Наиболее интересным, па мой взгляд, является тот факт, qr0) даже если 1у>ы следуете всем официальным правилам совместимости, не используете никаких недокументированных возможностей и применяете компи- [Ятор и. мастеров от самой Microsoft для генерации вашей программы, ее код Все равно может требовать модификации для обеспечения нормального внешнего вида под Windows 95. Я уж не говорю о том, что все ранее существовавшие 16-разрядные программы совершенно безвинно попадают под воздействие «новых правил игры». Построение примера: NOICON Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/ chap14/noicon Платформа: Win32 Инструкции по сборке: откройте при помощи Visual C++ 2.2 прилагающийся МАК-файл и скомпилируйте как обычно. Вы можете поэкспериментировать с различными стилями диалога и номерами версий, чтобы воспроизвести разные эффекты. (Не забудьте, что для установки номера версии, меньшего 4.0, вам понадобится компоновщик LINK.EXE от Visual C++ 2.0.) В файле NOICODLG.CPP, в методе OnlnitDialogO используется сообщение WM_SETlCON. Я включил в этот пример несколько заранее скомпилированных версий программы NOICON: NOlCON0.EXE — версия 3.1, использует «модальную диалоговую» рамку в главном диалоге; N01C0N1.EXE - версия 3.1, использует «тонкую» рамку в главном диалоге; NOICON2.EXE - версия 4.0, использует «модальную диалоговую» рамку в главном диалоге;
502 Разновилности ^. NOICON3.EXE — версия 4.0, использует «тонкую» рам t j*~^ главном диалог, не применяет сообщение WMSETICON; ' В ЪяЕФ NOICON4.EXE - версия 4.0, использует «тонкую» рамк , главном диалог, применяет сообщение WM_SETICON. Графические траектории ие функционируют под Windows 95 Графические траектории - это полезный, хотя в какой-то степени j мудреный, набор GDl-фупкцнй, который позволяет вам «аккумулировать» несколько графических команд перед тем, как фактически вывести их результаты на контекст устройства или осуществить другие действия над ними (например скопвертировать их в графические области). В данной книге я не собираюсь в подробностях объяснять принципы работы и методы использования графических траекторий; если у вас есть доступ к Microsoft Developer Network CD-ROM (а он у вас должен быть, если вы разрабатываете программы для Windows), то я предложу вам ознакомиться с некоторыми его статьями, посвященными траекториям (для их нахождения вы можете воспользоваться поиском ключевого слова «beginpath»). В свете интересующих нас вопросов совместимости, нам важен лишь тот факт, что графическая траектория как бы запоминает все рисующие GDI-вызовы, которые делает ваша программа между вызовами функций BeginPathO и EndPathQ. По крайней мере, именно так это будет происходить под Windows NT. Под Windows 95 запоминаться будут только следующие одиннадцать функций: ExtTextOutO, LineToO, MoveToExO, PolyBezierO, PolyBezierToO, PoiygonO, PolylineO, PolylineToO, PolyPolygonO, PolyPolylineO и TextOutC). Или, если посмотреть на это с другой стороны, следующие GDI- фупкцпп не будут включаться в траектории иод Windows 95: Arc(), ChordO, EllipseO, PieO, RectangleO и RoundRectQ. Эти списки взяты непосредственно из PSS-етатьп Microsoft Q125697, и я лично не проверял каждую из перечисленных функций. (Кстати, есть еще но крайней мере две GDl-функшш, А11" gleArcO и PolyDrawO, которые успешно включаются в траектории под Windows NT, но вообще не поддерживаются под Windows 95.) Если ваша программа использует графические траектории, то вы должны быть предельно осторожны, поскольку у программы нет способа определить факт использования GDI-функции, не включенной в траекторию - компИ' лягор об этом не предупреждает, и сам вызов никак ие индицирует так}'10 ошибочную ситуацию. В результате вы можете получить едва заметные оиШ^" при рисовании. Л иногда — взгляните, например, на рис. 14.7 не такие У* незаметные. Кстати эта иллюстрация получена при помощи програМ^ примера PATHS, которая поставляется на самом Microsoft Developer NeW°*\ CD-ROM -- я лишь закомментировал в ней несколько строк кода, котор1'11 сразу после запуска проверяли, работает ли программа под Windows N1- у завершали работу, если версия Windows оказывалась другой. Как видно11
ухастики» 503 ртинок, получающихся под Windows 95 и Win32s, у Microsoft были весьма весьма веские причины делать такую проверку. StrokeAndFHl Winding CltpPath Winding FUiPflth Stroke AndFiH Alternate CHpPath, Alternate Stroke Path StrpkeAntJRIl, Winding CiipPath, Winding StipkeAndFHL Aftein^tp CliyPaUvAltsniafe 4 /' 4. ) РПРЧР ClfpPeth, WlMffttg;: SfreiceAndHit Alle«i»tt 4 ItipFxtth, Alternate Рис. 14.7. Графические траектории на разных платформах Win32.
504 Разновилности ty/ Как я уже упоминал, отследить возникновение этой проблемы практичен невозможно, поскольку нет способа определить, происходит ли вызов данн ' GDI-функции между вызовами BeginPathO и EndPathO. (Я провес ' очевидную гипотезу, сразу же приходящую на ум - повторный вызов Ве</ PathO перед вызовом EndPathO, но это ничего не дало - при «внеурочно вызове функция BeginPathO радостно возвращает TRUE, сигнализируя ЧТ( все в порядке.) Возможно, существует какой-нибудь мерзкий незадок\- ментированный метод обнаружения данного условия, но я его еще не нашем Если какой-нибудь подобный метод известен вам, напишите мне об этом, пожалуйста. Как это часто случается в программировании, последнее средство защиты — ваше собственное прилежание — в данной ситуации является единственным средством. Я вовсе не люблю давать такой совет программистам, потому что я тоже программист и знаю, что у всех у нас бывают неудачные дни, и что вес мы иногда забываем о мелочах (например, как раз о таких, как поддержка той или иной GDI-функции при построении траекторий на различных платформах). Но что же тут поделаешь? Конечно же, было бы здорово найти какой-нибудь отладочный инструмент, который отслеживал бы возникновение обсуждаемой проблемы. (Кстати, от отладочной версии Windows 95 в данном случае помощи ждать не приходится — она никак не сигнализирует о «провалах» в траекториях.) А еще лучше было бы, если бы графические траектории поддерживались под Windows 95 и Win32s точно так же, как и под Windows NT. На самом деле, я немного покривил душой - я знаю один способ, который мог бы помочь справиться с нашей проблемой. Можно использовать библиотеку оберточных функций. Обертка для функции BeginPathO могла бы устанавливать внутренний для библиотеки флаг, сигнализирующий о том, что началось построение траектории И тогда обертки для всех GDI-функций, которые не включаются в траектории под Windows 95 или Win32s, могли бы в нужный момент индицировать возникновение ошибки. Разумеется, обертка для функции EndPathO сбрасывала бы данный флаг К сожалению, этот прием чреват появлением «ложных тревог»- например, после вызова обертки для BeginPathO можно, п° случайному недосмотру, вызвать «голую» функцию EndPathO вместо ее обертки — флаг «рисования траектории» не будет сборошен, и п°' следующие вызовы оберток графических функций могут индииир°ва1Ь возникновение ошибки там, где ее на самом деле нет Тем не менее, это1 недостаток далеко не фатален (по крайней мере, по сравнению с опасн0 стыо той дыры, которую мы пытаемся залатать данным механизмом)- поэтому, в зависимости от вашего конкретного проекта, использование Icl кого варианта защиты может оказаться вполне оправданным и полезНь1> (хотя бы на период тестирования и отладки)
у^[ИКИ»_ 505 Функция BetSysColor() не мойкет потерпеть неудачу (или все-таки мойкет?) Я не хотел бы заново пересказывать всю сагу о функции GetSysColorO- ;}апомню вам лишь о том, что, благодаря своему синтаксическому опреде- ейпю, эта функция не может недвусмысленно сигнализировать вызывающему 0ду о том, что произошла какая-то ошибка, или что ей был передан неправильна параметр. В результате ваша 32-разрядная программа может передать дикции GetSysColorO какую-нибудь специфичную только для Windows 95 0нстанту, и, работая под Win32s или Windows NT, получить в ответ значение ^оль, которое по-видимому означает провал вызова GetSysColorO- При этом заша программа не сможет отличить этот ноль — ошибку от совершенно законного, «успешного» возвращаемого значения, соответствующего черному цвету. {I тогда она легко может предстать перед глазами пользователя раскрашенной в самые, мягко говоря, интересные цвета. (Полное описание этой проблемы с GetSysColorO вы можете найти в главе 10 на стр. 345.) Win32s мойкет свихнуться, когда виртуальной памяти слишком много Вероятно, вы уже слышали старую программистскую поговорку о том, что нельзя быть слишком богатым, слишком стройным или иметь слишком много оперативной памяти, слишком много свободного места на винчестере и слишком мощный процессор. Что ж, Win32s 1.30 с очевидностью доказывает неправильность этой поговорки по крайней мере в отношении одного ресурса — виртуальной памяти. Дело в том, что эта финальная версия Win32s явно имеет проблемы при наличии слишком большого количества виртуальной памяти и может отказываться запускать программы, рапортуя об ошибке с Щоы 21. Microsoft прекрасно осведомлена об этой проблеме и даже предлагает до- в°льно простой способ ее обхода: значение ключа PageOverCommit в файле SYSTEM.INI не должно быть слишком большим. А точнее, это значение долж- 1,0 Удовлетворять следующему неравенству: (PageOverCommit + 1) * количество мегабайт ОЗУ < 256 Microsoft также добавляет, что при превращении этого неравенства в многое равенство приложение «может запускаться или не запускаться, в зави- i1i-4octii от самого приложения». Значение ключа PageOverCommit по умолчанию равно четырем, а это >НачлГТ) что вам понадобится довольно много оперативной памяти, чтобы столк- Уться с данной проблемой. (Каким бы ужасным это ни показалось читателю, |°т°рый откроет эту книгу через несколько лет, но сегодня 64 мегабайта ОЗУ вставляют собой целую пригрошню чипов. Что ж, те из вас, кто будет
506 Разновилности W; _ 11Ц1З2 читать эту книгу, сидя на высоком холме 2003 года, наверное вообще не бУД\', понимать, как мы могли что-то делать на этих жалких, немощных п., которые сегодня называются компьютерами.) 1ем не менее, раз этот ключ ществует, значит могут существовать и совершенно разумные причины которым вашим пользователям пли другим программам понадобится измени значение PageOverCommit. И если кто-то (или что-то) установит его, скаже\ равным восьми, то проблема проявится уже на 32 мегабайтах ОЗУ - то еп уже на вполне современной конфигурации настольного компьютера (напримеп на момент написания этих строк мой главный ПК имеет ровно 32 Мб ОЗУ) Кстати, Microsoft также говорит, что счастливые обладатели 128 или более мегабайт памяти вообще не смогут воспользоваться трюком с ключом Ра^еО- verCommint — им придется дожидаться официального устранения данной проблемы, которое планируется произвести в версии Win32s 1.30а. Я не стал писать никакой программы-примера для иллюстрации к данному ужастику, потому что эту проблему можно воспроизвести на любой 32-разрядной программе. Просто установите Win32s 1.30 на Windows 3.1(1), сделайте достаточно большим значение ключа PageOverCommit в секции [386enh] файла SYSTEM.INI, перезапустите Windows и попробуйте запустить первую попавшуюся Win32-nporpaMMy. Если для запуска вы воспользуетесь программой Program Manager, то вы получите диалог, сообщающий о том, что запущенная программа провалилась с кодом ошибки 21; File Manager от Central Point просто,скажет вам, что запуск не удался, и ни словом не обмолвится о коде ошибки. Я привел этот пример для того, чтобы лишний раз продемонстрировать, как инфраструктура Windows может легко сбить вас с толку Win32s совершенно необоснованно отказывается запустить программу, третье- сторонняя служебная программа для работы с файлами не дает вам достаточной информации о возможных причинах этого провала, а в результате вы (или кто-то из ваших пользователей) оказываетесь в совершенном недоумении, что же все-таки произошло. В девяти случаях из десяти за этим последует вывод о каких-то неполадках в самой программе, которая на самом деле ни в чем не виновата. Мораль всей этой истории проста: вы должны крепко подумать, преЖДе чем распространять Win32s 1.30 вместе с вашей публичной 32-разрядно11 программой. Если вы это сделаете, то ваша программа рано или поздно найДеТ тот компьютер, где Win32s еще не установлена, но уже есть все условия ДлЯ возникновения данной проблемы. И, значит, вашим пользователям гарантирована головная боль, а вам — дополнительные заботы по поддерг программы.
у^астики^» _ зи/ Win32s + RICHEDIT = Катастрофа Помимо обычных возможностей редактирования текста, одной из основах п необходимых функций стандартного поля редактирования должна быть .„особность сообщать окружающему миру, был ли изменен текст. Новый элемент управления Win32 — форматированное поле редактирования — удерживается в Win32s 1.30, но при этом сообщение EM_GETMODIFY 1аботает неправильно. Иными словами, на этой платформе у владельца такого 10Ля редактирования нет легального, документированного способа узнать, ivaciio ли сохранять его содержимое. В нормальных условиях, приложение посылает полю редактирования сообщение EM_GETMODlFY и в качестве возвращаемого значения получает шбо TRUE (если текстовый буфер был изменен), либо FALSE: if(SendMecGage(IDC_RICHEDIT,EM_GETMODIFY.O,0)) { /» Обработка измененного буфера */ Иод Win32s 1.30 этот вызов всегда возвращает FALSE. Для демонстрации этой проблемы вы можете использовать программу- пример RICHED. Запустите ее, введите какой-нибудь текст в поле редактирования, поэкспериментируйте с кнопками «Modified?», «Clear Modified» и «Set Modified» и посмотрите, что происходит. Под Windows 95 и Windows NT 3.51 вы получите ожидаемые результаты — введите текст с клавиатуры, и после нажатия на кнопку «Modified?» вы получите положительный ответ, кнопка «Clear Modifed» сбросит признак изменения, а кнопка «Set Modified» установит этот признак, даже если вы ничего не изменяли в тексте. Ничего особенного или волнующего — все как и было задумано. А теперь запустите эту же программу под Win32s 1.30, и вы никогда не узнаете, что текст Июле редактирования был изменен (даже после того, как вы явно установите признак изменения при помощи кнопки «Set Modified»). Вначале я совсем не собирался проверять поддержку (или отсутствие тако- в°й) всех ЕМ-сообщений форматированным полем редактирования. Но после многочисленных комментариев других программистов, прозвучавших в (Рорумах CompuServe, среди которых встречались и заявления типа «под Win32s вообще не работает ни одно из этих сообщений», я все-таки решил понтировать некоторые из них. И вот, что я обнаружил: В Сообщение EMSETBKGNDCOLOR. В официально выпущенной документации по Win32 SDK сначала сказано, что оно является «новинкой Windows NT», а затем отмечено (в Quicklnfo), что оно не поддерживается в Windows NT и Win32s. На самом деле это сообщение работает на этих платформах так же, как и под Windows 95.
508 Разновилносги\л/(ПЗ В Сообщение EM_SETLJMITTEXT. В документации по Win32 SDl' сказано, что оно работает иод Windows 95 и Windows NT, но не Гг Win32s. Согласно моим тестам с Win32s 1.30 и Windows NT 3..51 -4 утверждение является правдивым. Кстати, сообщет ЕМ_ЕХПМ1ТТЕХТ, которое, подобно EMJSETBKGNDCOLOR, об* явлено «новинкой Windows NT», не работающей под Windows NJT Win32s, в действительности работает на всех трех 32-разрядных платформах. В Сообщение EMJUNDO, предназначенное для отмены последних сделанных изменений, не работает под Win32s, хотя документация по Win32 SDK утверждает обратное. К счастью, «бракованность» Win32s в данном случае является по крайней мере последовательной: в ответ на сообщение EM_CANUNDO (посылаемое для выяснения способности поля редактирования выполнить команду EM_UNDO) вы всегда получите FALSE. Это значит, что ваше приложение по-прежнему может опираться на результат этого сообщения и не обещать пользователю того, что не может выполнить. В Сообщения ЕМJSETREADONLY и EMJGETLINECOUNT не работают иод Win32s вопреки документации, заявляющей об их полной поддержке всеми тремя 32-разрядными платформами. (Таким образом, под Win32s многострочное форматированное иоле редактирования не только никогда не скажет вам, произведены ли какие- либо изменения его содержимого, но далее не поделится с вами информацией о том, сколько строк текста в нем находится. В ответ на EM_GETLINECOUNT вам всегда будет возвращено число 0. Именно это неожиданное, малоприятное открытие и побудило меня охарактеризовать сочетание RICHEDIT и Win32s как катастрофу.) Надо отдать должное Microsoft - факт отсутствия поддержки некоторых ЕМ-сообщений все-таки был задокументирован (загляните в документацию по Win32 SDK и посмотрите тему, озаглавленную «Unsupported Edit Control Functionality», которую можно найти путем поиска образна «EM_SETBKGNDCOLOR»). Но ни одно из сообщений, упомянутых мною выше, в официальный «черный список» Microsoft почему-то не попало В программе-примере я оставил па месте весь код, который позволит вам поэкспериментировать со всеми этими сообщениями. jOlli Построение примера: RiCHt^ Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/ chap14/riched
Ужастики» 509 9 Платформа: Win32 Инструкции по сборке: откройте при помощи Visual C++ 2.2 прилагающийся МАК-файл и скомпилируйте как обычно. Запустите программу под Win32s 1.30, Windows 95, Windows NT 3.51 и поиграйте с кнопками. Закомментированный код в методе OnCreateO (файл EICHEDLG.CPP) даст вам возможность потестировать некоторые другие ЕМ-сообщения. Окна RICHED1T по умолчанию не посылают уведомление EN_CHANOE Этот ужастик посвящен не ошибке, а всего лишь одной антиитуитивной детали, которая может стоить вам нескольких часов, потраченных на мучительный поиск причин неправильной работы вашей программы (особенно в сочетании с предыдущим примером — проблемой с сообщением EM_GETMODIFY). Всем известно, что дочерние элементы управления иногда посылают своим родительским окнам так называемые уведомления — сообщения, вызванные различными событиями. Одно из наиболее привычных и широко используемых уведомлений — сообщение EN_CHANGE, которое посылается полями редактирования для индикации изменения их содержимого. Если снова вспомнить о предыдущем ужастике, то именно сообщение EN_CHANGE может послужить той палочкой-выручалочкой, которая спасет вашу программу от неспособности форматированного поля редактирования правильно обрабатывать сообщение EM_GETMODIFY под Win32s: ваша программа может отслеживать уведомление EN_CHANGE и при его приходе устанавливать свой собственный булевский флаг, сигнализирующий о наличии изменений текстового буфера. (То есть, называя вещи своими именами, при помощи EN_CHANGE ваша программа сможет худо-бедно добиться от форматированного поля Редактирования той функциональности, которая должна быть ему присуща по Пределепию.) Такой прием не блещет элегантностью, но работает. Если вы попытаетесь воплотить в жизнь этот фокус с отслеживанием уве- '^Денпя EN__CHANGE, то вы наверняка столкнетесь с небольшим сюрпризом: 1г° сообщение не будет посылаться форматированным полем редактирования 40 тех нор, пока вы явно не попросите его делать это. Да, да — в отличие от св°их более примитивных родственников, обычных полей редактирования, JkHa RICHEDIT по умолчанию не посылают уведомления EN_CHANGE. Выход из этого положения довольно прост — нужно вставить в нужное ме- Го программы всего лишь одну строчку: SendMeGGage(hWnd_RTF,EM_SETEVENTMASK,0.ENM_CHANGE),
51 0 РазновиАностиу\/;п 7 Обратите внимание, что в качестве параметра сообщен EM_SETEVENTMASK используется именно флаг ENM_CHANGE, а не са^ сообщение EN_CHANGE. Такую строку вы найдете в программе-приме RICHED, в методе OnCreateO, в файле RICHEDLG.CPP: послав это сообт ние с самого начала, программа отслеживает уведомления EN_CHANGE и дает писк при их возникновении (просто для того, чтобы подать какой-нибуд сигнал, свидетельствующий о том, что эти сообщения действительно "Ге. нерируются полем редактирования). Если вы закомментируете эту волшебную строчку с Send Messaged) и перекомпилируете программу-пример, она перестанет пищать. В заключение, обратите внимание, что и в данном ужастике документация по Win32 SDK наводит тень на плетень: справка Quicklnfo утверждает, что это сообщение не поддерживается на платформах Windows NT и Win32s, в то время как на самом деле оно прекрасно работает под Windows NT 3.51 ц Win32s 1.30. MoveFileExO не работает nag Windows 9S Документация Microsoft гласит, что функция MoveFileExO не работает под Windows 95. Как показывают мои собственные тесты, это правда Попробуйте вызвать эту функцию, и она вернет вам FALSE и установит расширенный код ошибки 120 (ERROR_CALL_NOT_IMPLEMENTED) - совершенно нормальная реакция системы на вызов заглушечного API. На первый взгляд, ничего особенного. Тем не менее, эта ситуация заслуживает немного более детального обсуждения, потому что именно эта функция является «штатным» средством Windows NT для отложенного удаления, перемещения или замены файлов (как правило, динамических библиотек), которые в данный момент используются системой. Механизм прост: ваша программа вызывает функцию MoveFileExO, передавая ей входное имя файла, выходное имя файла и флаг MOVEFILE_DELAY_UNTIL_REBOOT. И тогда переименование заданного файла (которое может означать его удаление, если в качестве выходного имени задан NULL) произойдет только после очередной загрузки системы. Именно этот механизм используется, когда некоторые инсталляционные программы просят вас перезагрузить систему, хотя и не делают никаких явных изменении в конфигурационных системных файлах. Поскольку функция MoveFileExO не поддерживается в Windows 95, #1,м естественно, нужен другой механизм для аналогичных манипуляций с пспо'1Ь зуемыми в данный момент файлами. И такой механизм в Windows 95 есть это файл WININ1T.INI. Ваша программа может добавить в этот файл иесколь^ ключей, описывающих требуемые операции, и во время следующей перезагрУ ки ваши желания будут исполнены системой. Эта возможность задоку^е
пркоменлаиии 51 I ^рована Microsoft в PSS-статье Q129532, где показаны следующие примеры ^пользования «аналога» функции MoveFileExO: [rename] NUL=C \TEMP TXT С \NEW_DIR\EXISTING TXT=C \EXISTING TXT С \NEW_DIR\NEWNAME TXT=C \0LDNAME TXT С \EXISTING ТХТ=С \TEMP\NEWFILE ТХТ Первая строка приводит к удалению файла TEMP.ТХТ. Вторая - к перемещению файла EXISTING.TXT в новый каталог. Третья — к перемещению и переименованию файла OLDNAME.TXT. И, наконец, четвертая — к перезаписи существующего файла EXISTING.TXT файлом NEWFILE.TXT. (Честно говоря, я бы предпочел, чтобы этот механизм был задокументирован ,де-нибудь в более сподручном месте — например, рядом с описанием функции MoveFileExO. Но это уже тема для отдельной дискуссии.) Никого из нас не должен удивлять тот факт, что Windows NT не поддерживает механизм WIN1NIT.INI, и поэтому вам не удастся самая простая и очевидная уловка — полное игнорирование функции MoveFileExO и использование только файла WININIT.INI (вероятно, по мнению Microsoft, это было бы слишком простым и легким решением). В заключение, еще одна маленькая, но важная деталь: программа WIN- INIT.EXE, которая собственно и выполняет инструкции, описанные в WININIT.INI, запускается на довольно раннем этапе загрузки системы, и поэтому вы не можете использовать в WININIT.INI длинные файловые имена. Рекомендации Я представил вашему вниманию довольно много подробностей на тему «единообразия и последовательности» Win32 API, и, тем не менее, я уверен, что это мое исследование приоткрывает лишь самый кончик вершины айсберга. И мы °пять приходим к закономерному вопросу: как все это сказывается на наших гфограмх\1истских проектах? В качестве ответа на этот вопрос, позвольте мне Предложить вам ряд общих рекомендаций, которые должны помочь вам пли ва- ш^й команде в целости и сохранности пройти сквозь все пороги и стремнины Win32 1. Не доверяйте ничему — ни поведению API в предыдущих версиях Windows, пп общим соображениям, ни даже официальной документации. Если ваша программа собирается работать на нескольких 32- разрядных платформах, вы должны протестировать каждый критичный API на каждой из целевых версий операционной системы. И точка. У вас просто нет выбора. Если вы считаете, что я перебарщиваю, вернитесь и снова просмотрите описанные в этой главе и главе 12
512 Разновилностиц/; проблемы и ошибки документации, а затем честно скажите мне (т бе) сколько из них не являлись бы для вас неожиданными? С В частности, не делайте никаких предположении по поводу значен - возвращаемых функцией GelLastErrorO. Как вы видели, они м0 :' сплыю варьироваться от одной платформы к другой. Вплоть до тар- случаев, когда па одной платформе данный API использует SetLastF гог(), а па другой — нет. Кроме того, не делайте никаких предположений в отношении каких- либо побочных эффектов, возвращаемых значений или других возможностей, которые не задокументированы явно, и которые не приняты Microsoft в качестве обязательств. Исключением может быть только такая ситуация, когда у вас буквально нет другого выхода. Зависимость от любого недокументированного поведения API всегда была дурным тоном в программировании вообще; а в мире программирования для 32-разрядных Windows такая бравада сравнима разве что с игрой в русскую рулетку, когда половина камор в барабане заряжена 2. Со всей серьезностью рассмотрите вариант отказа от поддержки Windows NT и Win32s. Я знаю, такой совет звучит как призыв к довольно радикальному шагу, и это на самом деле так и есть. Но если принять во внимание все те заморочки, которые я смог описать, п затем мысленно добавить к ним те проблемы, о которых я не знал пли которые не смог проверить прежде, чем включить в эту книгу, создание единой 32-разрядной программы, корректно работающей под Windows 95 н Windows NT, может стать настоящим кошмаром. И даже если вам удастся это сделать, вам поистине придется протащить верблюда сквозь игольное ушко. В первую очередь, данная рекомендация касается Win32s: эта разновидность Win32 представляет собой такое перманентное скопишь неприятностей, что поддержка ее, па мой взгляд, может быть оправдана только каким-нибудь немыслимым экономическим стимулом. (М«е кажется, что нахождение под воздействием сильного экономическое стимула в чем-то сродни влюбленности: если вас спросят, втюблены I!l вы, когда вы в этом сомневаетесь, то вы наверняка ответите «нет»;tb стоит вам уверовать в свою любовь, и уже ничто па свече вас в этом'11 разубедит.) Если вы помигаете, что это слишком экстремистская поз" ция, перечитайте те фрагменты данной главы, которые касаЮ'11 Win32s, сделайте обзор PSS-статей Microsoft об ошибках ограничениях Win32s, а затем напомните себе, что все это - 'п|1' часть проблемы. Признаюсь, меня моментально бросает в дрожь, *'
„е1(оменлаиии 513 только я задумываюсь о том, сколько еще неоткрытого и неизведанного можеть таиться под покровом Win32s. На всякий случаи, хочу обратить ваше внимание па то, чгю обнаружение факта работы программы иод Win32s и катапультирование достаточно простая процедура, если воспользоваться кодом из моей программы-примера W1N32VER. Все, что [зам нужно сделать это вставить в подходящее место примерно такие две строчки: if(ComplainIfNot(WV_W95 | WV_AnyNY, Sample program )) return FALSE, И тогда многие из ваших проблем исчезнут, как но мановению волшебной палочки. Если же вы все-таки решитесь поддержать Win32s в публичной программе, тогда вашей единственной надеждой на сохранение рассудка и здоровья является выполнение следующих правил: ® На протяжении всего цикла разработки (вплоть до даты выпуска финальной: версии) внимательно изучайте всю доступную вам документацию с целью отслеживать все новости, касающиеся Win32s. Как минимум, сюда входят выпуски Microsoft Development Network CD-ROM и Win32 SDK. На момент написания данной главы, последней официально распространяемой версией Win32s является версия 1.30.159, которую я категорически считаю нефункциональной. @ Выберите единственную, конкретную версию Win32s, на которой будет работать ваша программа. А затем сделайте так, чтобы она действительно могла запускаться только на :-)гой версии Win32s. © Выполните все тестирование под выбранной версией Win32s. Когда речь идет о поддержке Win32s, вопрос о тщательнейшем тестировании и проверке является жизненно важным для публичной программы Если вы проводите публичное бета-тестирование, обязательно найдите тестеров, которые работают именно с интересующей вас версией Win32s, и дайте им именно ту бета-версию вашей программы, которая работает только иод данной версией Win32s (то есть, буквально отказывается работать и под Windows NT, и под Windows 95, и под другими вариациями Win32s). Этот последний совет, вероятно, требует небольшого уточнения Вне зависимости от того, какие именно операционные системы вы собираетесь официально поддержать, вы должны предпринять все необходимые меры, чтобы обеспечить корректную рабслу вашей программы на всех этих системах. В нормальных условиях, такие мероприятия не яв-
514 Разновилности\л/1п ляютея слишком трудным делом, но Win32s, благодаря своим вертам, совершенно особый случай. И вин действительно прид/!* делать серьезнейший выбор между проведением цикла преде-п ' скрупулезного тестирования и отказом от поддержки Win32s. Если ваша программа должна работать на нескольких 32-разрядн, платформах, я настоятельно-рекомендую вам сделать обертку для у ждого API, который имеет хоть малейшие шансы стать источник проблем с совместимостью. И снова - тестируйте все и вся, стань-р скептиком. Более того - вы должны превратиться в законченною параноика. Всегда помните о типовых патологических случаях и никогда не забывайте о том, что большая часть данных, которые вы передаете при вызовах различных API, прямо или косвенно имеет происхождение из самых ненадежных источников, включая ввод пользователя, другие программы, INI-файлы, саму операционную систему Избегайте применения переходников (thunking), как чумы. Я слышал очень много жалоб со стороны программистов о проблемах, связанных с использованием переходников. Так много, что сам я практически не имею личного опыта в этой области — до спх пор я предпочитал следовать своему собственному совету и не связываться с переходниками Помните, что переходники, как правило, являются лишь временным решением, и весьма вероятно, что следующая версия вашей программы может вообще не требовать их использования. Если ваш проект нацелен на перспективу, вы должны с самого начала стремиться переносить все модули и компоненты па Win32 и тем самым минимизировать гу работу, которая заведомо будет впоследствии выброшена на свалку К сожалению, данная рекомендация фактически равносильна совету отказаться от поддержки Win32s. Те, кто решается на такую поддержку, обычно имеют настолько веские причины для этого, что попросту проигнорируют такой совет, от кого бы он ни исходил. И все- таки, если у вас есть выбор — я убедительно настаиваю на отказе от использования переходников На тот случай, если вы не до конца осознали важность этон моего совета, я повторю его снова- тестируйте, тестирУи,е тестируйте, тестируйте (по одному призыву «тестируйте» на ь' жду к) 32-разрядную платформу, птпос еще один - для Wiu если это уместно) Эю касается как написания 32-разряДнЬ программ, так и использования в 16-разрядных программах ^' цифичных возможностей Windows 95 (таких, как длинные Фс1' ловые имена) И еще одно последнее замечание не забывая11 тестировании
</;nclqws NT 4.0: свет в конце туннеля? 515 Построение примера: WIN32VER /^^) Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/ chap14/win32ver 6^% Платформа: Win32 Инструкции по сборке: откройте при помощи Visual C++ 2.2 прилагающийся МАК-файл и скомпилируйте как обычно. В f^*% качестве примера использования предлагаемых функций рассмотрите метод hiitlnstanceO в файле WVTEST.CPP. (Поскольку программа написана исключительно для демонстрационных целей, она будет выдавать несколько фиктивных диалоговых окон вне зависимости от того, на какой Win32- платформе она запущена.) Написание всех этих рекомендаций было для меня весьма болезненным процессом. Честно говоря, я ожидал, что к концу 1995 года программирование дш Windows продвинется до такого состояния, когда подобные советы больше не будут омрачать нашу и без того непростую работу. Я никогда не питал иллюзии в отношении горделивых и высокомерных заявлений Microsoft, подобных цитате, ставшей эпиграфом к данной главе (что говорить, все мы давно должны были привыкнуть к многочисленным аналогичным заявлениям и пустым обещаниям со стороны Microsoft, IBM и других компаний самых разных профилей и размеров), однако я искренне ждал, что они сделают хоть чуть- чуть больше шагов вперед, чем они сделали на самом деле. Windows Ш 4Л: свет в конце '^га книга создавалась как раз в промежутке между выходом в свет Windows ^ ir Windows NT 4.0. А когда я только приступал к ее написанию, компьютерная общественность все еще высказывала лишь туманные предположе- |,,я о том, какой номер версии будет иметь «Windows NT с интерфейсом от endows 95» 3.52, 3.6 или 4.0 До некоторого времени считалось общеизвестным, что Windows 95 и Win- '°^s NT должны будут сойтись в единый продукт, и что произойдет это где-то п^стя одну-две версии, следующие за Windows 95. Принимая во внимание все ^Шчпя между вариациями Win32, термин «сойтись» все больше и больше ^обретал, я бы сказал, пиротехнический оттенок (представьте себе, мцРимер, что-то вроде черно-белого клипа о лобовом столкновении двух 'цРовозов). Потом настало 11 сентября 1995 года, п в передовице InfoWorld M'fIi опубликованы слова Джима Оллчина (вице-президента отдела Microsoft,
е 516 Разновилности \лг — izjji32 ответственного за Windows NT), гласившие: «Слияния Windows NT н Wiivi У.) не произойдет, поскольку нельзя одним продуктом удовлетворить тре^ имя каждого клиента» Также цитировалось и такое заявление' «Мы ожп расширения использования Windows 95 и ее преемников на иотребптельст ' рынке Л перспектива NT превратиться в стандарт настольной п сервер,- системы для icopiiopamiii» Вы спросите, а зачем нам, разработчикам, беспокоиться обо всей ;)1( MapiceTHiiroBoii чепухе'-' Причина проста: по моему мнению, если Microsoft собирается совмещать эти два продукта, то очевидным следствием этого яв i<> лтя нулевая вероятность приведения к общему знаменателю реализаций Win3') на зтпх птатформах Иными словами, если Microsoft действительно поеледуе! плану, описанному Джимом Оллчппом, то нам еще долго придется уживаться с разтичиямп в реализациях Win32 ЛР]. На самом деле, то, к чему идет дело, должно было стать понятным еще тогда, когда Microsoft решила возложить создание параллельных версий Windows на две разные команды разработчиков. (Я сразу же вспоминаю старый морской афоризм о том, что не следует выходить 15 море с двумя часами — сде 1ав зто, 31)1 никогда не будете уверены в том, какие именно часы показывают более правильное время.) Теперь же я почти уверен, что Windows NT 4.0 буде! очень близко имитировать Windows NT 3 51 во всех нынешних разногласиях с Windows 95 п Win32s Я также не жду ничего, кроме роста количества несо- вместимостей и причуд, от привнесения в Windows NT элементов интерфейса от Windows 95 Что касается каких-либо прогнозов по поводу двух следующих версий Windows 95 (известных иод кодовыми названиями Nashville и Memphis), то я умываю руки. Однако, и тут я могу с уверенностью предсказать, что они наверняка прибавят забот и приключении разработчикам. 11а сегодня лучшей стратегией для пас является выжидание, написание оберточных функций и расчет на наши собственные усердие и прилежание. A namom мх стала четверо *£^Щ*у Готов побиться об заклад, 'теперь ваша голова уже кружи'0' от всех тгих разговоров о причудах и несовмсстпмостях \Vin°- API К сожалению, это все еще не конец сей грустной" повести, |1(1 g^vl) том)' что IBM реши ia вступить в зту игру IBM обьявпла о том, чю ведемся работа по добавлению в OS/2 так1'1 зываемых DevelopeT АР J Extensions (DAP1E, ранее известны* |К (^.-^чх названием ПАХ) И л о вовсе не какая то имитационная про*-'-'0' ^'^Ш ка которая позволит запускать иод OS/2 \Ут32-программЫ словам IBM, опубтпковаппым в The Developer Connection ^(l
Windows NT 4.0: свет в коние туннеля? 517 gZ?*^ (их аналоге Microsoft Developer Network News) в августе 1995 Е****^ года на странице 5: Упрощенно говоря, Developer API Extensions для OS/2 — это набор новых программных интерфейсов, добавляемых в операционную систему. Эти интерфейсы идентичны интерфейсам Win32. Для начала поддерэ1сивается более 700 функций и 300 сообщений, в точности соответствующих своим прототипам в Win32. В этой же статье говорится, что DAPIE будет поддерживать стандартные диалоги, графику, управление окнами и «другие сис- g-J[5) темные службы, включая работу с датой и временем, окружением, файловый ввод-вывод, управление памятью, управление модулями, печать, процессы и потоки, реестр и управление ресурсами». IBM также упоминает, что конкретный список интерфейсов был выбран на основании анализа 9 миллионов строк кода и различных данных, полученных от независимых разработчиков программного обеспечения. Цель IBM ясна: облегчить переносимость программ между Windows и OS/2 на уровне исходного кода. То есть IBM буквально намеревается предоставить возможность использовать один и тот же код для компиляции родных приложений для OS/2 и Windows, хотя это и будет достигаться после «преодоления некоторых проблем с организацией и поддержкой кода» (я рад, что мой бывший работодатель все еще не утратил былую сноровку в формулировании захватывающих дух недоговоренностей). Вероятно, у вас тут же возникает вопрос: какую же из трех нынешних версий Win32 будет имитировать DAPIE? Я не знаю точного ответа на этот вопрос (на момент написания этих строк я сильно сомневаюсь, что его знает хоть кто-нибудь за пределами IBM), но меня страшит стойкое подозрение, что ответом будет «все три понемножку». А это, de facto, означает ни что иное, как появление новой, четвертой разновидности Win32. (Ведь если Microsoft не удосужилась даже задокументировать такие вещи, как расширенные коды ошибок и точное поведение тех интерфейсов, ^2^) чьи реализации сегодня отличаются на разных платформах Windows, то как же IBM сможет разобраться в том, что же все-таки должны делать эти API?) Кроме того, существует много вопросов о поддержке новых возможностей Windows 95 и грядущей Windows NT 4.0 (например, IBM наверняка придется сделать свою версию COMCTL32.DLL); без такой поддержки Developer API Extensions реально окажутся всего лишь маленьким подмножеством Win32 API, которое, вероятно, никому не будет интересно.
51 8 Разновилности tyy ? Поймите меня правильно: в мои намерения вовсе не входят смешки или упреки в адрес моего старого работодателя. Я ли хочу, чтобы вы были готовы к появлению нового инструмента л переноса программ между OS/2 и Windows. И еще я хо^ обратить ваше внимание на то, как легко можно превратит хорошую идею в непроходимое болото. Например, никого не удцР ляет отсутствие MFC для OS/2. (В самом деле, окажись вы [г месте Microsoft, предоставили бы вы IBM лицензию на MFC для того чтобы cTcVio легче переносить Windows-приложения на OS/2? Я бы точно этого не сделал, ни за какие пряники.) А без общих для обеих операционных систем каркасных библиотек DAPIE оказываются практически бесполезным инструментом для переноса более новых Windows-проектов, поскольку сегодня каркасные библиотеки (и в особенности MFC) доминируют в разработке программного обеспечения для Windows. И если даже оставить в стороне вопрос о каркасных библиотеках, то DAPIE скорее всего будет представлять собой некий гибрид трех нынешних вариаций Win32 API, что является еще одним стимулом для написания и использования унифицирующей оберточной библиотеки.
Ресурсы
-f-lljjjjjj. Ввпп-варад шяощщманпщвая Give us the tools, and we will finish the Job Уши ion Чсрчи.ч ib Man is a tool-making animal. ЬспДуКсГиш Франк ни. Любые рекомендации по част инструментов программирования являются довольно рискованным предприятием. Технического писателя, пытающегося это тать, можно уподобить программисту, работающему вне компьютерной сети. Рынок программного обеспечения для разработчиков меняется с такой бешеной яростью, что даже хорошо продуманный, проницательный цт иозмолено, бле- чящнй совет, еде манный г» понедельник, может показаться пьяным бредом уже ''Чхугу после полудня И все-таки, несмотря на .тгот риск, я думаю, есть смысл предложить нам ,сьоюрые мои рекомендации при условии, что вы будете учитывать следующие м°менты' I. Все :>тп инструменты я использовал собственноручно и счел их достойными для того, чтобы рекомендовать их вам. Еетесствеппо, я не мог апробировать все инструменты разработки, доступные па сегодняшний день По.утому я не сомневаюсь, что некоторые из моих рекомендаций могут заставшь вас искренне удивиться: с какой стати автор советлчч пользоваться продуктом X, в то время как «всякий» знает, что продук1 Y лучшее Ответ на такой вопрос скорее всего будет в том, что я попросту не пепотьзова i продукт Y (например, из-за того, что его Производикмь не сподобился предоставить мне оценочную копию
522 Хит-парал программистского инструмент* такое, к сожалению, стучалось гораздо чаще, чем вы можете себе "1*4- ставить). Могло быть и так, что я использовал Y и при этом наш,., нем достаточное количество недостатков пли проблем — достаток для того, чтобы посчитать этот продукт незаслуживающим рекомен" ции. Если вы еще не уловили мою мысль, то я поясню: выбор и' струмента для своей собственной работы — это одно дело, а выбор г кового для публичной рекомендации - совсем другое. В послед^ случае я предъявлял к продукту гораздо более высокие требования Я ничего не буду говорить вам о тех продуктах, которые я оценивал затем отвергал. Признаюсь, это мое решение до сих пор вызывает во мне двойственные чувства. С одной стороны, тот воинствующий защитник прав потребителей, которые сидит во х\ше, просто-таки рвется наружу п требует громогласно заявить о тех феноменально плохих продуктах, с которыми я боролся во время написания этой книги, а также о некоторых отвратительных службах поддержки пользователей, которые не отвечают на электронные письма или сообщения в CompuServe. Я мог бы рассказать вам десятки ужасных историй на эту тему, и наверное ощутил бы чувство некоторого удовлетворения, изложив их все па бумаге. И о с другой стороны, я прекрасно понимаю, что как только подобные антирекомеидации попадают в печать (и особенно в книги), они остаются в печатном виде навсегда. Тем временем, объект критики может не стоять на месте, и тот же самый: продукт (или служба поддержки), который недавно так сильно возмутил меня, вдруг будет кардинально усовершенствован (или реорганизован) в течение нескольких ближайших месяцев. И я бы очень не хотел, чтобы в момент своего выхода в свет моя книга вдруг стала бы отговаривать читателей от использования по-настоящему хорошего инструмента И как бы тщательно я нп идентифицировал конкретные номера версий, такая опасность реально существует, и я предпочитаю не рисковать. Норой бывает очень поучительно взглянуть на жизнь как на последовательность импровизированных lQ-тестов - просто поразительно, насколько часто люди пли целые компании и организации с треском проваливают их. В этом смысле, один из аспектов моей работы над данной книгой все время вызывал во мне одновременно изумление и разочарова' нис* я неоднократно сталкивался с поставщиками программно10 обеспечения п службами поддержки, которые не могли (или не хотели отвечать даже на простейшие технические и маркетинговые вопросы, t[L смотря на то, что они прекрасно понимали — эти вопросы эадае! человеЬ пишущий книгу и оценивающий их продукт для возможной рекомеН> ции. (Вы не поверите, по иногда они засыпались даже на таком вопр°сЧ как «Появится ли версия данного продукта для Windows 95^») * которые вообще не отвечали на повторные .запросы по электронной и0'111
523 (включая одну компанию, которая осуществляла поддержку своих пользователей только по электронной почте) Угадайте с двух раз - попал ли хоть один из этих продуктов в список моих рекомендаций? Нет, я не буду сообщать вам названия этих продуктов и компаний. Я обошелся с ними так, как они того заслуживают — я просто проигнорировал их По моему глубокому убеждению, если они не считают нужным отвечать на вопросы обозревателей или недостаточно сообразительны для этого, они не заслуживают того, чтобы вы имели с ними дело. 3. Я буду четко указывать номера версий тех продуктов, которые буду рекомендовать. Хорошие программы редко становятся хуже в своих последующих версиях, однако вам стоит приложить некоторые дополнительные усилия и, по возможности, поинтересоваться более свежими комментариями о том или ином продукте. Вы должны быть уверены, что версия N+1 того инструмента, который мне так понравился, не оказалась переполненной новыми проблемами в смысле качества, совместимости и технической поддержки. 4. Я не буду делать никаких рекомендаций в отношении двух категорий продуктов. Первая — это компиляторы. Выбор компилятора настолько тесно связан с конкретными характеристиками проекта (такими, как язык программирования, целевые платформы, уровень знаний и мастерства разработчиков и т. д.), что было бы глупо и бессмысленно давать какие-либо общие рекомендации. Посетите некоторые телеконференции, и вы станете свидетелем бесконечных споров о языках, компиляторах и библиотеках (и вы наверняка удивитесь, как же все эти извечные бойцы-спорщики вообще находят время для написания кода). Иа самом деле, вы можете выбрать первый попавшийся продукт из этой категории, а затем вы обнаружите, что людей, преклоняющихся ^перед этим творением, так же много, как и тех, кто убежденно считает его самым худшим куском бинарного мусора, когда либо предлагавшегося почтенной публике. Вторая категория — это текстовые редакторы. В данном случае предложение каких-либо рекомендаций проблематично но другой причине — из-за так назваемого «синдрома утенка». Этим термином обозначают тот факт (или, быть может, это всего лишь миф городского жителя?), что едва вылупившиеся из яйца утята могут принять за свою мать любое крупное животное, которое находилось поблизости в течение достаточно большого промежутка времени. Подобно таким утятам, программисты и большинство других компьютерных пользователей часто без оглядки влюбляются в возможности и «идиото- синкразии» первого текстового редактора, с которым они проработали достаточно интенсивно. И после этого их никакими силами не заста-
5 24 Хит-парал программистского инструмент^ вишь отказаться от своего любимого редактора — они могут до К0[ жизни пе захотеть изменить ему с другим, даже объективно бо-, продуктивным и удобным инструментом Вы только взгляните, ско ко сил тратят производители текстовых редакторов на то, чтобы продукты мопл эмулировать раскладки клавиатуры, акселератор! другие возможное ш своих, конкурентов — не является ли :jfY красочным и убедительным доказательством существования «синдром- утенка»? Все рекомендованные мною продукты перечислены без какого-либо особою ранжирования win категоризации я просто расставил их названия в алфавитном порядке. Итак, пожалуйста, приготовьте (электронные) почтовые конверты для отправки заказов па покупку программного обеспечения... МШС Flowcharter 4.® от Micragrafx В мире пет такой программы, про которую я, будучи гизмонавтом первого ранга, решился бы сказать, что она имеет слишком много возможностей Но данный продукт несомненно близок к этому. ЛВС Flowcharter умеет строить стандартные потоковые диаграммы и включает в себя базовый пакет деловой графики, программу для просмотра ваших диаграмм (которую вы можете свободно распространять), построитель диаграмм на базе электронных таблиц, а также CD-ROM с более чем 200 шрифтами TrueType. Все это не имело бы никакого особого значения, если бы программа была трудной в использовании или нестабильно работающей По она не страдает ни тем, ни другим. Я очень быстро освоил этот продукт и не имел вообще никаких проблем при его использовании. Настоятельно рекомендую. aliClEASi Version III от Clear Software, Inc. Подобно большинству других аналогичных пакетов, allCLEAR строит п°' токовые, организационные, структурные и сетевые диаграммы, деревья прийЯ' шя решении и т.д. Кроме того, она имеет тысячу и одну из тех графических11 чертежных возможностей, которые вы можете ожидать от пакета такого тип*1 (Только, пожалуйста, не воспринимайте это как заявление, что в allCLEAb есть буквально все возможности, что и в конкурирующих с ним пакетах; Я lU проводил настолько детального сравнительного анализа чертежных nporpa>lM как класса.)
реКдменлаиии 525 Я рекомендую allCLEAR по трем причинам. Во-первых, я нахожу этот па- еТ необычайно практичным и удобным, в немалой степени благодаря его ревосходной документации, ориентированной на задачи. Во-вторых, он делает се> что мне нужно в области построения диаграмм и чертежей (но не забывайте, что мои требования к пакетам такого типа могут довольно сильно 1ТЛцчаться от ваших). И третья, наверное самая главная, причина — это спорность all CLEAR принимать чисто текстовые сценарии, описывающие диаграмму, которую необходимо построить. Как я уже упоминал ранее, эта, казалось бы, незначительная функциональная возможность может сослужить отличную службу таким пользователям, как программисты: она позволяет вам написать относительно несложную программу, которая будет читать данные из какого-нибудь специализированного источника, затем на основе этих данных писать текстовый сценарий построения диаграммы и в конце концов передавать этот сценарий пакету allCLEAR. Таким образом, простой и скромный тексто-\ вый сценарий может стать очень удобным мостиком между какими-то вашими данными (телефонным справочником вашей компании, черновым конспектом презентации, иерархией классов в вашей программе или библиотеке и т. д.) и высококачественным, законченным представлением этих данных в виде диаграммы или чертежа. Настоятельно рекомендую. Bounds Checker Professional ЗМ от Мп-Меда technologies, Inc. Bounds Checker Professional — это одна из тех программ, которые вы можете до конца оценить, только увидев их в действии собственными глазами. Bounds Checker Professional до предела облегчает процесс проверки вашей программы на наличие таких ошибок, как неправильное использование API, Утечка памяти, неправильное чтение и запись в памяти (например, чтение неинициализированного участка памяти или перезапись кучи), неправильное вращение с указателями и др. Кроме того, теперь Bounds Checker Professional VMeeT делать инструментовку во время компиляции1, что дает вам возможность отловить многочисленные, совершенно неожиданные для вас ошибки. Единственный серьезный недостаток Bounds Checker Professional состоит в т°м, что его самая полезная возможность — инструментовка при к°мпиляции — значительно увеличивает время сборки программы (до несколь- К1*х раз, в зависимости от используемых при этом опций). NuMega предлагает Для тех, кто незнаком (или недостаточно знаком) с Bounds Checker Pro инструментовка Представляет собой автоматическую вставку в отлаживаемый исходный код вызовов некоторых С11ециальных библиотечных функций Bound Checker Это позволяет отладчику знать весьма интимные детали исходного кода и отлавливать поистине удивительные ошибки, которые НеВозможно так быстро обнаружить никаким другим способом [Примечание переводчика ]
526 Хит-парал программистского инструм^Нт осуществлять сборку больших проектов в ночное время, что вероятно являе осмысленным решением для большинства команд разработчиков. И несм0 * на то, что данная деталь может показаться довольно неудобной, я думаю ч Bounds Checker Professional должен использоваться по возможности в кажа проекте по разработке публичного программного обеспечения, написанного языке C/C++. И самое подходящее время для его широкого и всеобъемлютег применения — цикл тестирования программного продукта. Очень настоятедьн рекомендую. Drag and File 1.0 far Windows 95 and Windows NT om Canyon Software На мой взгляд, Проводник от Windows 95 — программа, абсолютно непригодная для серьезной работы. (А я предполагаю, что все читатели этой книги используют компьютер довольно интенсивно, не время от времени Вероятно, это самое верное предположение из тех, что я сделал при написании этих страниц.) Поэтому вам просто необходимо иметь другую программу, подобную File Manager, которая заполняла бы все функциональные пробелы Проводника, но в то же время ио-прежнему умела обращаться с длинными файловыми именами, поддерживать drag-and-drop (включая сбрасывание файлов на поверхность стола и в окна других программ) и т. д. Именно тут на сцену выходит Drag and File. Drag and File — это как раз то, что нужно: 32-разрядный, полиостью осведомленный о всех возможностях Windows 95 заменитель для Проводника Если вы привыкли к File Manager настолько, что клавиша F8 уже въелась в вашу плоть и кровь как единственный способ инициировать операцию копирования файлов, то вы, по всей вероятности, полюбите Drag and File и почувствуете себя с ним, как рыба в воде. Drag and File - это условно-бесплатная программа, которую вы можете достать по любому из обычных каналов распространения таких программ, включая форум WINUTIL в CompuServe. ISY5 for Windows 4.0 от Odyssey Development Программное обеспечение для индексирования и последующего поиска текстовой информации можно уподобить устройству дистанционного управления телевизором: вначале вы думаете, что в нем пет никакой необходимости, н° стоит вам привыкнуть к нему, и вы уже представить себе не можете, как $ь жили без него раньше. Как я упоминал ранее, программы этого типа могут 6ыт чрезвычайно полезными для программистов, поскольку они дают вам возмо5Ь ность практически мгновенно находить ссылки на заданное ключевое с.ло& (имя переменной, функции и т. п.) в огромных объемах исходного кода (в #°
ое*дменлаиии 527 9 1Сции программ-примеров, в исходниках таких библиотек, как MFC, и, нако- е11> в вашем собственном проекте). У меня есть несколько причин предпочитать ISYS другим аналогичным р0дуктам, и, пожалуй, самая главная из них — элегантность в обращении е 7]р-архивами. Благодаря печально известной проблеме с размером кластера в гдТ (и тому факту, что даже в современном мире, где 1 мегабайт стоит всего )5 центов, все еще не существует такого явления, как избыток свободного места ^винчестере), я по возможности всегда стремлюсь хранить свои многочисленные архивы и коллекции исходного кода в сжатом виде. ISYS обращается с /IP-архивами как с подкаталогами и тем самым убивает для меня сразу двух зайцев — обеспечивает компактное хранение и безболезненное извлечение информации. Кроме того, мне очень нравится гибкость ISYS при настройке опций поиска, меньше чем за минуту я могу запустить эту программу и получить требуемый результат. Тем не менее, пожалуйста, имейте в виду, что ISYS имеет несколько острых углов и подводных камней. Например, инсталляционная программа 1SYS сбрасывает довольно много файлов в ваш системный каталог Windows. И все же среди всех аналогичных пакетов, которые я пробовал, именно ISYS до сих пор остается моим фаворитом, и я почти ежедневно использую его для раскопок в архивах исходных кодов. Microsoft Development Network CD-ROM от Microsoft Должно быть, это продукт уже сотню раз упоминался мной в этой книге. И это неспроста, потому что речь идет о самом лучшем самостоятельном источнике информации для разработчиков для Windows. Существуют несколько вариантов подписки на Microsoft Development Network CD-ROM, и самым полезным и эффективным вложением ваших денег, на мой взгляд, яв- [яется подписка на так называемый Level II. Эта подписка обеспечивает вам ^квартальное получение Development Library CD-ROM (также известного [1оА названием Level I), где вы найдете огромное количество технических статей Microsoft (включая все PSS-статьи, на которое я многократно ссылался в дан- н°й книге), статьи из журналов Microsoft Systems Journal и Dr. Dobb's Jour- ,lQl> спецификации, описания стандартов и многое, многое другое. Кроме того ihl получите американский и международный «пакеты» DDK, включающие са- 1Ь1е последние версии всех SDK и операционных систем. Однако будьте готовы к тому, что цепа этой сокровищницы может покайся вам слишком кусачей. По случайному совпадению, прямо во время на- Ч1сг*ния этой рекомендации я получил от Microsoft уведомление о возобновле- 1111 моей годовой подписки на MSDN Level II (интересно, это действительно '^Чайное совпадение?). Это обойдется мне в 554.04 доллара, включая налоги
528 Хит-парал программистского инструмент** и стоимость доставки, — согласитесь, это сумма, несколько превышают- карманную мелочь. И гем не менее, я считаю, что отдача от Microsoft Develr meat Network CD-ROM с лихвой окупает эти расходы, так что я обязателы? куплю подписку на следующий год. Что настоятельно рекомендую и вам. Multimedia ToolBook 3.0 от Asymetrix Asymetrix называет этот своп продукт «системой созидания multimedia» что на первый взгчяд звучит слишком похоже на уже приевшиеся помпезные рекламные определения. Но на самом деле, Multimedia ToolBook — это нечто большее, чем просто еще один компилятор, и, на мой взгляд, этот продукт действительно заслуживает самых громких эпитетов. ToolBook представляет вашу программу в виде нескольких наглядных уровней (так называемых «страниц») и использует свой собственный язык, Open Script, отдаленно напоминающий всем знакомый BASIC. Этот продукт не из тех, которые можно освоить мгновенно, но если вы являетесь опытным Windows-программистом, то ваша кривая обучения не будет ни слишком длинной, ни слишком крутой. Мультимедийная версия ToolBook поддерживает форматы Video for Windows, QuickTime, wave audio, MIDI, Macromedia Director и Autodesk Animator, а также многочисленные графические форматы, включая Kodak Photo CD Это необычайно мощный пакет, который может сохранить вам уйму сил, времени и средств при разработке мультимедийных приложений. Настоятельно рекомендую. Orpheus I.00 от TurboPower Software Company Разработчики из TurboPower Software уже давно имеют среди паскалистов самую высокую репутацию производителей первоклассных продуктов. И Orpheus ни на йоту не изменяет эту ситуацию. Orpheus - это библиотека самых разнообразных приспособлений для создания пользовательского интерфейса в программах, разрабатываемых при помощи Delphi. В нее входят такие элементы, как проверяемые поля ввода, текстовый редактор (способный работать с текстом размером до 16 Мб), кнопки' вертушки и виртуальное окно-список, которое позволит вам преодолеть ограничение Windows па количество элементов в стандартном окне-списКе Продолжая замечательную традицию других продуктов TurboPower Software этот пакет снабжен превосходной документацией (на самом деле, лучшей II3 тех, которые я видел у других третьссторонних инструментов разработки), n K нему прилагаются все исходные коды
Ое^оменлаиии эау Если вы работаете с Delphi, и если вашей целью является создание надежных» полезных и удобных программ без попутного изобретения множества ко- еС> тогда Orpheus просто обязан быть частью вашего арсенала инструментов, аастоятельно рекомендую этот пакет для всех пользователей Delphi. (Если вспомнить древнегреческую мифологию, то Orpheus (Орфей) — это ,р1я одного из ее персонажей, сына музы Калиопы, прославившегося замечательной игрой на лире. Когда Орфей путешествовал вместе с аргонав- тами, как известно, именно он спас своей музыкой команду Арго от соблазнительных сирен. /1авайте посмотрим, сможет ли этот Orpheus стать тем Орфеем, к0торьтй спасет нас, Windows-аргонавтов...) Partition Magic 1.02.177 от PowerQuest Corporation Как вы относитесь к продуктам, в названиях которых встречаются всякие привлекательные и многообещающие словечки типа «magic»? Я определено ненавижу такие продукты априори, они вызывают во мне раздражение. Но Partition Magic — это тот случай, когда броское название и в самом деле почти оправдано. Этот продукт делает всего одну вещь, но делает ее превосходно: он до предела облегчает вам обращение с разделами жесткого диска. Используя Partition Magic, вы можете менять размеры ваших разделов и перемещать их без необходимости переформатировать их и восстанавливать их содержимое. Он даже может сконвертнровать FAT-раздел в HPFS-раздел, что может приятно удивить многих пользователей OS/2. На самом деле у меня есть одна претензия к этому продукту, хотя она носит чисто косметический характер: меня немного нервирует тот факт, что интерфейс его DOS-версии идентичен интерфейсу версии для OS/2. И пусть вас не вводит в заблуждение приписка «for OS/2», которую вы видите на упаковке Partition Magic — в его поставку входят и 32-разрядная версия для OS/2, и чисто DOS-версия. Когда я поинтересовался у Разработчиков Partition Magic, каковы их планы в отношении версии для Windows 95, они ответили, что эта версия уже находится в работе и должна выйти в свет практически сразу после опубликования моей книги. Я уже много говорил о разумном, услужливом программном обеспечении. На мой взгляд, Partition Magic явлется превосходным примером именно такого 11Родукта. Очень и очень рекомендую. SourceSafe J.I от Microsoft В течение очень долгого времени я имел очень двойственное отношение — Что-то среднее между любовью и ненавистью — к системам управления Версиями, и в период написания этой книги эти мои чувства только усилились.
530 Хит-парал программистского инструмента» Я рассматривал практически каждый продукт этого типа, имевшийся на рынк и один за другим находил примеры ужасающей документации, невероятны' ограничений и других более мелких проблем, для описания которых, наверх понадобилась бы целая отдельная глава. И только один из оценивавшихся мной продуктов ~ SourceSafe — показался мне пригодным для постоянного рутинного использования в реал Бед работе. Эта программа далеко не является совершенной, но в целом ощ обеспечивает наилучшее соотношение «цена/выигрыш» В ней полностью реализована критическая масса функциональных возможностей, включая разветвление и слияние проектов Я был немного разочарован документацией SourceSafe, которая показалась мне слишком скудной для такого продукта. Но зато она не переполнена жаргоном, который воистину является бичом данного типа инструментария в целом. Рекомендую. System Commander 2.11 am V Communications Еще одно броское, рекламоподобное название. Я искренне надеюсь, что это не является проявлением глобальной тенденции. (Знаю, знаю — это звучит несколько неожиданно в устах автора Stickiest.) Но, подобно Partition Magic, данный продукт также делает одну-единственную вещь и делает ее так здорово, что удовольствие от его использования вам обеспечено: System Commander значительно упрощает работу с несколькими операционными системами на одном компьютере. Такая возможность особенно актуальна для Windows- программистов, если вспомнить о том, насколько обширно и тщательно должны мы тестировать наши публичные программы на разнообразных версиях и платформах Windows. System Commander позволяет вам установить на компьютере практически любую операционную систему из тех, чьи названия могут прийти вам на ум: DOS, Windows 3.1, Windows 95, Windows NT, OS/2, плюс некоторые разновидности Unix (включая Linux). Кроме того, этот продукт позволяет вам задать несколько конфигураций для одной и той же операционной системы — очень ценная подмога в тестировании. И если все это еще не произвело на вас достаточного впечатления, то я добавлю: документация System Commander предельно подробна и хорошо написана. Вместе с Partition Magic, System Commander образует выдающийся и просто незаменимый тандем для управления конфигурацией вашего компьютера. Очень настоятельно рекомендую. Windows 95 Device Driver Kit Если вы не пишете драйверы устройств (а этим занимаются совсем немногие), то на кой ляд вам нужна такая вещь, как DDK? Причина проста: в DDK вы можете найти такую информацию, которую больше нигде не найдете
рекоменлаиии ээ I Например, описание новых, нигде больше не задокументированных 16-разряд- [Ibix API, появившихся в Windows 95. Лучший путь раздобыть Windows 95 [}DK - приобрести подписку типа Level II на Microsoft Development Network. Windows NT 3.51 Если вы серьезно занимаетесь разработкой программ для Windows, в особенности публичных программ, написанных на языке C/C++, то с большой до- ieir вероятности вы работаете с системой, которая по производительности находится где-то на полпути в направлении BelchFire 5000 Wonderbox Иными словами, у вашего компьютера должно быть достаточно мегов и мипсов1 для запуска Windows NT. И если все это так, то вы должны установить у себя Windows NT. В любом случае Windows NT понадобится вам хотя бы для тестирования: не забывайте, что ваши 32-разрядные программы должны либо корректно работать на этой платформе, либо четко обнаруживать ее и элегантно отказываться от работы. Кроме того, вспомните, что вскоре после выхода этой книги должна быть выпущена в свет Windows NT 4.0. Эта версия должна включать в себя пользовательский интерфейс от Windows 95 и, предположительно, многие новые элементы Win32 API, которые сегодня являются уникальными для Windows 95. YAHU: еще одна утилита для работы с заголовочными файлами Этот маленький простой инструмент, вероятно, уже имеется у вас, только вы об этом еще не знаете. Данная утилита, которая вместе со всеми своими исходниками входит в поставку Microsoft Development Network Level I CD- ROM, предназначена для просмотра самой разнообразной информации, хранящейся в заголовках исполняемых файлов, включая список всех функций, экспортируемых динамической библиотекой. YAHU может оказаться очень полезной, когда вы играетесь с различными версиями 16- и 32-разрядных программ (я предостаточно занимался этим при написании данной книги и Думаю, что всем нам так пли иначе придется это делать в течение некоторого времени). От «Megs and MIPS», MIPS — единица измерения «скорострельности» процессора [Примечание переводчика ]
ттшмтельная Жизнь без музыки бьиш бы ошибкой. Фридрих Ницше Это справедливо и для программирования, вождения автомобиля и других занятий, которых слишком много, или которые слишком интимны для того, чтобы их перечислять. Я заметил, что всякий раз, когда я пишу или программирую, в мой плейер вставлен CD. Если по какой-нибудь причине я вынужден работать в тишине, у меня возникает такое чувство, будто я нахожусь в какой-то стерильной, скучной обстановке. И у меня есть сильное подозрение, что я не одинок — вы любите и слушаете «рабочую музыку» не меньше меня. Поэтому ниже я привел аннотированный список моих фаворитов. Мой электронный адрес указан во вступительном слове к этой книге — пишите, и я с удовольствием узнаю, что предпочитаете слушать вы. Но поскольку я назвал эту главу «Дополнительная литература», я также поместил в ней мои рекомендации в области книг. Я знаю, что мой подход может показаться странным. Но я только хотел лишний раз подчеркнуть, что действительный арсенал наших инструментов разработки — это бесконечный кон- пшуум, и даже такие вещи, как музыка, которую мы слушаем во время работы, являются частью этого спектра. Abrash, Michael, Zen of Graphics Programming, The Coriolis Group, ISBN ^вЗбУУ-Ов-Х, 1995 Эта книга является настоящей сокровищницей информации по 11Рограммпроваиию графики. Она охватывает такие вопросы, как аппаратура *GA, расщепление экранов, манипулирование палитрами, алгоритмы рисовали линий, окружностей, эллипсов и многоугольников, анимационные техно-
534 Аополнительная лите{ Ъ логии. И все эти темы изложены в красочном, привлекательном и понятн стиле. Не совершайте ошибку — не игнорируйте эту книгу только лишь лотом , что вы не собираетесь заниматься в ближайшем будущем написанием т,*' коуровневого графического кода или планируете использовать только GD] функции для рисования геометрических фигур и тому подобного. Я уже упоминал ранее, что все мы должны понемногу читать о других областях программирования — это не только отличный способ расширения кругозора но и эффективный путь отыскания и заимствования новых программистских технологий и приемов. И данная книга является отличным примером такого рода вспомогательного материала. Очень, очень рекомендую. Abrash, Michael, Zen of Code Optimization, The Coriolis Group, ISBN 1- 883577-03-9, 1994 В моей книге я лишь очень поверхностно затрагивал вопросы оптимизации и говорил, что вы должны заранее знать, когда вам придется выдавливать из своего кода каждый лишний байт, каждую лишнюю инструкцию процессора. И когда этот момент наступит, вам непременно понадобится книга Майкла. В ней освещены практически все аспекты оптимизации - от общих приемов выжимания из кода максимальной производительности, до многочисленных конкретных примеров на ассемблере для Intel 80x86. Очень настоятельно рекомендую Aitken, Peter, and Scott Jarol, Visual C++ Multimedia Adventure Set, The Coriolis Group, ISBN 1-883577-19-5, 1995 Каждый знает в общих чертах, что такое мультимедиа. Но я знаю не так уж много по-настоящему хороших книг о написании мультимедийных приложений, и эта — одна из них. В ней освещены многочисленные темы, которые несомненно будут интересны программирующим для Windows 95 на C/C++' гипермедиа, Windows MCI, манипулирование палитрами, видео-эффекты, анимация, программирование звука и др. Настоятельно рекомендую. Badalamenti, Angelo, Soundtrack from Twin Peaks, Warner Brothers, 9 26- 316-2 По-настоящему завораживающая музыка, которую можно слушать без остановки и по нескольку раз. Отлично идет под чашечку хорошего кофе п кусочек вишневого пирога.
хополнительная литература эээ Bayless, John, Bach on Abbey Road, Pro Arte, CUD 346 Bayless, John, John, Bach, Bat/less, Beatles, ProArte, CDD 413 Джон Бэйлесс исполняет свои собственные фортепьянные аранжировки пе- сен Битлз, делая это в стиле произведений И. С. Баха. Отличная музыка для отладки или программирования, особенно когда вы бьетесь над непрошибаемой проблемой и нуждаетесь в успокаивающем воздействии. Brooks, Frederick P., Jr., The Mythical Man-Month, Addison-Wesley, ISBN 0-2Ю-ОО65О-2, 1975 Если понятие «классика» применимо к такой молодой и скороспелой отрасли как компьютерная наука, то эту книгу можно смело назвать одной из классических. Прекрасно написанная и проницательная, эта книга должна ежегодно прочитываться всяким, кто участвует в разработке программного обеспечения (в любой роли, но особенно — в управлении этим процессом). Убедительно и настоятельно рекомендую. Byrne, David, David Byrne, Warner Brothers, 9 45558-2 Byrne, David, Rei Momo, Sire, 9 25990-2 Да, это тот самый Дэвид Берн из группы The Talking Heads. Классная вещь, но я нахожу, что его лирика немного затрудняет концентрацию на программировании. Просто потому, что эти стихи практически невозможно игнорировать. Calvert, Charles, Delphi Unleashed, SAMS Publishing, 1995, ISBN 0-672- 30499-6 ' Если вы расматриваете вопрос об освоении Borland Delphi, но не знаете Pascal (возможно, вы ищете альтернативу для C/C++, но этот язык — единственный, который вы до сих пор использовали всерьез), то вам следует прочитать данную книгу. Автор шаг за шагом познакомит вас и с архитектурой Delphi, и с Object Pascal, начиная с самых азов, типов данных и Delphi IDE, и кончая методикой ООП, написанием приложений для работы с базами данных и созданием повторно используемых компонентов. Настоятельно рекомендую. Canadian Brass, Bach: The Art of the Fugue, CBS Records, MK 44501 Прекрасная запись в исполнении квинтета Canadian Brass. Я очень рекомендую послушать все, что сотворила эта группа, но этот альбом, на мой Взгляд, является их лучшим произведением на сегодняшний день.
536 Аополнительная литерату Canadian Brass, High, Bright, Light and Clear -The Glory of Baron Brass, RCA, RCD14574 ' * Canadian Brass, Vivaldi: The Four Seasons, CBS Records, MK 42095 The Carl Stalling Project: Music from Warner Bros. Cartoons 1'936-195* Warner Bros., 26027-2 Возможно, это самая подходящая-фоновая музыка для программирования На этом CD вы найдете именно то, что заявлено в названии, причем в оригинальном исполнении. Только приготовьтесь к тому, что вы вряд ли сможете много папрограммпровать во время первой дюжины прослушиваний этого сборника. Carlos, Wendy, Switched-On Bach, CBS, MK 7194 Carlos, Wendy, Switched-On Bach 2000, Telarc CD-80323 Оригинал SOB, а также редакция к 25-й годовщине. Замечательная, очень замечательная вещь. Constantine, Larry, Constantine on Peoplezoare, Prentice Hall/Yourdon Press, ISBN 0-13-331976-8, 1995 Это коллекция статей Ларри Константина из рубрики Peoplcware журналов Computer Language Magazine и Software Development. В этих статьях вы найдете такое освежающее и бросающееся в глаза (я бы даже сказал, ошеломляющее) количество здравых идей и рассуждений, что вы наверняка подумаете, что Ларри и в самом деле является программистом. Настоятельно рекомендую. Coplien, Jim, Advanced C++; Programming Styles and Idioms, Addison- Wesley, ISBN 0-201-54855-0, 1992 Я думаю, очень немногие смогут прочесть эту книгу и заявить, что не нашли там ничего нового и цегиюго для себя. Настоятельно рекомендую для всех кодировщиков-тяжеловесов на языке C/C++. Crawford, Sharon and Charlie Russcl, OS/2 for Windows Users, Sybex, ISBN 0-7821-1528-4, 19941 Вы являетесь Windows-программистом от Бога; вы можете наизусть перечислить весь Windows API в обратном алфавитном порядке; вы знали °^ 1 Издание на русском я*ыкс Рассел, Кроуфорд OS/2 для пользователей Windows - СГК' Нигер, 1995 ISBN 5-88782-024-1
дополнительная литература 537 тличии модальных диалогов от немодальных еще с детского сада; вы — царь ! бог во всех Windows-проектах, ведущихся в вашей компании. И вот, в один прекрасный день ваш босс сообщает вам, что вы назначены на должность геХНИческого руководителя проекта по переносу ключевого, самого доходного в вашей компании Windows-продукта на OS/2 — на операционную систему, которую вы возможно видели всего один раз, да и то на фотографии компьютерного экрана в каком-то журнале. После того, как вы вырвете все во- юсы на своей голове и переломаете всю мебель в своем офисе, вы должны обязательно вспомнить про эту книгу pi прочитать ее от корки до корки. Именно зга книга поможет вам максимально быстро освоить OS/2 как пользователю. Авторы используют неординарный и очень эффективный подход к изложению многих вопросов, так что кривая обучения OS/2 для пользователей Windows оказывается на удивление легко преодолимой. Dolby, Thomas, Aliens Ate my Buick, Capitol, EMI CDP-7-48075-2 Dolby, Thomas, Astronauts and Heretics, Giant, 9 24478-2 Dolby, Thomas, The Flat Earth, Capitol, CDP 7 46028 2 Dolby, Thomas, The Gate to the Mind's Eye, Giant, 9 24586-2 Dolby, Thomas, The Golden Age of Wireless, Capitol, CDP 7 46009 2 Томас Долби (автор "She Blinded Me with Science") творит музыку, весьма и весьма непохожую на все, что вы слышали. А еще эта музыка оказывается на редкость подходящей в качестве фонового звука для программирования. Dorsey, Don, Bachbusters, Telarc, CD-80123 Замечательные электронные переложения нескольких композиций И. С. Баха. Dorsey, Don, Beethoven or Bust, Telarc, CD-80123 Дорси придает электронное звучание не только Баху, но и Бетховену. Duntemann, Jeff, and Ron Pronk, Inside the PowerPC Revolution, The Co- 'iolis Group, ISBN 1-883577-04-7, 1995 Эта книга с большим отрывом является самым полным источником 11нформации о PowerPC из всех, что я видел. Она рассказывает об истории Эт°го и других чипов, об архитектуре PowerPC и о многом другом.
538 Лополнительная литераг\ Ъ Duntemann, Jeff, Jim Mischel, and Don Taylor, Delphi Programming £ „ plorer, The Coriolis Group, 1995, ISBN 1-883577-25-X, 1995 ' *~ Во вступительном слове к своей книге Джефф пишет: «Основной идее" было составление такой презентации Delphi, которая не только позволила бь новичку запустить этот инструмент в дело как можно быстрее, но и осталась бь в его памяти необычайно приятным времяпровождением». Эта книга нравится мне по многим причинам, в том числе и потому, что она как нельзя лучше достигает поставленных автором целен. Это замечательное введение в Delphi которое помогло нескольким моим знакомым освоить этот продукт в наикратчайшие сроки. Glass, Philip, Koyaanisqatsi Soundtrack, Antilles 90626-2 Glass, Philip, Powaqqatsi Soundtrack, Elektra 79192-2 Glass, Philip, Solo Piano, CBS Records, MK 45576 Пятьдесят одна минута яркой, запоминающейся фортепьянной музыки Вы не сможете представить себе ничего подобного, пока не услышите это своими собственными ушами. Замечательно подходит для сеансов отладки и других утомительных занятий. Harrel, Lynn and Vladimir Ashkenazy, Beethoven: Cello Sonatas, London, 417 628-2 Katsaris, Cyprien, Beethoven/Liszt, Ninth Symphony, Teldec Невероятно — Ференц Лист писал транскрипции симфоний Бетховена для соло на фортепьяно! Если вы вообще знакомы с Девятой симфонией Бетховена, вы наверняка не можете себе представить, как мог этого добиться даже такой гениальный композитор, как Лист. Однако это факт. Lennox, Annie, Diva, Arista, 07822-18704-2 Lennox, Annie, Medusa, Arista, 74321-25717-2 Если бы могли программировать так же, как Энни Леннокс поет в этих альбомах, программное обеспечение в целом стало бы намного более интересным. Maisky, Mischa, /. S. Bach: Six Cello Suites, Deutsche Grammophon, 41^ 416-2 Прекрасная, настоящая музыка, идеально подходящая для долгих °г ладочных сеансов. Особенно для таких, когда у вас больше не остается никакп* объяснений происходящему, кроме уверенности в том, что ваш компьютер
дополнительная литература РЭУ 9 аБерняка свихнулся и неправильно выполняет какую-то последовательность инструкций. Mehta, Zubin/Israeli Philharmonic Orchestra, Liszt: Hungarian Rhapsodies, CBS, MK 44926 Если вы думаете, что классическая музыка не может быть забавной, послушайте этот диск. Особенно вторую рапсодию, которая наверняка знакома многим по музыкальным (и немузыкальным) теле- и радиопередачам. Meyers, Scott, Effective C++: 50 Specific Ways to Improve Your Programs md Designs, Addison-Wesley, ISBN 0-201-56364-9, 1992 Автор преподносит поразительное количество (около 200 страниц) практических советов для кодировщиков на C++, и все это делается в простом и аккуратном стиле, pi сдобрено отменным тонким юмором. Настоятельно рекомендую для программистов всех уровней мастерства, и особенно для тех, кто убежден, что больше не нуждается в таких книгах. Microsoft Corporation, Programmer's Guide to Microsoft Windows 95, Microsoft Press, ISBN 1-55615-834-3, 1995 Эта книга полна подробнейших деталей о программировании для Windows 95, многие из которых вы либо вообще не найдете ни в каких других источниках, либо нигде не изложены так же хорошо и упорядоченно. Очень рекомендую. Прмимо всего прочего, служит отменным однотомным доказательством того, что жизнь Windows-программистов круто и навсегда изменилась с приходом Windows 95 (на тот случай, если у вас еще есть какие-то сомнения по этому поводу). Midori, Paganini: 24 Caprices, CBS MK 44944 Изумительный сборник из 24 произведений великого представителя Мировой классики, Никколо Паганини. Если вас вдохновляют яркие образцы вЬ1сочайшего мастерства, вам должны очень понравиться эти записи. (Также обратите внимание на записи этих же произведений Паганини, сделанные ^рельмаком.)
540 Аополнительная литерат Murray, James D. and William vanRyper, Encyclopedia of Graphics p i Formats, O'Reilly & Associates, Inc., ISBN 1-56592-058-9, 1994 *c Эта книга объемом около 900 страниц, подробно описывающая десяц- графических форматов, действительно является настоящей энциклопедией Очень полезна в качестве наглядного пособия для изучения дизайна файловых форматов. Рекомендую. Mystery Science Theater 3000, a.k.a. MST3K Если вы не видели это телевизионное шоу, обязательно посмотрите одну или несколько серий. Perlman, Itzhak, Paganini: 24 Caprices, EMI CDC 7 47171 2 Еще одно замечательное исполнение знаменитых упражнений великого маэстро Паганини — настоящая пытка для скрипачей (и скрипок). (См. также записи Midori.) Pietrek, Matt, Windows Internals, Addison-Wesley, ISBN 0-201-62217-3, 1993 Зачем вам нужна книга, посвященная внутренностям Windows 3.1, в то время, когда Windows 95 и Windows NT уже стали вашим Настоящим и Будущим? Это просто: большая часть из того, что автор говорил тогда, актуально и по сей день (например, описание механизмов Windows для загрузки динамических библиотек). Настоятельно рекомендую. Queen, Classic Queen, Hollywood Records, HR-61311-2 Queen, Greatest Hits, Hollywood Records, HR-61265-2 Все плоды творчества Queen заслуживают того, чтобы потратить время на их прослушивание. Но если бы перед вами стояла задача выбрать всего два экземпляра для своей коллекции Queen, я бы порекомендовал именно эти два сборника. Raitt, Bonnie, Luck of the Draw, Capitol C2-96111 Raitt, Bonnie, Nick of Time, Capitol CDP 7 91268 2 Если вы хотите, чтобы ваша работа сопровождалась по-настоящему веЛП' коленным звуком и мастерским исполнением, тогда ничего нет лучше этих ДВУ* дисков Бонни Рэйт. Очень классные вещи.
дополнительная литература 541 Schulman, Andrew, Unauthorized Windows 95, IDG Books, ISBN 1-56884- 305-4, 1994 Помните все эти... гм... недоразумения, с которыми мы сталкивались еще до выхода Windows 95 в свет? В частности, взаимосвязь DOS и Windows 95. Двтор этой книги проводит глубочайшее исследование этих проблем и многих других аспектов Windows 95, приоткрывает вашему взору самые потаенные уголки архитектуры Windows 95. Настоятельно рекомендую, хотя данная книга вряд ли пригодится непрофессиональным программистам. Special EFX, Slice of Life, GRP Records, GRP-D-9534 Что сказать, эта музыка нравится каждому. И в то же время она является замечательным аккомпанементом для программирования и отладки. Stroustrup, Bjarne, The Design and Evolution of C++, Addison-Wesley, ISBN 0-201-54330-3, 1994 Либите ли вы C++ или ненавидите его, используете ли вы его профессионально или только по-любительски, вам следует прочесть эту книгу. Написанная «отцом-основателем» языка C++, она очень подробно разъясняет, почему те или иные вещи в этом языке были реализованы именно так, а не иначе. А также почему некоторые возможности (многие из которых казались привлекательными самому автору) так никогда и не были реализованы. Настоятельно рекомендую. Wakeman, Rick, The Myths and Legends of King Arthur and the Knights of the Round Table, A&M, CD 3230 Wakeman, Rick, The Six Wives of Henry VIII, A&M, 393 229-2 Эти интереснейшие композиции, их мастерское исполнение делают эти записи выдающимся примером фоновой музыки для программирования. Хотя некоторые из этих произведений кажутся мне слишком интенсивными, слишком впечатляющими для того, чтобы слушать их во время сеансов отладки. Walnut Creek CD-ROMs Включая The С Users' Group Library, Turbo User Group CD-ROM, Perl, ClCA Shareware for Windows, Hobbes Archived OS/2, и Source Code CD- Rom. Превосходные источники информации, включающие в себя исходные КоДы, примеры проектов и дизайнерских решений, некоторые очень полезные
542 Аополнительная лит*^ —^T>5j утилиты. Очень рекомендую, особенно в паре с хорошим пакетом дЛя дексирования и поиска текстовой информации. ин- Windows 95 Resource Kit, Microsoft Press, 1995 Это очаровательный набор из книг и дисков - более 1,300 страниц сам ' разнообразной информации о Windows 95: установка и настройка, работа сети, управление системой, электронная почта, реестр, основы архитектуры прочая, и прочая. Используете ли вы Windows в корпоративной сети иди и отдельном компьютере, запускаете ли вы ее просто как пользователь и ш разрабатываете для нее программы, -- эта книга, вероятно, будет незаменимым настольным пособием для вас. Настоятельно рекомендую. Шризовой раздел: три неожиданных открытия Нравятся ли вам книги, которые способны изменить ваш взгляд на мир? Если вы так же, как и я, любите такую литературу, вам наверное будет интересно взглянуть на краткий список моих фаворитов — произведений, которые, на мой взгляд, доставят удовольствие большинству читателей (в особенности - и рограммпстам). Weidenfeld and Nicolson, The Mezzanine, Weidenfeld & Nicolson, ISBN 1- 55584-258-5 Довольно объемистая (135 страниц) новелла, все действие которой происходит в течение 30-секундиой поездки на эскалаторе. Если это не заинтригует программиста с его любовью к мелочам и деталям, то я не знаю, что еще сможет вас так привлечь. Также рекомендую прочитать другой «мгновенный роман» - Baker's Room Temperature. Carse, James P., Finite and Infinite Games, The Free Press, ISBN 0-02-905- 980-1 В этой книге, состоящей из 101 короткой главы, автор бросает взгляд на теорию игр и на то, как они соотносятся с жизнью общества. Searle, John R., The Construction of Social Reality, The Free Press, ISBN^' 02-928045-1 Тема этой книги как нельзя более точно отражена в ее названии. Очень глу бокий, интригующий материал. Очень рекомендую.
JjjJlLAdJj ШедтЖет — cmm&m шМинченнтт в мире, ничего ше демтшщшя... Example is always more efficacious than precept. С.шюэл Джонсон Example is the school of mankind, and they will learn at no other. Эдмунд Бсркс На протяжении всего повествования .тгоп книги я рассказывал о многих программистских приемах и технологиях, попутно приводя отдельные программы-примеры или даже просто демонстрационные фрагменты кода. Я глубоко Убежден в полезности таких примеров, поскольку они зачастую могут заменять собой целые страницы прозы и при этом передавать концепции программировали кратко [I ясно. Тем не менее, искусственность этих узких, специализированных примеров, как правило, несколько снижает их практическую Полезность: вне зависимости от того, насколько тщательно они подобраны и 'формлепы, изолированные лоскутки кода чаще всего являются чем-то вроде 'Пличных орхидей, которые лишь обещают, что смогут выжить в зябком (а 1|,1,Це морозном) реальном мире. Именно поэтому я хотел продемонстрировать iC Же самые приемы, объединив их в еще один, более объемный и близкий к •ильной программе пример, с которым вы потом могли бы эксперимен- 11Ровать, уже находясь в условиях, максимально приближенных к «боевой об- Га"овкс». гГакова история и истоки Me&aZcro. Программа-пример Mc^aZero использует следующие приемы, описанные в *1,1|[Иой книге: Г I
546 Приложение а Е_ Самозащита от внешнего вмешательства. в Корректный показ значка в программе, основанной на диалоге. В Использование умного файла данных (включая отдельную утидцт, для дамппига такого файла). В Обработка drag-and-drop. В Явное управление динамическими библиотеками. В Замкнутость. Чтобы быть предельно точным, я должен заметить, что заголовок этого приложения чуточку утрирован: MegaZero все-таки кое-что делает - она поддерживает список последних десяти файлов, которые были на нее сброшены, а также помнит дату последнего своего запуска. Но поскольку она ничего не делает ни с этими файлами, ни с этой датой (кроме их показа пользователю), все это достаточно близко подходит под определение «бездейственности», так что, я надеюсь, никто не будет особо усердно оспаривать корректность заголовка Должен также признать, что при написании MegaZero я не всегда и не до конца следовал некоторым из моих собственных советов и рекомендаций Например, в книге я не раз акцентировал ваше внимание па том, что любой диалог в публичной программе должен содержать кнопку Help или иным способом предлагать пользователю возможность получить дополнительную, подробную справочную информацию. MegaZero делает такие предложения в некоторых случаях (например, при перезаписи файла данных или при неспособности загрузить собственные динамические библиотеки), но при этом я не стал напрягаться и создавать соответствующий НLP-файл. Более серьезное нарушение моих собственных правил касается двух вспомогательных подпрограмм, FileExistsO и AppendSlashO, помещенных в динамическую библиотеку TOOLBOX.DLL. Чтобы читатель не подумал, что я вдруг отказался от своего стремления избегать использования DLL, позвольте мне сразу уточнить: в реальной программе, особенно в публичной, я скорее всего не использовал бы DLL таким образом, поскольку эти две подпрограммы с очевидностью не должны связываться динамически. Обратите внимание на то, что функция FileExistsO является особенно неподходящей для помещения се 13 динамическую библиотеку и последующего доступа к пей через функции' [илюз, поскольку такой подход вынуждает вас определить для нее еще 00° специальное возвращаемое значение, соответствующее ситуации, когда не была загружена сама DLL. В MegaZero я выбрал для этого значение FALSE, KilK наиболее безопасное. Но оно, конечно же, не является наилучшим выбором, а°' тому что это значение неотличимо от индикации отсутствия заданного файла Программа может случайно вызвать функцию FileExistsO без предварительно11 проверки, загружена ли DLL (пли вообще без предварительной поиъгп*11 загрузить DLL), и получить в ответ ложное отрицание. В свою очередь, ttoJlb
dpeaZero — самая законченная в мире, ничего не лелаюшая... 547 9 ,0затель наверняка посчитает такое поведение программы странным, так как на сообщит ему об отсутствии файла, который на самом деле существует. Из оГо следует очевидный вывод: помещение в DLL даже самых, казалось бы, .ростых функций требует серьезного предварительного обдумывания, так как нГ() может потребовать переделки интерфейса этой функции всего лишь для ого, чтобы научить ее сигнализировать о своем «внеурочном» вызове — вы- olJe в тот момент, когда соответствующая днпамтгческая библиотека не -(1 гружен а. Построение примера: MegaZero Местоположение: http://www.symbol.ru/russian/library/prof_prog/source/app_a/ megazero http://www.symbol.ru/russian/library/prof_prog/source/app_a/ toolbox http://www.symbol.ru/russian/library/prof_prog/source/app_a/ patcher http://www.symbol.ru/russian/library/prof_prog/source/app_a/ dumper Платформа: Win32 Инструкции по сборке: откройте при помощи Visual C++ 2.2 прилагающиеся МАК-файлы и скомпилируйте как обычно. Эти проекты используют относительные пути для ссылок на включаемые файлы, поэтому вы должны либо скопировать на свой жесткий диск все дерево SOURCE\APP_A целиком, либо должным образом подредактировать утверждения «#include» в исходных кодах. Как только вы построите программу PATCHER.EXE, скопируйте ее в тот каталог, где находится главный исполняемый файл MegaZero (MEGAZERO.EXE), и запустите. Это позволит основной программе, MegaZero, осуществлять самопроверку. Чтобы утилита DUMPER.EXE могла работать, файл MEGAZERO.DAT, создаваемый главной программой, должен находиться в одном и том же каталоге с DUMPER.EXE. Листинг А.1. SOURCE\APP_A\MEGAZERO\CONFIG.H ttifndef CONFIG_H «define __CONFIG_H__ «include <pchpack1 h> // Хотим однобайтовое выравнивание - пожалуйста void LoadConfigData(void), void SaveConfigData(void),
Приложен Gtrucl config_type chai fn1[MAX_PATH], char fn2[MAX_PATH], char fn3[MAX_PATH], char fn4[MAX_PATH], char fn5[MAX_PATH]. char fn6[MAX_PATH]. char fn7[MAX_PATH]. char fn8[MAX_PATH], chat fn9[MAX_PAFH], char fn10[MAX_PATH] mt last_runjnontn. mt lact_njn_day mt lact_run_year. BOOL locked, char pacsword[MAX_PW_LEN +1], int ieserved[10], // Наш глобально доступный масив конфигурационных данных extern config_type config // Наш заголовок который обычно не используется никем извне данной // единицы компиляции #detine eye_catchei_ien 9 strucr confiq_heacier type chai eye_catchei[eye_catcher_len], WORD majoinversion, WORD nunor_vercion. i «include <poppacl< h> // Возвращаемся к тому выравниванию, которое было раньше #endif Листинг А.2. SOURCE\APP_A\MEGAZERO\CONFIG.CPP /* Copyright Lou Grinzo 1995 Zen of Windows 95 Programming */ «include Gtdafx h «include 'config h «include Ю h «include tb_int h «include toolbox h // Наш глобальный массив конфигурационных данных, который будет перекрыт // считанными из файла данными (при условии, что этот файл существует) config_type config = [no [no [no [no [no [no [no "[no [no [no cettinq] ' Getting] Getting] Getting] Getting] Getting] Getting] ' Getting] Getting]" Getting] // fn1 // fn2 // fn3 // fn4 // fn5 // fn6 // fn7 // fn8 // fn9 // fn10
^gggZero — самая законченная в мире, ничего не лелаюшая... 549 0, 0, 0 FALSE, { 0 0 0 0, 0. 0 0, 0 // la3t_run_inonth // last_run_day // lact_run_year // locked // password 0. // reserved 0 , static cha-' aaia_fn[] = megazero dat , config_header_type config_header = 'megazero // eyecatcher (9 chare, mcl term null) 1, // major_version 0 // minor_version //////////////////////////////////////////////////////////////////////////// // Заголовки для локальных подпрограмм //////////////////////////////////////////////////////////////////////////// static BOOL BuildFM(char *fn, size_t max_len), static BOOL CreateNewFile(void), static UINT GetBaseDirectory(LPTSTR lpBuffer,UINT uSize). static BOOL IsOurFile(FILE *f). static BOOL Renamelmpostor(void) static void SetUpData(void), static BOOL StoreData(FILE *f), static BOOL ctrlcat(char *dest. const char *src, size_t max_len), static BOOL strlcatAtomic(char *dest. const char *src, cize_t max_len), l/l/ll II III llll/l/l/ll/1 Ill/Ill III Illll/lllllllll/llllllllHHlllll/l III/III // Экспортируемые подпрограммы //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// // LoadConfigData() Загружает в память массив конфигурационных данных, // считанный из нашего специализированного файла данных Эта функция // обрабатывает все патологические случаи файл не найден, файл найден, но // в действительности не является нашим файлом, и т д Если пригодного файла // не нашлось то все значения по умолчанию для configjieader и config // сохраняются, и их можно использовать как есть п i/ii ii/iiii/i/ii/1 i/iii/iiiiiiiiii/iiiii tin in шп/пт/п т пит ш void LoadConfigData(void) char fn[MAX_PATH], FILE *f. lfCBuildFNCfn.cizeofCfn))) { ttifdef .DEBUG OutputDebugStringC*** LoadConfigData Невозможно сконструировать имя файла данных \п #endif return // Обработать особый случай и убрать его с дороги if('FileExists(fn)) { #ifdef „DEBUG OutputDebugStringC*** LoadConfigData Файл данных отсутствует, \п ), 0utputDebugStnng( создаем новый \п ), ttendif CreateNewFile(),
Приложение return if((f = fopen(fn, rb )) == NULL) #ifdef _DEBUG OutputDebugString( *** LoadConfigData Невозможно открыть файл данных \п'), tfendif return, if(lGOurFile(f)) { #ifaef _DEBUG OutputDebugStnngO*** LoadConfigData IcOurFileO == TRUE\n ), tfendif // Перешагиваем через заголовок, который уже прочитан foeek(f cizeof(config_header),SEEK_SET), if(fread(&config,cizeof(config),1.f) == 0) #ifdef _DEBUG OutputDebugStringC*** LoadConfigData Невозможно прочитать файл данных\п ), «endif // Если бы нам было нужно обрабатывать более чем одну версию файла данных, // то именно в этом месте кода мы прочитывали бы те или иные расширения // формата, в зависимости от номера его версии *cloce(f) elce { #ifdef „DEBUG OutputDebugStringC *** LoadConfigData IcOurFileO == FALSE\n'), #endif fclooe(f), if(RenameImpo5tor()) CreateNewFile(), )lIII//////I/l/n/lIll/Illllllll/lll/lll/IIl/ll/ll/lll/l/llIII/III//III///// II SaveConfigData() Сохраняет находящийся в памяти массив конфигурационных // данных в конфигурационный файл, имеющий специальный заголовок Эта функция // обрабатывает все патологические случаи файл не найден, файл найден но // в действительности не является нашим файлом; и т д 11/11/I/IIIIIIIII/I/III//I/IIIIIIIIIII//IIIII/I/II/III/I/IIIIIIIIIIIII/I/III void SaveConfigData(void) { char fn[MAX_PATH], FILE *f, lfCBuildFNCfn.GizeofCfn))) tfifdef _DEBUG OutputDebugStringC *** SaveConfigData Невозможно сконструировать имя файла данных \п #endif return, // Как же это могло случиться9 Возможно, пользователь или какая-то другая // программа удалили наш файл данных, пока мы работали9 Неважно В любом // случае эту ситуацию можно обработать
\egaZero — самая законченная в мире, ничего не лелаюшая... lfCFileExiGtG(fn)) { #ifdef _DFBUG OutputDebugStnng( *** SaveConfigData. Файл данных отсутствует. \n* ), OutputDebugString( создаем новый \п ), #endif CreateNewFile(), return, // Открываем файл if((f = fopen(fn, rb+ )) == NULL) { #ifdef _DEBUG OutputDebugStnngC'*** SaveConfigData Невозможно открыть файл данных \п"), #endif return, // если он наш lfdcOutFile(f)) tfifdef _DEBUG OutputDebugStringC *** SaveConfigData IsOurFileO == TRUE\n" ), #endif // записываем заголовок и данные StoreData(f), // и дело сделано fcloce(f), } elce { #ifdef .DEBUG OutputDebugStringC'*** SaveConfigData' IsOurFileO == FALSE\n'), tfendif // . если файл не наш, то мы закрываем старый файл, оставляя его // нетронутым, переименовываем его, а затем создаем новый файл и // сбрасываем в него наши конфигурационные данные из памяти fcloce(f). lf(RenamelmpostorO) CreateNewFile(), i //////////////////////////////////////////////////////////////////////////// // Локальные подпрограммы ////////////У/////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// // BuildFN() Конструирует полное имя нашего файла данных Результирующая // строка помещается прямо в буфер, предоставленный вызывающим кодом // // Возвращает TRUE, если строка была успешно сконструирована В противном // возвращает FALSE // // max_len включает завершающий нуль //////////////////////////////////////////////////////////////////////////// static BOOL BuildFN(char *fn, oize_t max_len) { UINT re = GetBaoeDirectory(fn,max_len);
552 Приложение А iff(re ==0) || (re > max_len)) { tfifdef _DEBUG OutputDebugStimg( *** BuildFN Невозможно выяснить базовый каталог \п ), tfendif return FALSE // Нам не нужно проверять сделала ли ч-то-нибудь функция AppendSlach и было // ли достаточно мес^а для завершающего символа обратной косой черты (если // ее нужно было добавить; Функция GtrlcatAtoinic() обработает за нас // проблему нехватки места AppendSlash(fn,inax_len - 1), i eturn ctrlcatAtoinic(fn, data_fn, inax_len), //////////////////////////////////////////////////////////////////////////// // CreateNewFileO Создает новую копию нашего специализированного файла // данных, заполняя его данными, находящимися в памяти Обратите внимание /7 что эта функция НЕ проверяет наличие уже существующего файла - она молча // грохнет одноименный файл Все мероприятия по обнаружению такого файла и // сохранению его содержимого должны быть проведены до вызова данной функции1 //' // Возвращает TRUE, если файл был успешно создан, в противном случае // возвращает FALSE // I//II/II/If/III//Ill/IllII//III/I/IIII/////III/llllllllllll/llll/llllI/IIIII ctatic BOOL CreateNewFile(void) { char fn[MAX_PATH], FILE *f, if('BuildFN(fn,cizeof(fn))) { tfifdef _DEBUG OutputDcbugString( *** CieateNewFile Невозможно сконструировать имя файла данных \п ) #endif return FALSE. if((f - fopen(fn, wb )) '= NULL) { BOOL tc = StoreData(f), fclooe(f), return re, elce #itdef .DEBUG OutputDebugString( *** CreateNewFile Невозможно открыть файл данных\п ), #endif return FALSE, /////////////////////////////////////////////////////////////////////////// // GetBaoeDirectoryO Определяет каталог используемый для хранения нашего // файла данных В текущей версии это всегда главный каталог Windows, и // поэтому данная функция имитирует синтаксис GetWindowoDirectoryO /////////////////////////////////////////////////////////////////////////// UINT GetBaseDirectory(LPTSTR lpBuffer,UINT uSize)
^egaZero — самая законченная в мире, ничего не лелаюшая... 553 lfdpBuffer == NULL) { #ifdef _DEBUG OutputDebugStnng( **** GetBaceDirectory Обнаружен нулевой указатель\п ), #endif return 0, return GetWindowsDirectory(lpBuffer. uSize). /////////////////////////////////////////////////////////////////////////// // IcOurFileO Проверяет заданный открытый файл и определяет, является ли // он носителем данных в нашем специализированном формате /7 // Если файл является нашим . возвращает TRUE в противном случае возвращает // FALSE Вне зависимости от содержимого файла, по окончании работы этой // функции позиция чтения/записи всегда выравнивается влево (в начало файла) /////////////////////////////////////////////////////////////////////////// Gtatic BOOL IcOurFile(FILE *f) { // Проверяем длину файла Если она не совпадает с тем, что нам нужно, // значит файл точно не наш if(_filelength(_fileno(f)) '= cizeof(config_header_type) + Gizeof(config_type)) leturn FALSE, // Проверяем содержимое заголовка contig_header_type tect_header if(tread(&tect_header,cizeof(tect_header), 1,f) < 1) return FALSE, // Назад, в начало файла fGeek(f,0,SEEK_SET), // Если нужный маячок не найден, значит файл ие "наш* if(memcinp(&tect_header eye_catcher,&config_header eye_catcher, cizeof(teot_header eye_catcher)) ' = 0) return FALSE, // Пока мы признаем только версию 1 0 if((teot_ header ma]or_vei'Gion > 1) || (teot_headei in]noi_veroion > 0)) // Возможно, здесь нужно будет выдать пользователю сообщение о /' проблеме с версией ieturn FALSE, // Делаем это копирование, чтобы другие части программы могли корректно // использовать заголовочную информацию, включая код версии ineincpy(&config_header, &test_header, cizeof(teGt_header)), •-eiurn TRUE ////////////////////////I////////////////////////////////////////////////// II RenamelmpoGtoi() Кто-то другой определил, что существует файл точно с // тем же полным именем, которое мы хогим использовать, и что этот файл не // является нашим Поскольку нам неизвестно содержимое этого файла, мы не // можем просто удалить его Но нам нужно сохранить наши данные, поэтому мы // сначала переименовываем блокирующий файл // // Данная функция НЕ проверяет ни существование блокирующего файла, ни его // формат1 // // Возвращает TRUE если переименование завершилось успешно, в противном // случае возвращает FALSE
Приложен /////////////////////////////////////////////////////////////////////////// static BOOL Renamelmpoctor(void) { char blockmg_fn[MAX_PATH], new_fn[MAX_PATH], win_dir[MAX_PATH], if(' BuildFN(blocking_fn,cizeof(blocking_fn))) { ffifdef _DEBUG OutputDebugString( *** Renamelmpoctor Невозможно сконструировать имя блокирующего файл' \п"). ffenaif return FALSE UINT re = GetBaceDirectoty(win_dir,cizeof(win_dir)), if((rc ==0) ji (re Gizeof(wm_dir))) > #ifdef _DEBUG OutputDeDugString( *** Renamelmpoctor Невозможно определить базовый каталог\п'), tfendif return FALSE, if(GetTeinpFileName(win_dir. OLD ,0.new_fn) == 0) { tfifdef .DEBUG OutputDebugString( *** Renainelmpootor Невозможно сконструировать "), OutputDeDugString( имя временного файла \п ). #endi^ return FALSE, «lfdef _DEBUG OutputDebugString( "*** Renamelmpoctor имя блокирующего файла "); OutputDebugString(blocking_fn), OutputDebugStringC \n*** Renamelmpoctor новое имя файла. '), OutputDebugString(new_fn), OutputDebugString('\n* ) flendif if(CopyFile(blockmg_fn new_fn, FALSE)) char ouffet[1024] wcprintf(Duffer,"Ваш существующий файл \n \'%g\ \пбыл переименован \п \ %gn \п\пВам нужна помощь9 blockmg_fn, new_fn), if(МеьсадеВох^О,Duffer Внимание1 \MB_YESN0 | MB_IC0NEXCLAMATI0N j MB_TASKM0DAL) == IDYES) MeccageBox(0, Представьте, как в этом месте появляется превосходная, интуитивная, контекстно-зависимая подсказка ", Помощь ,МВ_0К | MB_IC0NINF0RMATI0N | MB_TASKM0DAL), return TRUE: V elce { tfifdef _DEBUG OutputDebugStnng( *** Renamelmpoctor- ПРОВАЛ.\n'), tfendif return FALSE,
[AegaZero — самая законченная в мире, ничего не лелаюшая... 555 .'I / IIIП1/11//I/II/I/II/I III III lll/ll III/IIIII/IIII/II//I III IIIII/Ill/Ill// // SetUpDataO Устанавливает текущую дату в соответствующие поля нашего // массива конфигурационных данных, а также гарантирует, что все строки с // именами файлов должным образом завершаются /lllll/l/lllinilllllll/!///ll/l/llll//lllll/lllllllllilll/lilll/ll/llllll/ static void Setl)pData(void) // Заносим сегодняшнюю дату с заголовочные поля tnne_t now, tin now2, tnne(&novO, now2 = *localtnne(&now) config lact_run_month = now2 tmjnon + 1, // месяц отсчитывается от нуля (,9) config lact_run._day = now2 tmjnday, config lact_run_year = now2 tm_year, // Если мы собираемся сконвертировать файл данных в формат новой версии, // тогда именно в этом месте следует должным образом обновить мажорный и // минорный номера версии // Вероятно это смотрится как вершина паранойи, но по крайней мере это // гарантирует нам, что все переменные с именами файлов, что бы в них ни // оказалось, завершены нуль-символом config rn1[cizeof(config fn1) config fn2[cizeof(config fn2) config fn3[cizeof(config fn3) config fn4[cizeof(config fn4) config fn5[cizeof(config fn5) config fn6[cizeof(config fn6) config fn7[cizeof(config fn7) - 1] config fn8[sizeof(config fn8) - config fn9j~cizeof (config fn9) - config fn10[cizeof(config fnIO) 'eturr, l/ll/11П11III/II/ill/Ill/Ill/1/III/ll/ll/l/lП111III IIIIIIII/II/1 IIIl/ll/l 11 StoreDataFile() Сохраняет заголовок и сами данные в заданный открытый // файл (предполагается, что файл открыт для бинарной записи) // // Возвращает TRUE, если запись прошла успешно, в противном случае // возвращает FALSE п iiiiii п41 ii п'in i iiiч/пи/ч/пи in i iiiiii n iii/ii/ii inn mi7i'in'ii/i static BOOL StoreData(FILE *f) // Очищаем данные, обновляем номер версии, и т д SetUpDataO if(fvvrite(&configJieader cizeof (config_header). 1, f) <. 1) ttifdef „DEBUG OutputDebugString( *** StoreData Невозможно записать в файл данных\п'), #endif return FALSE, // Если у формата заголовка есть какие-то расширения, зависящие от версии // их следует записать в файл именно здесь if(fwrite(&config,cizeof(config),1,f) s 1) { tfifdef _DEBUG OutputDebugStnng( *** StoreData Невозможно записать в файл данных4n ), 1] = 1] = 1] = 1] = 1] - 1] = 1] = 1] - 1] - - 1] = '\о •\о •\о АО '\о '\о '\о АО ЛО \о
556 Приложение л ttendif return FALSE, // Если у формата самих данных есть какие-го расширения, зависящие от версии, // их следует записать в файл именно здесь fclose(f), /'/ Если мы дошли до этого места значит все прошло согласно плану return TRUE Листинг А.З. SOURCE\APP_A\MEGAZERO\MEGAZDLC.H // inegazdlg h header file // ///////////////////////////////////////////////////////////////////////////// // Диалог CMegazeroDlg class CMegazeroDlg public CDialog j // Const ruction public CMegazeioDlg(CWnd* pPaient - NULL),// стандартный конструктор // Dialog Data //{{AFX_DATA(CMegaze roDlg) enum { IDD = IDD_MEGAZERO_DIALOG 1, // NOTE the ClassWizard will acid data members heie //))AFX_DATA // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CMegazeroDlg) piotected virtual void DoDataExchangefCDataExchange* pDX),// DDX/DDV support //}JAFX_VTRTUAL // Implementation protected HICON in_ nlcon void DoLockUnlock(void) void DoSelfCheck(void), void OnLockUnlock(void), void ProcessDroppedFile(char * fn), void UDdateFileNames(void), // Generated message map functions //{{AFX_MSG(CMegazeroDlg) virtual BOOL OnlnitDialogO alxjnsg void OnSysCommand(UINT nID, LPARAM IParam), afx_msg void OnPainc(), afxjnsg HCURSOR OnQuerVDragIcon(), afxjnsy yoid OnDeotrovO. afx_msg уою 'jnDropFilec4HDR0P hDiopInfo), //!}AFX_MSG DECLARE_MESSAGE_MAP() Аистинг А.4. SOURCE\APP_A\MEGAZERO\MEGAZDLGXPP /* Copyiiqht Lou Grinzo 1995 Zen of Windows 95 Progi ainining */ // inegazdlg epp implementation file //
^egaZero — самая законченная в мире, ничего не лелаюшая... 557 ^include "ctdafx h #include megazero h #include megazdlg h «include \\patcher\\checker h' #mclude config h #include lock h #ifdef _DEBUG яш-idef THIG_FILE static char BASED_CODE THIS_FILE[] = FILE , tfendif >/"/;//'/////////////////////>'////////////////////////////////////////////// // Диалог CAboutDlg, используемый для выдачи информации о программе class CAboutDlg public CDialog { public CAboutDlgO. // Dialog Data //{{AFX_DATA(CAboutDlg) enum { IDD = IDD_AB0UTB0X }, //}}AFX_DATA // Implementation protected virtual void DoDataExchange(CDataExchange* pDX),// DDX/DDV support //{{AFX_MSG(CAboutDlg) virtual BOOL OnlmtDialogO. afxjncg void OnDestroyO, //}}AFX_MSG DECLARE_MESSAGE_MAP() CAboutDlg CAboutDlgO CDialog(CAboutDlg IDD) //{{AFX_DATA_INIT(CAboutDlg) //}}AFX_DATA_INIT void CAboutDlg DoDataExchange(CDataExchange* pDX) CDialog DoDataExcnange(pDX), ,//{{AFX_DATA_MAP(CAboutDlg) //}}AFX_DATA_MAP BEGIN_MESSAGE_MAP(CAboutDlg, CDialog) //{{AFX_MSG_MAP(CAboutDlg) //)}AFX_MSG_MAP END_MESSAGE_MAP() II/II//IIIII/I//IIII/IIII//II//IIII/IIIIIIIIII/IIIIIIIIIIIIIIIIIIIIIIIIIIIIII II CAboutDlg meccage handlers BOOL CAboutDlg OnlmtDialogO { CDialog OnlmtDialogO, CenterWindow(), // TODO Add extra about dig initialization here return TRUE, // return TRUE unlecc you Get the focuc to a control ///////////////////////////////////////////////////////////////////////////// // Диалог CMegazeroDlg
558 Приложение CMegazeroDlg CMegazeroDlg(CWnd* pParent /*=NULL*/) CDialog(CMegazeroDlg IDD, pParent) //!{AFX_DATA_INIT(CMegazeroDlg) // NOTE the ClaccWizard will add member initialization here //)jAFX_DATA_INIT // Note that .oadlcon doec not require a Gubcequent DeGtroylcon in Win32 m.hlcon = AfxGetApp(}->LoadIcon(IDR_MAINFRAME), void CMegazeroDlg DoDataExchange(CDataExchange* pDX) CDialog DoDataExchange(pDX), //{{AFX_DATA_MAP(CMegazeroDlg) // NOTE the ClaGGWizard -all add DDX and DDV calls here //})AFX_DATA_MAP BEGIN_MESSAGE_MAP(CMegazeroDlg. CDialog) //{{AFX_MSG_MAP(CMegazeroDlg) ON_WM_SYSCOMMAND() ON„WM_PAINT() ON_WM_QUERYDRAGICON() 0N_WM_DESTR0Y(; 0N_WM_DR0PFILES() /,/}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CMegazeroDlg message handlers BOOL CMegazeroDlg OnlmtDialogO { CDialog OnlmtDialogO, CenterWindow(), // Добавляем пункт "About " к системному меню // Константа IDM_AB0UTB0X должна быть в диапазоне системных команд ASSERT((IDM_ABOUTBOX & OxFFFO) == IDM_AB0UTB0X); ASSERT(IDM_ABOUTBOX < OxFOOO), // и IDM_SELFCHECK тоже ASSERT((IDM_SELFCHECK & OxFFFO) == IDM_SELFCHECK), ASSERT(IDM_SELFCHECK < OxFOOO) // и IDM_L0CKUNL0CK тоже ASSERT((IDM_L0CKUNL0CK & OxFFFO) == IDM_L0CKUNL0CK); ASSERT(IDM_L0CKUML0CK < OxFOOO), CMenu* pSycMenu = GetSystemMenu(FALSE), CString GtiAboutMenu GtrAboutMenu LoadStnng( IDS.ABOUTBOX) if (' Gt rAboutMenu IsEinptyO) pSyGMenu->AppendMenu(MF_SEPARATOR), pSyGMenu->AppendMenu(MF_STRING, IDM_SELFCHECK, «.Выполнить самопроверку ), DSycMenu->AppendMenu(MF_STRIN6, IDM_L0CKUNL0CK,' &Замкнуть/разомкнуть ), pSysMenu->AppendMenu(MF_SEPARATOR), pSycMenu->AppendMenu(MF_STRING, IDM_AB0UTB0X, GtrAboutMenu), // Считываем конфигурационный файл и загружаем данные LoadConfigData(), // Устанавливаем дату последнего запуска if(config lact_run_year -= 0)
MegaZero — самая законченная в мире, ничего не лелаюшая... 559 SetWinaowText( MegaZero [первый запуск] ), elce char buffer[100], wspnnff(buffer,' MegaZero [последний раз запущена %d/%d/%d] , config lact_run_month,config lact_run_day,config last_run_year). SetWindowText(buffer), // Устанавливаем поля с именами файлов UpdateFileNamecO // Обеспечиваем показ нашего собственного значка а не стандартного PostMeGsage(WM_SETICON (WPARAM)TRUE,(LPARAM)m_hIcon), // Сообщаем Windows, что мы будем принимать сброшенные файлы DragAcceptFileo(TRUE), if(config locked) // Вызываем DoLockllnlock(), только если мы замкнуты DoLockUnlock(), return TRUE, // return TRUE unlecc you set the focuc to a control void CMegazeroDlg OnSycCoinmand(UINT nID, LPARAM lParam) UINT the_real_nID = nID & OxFFFO CAboutDlg dlgAbout, owitch (the_real_nID) { case SC_RESTORE // Если мы замкнуты, обрабатываем эти сообщения, как обычно сазе 3C_MAXIMIZE // В противном случае выдаем диалог для ввода пароля if(config locked) OnLockUnlockO. else CDialog OnSysCoinmand(nID, lParam), break, cace IDM_AB0UTB0X. dlgAbout DoModaK), break, cace IDM_SELFCHECK DoSelfCheck(), break cace IDM_lOCKUNLOCK OnLockUnlockO, break, default CDialog OnSycCommand(nID, lParam), // Если вы добавите в диалог кнопку минимизации, то для отображения значка // в минимизированном состоянии вам понадобится нижеприведенный код void CMegazetoDlg OnPaint() { if (IcIconicO) { CPaintDC dc(thic), // контекст устройства для графического вывода SendMeocage(WM_ICONERASEBKGND, (WPARAM) dc GetSafeHdc(), 0), // Центрируем значок в клиентсюм прямоугольнике mt cxlcon = GetSyctemMetncs(SM_CXIC0N). int cylcon = GetSyctemMetricc(SM_CYICON), CRect rect,
Приложение GetClientRect(&rect), int * = (rect Width»') - cxlcon + 1) / 2 jnt у = (rect Height() - cylcon + 1) /2, // Рисуем значок dc DrawIcon(x, y, m_hIcon) elce CDiaiog OnPamtO, // Система вызывает эту функцию для получения курсора, который она должна //' показывать когда пользователь будет перетаскивать минимизированное окно HCURSOR CMegazeroDlg OnQueryDragIcon() { return (HCURSOR) m.hlcon, i. // Осуществляем самопроверку путем вызова функции GoodCRC() из CHECKER CPP void CMegazeroDlg DoSelfCheckO lf(GoodCRCO) MeccageBox( Самопроверка прошла успешно' , MegaZero ,MB_0K | MB.ICONINFORMATION), elce if(MeccageBox( "Самопроверка провалилась'\п\пХотите получить помощь9' , MegaZero ',MB_YESN0 | MB.ICONQUESTION) == IDYES) MecsageBox( Представьте чудесную подробную контекстно зависимую подсказку , Help MB_0K | MB_IC0NINF0RMATI0N), void CMegazeroDlg OnDeotroyO CDiaiog OnDestroyO, SaveConfigData(), )/////II/II/I/////////////I////1/1/////!I/If////////(//////I//III///////I/I/ II OnDropFileG() Обрабатывает один или несколько сброшенных файлов /II/III/IIIII/II111/111/1111IIIlllllll/III III III IIIllll/lIII III III/III III III void CMegazeroDlg OnDiopFileG(HDROP hDropInfo) char file_buffer[MAX_PATH] UINT nuin_filec, l, // Обеспечиваем всплывание нашего окна на первый план, поскольку I/ Wmdowc 95 имеет свое собственное, довольно странное мнение // по поводу того, что надо делать с целевым окном при сбрасывании файлов SetForegroundWindow(), // Выясняем количество сброшенных файлов num_filec = DragQueiyFile(hDioplnfo,Oxffffffff,file_buffer,sizeof(file_buffer))( // Теперь выясняем имена и обрабатываем каждое из них по очереди for(i = 0 1 's nuin_filec, i++) { DragQueryFile(hDropInfo,l,file_buffer,cizeof(file_buffer)), ProceGGDroppedFile(file_buffer), // Сообщаем Wmdowo, что мы закончили обработку сброшенных файлов DragFiniGh(hDropInfo),
fsAegaZero — самая законченная в мире, ничего не лелаюшая... 561 /1/1///////!> ((///////If////////////////I/////////III////////////////II///// ,// ProcecsDroppedFileO Добавляет имя файла в нащ массив конфигурационных данных 11II11IIIII I/1 ll/ll lllll/llllillll lll/ll lll/IIUIUIUIIIUI III II111/11II III void CMegazetoDlg ProceccDroppedFileCchai *fn) if(fn == NULL4- > ififdef _DEBUG OutputDebugString( *'* ProceGcDroppedFile Обнаружен нулевой указатель1 м \п ) #endif return, cize_t fn_len = elilen(tn>, // Обратите внимание, что благодаря этой проверке, мы можем затем не беспокоиться // о превышении допустимой длины config fn1 при копировании fn в этот буфер if((fnjen =-0) || (fn_len ^ Gizeof(config fn1) - 1)) tfifdef _DEBUG OutputDebugString( *** Pi oceccDroppedFile Обнаружено неправильное значение fnMI\n ), tfendif return // Смещаем старые элемент на одну позицию вниз otrcpyfconfig fn10 config fn9) cti cpvUonfig fn9 config fn8), Gtrcpylconfig fn8,config fn7), Gtrcpy(config fn7 config fn6), Gtrcpy(config fn6,config fn5), Gtrcpy(config fn5,config fn4) Gtrcpy(config fn4,config fn3), otrcpy(config fn3,config fn2): Gtrcpy(config fn2 config fn1) // и вставляем новый в начало списка Gtrcpy(config fn1,fn), // Обновляем наш внешний вид, чтобы отобразить изменения UpdateFileNamesC), //////////////////////////////////////////////////////////////////////////// // UpdateFileNaineoO Обновляет наш главный интерфейс, показывая находящиеся // в памяти имена файлов I/II/I///////////////!////////HI///////III///!/////////////II////III////!// void CMegazeroDlg UpdateFileNameG(void) i SetDlgIteinText(IDC_FN1, config fn1), SetDlgItemText(IDC_FN2,config fn2), SetDlgllemText(IDC_FN3.config fn3), SetDlgItemText(IDC_FN4,config fn4). SetDlgItemText(IDC_FN5,config fn5), SetDlgIteinText(IDC_FN6 config fn6). Gel-Dl(jIteinText(IDC_FN7 config fn7) SetDlgItemText(IDC_FN&,config fn8), SetDlgItemText(IDC_FN9,config fn9), SetDlgItemText(IDC_FN10.config fn10), /!////////(/////////!/////II/(//If///11///(//////((/(////I/////////////////// // OnLockUnlockO //
5£2 Приложение д (//^М^в^зы^аем §pLockUntock() только после должной проверки состояния void CMegazeroDlg' OnLockllnlock(void) { if(config locked) { TRACE("OnLockUnlock- IsLockedO ==TRUE\iT), iff UnlockPWDlgO) // FALSE Теперь мы не замкнуты DoLockUnlock(), else TRACE( "OnLockUnlock- IsLockedO == FALSE\n* ), lf(LockPWDlgO) // TRUE Теперь мы замкнуты DoLockUnlock(), ///////////////////////////////////////////////////////////////////////////// // DoLockUnlock() // Этот код предполагает, что он вызывается только тогда, когда это необходимо // (то есть когда состояние замкнутости программы действительно изменяется) void CMegazeroDlg::DoLockUnlock(void) { CMenu* pSycMenu = GetSyGtemMenu(FALSE), if(config locked) { // Меняем значок - демонстрируем замкнутость DeGtroyIcon(m_hIcon), mjilcon = AfxGetApp()->LoadIcon(IDR_LOCKEDMAINFRAME); SetClaccLong(AfxGetApp()->m_pMainWnd->m_hWnd,GCL_HIC0N,(long)m_hIcon), PostMecGage(WM_SETICON.(WPARAM)TRUE,(LPARAM)mJiIcon); // Если в„ нормальном состоянии программа принимает сбрасываемые "' \^/* на нее файлы, то т\т нужно это 'запретить DragAcceptFileG(FALSE) // Блокируем соответствующие пункты меню pSyGMenu->EnableMenuItem(IDM_SELFCHECK,MF_BYCOMMAND | MF_GRAYED), pSyGMenu->EnableMenuItein(IDM_ABOUTBOX.MF_BYCOMMAND | MF_GRAYED); // Минимизируем себя PoGtMeGcage(WM_SYSCOMMAND,SC_MINIMIZE. 0), else // Меняем значок на нормальный DeGt roy Icon (in_h Icon). m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME), SetClaGGLong(AfxGetApp()->m_pMainWnd->in_hWnd,GCL_HICON,(long)in_hIcon), PoGtMeGGage(WM_SETICON,(WPARAM)TRUE.(LPARAM)m_hIcon), // Если в нормальном состоянии программа принимает сбрасываемые // на нее файлы, то тут нужно это вновь разрешить DragAcceptFilec(TRUE). // Деблокируем соответствующие пункты меню pSycMenu->EnableMenuItem(IDM_SELFCHECK.MF_BYC0MMAND | MF_ENABLED); pSycMenu->EnableMenuItem(IDM_AB0UTB0X.MF_BYCOMMAND | MF_ENABLED); // Восстанавливаем свое окно PoctMeGGage(WM_SYSCOMMAND,SC_RESTORE,0),
()dd\/0J8zc68W3 c!cIvoj9zb69W0 11013.011.1 }guog ddvoJ9zc69W0 // ///////////////////////////////////////////////////////////////////////////// OdVW 39VSS3W 0N3 (di9HU0' ddyuiMO 'd"13H~ai)aNVWW03~N0 0SW"XdV{{// ,эроэ p9icj9U96 ±o G>poxq эсэид ui ээс поЛ }.bum цаз ion Oa // 9J9U, GOJOEUI 6UTddBUI 9A0UI9J pUE ppC XI™ pJBZI/VjGGBXQ Э1Ц - 3J_0N // (ddvoJ9ZB69W0)dVW"9SH"X3V}}// (ddyuiMO 'ddvoj9Zc69W0)dVW"30VSS3W~NI03a ddvoj9ZB69HQ // ///////////////////////////////////////////////////////////////////////////// ^IPU9# :~3iid~ = []3iid"siHi 3aoo"a3Sva_JCMo oubig 3113 SIHl ^эрип# эпаза ^p^i# „ц 3.uT~q},. 9pnxoui# .Ц J9A££UIM 9pnX0UI# „4 бхргсбэш. эрпхэит# 4 0J9Zb69ui 9pnxoui# ..Lj X^Bp^G , 9pnX3UI# // нинэжои^и аооэвим эинэйэаои ±9Bi/9tf9duo ddo oj9zb69ui // dd3'OIHZV91W\0>l3ZVD3W\V~ddV\33»nOS 9V Jhmidmv iiii I/IUIUUIIIIIIIIIIII/IIII/IIIIIIII/II/II/I//II/II/I///in imi/n in i/i ' { ()dVW"39VSS3W"3HV103(3 9SW XdV.'l// , ЭРОЭ p9}BJ9U96 ^0 G>jOOXq 9S9M1 UI 99G ПОЛ }ВЦМ JJQ3 ION 00 // *9J9ij сиоиэигц J9qui9ui элошэл рив ррв цтм pjBziMGGBXQ эид - 3ion // (ddvojazc6awo)9SW"XdV}}// U0T^BiU9UI9XdUII // lV01HIA"XdV{{// 'OaouB^Guiiiui 1008 "[вгцлл :oixqnd (ddyoj9ZB69W0)1VnidlA'XdV}}// G9pTJJ9A0 UOTiOUn^ Хсп:М1Л P91CJ9U96 pJBZT/VjGGBXO // G9pIJJ9A0 // •()ddvojazB69wo oixqnd } ddyuiMO oixqnd ddvoj9zc69WQ ggbxg // ddo*oj9ZB69ui эиивф 9 wo boobi/» ojoie (хипвеиисэс) // ddyoj9ZB69W0 // iiiiiiiiiii/iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiuiiiiiiiiiuiiiiiiiiiiii/iiimi GXoquiAc uibui // ц 90jnoG9J spnxouifl ^IPU9# HOd -'OJ. a"[TJ. Giq; 6uipnxoui 9J0^aq ,ц x^bpig 9pnxoui jojj9# ~H"NIMXdV" ^puji# // 0d3ZV93W винэжоиийи butf иивф i/mHhoaoi/ojBe niqHaBi/j ц снэгвбэш // HOJHZV91W\OM3ZV93W\VddV\33linOS TV Jhmidmv roc "'Ktmoievdv зн омыт 'aduw а иеннэнномее neweo — ojszeSa^/
Приложение I/ TODO. add construction code here, // Place all significant initialization in Initlnstance )lllllll/llllll/lll/ll//l/lll/ll/ll/lllllllllll/lllllll/U/II/llll/lll/l/lIII // The one and only CMegazeroApp object CMegazeroApp theApp II III/IIIIII11IIi/llllIII Hill/III IIIl/lllllll/I II III/II/III II/II/I//II III/I/ II CMegazeroApp initialization BOOL CMegazeroApp InitlnstanceO { lf('LoadTBO) { if( МеззадеВох(0, Невозможно загрузить TOOLBOX DLL \п\пВам нужна помощь7 , MegaZero , MB_YESN0 | MB.ICONQUESTION | MB_TASKMODAL) == IDYES) MessageBox(0, Представьте, как в этом месте появляется превосходная, интуитивная, контекстно зависимая подсказка ', Помощь ,МВ_0К | MB_IC0N1NF0RMATI0N | MB_TASKM0DAL) return FALSE. if(ComplainIfNot(WV_AnyNT | WV_W95, MegaZero )) { UnloadTBO, return FALSE, } // Стандартная инициализация // Если вы не используете эти возможности и хотите уменьшить размер финальной // версии вашего исполняемого файла, вы можете удалить из нижеследующего // фрагмента конкретные инициализирующие подпрограммы, которые вам не нужны LoadStdProfileSettingGO, // Загружаем стандартные опции (включая список последних // использованных файлов) из INI-файла CMegazeroDlg dig m_pMainWnd = &dlg, mt nResponce = dig DoModal(), if (nResponce == IDOK) { // TODO Place code here to handle when the dialog is // dismissed with OK elGe if (nResponse == IDCANCEL) { // TODO Place code here to handle when the dialog is // dismissed with Cancel i // Выгружаем нашу DLL Гигиена превыше всего UnloadTBO, // Поскольку диалог был закрыт, возвращаем FALSE, чтобы завершить // работу программы и не запускать цикл обработки сообщений return FALSE, Листинг А.7. SOURCE\APP_A\MEGAZERO\TB_INT.H #ifndef __tb_mt_h_ #define tb int h
f\4egaZero — самая законченная в мире, ничего не лелаюшая... 565 // Загружает TOOLBOX DLL и подготавливает ее для использования Возвращаемое значение /7 указывает загружена ли библиотека и готова ли она для использования, а не просто // была ли она загружена именно при этом конкретном вызове extern BOOL LoadTB(void), extern void UnloadTB(void) extern BOOL GotTB(void) extern void AppendS]ach(chai *dect, oize_t max_len), extern BOOL rileExicts(const char *fn) Jfendif Листинг A.8. SOURCE\APP_A\MEGAZERO\TB INT.CPP /* Copyright юн Grinzo 199b Zen of Windowo 95 Programming */ «include 'ctdafx h> «include Lb_int h typedef void (*pfnAppendSlash)(char *dect cize_t max_len), typeaef BOOL (*ptnFileExictc)(conct char *fn) // Анкеры подпрограмм расположенных в DLL static pfnAppendSlach AppendSiachAnchoi = NULL static pfnFileExistc FileExictoAnchot = NULL, // Анкер для самой DLL static 4INSTANCE hTB = NULL /' Загрузить DLL и установить связи с необходимыми подпрограммами BOOL LoadTB(voich i it(hTB '= NULL) { #ifdef .DEBUG OutputDebugGtring( *** LoadTB() вызов при уже загруженной DLL \n ), tfendif return TRUE lTc(hTB = loadLibtary( toolbox dll")) •= NULL) if- (UAppendslachAnchor - (pfnAppendSlach) GerProcAddiecoOiTb, AppendSlach ')) == NULL) ij ((FiieExictsAncnor = (pfnFileExicts) GetPiocAdarcccdvb PileExictc )) -= NULL)) tfifdef .DEBUG OuTputDebugSl ring( *** LoadTB() не удалось найти подпрограммы в DLL\n ) tfendif UnloadTBO. return FALSE, else #ifdef .DEBUG OutDiitDebuqString( *** LoadTB( ) не удалось загрузить DLL\n ). ftendit Unload"! ВО return FALSE, return TRUF,
Приложение л 17 Выгрузить DLL и должным образом установить значения всех анкеров voio UnloadTB(void) if(GoTTB()) creeLibrarv(hTB), пТЬ - NULL AppendSlasnAnchor = NULL. FiieExiGtcAnchor - NULL, void AppendSlach(char *deot, cize_t max_len) lf(GotTBO) AppendSlaohAnchor(deGt,inax_len), «lfdef .DEBUG elce OutputDebugStnngC *** AppendSlashO вызов в отсутствие DLL1 \n"); «endif BOOL FileExictG(conot char *fn) {•- I .r *f/. -' - * , lf(GotTBO) return FileExiGtGAnchor(fn), elce { «lfdef _DEBUG OutputDebugStnngC*** FileExictcO вызов в отсутствие DLL'\n"'); «endif return FALSE, // Загружена ли DLl вместе со всеми необходимыми функциями9 BOOL GotTB(void) return (hTB '= NULL), Листинг А.9. SOURCE\APP_A\MEGAZERO\WIN32VER.H «lfndef win32ver_h «define __wm32ver_h__ // При добавлении платформ нужно соответственно подправить реализацию метода Dolt() «define WV_UNKNOWN 0 «define WV_WIN32S 1 «define WV_W95 2 «define WVJnITWS 4 «define WV.NTSERVER 8 «define WV_NTAS 16 «define WV.AnyNT (WV_NTWS | WV.NTSERVER | WV_NTAS) // Извлекает код WV_* только для OS DWORD GetOS(void). // Извлекает код WV_* и всю информацию о версии void GetOSVercion(DWORD *og. DWORD «major. DWORD *inmor, DWORD *build), // Работаем ли мы под Windows 959 BOOL lGW95(void). // Работаем ли мы под какой-либо разновидностью Wmdowc NT?
— самая законченная в мире, ничего не лелаюшая... BOOL iGNT(void), // Работаем ли мы под Windowc NT 4.0 или более новой версией9 // (Предположительно, эта версия имеет такую же оболочку, что и Windows 95.) BOOL IsNT400rLater(void), // Работаем ли мы под версией Windows, которая имеет такую же оболочку, что // и Windowc 95? (W95 or NT >= 4 0) BOOL IsW95Shell(void), // Выдает MecsageBox, сообщающий пользователю, что эта программа требует одну // из версий Windowc, заданных в needed_0S. Значение параметра title // используется в качестве заголовка MecsageBox // // Возвращает TRUE, если MecsageBox был показан, то есть если программа не запущена // ни под одной из заданных разновидностей Windows и, следовательно должна завершить // свою работу // // Возвращает FALSE, если программа запущена под одной из заданных разновидностей // Windows BOOL CoinplamIfNot(DWORD needed_0S, char *title), #endif Листинг А.10. SOURCE\APP_A\MEGAZERO\WIN32VER.CPP // WIN32 Version Services // Copyright 1995 Lou Grinzo // Zen of Windows 95 Programming «include 'stdafx h «include win32ver IV' static BOOL have_version_mfo = FALSE, static DWORD Wmdows.version = WVJJNKNOWN; static DWORD major_version = 0, static DWORD minor_version = 0; static DWORD build_number = 0; // Идиома локальной функции. За объяснением обращайтесь к книге, class pckComplainlfNot { public pckComplainlfNot(void); BOOL DoIt(DW0RD Aneeded.OS, char *Atitle); private mt platforms_needed, platforms_remaining; char *title, char mcg_text[500]; DWORD needed_0S. mt Count0neBits(DWORD x); void AddPlatform(DW0RD platform, char *platform_naine), pckComplainIfNot& operator=(const pckCoinplamIfNot& x). pckComplainlfNot (const pckComplamIfNot& x); static void GetVersionlnfo(void), DWORD GetOS(void) { if (' have_version_mfo) GetVersionlnfoO, return Windows_version, I void Get0SVersion(DW0RD *os, DWORD *тазог, DWORD *minor, DWORD *build)
568 Приложение { if ('have_vercion_info) GetVercionInfo(), *og = Windowc_verGion, *major = major_vercion *inmor = minor_\/ercion, *build = build_number return, BOOL IoNT(void) { if('have_vercion_info) GetVerGionInfo(), return (Windowc_verGion == WV_NTWS) || (Wmdows_vercion == WV_NTAS) || (Wmdowc_vercion == WV_NTSERVER), BOOL lGW95(void) { if('have_verGion_info) GetVeroionInfo(), return (WindowG_verGion == WV_W95), } BOOL IsNT400rLater(void) { if (' have_verGion_mfo) GetVersionInfo(), return (IsNT() && inajor_verGion >= 4), BOOL lGW95Shell(void) { if(•have_version_info) GetVercionlnfoO, return (WmdowG_verGion == WV_W95j || IcNT400rLater(). i pckComplamlfNot' pckComplainlfNot(void) { } pckComplainlfNot DoIt(DW0RD Aneeded_OS, char *Atitle) i if('have_verGion_info) GetVerGionInfo(), // Мы имеем то, что требуется вызывающему коду, так что катапультируемся if(Aneeded_OS & Windowc_version) return FALSE, // Модифицируйте следующий оператор должным образом, если будут определены // дополнительные платформы Этот оператор всего лишь обеспечивает наличие // только допустимых битовых флагов в needed_0S needed_0S = Aneeded_OS & (WV_WIN32S | WV_W95 | WV_AnyNT), platformc_remaining = platfonnG_needed = CountOneBitG(needed_OS), title = Atitle, Gtrcpy(mGg_text, Извините, эта программа требует ), AddPlatform(WV_WIN32S, Win32G'), AddPlatfonn(WV_W95, Windowo 95 ), AddPlatform(WV_NTWS,'Windows NT Workstation ), AddPlatfonn(WV_NTSERVER, Windows NT Server ), AddPlatfonn(WV_NTAS "Windows NT Advanced Server ), MessageBox(0,msg_text,title,MB_0K | MB_IC0NEXCLAMATI0N), return TRUE,
fciegaZero — самая законченная в мире, ничего не лелаюшая... 569 int pckComplainlfNot CountOneBits(DWORD x) int count = 0, for(int i=0, l \ Gizeof(x) " 8, i++) if(x & (DWORD)1) С0Ш1Т++, X - X •> 1, return count, void pckComplainlfNot AddPlatform(DWORD platform, char *platform_name) { if(needed_0S & platform) { strcat(mcg_text,platform_name), platfonnG_t emaining--, Gwitch(platforms_remaining) { case 0 Gtrcat(mGg_text, ' ), break, сазе 1 if(platforms_needed > 2) Gtrcat(mGg_text, , или ), elce Gtrcat(msg_text, или ); break, default Gtrcat(mcg_text, , '), // Простая оберточная функция (функция-шлюз) BOOL ComplainIfNot(DW0RD needed_OS, char *title) { pckComplainlfNot worker, return worker DoIt(needed_0S,title), static void GetVerGionlnfo(void) { 0SVERSI0NINF0 ogvi. have_vercion_mfo = TRUE, meincet(&OGvi, 0. cizeof(OSVERSIONINFO)), ogvi dwOSVercionlnfoSize = Gizeof (0SVERSI0NINF0), GetVerGionEx(&OGvi) major_verGion = ogvi dwMajorVercion, minor_verGion = ogvi dwMinorVersion, build_number = ogvi dwBuildNuinber & OxFFFF, WmdowG_verGion = WV_UNKN0WN, if(ogvi dwPlatfonnld == VER_PLATF0RM_WIN32g) WindowG_verGion = WV_WIN32S, else if(0Gvi dwPlatfonnld == VER_PLATF0RM_WIN32_WIND0WS) WmdowG_verGion = WV_W95, е1зе
570 Приложен^ д if(osvi.dwPlatformId == VER_PLATF0RM_WIN32_NT) { Wmdows_version = WV_NTWS; // По умолчанию такая, пока не // разобрались до конца HKEY the.key, BYTE nt_type[100]; DWORD type_size = sizeof(nt_type); if(RegOpenKeyEx(HKEY_LOCAL_MACHINE', '•SYSTEM\\CurrentControlSet\\Control\\ProductOptionG , 0,KEY_READ,&the_key) == ERROR.SUCCESS) if(RegQue ryValueEx(the_key ProductType ,0,NULL,nt_type.&type_Gize) == ERR0R_SUCCESS) { if(stncmp((char *)nt_type, "SERVERNT") == 0) Wmdows_version = WV_NTSERVER, else if(stricmp((char *)nt_type."LANMANNT") == 0) Windows_version = WV_NTAS, else Wmdows.version = WV_NTWS, i RegCloseKey(the_key); } Листинг А.11. SOURCE\APP_A\MEGAZERO\LOCKPW.H // lockpw.h : header file // ///////////////////////////////////////////////////////////////////////////// // LockPW dialog class LockPW public CDialog { // Construction public LockPW(CWnd* pParent = NULL); // стандартный конструктор // Dialog Data //{{AFX_DATA(LockPW) enum { IDD = IDD.LOCKPW }; // NOTE the ClassWizard will add data members here //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(LockPW) protected* virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL // Implementation protected: // Generated message map functions //{{AFX_MS6(LockPW) virtual void OnOKO; virtual BOOL OnlnitDialogO; //}}AFX_MSG
tfegaZero — самая законченная в мире, ничего не лелаюшая... $$7Л DECLARE_MESSAGE_MAP() Листинг A.12. SOURCE\APP_A\MEGAZERO\LOCKPW.CPP // Copyright 1995 Lou Gnnzo // Zen of Windows 95 Programming // lockpw cpp implementation file // «include otdafx h «include megazero h «include lockpw n «include config h «include toolbox h" «lfdef .DEBUG «undef THIS.FILE static char BASED_CODE THIS_FILE[] = __FILE„; «endif ///////////////////////////////////////////////////////////////////////////// // LockPW dialog LockPW- LockPW(CWnd* pParent /*=NULL*/) CDialog(LockPW -IDD, pParent) { //{{AFX_DATA_INIT(LockPW) // NOTE the ClacsWizard will add member initialization here //}}AFX_DATA_INIT void LockPW DoDataExchange(CDataExchange* pDX) * v ' { CDialog DoDataExchange(pDX); //{{AFX_DATA_MAP(LockPW) // NOTE the ClaccWizard will add DDX and DDV calls here //)JAFX_DATA_MAP BEGIN_MESSAGE_MAP(LockPW, CDialog) //{{AFX_MSG_MAP(LockPW) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // LockPW inescage handlerc void LockPW.-OnOK() { char pw1[MAX_PW_LEN + 1] = ", char pw2[MAX_PW_LEN + 1] = " . GetWmdowText( GetDlgItem(m_hWnd. IDC.PW1), pw1, sizeof(pw1)), GetWmdowText( GetDlgItem(in_hWnd. IDC_PW2), pw2, oizeof (pw2)), // Избавляемся от этих ужасных лидирующих и завершающих пробелов ctnpLT(pwl); ctnpLT(pw2), if(ctrcmp(pw1.pw2) '= 0) { MeosageBox(m_hWnd, 'Введенные вами пароли не совпадают \п\п(1ожалуйста, попробуйте еще раз , MegaZero ,MB_0K | MB_IC0NEXCLAMATI0N), return,
572 Приложение Lri'^tr]enipw^ MIN_P№_LEN) Lt( MeGGacjeBox(in_hWiid, Ваш пароль слишком юроткий чп\пВам нужна помощь9 MegaZero MB_YESN0 | MB_ICONQUESTION) == IDYES) MeGcageBox(m_hWnd, Представьте как в этом месге появляется превосходная интуитивная контекстно-зависимая подсказка ', Помощь МВ_0К | MB_IC0NINF0RMATI0N), return. // Гели мы доорллись до эгого места, все должно быть в порядке, так что // мы смело идем дальше и замыкаем программу coring locued - TRUE strcpy(config pacowoid pw1) CDialog OnOK() BOOL LockPW OnlmtDialogO { CDialog OnlmtDialogO, CenterWindow() // TODO Add extta initialization here SendDlgIteinMeGGage(IDC.PW1,EM_LIMIГТЕХТ,MAX_PW_LEN,0), SeirdDlgItemMeccage(IDC_PW2,EM_LIMITTEXT,MAX_PW_LEN.O), return true // return TRUE unleGG you Get the focuG to a control /' EXCEPTION OCX Property Pagec chould return FALSE Аисгинг А.13. SOURCE\APP_A\MEGAZERO\UNLOCKPW.H // unlockpw h header file // ///////////////////////////////////////////////////////////////////////////// // UnlocKPW dialog claGG UnlockPW public CDialog { // ConGtruction public UnlockPW(CWnd* pParent = NULL) // стандартный конструктор // Dialog Data //{{AFX_DATA(UnlockPW) enuin { IDD = IDDJJNLOCKPW ! // NOTE the ClaGGWizard will add data memberG here //)}AFX_DATA // Over ridec // ClaGGWizard generated virtual function overndeG /,/{{AFX_VIRTUAL( UnlockPW) piotected vntual void DoDafaExchange(CDataExchange4 pDX) // DDX/DDV Gupport //;:afx_virtual // Implementation piotected // Geneiated meoGage map functiono //{{AFX_MSG(UnlockPW)
^egaZero — самая законченная в мире, ничего не лелаюшая... 573 virtual void OnOK(), " virtual BOOL OnlmtDialogO, /'!JAFX_MSG DECLARE_MESSAGE_MAP() Листинг А.14. SOURCE\APP_A\MEGAZERO\UNLOCKPW.CPP // Copyright 1995 Lou Grinzo // Zen of Windows 95 Programming // unlockpw cpp implementation file // #include stdafx h «include megazero h «include unlockpw h «include config h «include '"toolbox h" «lfdef _DEBUG «undef THIS_FILE static char BASED.CODE THIS_FILE[] = __FILE__, «endif ///////////////////////////////////////////////////////////////////////////// // UnlockPW dialog UnlockPW UnlockPW(CWnd* pParent /*=NULL*/) CDialog(UnlockPW IDD, pParent) //{{AFX_DATA_INIT(UnlockPW) // NOTE the ClassWizard will add member initialization here //}}AFX_DATA_INIT ». void UnlockPW DoDataExchange(CDataExchange* pDX) { CDialog 'DoDataExchange(pDX), //{{AFX_DATA_MAP(UnlockPW) // NOTE the ClaccWizard will add DDX and DDV calls here //}}AFX_DATA_MAP BEGIN_MESSAGE_MAP(UnlockPW CDialog) //{{AFX_MSG_MAP(UnlockPW) //}!AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // UnlockPW message handlers void UnlockPW OnOK() char pw[MAX_PW_LEN + 1] = GetWmdowText( GetDlgItem(m_hWnd, IDC^PW), pw, sizeof (pw)), // Избавляемся от этих ужасных лидирующих и завершающих пробелов ctripLT(pw). if(strcmp(pw config password) ' = 0) MessageBox('Неверный пароль1 MegaZero .MB_0K | MB_IC0NfcXCLAMATI0N), return, V // Если мы добрались до этого места, все должно быть в порядке, так что // мы смело идем дальше и размыкаем программу
574 Приложение /\ config.locked = FALSE; config.pasGword[0] = '\0'; CDialog::OnOK(); BOOL UnlockPW::OnInitDialog() { CDialog -OnlmtDialogO. CenterWindowO, SendDlgItemMeGGage(IDC_PW,EM_LIMITTEXT,MAX_PW_LEN,0), return TRUE, // return TRUE unleGG you Get the focuG to a control // EXCEPTION OCX Property PageG Ghould return FALSE Листинг А.15. SOURCE\APP_A\MEGAZERO\TOOLBOX.H #ifndef __T00LB0X_H__ «define „T00LB0X_H__ void GtripLeadmg(char *g), > , void GtnpLT(char *c), void GtripTrailmg(char *g): BOOL Gtrlcat(char *dest, conGt char *Grc, Gize_t max_len), BOOL GtrlcatAtomic(char *deGt, conGt char *crc, size_t max_len); tfendif // __T00LB0X_H__ Листинг А.16. SOURCE\APP_A\MEGAZERO\TOOLBOX.CPP // Copyright 1995 Lou Grinzo // Zen of WmdowG 95 Programming «include "Gtdafx h" «include "toolbox h" /I HI I Ill/Ill IIIlllllllllII/II III/lllllIII I IIIllllIIIII/IIIIII/III IIIII/III /I GtripLeading удаляет лидирующие пробелы и символы табуляции из строки // 7/ Проверка параметров // NULL или пустая строка отвергаются /III/I/III///////К/111/11/////III/////////////////(//1/(1////!/!/////(//// void GtnpLeading(char *s) К #ifdef .DEBUG if((G == NULL) || (*g == *\0')) OutputDebugString("GtripLeading: обнаружен недопустимый параметр1\п ), «endif if((G == NULL) || (*s == Л0')) return; char *z = g, while(*z && ((*z == ' ') || (*z == At'))) z++, lf(G »= Z) memmove(G,z.Gtrlen(z) + 1), // Безопасно - переполнения не будет i /////////////////////////////////////////////////////////////////////////// // GtripLT: удаляет лидирующие и завершающие пробелы и символы табуляции из строки //
^egaZero — самая законченная в мире, ничего не лелаюшая... /I Проверка параметров * // NULL или пустая строка отвергаются /////////////////////////////////////////////////////////////////////////// void Gtripi_T(chat *з) > ffifdef .DEBUG if(Co == NULL) || (*c == '\0')) OutputDebugStnngC GtripLT обнаружен недопустимый параметр1 \п ), tfendif if((G == NULL) || (*g == Л0')) ieturn, GtripLeading(G). GtripTrailing(G), )ll//lllllIII 11/11//Illlllflllll/llllllllll/I/IIIIIIIIIIIIII/lIIIllll/ll/ll II GtripTrailmg удаляет завершающие пробелы и символы табуляции из строки II ,* . // Проверка параметров. // NULL или пустая строка отвергаются ////////////////////////////////////'II///II/IIIIII III'///////41 III III IIIЧ/l/ void GtripTrailing(char *g) { ttifdef .DEBUG if((c == NULL) || (*g == '\0")) OutputDebugStnng('GtripTrailing. обнаружен недопустимый параметр1\п"), #endif if((G == NULL) M (*s == '\0')) return, '% .к ?> , ,, ^ mt z = ctrlen(G), while((z >= 0) && ((g[z] == ' ') || (g[z] == \f ) || (s[z] == АО'))) z--, G[Z + 1] = 0. /////////////////////////////////////////////////////////////////////////// // GtrlcatAtoinic Прицепляет строку Grc к концу строки deGt с учетом максимально // допустимой для deGt длины (шах_1еп) и всегда обеспечивает должное завершение // Эта функция сработает только тогда, когда все символы из зге поместятся в deGt // // max_len включает завершающий NULL // // Возвращает TRUE, если прицепление произошло, и FALSE - в противном случае // // Проверка параметров* // NULL или пустая строка в качестве Grc или deGt отвергаются // Значения шах_1еп. равные 0 или меньшие начальной длины dect, отвергаются //////////////II/////////////////////////////////////////////////////////// BOOL GtrlcatAtomic(char *dect. conct char *gtc. Gize_t max_len) { #ifdef .DEBUG if((deGt == NULL) || (Grc == NULL) || (max.len ==0) || (Gtrlen(dect) >= max.len - 1)) OutputDebugStnngC "GtrlcatAtoinic обнаружен недопустимый параметр1 \п"), #endif if((dect == NULL) || (зге == NULL) || (max.len ==0)) leturn FALSE, 575
576 Приложение л UINT d_len = Gtrlen(deGt), if(d_len + r,trlen(crc) >= max_len - 1; // Проверяем, все ли символы влезут return FALSE otrncat(dect vc,max_]en - d_jen - 1;, return TRUE, II strlcat Прицепляет строку ore к концу строки dect с учетом максимально // допустимой для dect длины (шах_1еп) и всегда обеспечивает должное завершение // Эта функция прицепит к dect столько символов, сколько в нее поместится // II max_len включает завершающий MULL // // Возвращает TRUE, если прицепление произошло, и FALSE - в противном случае // // Проверка параметров // NULL или пустая строка в качестве • или deot отвергаются // Значения max_len. равные 0 или меньшие начальной длины dect, отвергаются /////////////////////////////////////////////////////////////////////////// BOOL Gtrlcat(cnai *de3t, const char *crc oize_t max_len) ttifdef _DEBUG if ((dect == NULL) || (зге ■== NULL) || (max_len ==0) || (otrlen(dect) >= inax_len - 1)) OutputDebugStnng('ctrlcat обнаружен недопустимый параметр'\п ) ffendif if((dect == NULL) || (crc == NULL) || (max.len == 0)) return FALSE UINT cl.len = otilen(dect), if(d_len 4= max_len - 1) return FALSE 3trncat(oeot,crc.max_len - d_len - 1), return TRUE Листинг А.17. SOURCE\APP_A\TOOLBOX\TOOLBOX.CPP h Copyright Lou Grinzo 1995 Zen of Windows 95 Programming */ «include <windows.lv /////////////////////////////////////////////////////////////////////////// // AppendSlach Добавляет обратную косую черту С\\') в конец заданной // строки только в том случае, если для этого есть свободное место, и // если эта строка уже не заканчивается таким символом // // cie^t строка, которую нужно проверить и изменить // max_Len максимально допустимая длина строки dect, включая завершающий NULL // Проверка параметров // NULL или пустая строка в качестве dest отвергаются // значения max_len, меньшие 3, отвергаются IIIIII/I/II/II//III/IIIIIIII//IIIIIIIII/IIIII/I/I//II//II/IIIIIIII/IIIIIIII void AppendSlash(char *dect oize_t max_len) #ifdef _DEBUG if((deot == NULL) || (*dect == '\0') I I (max_len < 3)) OutputDebugStnng( "AppendSlach обнаружен недопустимый параметр'\п ),
его — самая законченная в мире, ничего не лелаюшая... #endif * if((elect == NULL) || (*dest == '\0')|| (max_len < 3)) return, if((ctrlen(dest) + 1 < max_len) && (deGt[otrlen(dect) - 1] •= '\\')) Gtrcat(dect, \\ ), )llIIIII/11/I IIII/I IIIIII III IIIIII I/1II/II III/llllII11IIII11IIII/11/1IIIIII Iy FileExicto Проверяет предположение о том что заданный файл // существует и НЕ является каталогом /' /I Возвращает TRUE, если файл существует или FALSE - если файл // не существует или является каталогом // // Проверка параметров. // NULL или пустая строка отвергается /////////////////////////////////////////////////////////////////////////// BOOL FileExictG(conct char *fn) К #ifdef .DEBUG if(fn == NULL M (Gtrlen(fn)) == 0) OutputDebugStnng( FileExiGtG1 обнаружен недопустимый параметр1\п"), #endif xf(fn == NULL || (Gtrlen(fn)) == 0) return FALSE, DWORD dwFA = GetFileAttnbutes(fn). if(dwFA == OxFFFFFFFF) return FALSE, elGe return ((dwFA & FILE_ATTRIBUTE_DIRECTORY) '= FILE_ATTRIBUTE_DIRECTORY), Листинг А.18. SOURCE\APP_A\DUMPER\DUMPER.CPP //////////////////////////////////////////////////////////////////////////// // Dumper считывает и показывает содержимое специализированного файла // данных MEGAZERO DAT Также выполняет некоторые минимальные проверки // // Copyright Lou Grinzo 1995, Zen of Windowc 95 Programming //////////////////////////////////////////////////////////////////////////// «include <windowG Iv «include <ctdio h^ «include . \\megazero\\config h" void DumpHeader(const config_header_type *ch); void DuinpData(conct config_type *c), void Pauce(void), mt inain() { char fn[] = "megazero dat", FILE *f, config_header_type config_header, config_type config: if((f = fopen(fn, rb')) == NULL) { printfC Невозможно открыть входной файл \'%g\' \n ,fn),
578 Приложение PauceO. return 0, if(fread(&config_header,Gizeof(config_header),1,f) '= 1) fcloGe(f), printf( Невозможно прочитать заголовок из входного файла \'%з\ \n',fn) PauceO return 0 DuinpHeader(&config_header) PauseC> , if(fread(&config,3izeof(config). 1, f) '= 1) { fcloce(f). pnntf("Невозможно прочитать данные из входного файла \'*%с\ ' \n", fп), PauceO, return 0; DumpData(&config), printf( 'Дело сделано' ), PauceO, fcloce(f), return 0; )l/III Ill/Ill//IIIIII//IIIIIIIIIIIIIIll/l/lllll/lllllll/l/l/l/l11/11/I/III/I II PrintCharO Печатает заданный символ, заменяя 0x00 символом подчеркивания, // а все контрольные символы - знаками вопроса //////////////////////////////////////////////////////////////////////////// void PrintChar(char с) { if(c == '\0О pnntf( _ ), elce if('iGcntr](c)} printt( %c c), elce pnntf( ">' ), )llll//lIII////III//lllll/l/l/llllllll/IIIII/I////1//I//IIIII/I/I//II/////// II DuinpHeader(). Показывает поля заголовка файла 11 III III II11/11/1III III III II III IIIIII/I Il/l/lllllllllIII III//II/111/11/III II void DuinpHeader(conct config_header_type *ch) printf('"0Tne4aTKH пальцев *); for(int i=0, l ' eye_catcher_len, i++) PrintChar(ch->eye_catcher[i]), printf( '\n\n' ), if(Gtrncinp(ch->eye_catcher. "megazero ,eye_catcher_len) == 0) printf( Отпечатки пальцев верны \ri\n ), elce printf( Отпечатки пальцев неверны \n\n ). printf( Версия файла %hu %hu\n\n , ch->major_vercion. ch->imnor_verGion), )llI/III ,41II/III//!11/11 I/Ill!/lllllIIIll/l/lIIII111/11 IIIl/ll/llllllllllI/ I/ DumpFNO Показывает одно из fn-полей из раздела данных файла ////////II////I///////III//////////////////////(///(///////////////II/III///
^egaZero — самая законченная в мире, ничего не лелаюшая... 579 void DuinpFN(const char *fn,int fn_num) » { printfC FN%d ,fn_num), for(int 1=0; l ' MAX_PATH. i++) PrintChar(fn[i]), printf(*\n ). //////////////////////////////////////////////////////////////////////////// // DumpDataO Показывает поля из раздела данных файла; проверяет // зарезервированные поля на равенство нулю ///////////////////////////////Л//////////////////////////////////////////// void DumpData(conct config_type *c) { int i, DumpFN(c->fn1,1), DumpFN(c->fn2,2), DuinpFN(c->tn3 3), Pauce(), DuinpFN(c->fn4.4)- DumpFN(c-4fn5 5) DumpFN(c->fn6,6): Pauce(), DumpFN(c->fn7.7), DumpFN(c->fn8,8), DumpFN(c->fn9,9); DuinpFN(c->fn10,10). Pauce(), printf( 'Дата последнего запуска (М/Д/Г)* %d/%d/%d\n\n", c->laGt_runjnonth,c->laGt_run_day.c-slaGt_run_year); if(c-4locked) pnntf( Приложение замкнуто \n\n'), е1зе pnntf( 'Приложение не замкнуто \n\n ), pnntf( Пароль "), for(i = 0, l < Gizeof(c->paGsword), i++) PrintChar(c->pa3Gword[i]). printf("\n\n"), printf( Зарезервированные поля '), for(i = 0, l < Gizeof(c->recerved) / sizeof(int), i++) printf( "%d ",c->reGerved[i]): Dnntf( \n\n ) for(i = 0. l s 3izeof(c->recerved) / oizeof(int), i++) if(c->recerved[i] '= 0) { pnnrf( *** He все зарезервированные поля равны нулю'' ' \n\n ), return //////////////////////////////////////////////////////////////////////////// void Pause(void) { pnntf('Нажмите ENTER \n\n' ), getchar(),
580 Приложение л Листинг А.19. SOURCE\APP_A\PATCHER\PATCHER.cpP IIIIIIII/I/II/IIIl/l/lI IIIll/l/IIII III/III III III II/II III IIIl/l/III III II III II III I/ PATCHER CPP Утилита для латания файла в виде консольного 1л/1М32-приложения // // Copyright 1995 Lou Gnnzo // Zen of Windowc 95 Programming #include 'staafx h «include <stdio h> «include <io h> «include "checker h' FILE* f conct buffer_size = 16384, unsigned long i, crc_start, file_pos, eye_catcher_type pattern, unsigned short the_crc, BYTE the_byte, char buffer[buffer_size], long buffer_pos. data_size, char NextByte(void), void ReadNextCatcher(eye_catcher_type & c), int main() // Измените следующую строку для указания имени другого файла, // подлежащего обработке1 char * file_name = megazero exe", // " иммппппншипш pnntf ('Собираемся обработать файл \ %s\".. \n\n",file_name), if('(f = fopen(file_name,"rb" ))) printf( 'Невозможно открыть файл.\п\п\п"), else { buffer_pos = buffer_size, data_size = 0, file_pos = 0, pnntf( Поиск начала CRC-данных \n"), // Заполняем образец первыми байтами из файла for(i = 0. 1 < sizeof(pattern), i++) pattern[i] = NextByteO, crc_ctart = 0, // Инициализируем значением *'не найдено" // Сканируем оставшуюся часть файла в поисках маячка // Обратите внимание, что мы НЕ останавливаемся на первом найденном // маячке1 Мы должны обязательно просканировать весь файл и убедиться, // что больше нигде не затесалась вторая копия маячка unsigned long limit = _filelength(fileno(f)) - sizeof(pattern), for(i = 0, l < limit; i++) { if('ineincinp(crc_data eye_catcher, pattern, sizeof(pattern))) if(crc_start ~ 0) ptintf( В файле обнаружено несколько копий маячка1\п\п"), fclose(f). getchar(); return 0; else crc_start = file_pos - sizeof(pattern),
rfegaZero — самая законченная в мире, ничего не лелаюшая... 581 ReadNextCatcher(pattern), fcloce(f), if(crc_Gtart == 0) printf( He найден маячок'\n\n ), getchar(), return 0, // В данном месте можно было бы сделать проверку, не является ли файл // уже подлатанным должным образом, и, если это так, катапультироваться // с соответствующим сообщением Но латание не является деструктивной // операцией и кроме того работает довольно быстро, поэтому вероятно // не стоит тратить силы на такую проверку, особенно если учитывать, //' что речь идет о создании простого программистского инструмента рппт.т("Маячок найден Выполняется расчет CRC. \n\rT ), if('FindCRC(file_name,crc_ctart,&the_crc)) { printf (""Невозможно вычислить CRC-значение для файла' \п\п"); getchar(), return 0, i if('(f = fopen(file_name, "rb+"'))) { printf( Невозможно открыть файл заново1\n\n ), getchar(), return 0, // Мы вычислили необходимые данные, теперь пора подлатать // структуру crc_data в файле. // Перемещаемся на начало той области файла, которую мы собираемся // перезаписать (которое является НЕ началом CRC-данных, а определяется // полем crc_Gtart) // // В данном месте предполагается определенный вид структуры crc_data_type' fseek(f,crc_otart + cizeof(pattern),SEEK_SET), // Записываем данные в файл fwrite(&crc_otart,1,sizeof(crc_start),f), fwrite(&the_crc,1tGizeof(the_crc),f). /7 А теперь домой fclooe(f) printf('Файл \ %g\ обработан \n".file_name), printf(" смещение CRC' %d\n",crc_Gtart); printf( " значение CRC %d\n",the_crc), printf ("\пГотово'' '\n\n"), printf( (Нажмите ENTER) "), getchar(), return 0, i II/I//III IIIII///I//IIIIIII/II III III///III//Ill/Ill III III 11/111 III II//III/III/I II NextByte() Возвращает следующий байт исходного файла, используя // буферизованный ввод char NextByte(void) { if(buffer_poG >= data_Gize) {
582 Приложение д data_size = fread(buffer,1,buffer_size,f), buffer_pos = 0 buffer_pos++ file_pos++, return Duffer[buffer_pos - 1], i iii/i/i ii in/in i in/iiiiiiii//in//I/1 ii ii 11 ii iii/i in///in//i/in i/i/i/mi II ReadNextCatcher() Считывает следующий символ и добавляет его к нашему // текущему "кандидату на совпадение с маячком void ReadNextCatcher(eye_catcher_type & с) ineminove(&c[0],&c[1],sizeof(c) - 1), c[oizeof(c) - 1] = NextByteO. Листинг А.20. SOURCE\APP_A\PATCHER\CHECKER.CPP /////////////////////////////////////////////////////////////////////////////// // CHECKER CPP Ядро, осуществляющее собственно CRC-вычисления // // Copyright 1995 Lou Grinzo // Zen of Windows 95 Programming #mciude stdafx h «include '"checker h «include <stdlib h> «include votdio h> «include mo h> «include <malloc h> «include <assert h> static long buffer_poG, data_Gize, static BYTE *buffer, static const alloc_buffer_size = 4096, static FILE *f, // Локальные функции static unsigned short UpdateCRC(BYTE new_byte, unsigned short crc), static char NextByte(void), // Эта переменная типа crc_data_type должна быть именно здесь И именно // она будет использоваться в приложении в качестве носителя вычисленного // значения CRC crc_data_type crc_data = { ' М\ 'Е\ 'G', 'А', 'Г, ' Е\ 'R', '0', // eye_catcher 0, // crc_start 0 // crc_value l/i/lIll/Ill/III IIII//IIIIII////I/I/IIII IIII//I/////IIIIIIIIIIIIIIIIIIIIIIII III II Таблица, которую использует функция UpdateCRC() для вычисления CRC static unsigned short crctab[256] = { 0x0000, 0x8108. 0x1231, 0x9339, 0x2462, 0xa56a, 0x3653, 0x1021, 0x9129. 0x0210, 0x8318, 0x3443, 0xb54b, 0x2672, 0x2042, 0xa14a, 0x3273, 0xb37b, 0x0420, 0x8528, 0x1611, 0x3063, 0xb16b. 0x2252, 0xa35a, 0x1401, 0x9509, 0x0630, 0x4084, 0xc18c, 0x52b5. 0xd3bd, 0x64e6, 0xe5ee, 0x76d7, 0x50a5, Oxdlad, 0x4294. 0xc39c, 0x74c7, 0xf5cf, 0x66f6, ОхбОсб, Oxelce, 0x72f7, 0xf3ff, 0x44a4, 0xc5ac, 0x5695, 0x70e7, Oxflef, 0x62d6, 0xe3de, 0x5485, 0xd58d, 0x46b4,
о -h 3 О 3" -h ~Ь О U О О r+ J3 l—1 СО □О CD - CD --s I-» CD CD О Z r+ ^ rt СТЮ I rt 3 rt- згосг-ьссгзос:'—> с CDO-s -* -h CD -* -* О -* Q. X 3 II 3 -h О. О 3 i—>3 I CD I—' CD "Л -h Ж О 2 3> О 'j> О О 3 Г- "О Г— Н Я (Q J3 СЛ (D CO N - ^1 ГП 3 ГП - CD j_j S - • ^-ч - | О X h-> X -h -ft Ч 3 ^ 3 CD О -h -h CD О CD Q. \ О \ 3 СЛ H О "s. ZO \ !-• S ZO \ О \ О CQ S< О \ \ CT CO H О \ -h \ О -\ СО О \ 3 ~S. -< г+ СХЗ \ О -5 II S« po -Q O. О CJ X r+ O CD О О -h ZO -h О О -1 О 3 о ч о CD ч ч CD 7* < J= ТЗ < 3 -D О 4 CD X со \. з m з I \ \ CD CD P \ О "S. £ 3 S X \ -л \ I CD I CD \ 0 \ CT € СГ X \ 1 \ ^ I ^ S \ tl \ rt СГ rt- CO \ r+ \ CD ^ CD \ ooooooooooooooooooooooooo CD CD -si -h .Cs> О СЛ О. ГО CJ CD -ft О О. СО а ОЭ CO СЛ -si Nl -ft G5 CD СЛ a Ь О CO CT CO О" О CD —i CD -h Nl CD CD СГ CJ CD CO CD Ю ГОО) -J. CD О CD -h si cj cj -h О О СЛ CD О) a Ul О -P=> CT ooooooooooooooooooooooooo si -h ГО CD СЛ CZL -lb. О CO CT CD -ft О a CJ СГ OO CD O) -vj COWOOtJ1WO)Cn-h-1\ CDCD ~>l -h ^ О СЛ О- ГО CJ XXX XXX M P —i. СО О OO CD CB -h -si О j^mrocjo-i-ha! Q.O i"- О О (D ID CJ CO CT a CD CD со сг о oo —i со cd о -h vj i СЛ CO CO CD si CD CD si Q. СЛ CJ ooooooooooooooooooooooooo XXXXXXXXXXXXXXXXXXXXXXXXX -fc-O СЛ Q G5 CD s|-hOOO-acorOCJ CO CT О- СЛ О .&> -h si CD CDCD CD -hO Q. CJ СГСОСОСП-ч1^СЛМСОО-1-ьа) Q.O Q CJ CD CD nI СЛСЛ010)и«00(0(ОР CJ -h -ft О О Q.Q.CD CD a D" CD CD -i сло-^о sj -h en cd —icoooococrrocj a en о *. -h -si cd ro со ooooooooooooooooooooooooo О. .С». О Nl ■+ CT) CD —i CO -hO Q. CJ C7 CD (O O) 4 - _л _». ro ro — — OO CO CT ГО CJ О -Jb. СЛ ГО CO О —i -ft CD CD CD a a (D CD -h-h -fc> О СЛ а О) CD sl-hOCDJCDfOO CO а О -fc> CL. СП CD ГО -h si CO а. о ст со cd oo -j О О CD CD CJ CJ CO О. СП CD CD -h -si CD ooooooooooooooooooooooooo rOCJ CO CT О CD —i (О О) CD -si -h J>- О СЛ O- CD -hO Q.CJ aootDOJS-MJiMCOOa CDCOCJ CJ -h -ft О О СПСЛГОГОСОСООО со a ro и —i ю о со -vi •+ ro ш сло.4^0 CO CJ ГО CD —i OO О CD Q. О CT CO CO CD -i ГО ГО -v| -vl *> J> l CO CJ ГО CD -i OO О ooooooooooooooooooooooooo X X X X X X X X X X X X X X CO CT ГО CO -i<0003-s|-tiC7)CD СЛ а Л О CO rOCTUCOOCD-iCD CD -hO Q.CJ СГООСОГО-д-СьСЛГОСОО—i -h CD CL О CTCJ CD OO si ct a oo со a a cd cd Ni-NiA^-iurorocouoocncnroro-h ГО И U CT О CO -1 CD O) CD si -h -fc* О СЛО-CJ rOCTWCOOCO-iCD ooooooooooooooooooooooooo xxxxxxxxxx X X X X OO —i CO ГО CJ W CT А О СЛ Q CJ) Ф si -h CO -». OO О СТ СО СО ГО O- CD -hO O- Co CTCOCDCnsJ^CnrOUO-Ji-hCD CZLO CTCJ CO OO si Q.Q.CD CD CTC7CDCD-i-irOrOs|sl45.^UlCriCnO)OJCOOOra —i со о оо со ст го со cno.^o sj -h en cd со —i оо о ст со со ro o. oooooooooooooooo ХХХХХХХХХХХХХХХХлл лсоооосостюв ел a *. о sj -h о cd ooc CD -ft О O- CJ CTOOracns|^(jirOWO-i-hCD -ft -ft О О CDCOCJ CJ COCOOOCJICnrorosJs О CD —i CO ГО CJ W CT Д О СЛ a CJ) CD si -h CD С oooooooo ^^•^xxxxx i CJ ГО CT CO О ct cj со oo si . -i -i ГО ГО CT i CO ГО CT CO О
584 Приложение д II Вычисляем CRC для той части файла, которая предшествует // структуре данных CRC for(i = 0, l < crc_start, 1++) *the_crc = UpdateCRC(NextByte(),*the_crc), // затем перешагиваем через всю структуру . for(i = 0, 1 < cizeof(crc_data_type), i++) NextByteO, // и заканчиваем вычисления, обрабатывая остаток файла for(i = crc_ctart + sizeof(crc_data_type), i < limit, i++) *the_crc = UpdateCRC(NextByte(),*the_crc), free(buffer), fcloce(f). return TRUE, \ /////////////////////////////////////////////////////////////////////////////// BOOL GoodCRC(void) char fn[MAX_PATH], unsigned short the_crc, if(crc_data start '= 0) < GetModuleFileName(NULL.fn.sizeof(fn)), // Если что-то неладно, мы возвращаем FALSE if(,FindCRC(fn,crc_data.ctart,&the_crc)) return FALSE; else return the_crc == crc_data crc_value, else return FALSE, // Данных нет, нет и смысла выполнять вычисления III III/II/1/III/11/1 III I/I/III/I/III/IIII//IIIII III III/nil/III III III III/III III II NextByteO Возвращает следующий байт исходного файла, используя // буферизованный ввод, char NextByte(void) { if(buffer_pos >= data_size) { data_size = fread(buffer.1,alloc_buffer_size,f), buffer_pos = 0, buffer_pos++, return buffer[buffer_pos - 1],
JJjJ±LA±)jJE3J]J-& №оиайект программистских иебылиц The foul sluggard's comfort: "It will last my time." Томас Карлайл Теперь, когда я задал нужную тональность при помощи насмешливой цитаты Карлайла, позвольте мне заверить вас, что я сам совершал каждый из грехов, о которых я собираюсь говорить ниже. Некоторые из них я совершал даже неоднократно, пока наконец не усвоил урок. Поэтому я думаю, что у меня есть полное право говорить об этих вещах — право, заработанное моими собственными синяками. Я хотел бы пояснить, почему я решил включить в книгу это приложение. Я сделал это не для того, чтобы лишний раз поупражняться в бросании камней в программистов - такое занятие не приносит прибыли (во всех смыслах этого слова), и, кроме того, я слишком хорошо отношусь к программистам. Тем не менее, я подумал, что было бы правильно еще один, последний раз пройтись по всем этим небылицам и байкам. Несмотря на то, что большинство из них уже упоминалось в книге, именно здесь я могу более полно и подробно поговорить об их истинной природе и напомнить вам (и себе), почему эти, казалось бы, безвредные отговорки и оправдания могут быть столь опасными для наших программ и наших пользователей. В конце концов, этот своеобразный «заповедник» поможет нам быть настороже, быстро обнаруживать проявления этих вредных тенденций и быть готовыми к сопротивлению, из чьих бы уст ни раздавались эти обманчивые поговорки. «Ошибки есть в любой программе» Это чемпион среди программистских небылиц; звучит намного чаще других и одновременно намного опаснее других баек. Я слышал ее от программистов сотни раз, и всегда она имела один и тот же подтекст: если программы, написан-
586 Приложение $ ные другими, очевидно несовершенны, то с какой стати должен я стремиться к достижению неимоверно высокого уровня качества? Такая мысленная установка — самый крутой, самый скользкий карниз во всех вопросах программировд- ипя, и она моментально приводит к непродуманному дизайну, небрежному кодированию, бестолковому оформлению и вообще - к бесцеремонному отношению к вопросам качества, удобства и практичности программ. Подобно другим небылицам, эта является особенно притягательной, поскольку звучит она больше, чем правдоподобно — достаточно часок-другой поработать на компьютере с практически любой коммерческой программой, и вы уже не будете оспаривать это утверждение. А теперь давайте взглянем на эту формулу с другой стороны: когда вы в следующий раз выложите изрядную сумму за какую-нибудь программу или инструмент, и когда ваше приобретение окажется настолько бракованным или плохо документированным, что вы часами будете не работать, а лишь шепотом материться, вспомните эту байку. В девяти случаев из десяти именно она послужила оправданием для программистов и менеджеров, отвечающих за бинарного монстра, который только что сожрал кучу вашего времени и денег. «Невозможно проверить все» / «Зачем зря тратить ресурсы процессора?» Это серебряный призер на чемпионате опасных небылиц. В частности, вторая поговорка ведет к самым потрясающим решениям при кодировании. Обратите внимание, что я не упомянул проектировочные решения, потому что они тут ни при чем. Эти небылицы вступают в игру уже на микроуровне, когда программисты уже в окопах и на лету решают, следует ли проверять те или иные параметры или каким-то другим способом обеспечивать правильность своих предположений. В данном случае проблема состоит в том, что эти поговорки приводят к написанию кода, в котором ужасающе не хватает защитных барьеров и фильтров, препятствующих распространению плохих данных по всей программе. Кстати, вторая формулировка является особенно изощренной: многие программисты давно усвоили, что подобного рода клятвенные уверения в заботе о производительности часто приятны для слуха руководства, а значит являются отличным способом увильнуть от неприятной работы, которую программисты должны дела/т?. А в результате на свет появляются пустотелые, хрупкие программы, отладка которых мгновенно превращается в кошмар, когда неизбежные ошибки, наконец, возникают и становятся неуловимыми мстителями за программистскую лень и безответственность.
Конспект программистских «Я был вынужден использовать инструмент X, потому что так решило руководство / больше ничего подходящего не было / у меня не было времени на изучение более правильного» Поговорки такого сорта — это не более чем классическое увиливание от ответ1 ственности. Как я уже упоминал ранее в этой книге, и в самом деле бывают случаи, когда у программистов действительно нет выбора: экономические условия и декреты начальства могут быть довольно жесткими и безжалостными руководителями проекта. Но гораздо чаще эти слова служат просто оправданием выбора более легкого пути или даже свидетельством чьей-то некомпетентности. Например, «Почему вы не поместили те значки в главный исполняемый файл?» - «Я ие мог этого сделать - Windows позволяет использовать только первый значок в файле» (конечно же, это неправда). Данная небылица также связана с одной из самых распространенных и тревожных тенденций, свойственных программистам -- страстью к игре. Давайте ие будем лукавить — программистам, как группе, нравится играть со своими инструментами больше, чем представителям какой-либо иной профессии. Это мое суждение — не только взгляд со стороны, оно целиком подтверждается моим собственным опытом. И если соединить эту тенденцию с вышеприведенными поговорками, вы получаете формулу того, как многие программисты маскируют свое стремление использовать неправильный инструментарий только потому, что работа с ним является для них развлечением. А теперь добавьте сюда почти всеобщую, присущую и программистам, и руководителям, склонность играть в служебные политические игры и заниматься «строительством империй», — и вам уже покажется просто чудом тот факт, что кто-то в этом бизнесе вообще способен довести до конца хоть один программный проект. «Я сразу разберусь, что все это значит, когда увижу этот код снова» Нет, скорее всего это не получится. По очень простой причине: помните формулу «you(now) != you(later)»? И данная поговорка является лишь еще одним вариантом оправдания своего стремления срезать углы, особенно в таких делах, как написание комментариев и документации. Никто не любит писать комментарии и документацию. И, смею вас уверить, я тоже ненавижу это занятие не меньше других. И тем не менее, я пишу весьма обширные комментарии в своем коде. Почему? Да потому, что я знаю очень простую вещь: каждая минута, потраченная мной сейчас на эту писанину, наверняка сэкономит мне три минуты в перспективе. А еще эти комментарии и документация значительно уменьшают вероятность того, что позднее я неправильно пойму свой собственный код и внесу в него очень дорогостоящую ошибку. Не знаю, как вам, а мне такой расклад кажется поистине сделкой века.
588 Приложение # «Зачем же комментировать? Этот код само документирован!» Такая поговорка всегда вызывает у меня смех. Дело в том, что как только подобное заявление оказывается необоснованным (такое случается примерно с> половине случаев), оно является на 50 процентов оправданием и на 50 процентов амбициозным вызовом. Говорящий не просто ищет в этих словах отговорку чтобы не писать какой-то комментарии, но и провоцирует слушающих в надежде, что кто-нибудь запротестует и начнет утверждать, что код недостаточно ясен и все-таки требует комментирования. И если такой протест возникнет, наивный оппонент моментально попадает в хитро поставленную ловушку: он как программер тут же будет обвинен в недостаточном уровне мастерства и профессионализма. Поведение такого сорта — не что иное, как ребячество. И я не стал бы обращать на ото внимания в своей книге, оставив подобные темы для психологов. Но, к сожалению, оно далеко не безобидно и прямиком ведет к серьезному ухудшению сопровождаем ости кода, а, значит, к большему количеству ошибок, с которыми нам всем потом придется бороться. И в этом смысле я не могу обойти этот важный момент в данной книге и считаю своим долгом напомнить, что все мы, как программисты, должны беспокоиться об этом. «Если я задокументирую это, то тем самым просто добавлю забот при сопровождении» Ага, еще одна отговорка для увиливания от документирования кода. Я вспоминаю об этой байке всякий раз, когда открываю документацию по Win32 API и вижу, как много простора оставлено в ней для моей богатой фантазии. (Я пощажу вас и не буду повторять все те тирады про расширенные коды ошибок и другие грехи Win32 API; все эти подробности упоминаются почти повсюду в этой книге.) Как правило, подобные речи мы слышим тогда, когда говорящий не хочет связывать себя текущим поведением кода или программы п документировать все причуды текущей реализации. Во многих случаях говорящий даже не знает всех этих причуд, которые должны быть задокументированы, и, следовательно, ему пришлось бы потратить немало времени па тестирование и изучение исходного кода, прежде чем документация могла бы быть закопчена. (Я сильно подозреваю, что именно такой ситуацией обусловлено текущее состояние документации по Win32 API.) Как это свойственно многим программерским небылицам, данная поговорка подкупает правдивостью своего звучания: да, конечно же, документирование превратится в дополнительные заботы. Но слишком часто за этим кроется боязнь совершенно другого - боязнь серьезно ограничить свою собственную «свободу творчества» при дальнейшей работе над кодом. Ведь как только работа кода задокументирована, любое последующее изменение может
Конспект программистских небылиц 589 на совершенно законных основаниях рассматриваться как ошибка. Я не испытываю ни малейшей симпатии к программистам, пытающимся подобным образом увильнуть от ответственности; в конце концов, именно четкая постановка требований и последующее их выполнение — это и есть та цена, которую ,мы должны платить за право называть программирование профессией. «Если вы вызовете функцию и передадите ей плохие данные, вы получите то, чего заслуживаете» На мой взгляд, этот образчик особенно предосудителен, потому что такие слова, как правило, произносятся одним программистом (автором некоторого кода) в адрес других программистов той же самой компании (желающих использовать этот код). Результатом же подобных заявлений может стать весьма серьезная конфликтная ситуация. Типичный сценарий развития событий таков. Программист-пользователь обнаруживает, что, если он случайно передаст функции плохие данные (а зачастую у него попросту нет никакой возможности проверить эти данные до вызова), то эта функция приведет к краху программы, или к краху всей системы, или испортит какие-либо данные, или сотворит еще что-нибудь ужасное. При этом для программиста-пользователя вполне очевидно, что вызываемый код должен быть достаточно сообразительным для того, чтобы обнаруживать недопустимые значения параметров и элегантно реагировать на них. В то же время программисту-автору точно так же очевидно, что документация к его коду (если таковая вообще существует) вообще ни слова не говорит о проверке параметров, и поэтому не следовало на такую проверку и рассчитывать. Кто нрав? Эта ситуация вмиг превращается в Великое Противостояние Двух Эго, и ничего не остается делать, как срочно призывать в третейские судьи руководство, которое почти никогда не способно решить такой конфликт до конца. (Пример: руководство быстренько стряпает компромиссное решение, согласно которому в документацию включается явное заявление об отсутствии проверки параметров, а автор спорного кода дает обещание «серьезно изучить» вопрос о реализации такой проверки в следующей версии. Конечно же, подобные обещания почти никогда не выполняются. А тем временем руководство распирает от гордости, что ему удалось потушить конфликт в технической области и найти компромисс, в котором обе стороны что-то выиграли и что-то проиграли. Но в итоге настоящим проигравшим оказывается пользователь, поскольку в программе остается — если и не навсегда, то как минимум надолго — серьезная брешь, угрожающая качеству.) Единственно правильное решение — вообще избегать возникновения подобных ситуаций путем предварительного (на как можно более ранней стадии проекта, еще до того, как программисты начнут писать код, полный допущений и предположений, и до того, как они глубоко окопаются на своих позициях) определения, какие конкретно функции будут или не будут заниматься
590 Приложение в проверкой параметров, обнаружением различных ошибок и уведомлением о них. Это особенно важно в тех случаях, когда одна из сторон (вызываемый или вызывающий код) не имеет возможности осуществлять те или иные проверки* и, следовательно, другая сторона просто обязана взять эту задачу на себя. Только в этом случае проблемы либо будут вообще ликвидированы в зародыше, либо будут гораздо более легко разрешимы в последствии — без привлечения руководства и без бессмысленного бодания между программистами. «Если пользователь не может в этом разобраться, ему вообще незачем использовать программу» Конечно, давайте сваливать все на тупого пользователя. Разве найдется более легкий и быстрый способ повысить удобство и качество вашей программы? Все эти проблемы интерфейса, человеческий фактор - чепуха и скука. В конце концов, мы ведь настоящие программисты] Будем писать только новый код! К черту ошибки и сбои, полный вперед! О'кей, я уже остыл. Я прошу прощения за то, что взобрался на стол и кричал. Но, честное слово, меня просто бесит от того, что вокруг так много разработчиков и целых программных компаний, которые забывают один фундаментальный закон нашей профессии: без пользователей мы — никто. Плохо продуманный, неудобный интерфейс пользователя (а я уверен, что все мы видим его практически на каждом шагу) - это очень плохо. Но есть еще худшая вещь ~ бесполезная система помощи в программе. Очень часто мы сталкиваемся с какой-то непонятной деталью в приложении лишь для того, чтобы нажать кнопку «Help» и прочитать подробную и красочную повесть о чем угодно, но только не о том, что произойдет, если мы изменим конкретную настройку в программе. «Видите? Работает!» Есть ли в процессе разработки программного обеспечения что-нибудь еще менее популярное, чем написание документации? Конечно же, есть: тестирование. Именно в этой всеобщей нелюбви к тестированию и кроются корни данной поговорки: иногда программисты используют минимальные, смехотворно недостаточные тесты, чтобы «доказать» себе и другим корректность своего кода и, следовательно, ненужность какой-либо дальнейшей работы над ним (включая тестирование). Как гото часто бывает с программерскими байками, эта является очень привлекательной, так как в теории она вполне правдоподобна: так же, как код бывает действительно самодокументнрованным, встречаются такие маленькие и простые подпрограммы или фрагменты кода, которые и в самом деле требуют лишь минимального тестирования, или даже не требуют никаких проверок во-
Конспект программистских небылиц обще. В таких случаях фраза «Смотри, работает!» уже является не байкой, а лишь констатацией очевидного. К сожалению, оспаривание этого заявления может оказаться очень затруднительным, поскольку такая дискуссия неизбежно превратится в простое испытание силы воли. Автор обсуждаемого кода утверждает, что минимальная проверка «доказывает» корректность кода, в то время как оппонент фактически вынужден претендовать на лучшее знание этого кода. И когда аутсайдер спрашивает о каком-то конкретном случае, автор обычно делает одно из двух: либо пускает в ход другую байку для отражения «наезда», либо (гораздо реже) признает существование проблемы, исправляет ошибку, а затем все начинается сначала — автор вновь провозглашает код корректным и не требующим дальнейшего тестирования («в нем была одна проблема, и я ее исправил»). Программисты есть программисты, и такая бесплодная дискуссия может сильно накалить обстановку в команде, особенно в период авралов в проекте, и тогда вопрос из технического перерастает в чисто политический или, если угодно, в социальный. Вашей целью должно быть распознавание и нейтрализация подобных проблем в зародыше, задолго до того, как их придется решать административными средствами. «Я просто помещу ненадежный код в DLL, чтобы потом было проще обновить программу, если что-то будет не так» Эта небылица вообще является ужасной глупостью. По двум причинам. Во- первых, структура продукта оказывается сформированной или измененной под воздействием фактора, совершенно не относящегося к делу. А во-вторых, как только вы определяете какой-то код как «ненадежный», тем самым вы признаете, что недостаточно компетентны в нем и не можете уверенно обращаться с ним (Я не хочу упрекать вас или других программистов за это; я сам оказывался в такой ситуации столько раз, что не возьмусь подсчитать — спасибо за это начальникам и клиентам с их постоянной манерой выдвигать в последнюю мнуту самые неожиданные новые требования. Когда же наступит то замечательное время, когда все непрограммисты в мире, наконец, поймут, что программирование является почти таким же специализированным и фрагмен- тпрованиым занятием, как медицина или машиностроение?) И когда такое признание сделано, вы быстренько решаете спихнуть этот «сомнительный» код в динамическую библиотеку в надежде, что потом вы сможете оперативно произвести ремонт, если что-то пе будет работать как положено. Если же вы будете до конца честны перед собой, то вы сами поймете, что вы просто- напросто прикрываете свою задницу вместо того, чтобы оформлять продукт должным образом. Лучше всего вообще не доводить дело до подобных публичных признаний - просто скажите вашему руководству, что вам необходимо еще немного времени на изучение некоторых аспектов API, третьесторонней библио-
592 Приложение в теки или другой детали проекта, которая вызывает трудности. Тогда вы сможете выполнить работу правильно, не вовлекая в это дело ни в чем не повинные динамические библиотеки. Во многих случаях такой подход находит понимание, и, в конечном итоге, в выигрыше окажутся все заинтересованные участники процесса.
JJjJlLAjJlulujJjT' БмВАёютска Win32n В этом приложении вы найдете начало того, что я рассчитываю превратить в долгий и довольно интересный эксперимент, в котором, я надеюсь, вы тоже примете участие. Представленная ниже библиотека Win32u является заготовкой оберточной библиотеки, главным предназначением которой является унификация поведения Win32 API на трех 32-разрядных платформах Windows (Win32s, Windows 95 и Windows NT). Кроме того, в нее включен ряд функций, решающих другие, менее глобальные задачи. Мои планы в отношении библиотеки Win32u широко открыты. Я имею самые серьезные намерения продолжить свои исследования проблем совместимости Win32 и соответствующим образом модифицировать и развивать эту оберточную библиотеку. Как минимум, к моменту выхода следующей редакции этой книги я должен буду опубликовать самую последнюю ее версию. Со временем я хотел бы сделать версии Win32u для ObjectPascal, MFC и VCL (например, создать класс uCDC, наследуемый от класса CDC из MFC, и сделать так, чтобы его методы вызывали оберточные функции из Win32u). Но стартовать я решил с «чистого» C++, поскольку именно такой вариант будет иметь наиболее широкую область применения. Разумеется, серьезный подход к подобному предприятию сразу же поднимает множество вопросов. Какие проблемы Win32 API должна охватывать эта библиотека? Насколько агрессивно должна она прятать различия между 32- разрядными платформами? Насколько близко должны оберточные функции имитировать лежащие в их основе исходные функции? Должны ли обертки использоваться только на этапе отладке, или их следует сделать постоянной составной частью приложения? И это только начало. Мой внутренний голос подсказывает, что все эти вопросы никак не охватить одной библиотекой. Вполне вероятно, что понадобится разбить Win32u на несколько независимых частей нечто вроде суб-библиотек — которые позволяли бы программистам выбирать лишь какие-то подмножества Win32 API для его унификации и дальнейшего использования. Например, я легко могу себе представить кого-то, кто пишет приложение, в основном ориентированное на графику и, следовательно,
594 Приложение с реально нуждается только в тех обертках, которые решают проблемы с 16/32- разрядными графическими координатами, а остальная часть Win32u не представляет для него практически никакого интереса. Как вы маЛете принять в этот участие Я действительно был бы очень рад услышать ваши отзывы об этом проекте. Пожалуйста, присылайте мне ваши комментарии и, в частности, делитесь вашим собственным опытом в области обнаружения и преодоления несовместимостей в Win32 API. (Мои электронный и обычный почтовые адреса указаны во вводной части книги.) Но, пожалуйста, имейте в виду, что мне понадобятся хотя бы минимальные подробности, если вы захотите сообщить мне о какой-нибудь проблеме в Win32. Не надо присылать простые однострочные письма, говорящие лишь что-то типа «Функция FredO не работает». Обязательно сообщите мне, на каких 32-разрядных платформах вы проводили тестирование и на каких из них проблема проявлялась; кроме того, если вы уже обращались в Microsoft с вопросами по этому поводу, расскажите мне, что они вам ответили. Разумеется, вовсе ни к чему писать «Войну pi мир». В большинстве случаев достаточно будет нескольких предложений. Если это для вас предпочтительнее, вы можете прислать мне исходный код (на C/C++ или Object Pascal) программы- примера и пояснить, в чем заключаются различия ее поведения на разных платформах. Этого будет более чем достаточно для того, чтобы ввести меня в курс дела. Что включена в Win32u Пожалуйста, учтите, что в данной книге на самом деле представлена лишь версия 0.01 библиотеки Win32u — на данном этапе она предназначена скорее для демонстрации концепции, для обозначения некоторой стартовой точки дальнейшей работы, но никак не для того, чтобы вы тут же пытались использовать этот код в своих реальных проектах. По электронной почте я обсуждал проблемы, связанные с Win32, со многими людьми, но пока еще не получил достаточно серьезных и детальных откликов по поводу моего первого приближения Win32u. (Меня немного пугает мысль о том, сколько почты я получу после того, когда эта книга попадет на полки, и читатели начнут посылать мне свои ужастики про Win32 и предложения внести те или иные изменения в Win32u...) Что касается конкретных деталей, затронутых в данной первой публичной версии Win32u, то основное внимание я уделил тем вещам, которые сама Mi-
библиотека Win32u crosoft официально признала проблемами. Кроме того, я включил в библиотеку решения наиболее важных из тех проблем, которые я обнаружил в результате своих собственных экспериментов или при помощи других коллег по профес: сии. (Плюс несколько патологических случаев, которые очень раздражали меня лично, и которые я был вынужден «залечить» немедленно.) Общая философия и noggepikka версий Windows Как я неоднократно повторял в дайной книге, одним из ключевых принципов в разработке программного обеспечения является стремление к балансу. Следуя этому правилу при написании Win32u, я не пытался отслеживать буквально все ошибки, возможные при вызовах оберточных функций, но я постарался сделать так, чтобы все общие патологические случаи обнаруживались и адекватно обрабатывались. Hanpmiep, эта версия библиотеки следит за такими вещами, как передача пулевого указателя, но не проверяет правильность различных дескрипторов (дескрипторов окон, контекстов устройств и т. д.). Конечно же, наиболее критичным вопросом при создании подобных оберточных интерфейсов является обеспечение совместимости с различными версиями Windows, включая те, которые еще не существуют. Поэтому я приложил немало усилий для того, чтобы функции в Win32u опирались на как можно меньшее число предположений и допущений о той системе, на которой они работают. Обертки для сообщений окон-списков Поскольку поддержка окон-списков все еще является 16-разрядной в Windows 95 и Win32s, это является причиной многочисленных проблем на этих платформах, самая серьезная из которых — «молчаливое» отсечение старших 16 бит у индексов строк. Я включил в Win32u набор оберток для LB-сообще- иий, каждая из которых проверяет заданные индексы строк па принадлежность диапазону 16-разрядных целых чисел. Если такая проверка дает положительный результат, то переданное значение индекса не будет обрезаться, и оберточные функции смело обращаются к соответствующему «голому» Win32 API. Если же проверка проходит неудачно, оберточные функции ничего не делают и возвращают признак ошибки. Обертки для GOI-фднкций Эта группа оберточных функций решает проблему, аналогичную ситуации с индексами строк в окнах-списках: передаваемые графические координаты проверяются на принадлежность диапазону 16-разрядных целых чисел. Естест-
596 Приложение с венно, я не делал оберток для тех графических функций, которые не работают с координатами или вообще не поддерживаются в Windows 95. Вспомогательные обертки и функции Я также включил в Win32u несколько других, вспомогательных функций Например, таких как обертка дтя GetShortPathNameO, которая призвана унифицировать поведение этой функции и использование ею расширенных кодов ошибок, видоизмененный вариант функции GetDlgltemlntO, который индицирует успех/неудачу не через параметр, а через возвращаемое значение; пара оберток для сообщения LB_SELITEMRANGEEX, которая освобождает вас от необходимости пользоваться дурацкими соглашениями для параметров этого сообщения. Проверка платформ и отладочный рыким Реализация проверок в обертках для окон-списков и графических функций имеет две интересные особенности, которые я хотел бы пояснить отдельно. Во-первых, проверка параметров может делаться либо с оглядкой, либо без оглядки на конкретную 32-разрядную платформу, на которой работает вызывающая программа. Эта возможность обеспечивается флагом DoPlatform- Check, который в свою очередь может быть доступен через функции GetPlat- formCheckO и SetPlatformCheckO. Если значение этого флага равно TRUE, то параметры проверяются только при работе под Windows 95 или Win32s; если же оно равно FALSE, то проверки делаются всегда. Вызывающая программа может изменять значение данного флага во время своего выполнения. Честно говоря, я не уверен, что вам по какой-либо причине захочется это делать, однако реализация этой возможности была настолько простой и недорогой, что я добавил ее. Разумеется, она может быть так же легко убргша из библиотеки. Вторая особенность — различия в поведении оберток в рабочем и отладочном режимах. Во время отладки, те подпрограммы, которые реально выполняют проверку параметров, будут использовать функцию OutputDebug- StringO для извещения вас о возникающих неполадках с параметрами. Вы можете изменить такой способ уведомления на запись в файл-журнал, на вызов функции MessageBoxO или еще на что-нибудь, но вашему вкусу. Листинг С.1. WIN32U.H #ifndef __win32u_h__ «define __win32u_h__ BOOL GetPlatforiT)Check(void), BOOL SetPlatformCheck(BOOL new_platfonn_check), // Обертки для LB-сообщений, проверяющие недопустимые значения индексов строк LRESULT LBDeleteString(HWND lb_handle,WPARAM index),
Библиотека Win32u 597 LRESULT LBFindStnng(HWND lb_handle, WPARAM index, LPCTSTR string), LRESULT LBFindStnngExact(HWND lb_handle,WPARAM index,lPCTSTR string), LRESULT LBGetItemData(HWND lb_handle,WPARAM index), LRESULT LBGetItemHeight(HWND lb_handle,WPARAM index), LRESULT LBGetItemRect(HWND lb_handle,WPARAM index,LPRECT rect), LRESULT LBGetSel(HWND lb_handle,WPARAM index), LRESULT LBGetSelIteins(HWND lb_handle,WPARAM index,LPINT buffer) LRESULT LBGetText(HWND lb.handle,WPARAM index,LPCTSTR buffer), LRESULT LBGetTextLen(HWND lb_handle,WPARAM index), LRESULT LBInsertStrmg(HWND lb_handle.WPARAM index,LPCTSTR buffer), LRESULT LBSelectString(HWND lb_handle,WPARAM index,LPCTSTR buffer), LRESULT LBSelItemRangeEx(HWND lb_handle.WPARAM wFirst.LPARAM wLast), LRESULT LBSetCaietIndex(HWND lb_handle,WPARAM index), LRESULT LBSetCurSeKHWND lb_handle,WPARAM index) LRESULT LBSetIteinData(HWND lb.handle.WPARAM index,LPARAM dwData), LRESULT LBSetItemHeight(HWND lbjiandle,WPARAM index,LPARAM cyltein), LRESULT LBSetTopIndex(HWND lb_handle,WPARAM index), // Обертки для графических функций, проверяющие недопустимые значения координат BOOL uArc(HDC hdc.int nLeftRect, mt nTopRect, mt nRightRect, int nBottoinRect, int nXStartArc,int nYStartArc,int nXEndArc.int nYEndArc), BOOL uBitBlt(HDC hdcDest.int nXDest.int nYDest.mt nWidth.mt nHeight, HDC hdcSrc.int nXSrc.mt nYSre. DWORD dwRop), BOOL uChord(HDC hdc int nLeftRect. mt nTopRect, mt nRightRect. int nBottoinRect, int nXRadiall,int nYRadiall,int nXRadial2.mt nYRadial2), HRGN uCreateEllipticRgn(mt nLeftRect, int nTopRect, mt nRightRect, mt nBottoinRect), HRGN uCreateEllipticRgnIndirect(CONST RECT *lprc), HRGN uCreatePolygonRgn(CONST POINT *lppt,int cPoints.int fnPolyFillMode), HRGN uCieatePolyPolygonRgn(CONST POINT *lppt,CONST INT *lpPolyCounts, int nCount,int fnPolyFillMode). HRGN uCieateRectRgn(int nLeftRect.int nTopRect int nRightRect,int nBottoinRect), HRGN uCreateRectRgnIndirect(CONST RECT *lprc), HRGN uCreateRoundRectRgn(int nLeft.int nTop,int nRight.int nBottom, mt nWidth, int nHeight), BOOL uDPtoLP(HDC hdc.LPPOINT lpPomts.int nCount), BOOL uEllipse(HDC hdcint nLeftRect, int nTopRect, mt nRightRect, int nBottoinRect), mt uExcludeClipRect(HDC hdcint nLeftRect, int nTopRect int nRightRect, int nBottoinRect) BOOL uExtFloodFill(HDC hdc, int nXStarUmt nYStart, COLORREF crColor, UINT fuFillType), BOOL uExtTextOut(HDC hdc.int X,int Y.UINT fuOptions.CONST RECT *lprc, LPCTSTR IpString,UINT cbCount,CONST INT * lpDx), BOOL uFloodFill(HDC hdcint nXStart.int nYStart, COLORREF crFill), BOOL uFiaineRgn(HDC hdc HRGN hrgn.HBRUSH hbr.int nWidth.int nHeight), COLORREF uGetPixeKHDC hdc.int nXPos.int nYPos), mt uIntersectClipRect(HDC hdcint nLeftRect. int nTopRect, int nRightRect, int nBottoinRect), BOOL uLineTo(HDC hdc.int nXEnd int nYEnd), BOOL uLPtoDP(HDC hdc.LPPOINT lpPomts.int nCount), BOOL uMaskBlt(HDC hdcDest.int nXDest int nYDest.mt nWidth, mt nHeight, HDC hdcSrc.int nXSrc.int nYSrc, HBITMAP hbinMack.int xMask.mt yMask, DWORD awRop) BOOL uMoveToEx(HDC hdc mt X.int Y.LPPOINT lpPoint) int uOffsetClipRgn(HDC hdcint nXOffset.int nYOffset), int uOffsetRgn(HRGN hrgn.int nXOffset.int nYOffset), BOOL LiOffsetViewportOrgEx(HDC hdcint nXOffset.int nYOffset, LPPOINT lpPoint), BOOL uOffsetWindowOrgEx(HDC hdc,int nXOffset.int nYOffset,LPPOINT lpPoint);
598 Приложение с BOOL uPatBlt(HDC hdcmt nXLeft.int nYLeft.mt nWidth.mt nHeight, DWORD dwRop), BOOL uPie(HDC hdc int nLeftRect.mt nTopRect.mt nRightRect int nBottomRect, int nXRadiall.int nYRadiall int nXRadial2,int nYRadial2), BOOL uPolyBezier(HDC hdc CONST POINT *lppt.DWORD cPoints), BOOL uPolyBezierTo(HDC hdc CONST POINT *lppt,DWORD cCount), BOOL uPolygon(HDC hdc,CONST POINT *lpPoints,int nCount), BOOL uPolyline(HDC hdc,CONST POINT *lppt,int cPointc) BOOL uPolylineTo(HDC hdc,CONST POINT *lppt,DWORD cCount), BOOL uPolyPolygon(HDC hdc,CONST POINT *lpPointc,LPINT IpPolyCountc,mt nCount), BOOL uPolyPolylme(HDC hdc,CONST POINT *lppt,CONST DWORD *lpdwPolyPoints, DWORD cCount), BOOL uPtInRegion(HRGN hrgn.int X,mt Y); BOOL uPtVicible(HDC hdc int X,int Y), BOOL uRectangle(HDC hdcmt nLeftRect, int nTopRect.mt nRightRect, mt nBottomRect), BOOL uRectInRegion(HRGN hrgn,CONST RECT *lprc), BOOL uRectVicible(HDC hdc,CONST RECT *lprc), BOOL uRoundRect(HDC hdc, int nLeftRect, int nTopRect.mt nRightRect, int nBottomRect, int nWidth.mt nHeight), BOOL uScaleViewportExtEx(HDC hdc,int Xnum,int Xdenom,mt Ynum.int Ydenom, LPSIZE IpSize), BOOL uScaleWindowExtEx(HDC hdc,int Xnum,int Xdenom,int Ynum.int Ydenom, LPSIZE IpSize), UINT uSetBoundsRect(HDC hdc,CONST RECT *lprcBoundG,UINT flags); BOOL uSetBrushOrgEx(HDC hdc,int nXOrg.int nYOrg,LPPOINT lppt), BOOL uSetPixelV(HDC hdcmt X. int Y.COLORREF crColor), BOOL uSetRectRgn(HRGN hrgn.int nLeftRect,int nTopRect int nRightRect. int nBottomRect), BOOL uSetViewportExtEx(HDC hdc int nXExtent.int nYExtent,LPSIZE IpSize), BOOL uSetViewportOrgEx(HDC hdcmt X.int Y, LPPOINT lpPoint), BOOL uSetWmdowExtEx(HDC hdcmt nXExtent.int nYExtent, LPSIZE IpSize), BOOL uSetWindowOrgEx(HDC hdcmt X.int Y, LPPOINT lpPoint), BOOl uStretchBlt(HDC hdcDect.mt nXOriginDeot.int nYOnginDest,int nWidthDest int nHeightDest HDC hdcSrc.int nXOriginSrc,int nYOnginSrc,int nWidthSrc, int nHeightSrc.DWORD awRop), BOOL uTextOut(HDC hdc, int nXStart.int nYStart, LPCTSTR IpString.int cbStnng), // Вспомогательные обертки и функции DWORD uEMGetLineCount(HWND editjiandle), BOOL uDirExiGtG(conot char *dn), BOOL uFileExiGtc(conGt char *fn); BOOL uGetDlgItemInt(HWND hDlg.mt nlDDlgltem. UINT* lpValue.BOOL bSigned), BOOL uGetPrivateProfileStnng(LPCTSTR lpAppName,LPCTSTR lpKeyName, LPCTSTR lpDefault.LPTSTR lpReturnedString,DWORD nSize,LPCTSTR lpFileName), DWORD uGetShortPathName(LPCTSTR long_path,LPTSTR buffer.DWORD buffer_len), BOOL uGetSysColor(int nlndex. DWORD *color), BOOL uLBDeGelItemRange(HWND lb, mt start, int end), BOOL uLBSelItemRange(HWND lb, int start, int end), #endif Листинг С.2. WIN32U.CPP /* Copyright Lou Grinzo 1995 Zen of WindowG 95 Programming */ #include <wmdows h> «include 'win32u h' //////////////////////////////////////////////////////////////////////////// // Нижеследующий флаг определяет, будут ли проверки индексов для окон-списков
Библиотека Win32u 599 // и графических координат чувствительны к платформе, на которой работает // программа. Если этот флаг установлен в FALSE, то проверки осуществляются // всегда, а если TRUE - то только при работе под Windows 95 или Win32s //////////////////////////////////////////////////////////////////////////// static BOOL DoPlatformCheck = FALSE, static DWORD Wmdows_version, static DWORD major_version static DWORD ininor_version static DWORD build_number static BOOL have_version_info = FALSE III/llllll/lllllllll/llIII III///////////II/lllllllllllllllllllll/llllll/ll/l // Выясняет текущую установку для проверки платформы //////////////////////////////////////////////////////////////////////////// BOOL GetPlatformCheck(void) { return DoPlatformCheck. ////!(/////////////////I!!!////(//Iff///lll/l//!////(/II/(/////////////I//// I/ Позволяет вызывающему коду задать условие чувствительности проверок к // платформе Возвращает старое значение этой установки IIII/1/III III IIllllllllIII/llllI//IIIIIIIIII//I/III/III/Illll/ll//Ill/Ill III BOOL SetPlatfonnCheck(BOOL new_platform_check) { BOOL temp = DoPlatformCheck, DoPlatformCheck = (new_platfonn_check '= FALSE), // Форсируем TRUE или FALSE return temp, )lI/I/II III/111/I IIllll/I III11llll/llIIII/llllllllllllIIIII/I/I/I/II/I///III /I Определяет версию и платформу Windows, используя Win32 API III/I III III III III III III/llllIII IIIlll/lIllllllllIIIIIIIIIII/1111/11/11//1111 static void GetVersionlnfo(void) { OSVERSIONINFO osvi; memset(&osvi 0. sizeof(OSVERSIONINFO)), osvi dwOSVercionlnfoSize = sizeof (OSVERSIONINFO)1 GetVersiontx(&osvi), Wmdows_version = osvi dwPlatformld, inajor_version = osvi.dwMajorVersion, minor_version = osvi dwMmorVersioa, build_number = osvi dwBuildNumber & OxFFFF )/ll//ll11////////////////////////////////////////////////////////////////// // Возвращает TRUE, если мы работаем под Windows NT В противном случае (мы // работаем под Windows 95 или Win32s) возвращает FALSE II III/Ill/Ill//ll/llll/llllllllllllllllllllllllllllllllllIII/ll/lll/lll/l/ll BOOL IsWmNT(void) if(■have_version_info) GetVercionlnfoO, return (Windows.version == VER_PLATF0RM_WIN32_NT), )lIllllllllII11/1IIIIIIIIIIIIIIlll/lI IIIIIIllllllllIIIII IIII IIII/l/ll/l/l/I/ I/ Возвращает TRUE, если мы работаем под Windows NT В противном случае (мы // работаем под Windows 95 или Win32s) возвращает FALSE ll/llll ll/l/ll III III/III//II1/1II l/l/l/l/ll/l/ll ПИП ЦИННИИ///llll 1/1 BOOL IsVer3510rLater(void) {
600 Приложение с if('have_version_info) GetVersionlnfoO, return ((inajor_vercion > 3) | | ((major_version == 3) && (ininor_vercion >- 51))), \ )/////////////I//////I///I////////I/////////////////////I//////I//////////// // BadLBIndex Возвращает TRUE, если заданный индекс для окна-списка непригоден // для использования, и FALSE в проти-вном случае // Другие подробности смотрите выше, в комментарии перед самым определением // флаговой переменной DoPlatfonnCheck //////////////////////////////////////////////////////////////////////////// BOOL BadLBIndex(WPARAM index) { if(DoPlatformCheck && IsWinNTO) return FALSE, else { #ifdef _DEBUG if(index > 32767) { OutputDebugString("\n*** BadLBIndex Значение индекса отвергается' ***\n\n"), return TRUE, i #endif return (index > 32767), )////////////I///I////1//////////1/////////1///////I/////I///I/I/11///////// // IcBadGDICoord Возвращает TRUE, если заданная координата не попадает в // диапазон 16-разрядных знаковых целых чисел //111/1/1/11/II/1//111/11 III/II//I/I/II/I/I//II/I//I/IIII/////I//II////HII/ BOOL IsBadGDICoord(int с) { return ((с > 32767) || (с < -32768)); i //////////////////////////////////////////////////////////////////////////// // BadGDICoordo Возвращает TRUE, если какая-либо из заданных графических // координат непригодна для использования, и FALSE в противном случае // Другие подробности смотрите выше, в комментарии перед самым определением // флаговой переменной DoPlatfonnCheck ////////а///////а/(/(/((((/к////а ((///(///а////////////а///////it I//// BOOL BadGDICoordsdnt c1. int c2, int сЗ = 0, int c4 = 0, int с5 = 0, int сб = 0) { if(DoPlatfonnCheck && IsWinNTO) return FALSE, elce { #ifdef .DEBUG if(IcBadGDICoord(c1) || IcBadGDICoord(c2) || IcBadGDICoord(c3) |'|) IcBadGDICoord(c4) || lGBadGDICoord(c5) || IcBadGDICoord(c6)) { OutputDebugString( AnBadGDICoordc. Значение координат(ы) отвергается'\n\n'), return TRUE, #endif return (iGBadGDICoord(d) || IcBadGDICoord(c2) || IsBadGDICoord(c3) || IcBadGDICoord(c4) || IoBadGDICoord(c5) || lGBadGDICoord(c6)),
Библиотека Win32u 601 )II/I III/III/III/II III//III//////////II/II/III I/1/II/III/III/IIIIIIIIIIIIIII iiii nil iiiiiiiiiiiiii/iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii шипи и II Обертки для LB-сообщений ////////I////////I//////////////I//////////////I/////////////////I////////// /////////////////////////////I//////////////I/////////////////////////////// //////f///i/l/////lll///l//l///////l////t///ll///////////////////t/tt//////l /I LBDeleteStnng Обертка для сообщения LB_DELETESTRING I lllll I//I/ll/llllllllllll/III/l/ll/lll/lll'll lllll llllllll/ll//I////////III/ LRESULT LBDeleteStnng(HWND lb_handle,WPARAM index) { if(BadLBIndex(mdex)) return LB_ERR, return SendMeccagedb.handle, LB_DELETESTRING, index, 0), i )lIIIIIIII/III/IIII11/ll/l/l/I Ill/IllIIIII/III III//II/IIIIIII/III/II/III/III /I LBFmdString Обертка для сообщения LB.FINDSTRING III IflllllIII III//III/llllll/I IIIlllllllllllllIII/lll/l/l/l/ll/llIII I/III/I/ LRESULT LBFmdStnng(HWND lb_handle, WPARAM index, LPCTSTR string) { if(BadLBIndex(index)) return LB_ERR; return SendMessage(lb_handle,LB_FINDSTRING, index, (LPARAM)Gtnng), } //////////////////////////////////////////////////////////////////////////// // LBFmdStnngExact- Обертка для сообщения LB_FINDSTRINGEXACT llllllIll/Ill/IIIII////IIII/II/IIII/IIIIIII/1F1/////I//IIII//IIIIIIIIIIIIIII LRESULT LBFmdStnngExact(HWND lb_handle,WPARAM index, LPCTSTR string) { if(BadLBIndex(index)) return LB_ERR, return SendMessage(lb_handle,LB_FINDSTRINGEXACT,index, (LPARAM)strmg), } llllll/Ill/Ill III III Milll/l/lIIII//IIIIIII///III/III////////(((((/////(//// II LBGetltemData Обертка для сообщения LB_GETITEMDATA I/1IIIIIII III III III III 11/1II I/1III III II/III IIIII I/IIIII///III I/II11//IIIIIII LRESULT LBGetItemData(HWND lb.handle,WPARAM index) { if(BadLBIndexdndex)) return LB_ERR, return SendMessage(lb_handle,LB_GETITEMDATA,index,0), 1. )llllllll/lllIIIIIII IIIllllllllll/lllll/llllllllll/lllllllllllllllllllllllll II LBGetltemHeight Обертка для сообщения LB_GETITEMHEIGHT lllllllllllllIII/llllllIIIlllll/III//ll/l/lllll/lll/llII/ll/l/lll/llIl/l/l/l LRESULT LBGetltemHeight(HWND lb_handle,WPARAM index) { if(BadLBIndex(index)) return LB_ERR, return SendMecsage(lb_handle,LB_GETITEMHEIGHT,index, 0), //////////////////////////////////////////////////////////////////////////// // LBGetltemRect Обертка для сообщения LB_GETITEMRECT IIllllllllllll/lIIIl/ll/llllllllllllllllirillllllll/lllllll/llllllllIIIlllll LRESULT LBGetltemRect(HWND lb_handle,WPARAM index,LPRECT rect) {
((хэрит)хэри1д-]рса)^т } (js^nq aiSlOdl'xapuT WVdVd/OlPucu, сц (]NMH)6utj}S139-[9S81 linS381 //////////////////////////////////////////////////////////////////////////// 9NiyiS10313S 81 ьинэ1л90оэ ы/tf c»±d9go 6uT.us:pa-[as81 // //////////////////////////////////////////////////////////////////////////// '(Jaiinq(wvaVd1)'xapur9NIdlSia3SNr81l9IPucLrqi)a6cGG8Wpu8S ujniaj аУЗ~81 ujniaj ((x9pui)x9puia-]pBa 99 (L-(aaOMO) =. хэрит))^т ,во>1Э1/ни tiuV i- эинэьсне qxwmadecd онав вэхэйийи wch // } (jaiinq dlSlOdl'xaput HVaVd/Olpucu. qi 0NMH)6uTJisiJaGuian nnS381 //////////////////////////////////////////////////////////////////////////// 9NiaiSia3SNI 81 винэШдооэ Bi/tf E»±d9go 6uTJisajasuiai // 11/11/ll/llllIII III/////////1/Ill/Ill/III/IU///I///II//IIIII/I/II/ll/ll/lII '(0'x3pui,N311X31130~81,3lpuBM"qi)96csG9Wpu9S ujni9J 'da3~81 u-iruaj ((X9pUT)X9pUiaipca)lI } (хэрит wvaVdlOTPUBifqi 0NMH)uaiix9ii90ai linS381 //////////////////////////////////////////////////////////////////////////// N311X31139 81 винэшдооэ but/ c»±d9go •иэ-цхэцэдап // lllllllllll/lllllllllllllllllllll/llllllllllll/lllllllllllllllllllllllllllll { '(J8iinq(wvaVd1)'x9pui'1X31133 81'Э1Ривц qi)96cGG9WPU9S ujni9J 'ааЗ~8"1 ujniaj ((xaDUT)yapuiaipc8)ii } (J9iinq dlSlOdl'хэрит WVHVd/OlpuEifqi aNMH)!X9ii9oai l"inS3d1 iiiiiniiiiiiiiiiii/iii/iiiiiiiiiiiiii//i////i/t ft/////////////////////ч in 1X31130 81 винэйдооэ виУ G»±d9Q0 }хэцээ81 // lllllllll/ll/lllllllllllllllllll/lllll/llllllll/lll/lll/lllllllll/ll/l/lllll '(ja^nq(wvdVdl)'хэрит'sW31I13S139_81'8lpuBM"qi)96cGG9Wpu9S turuaj 'ааз~81 ujn^9j ((xapuT)xapuiaipca)^i } (ja^nq iNIdl'хэрит WVdVdM'sipucM qi ONMI-OsuJainasiaoai linS3H1 //////////////////////////////////////////////////////////////////////////// SW31I13S139 81 винэШдооэ Bi/tf exidago cuiainasiagai // //////////////////////////////////////////////////////////////////////////// '(О 'хэрит '13S139~81 '9ipucL]"qx)96cGG9Wpu9S шгцэл •ааз~81 ujn^gj ((X9pUT)X9pUI31PBa)^I } (харит WVbVdM'9Ipucq-qi ONMH) l^S^oai linS3H1 III II III III III III III III III IIIIII fl/l/llj IIIIII III III/HI I III III III П11ППП 13S139 81 винэШдооэ Butt oudago Tas^oai // /////////////////I/I//I////////////////!/////////Ill//////////////////////// Uoaj(wVHVdl) хэрит •i038W31I139"81 aipucif qjjaBcccawpuas luruaj 'ad3"81 ujruaj ((x9puT)x9puiaipBa)JT Э эинэжоуис/ц гоэ
Библиотека Win32u 603 return SendMeGGage(lb_handle,LB_SELECTSTRING,index, (LPARAM)buffer), //////////////////////////////////////////////////////////////////////////// // LBSelltemRangeEx Обертка для сообщения LB_SELITEMRANGEEX //////////////////////////////////////////////////////////////////////////// LRESULT LBSelItemRangeEx(HWND lb.handle,WPARAM wFirct,LPARAM wLaGt) { if(BadLBIndex(wFirGt) || BadLBIndex(wLaGt)) return LB_ERR. return SendMeGGage(lb_handle,LB_SELITEMRANGEEX,wFirct,wLast), //////////////////////////////////////////////////////////////////////////// // LBSetCaretlndex Обертка для сообщения LB_SETCARETINDEX //////////////////////////////////////////////////////////////////////////// LRESULT LBSetCaretIndex(HWND lb_handle,WPARAM index) { if(BadLBIndex(mdex)) return LB_ERR, return SendMeGGage(lb_handle,LB_SETCARETINDEX,index,0), i //////////////////////////////////////////////////////////////////////////// // LBSetCurSel Обертка для сообщения LB.GETCURSEL //////////////////////////////////////////////////////////////////////////// LRESULT LBSetCurSel(HWND lb_handle,WPARAM index) { // Нам придется явно разрешить значение -1 для индекса1 if ((index '= (DWORD)-"!) && BadLBIndex(index)) return LB_ERR, return SendMeGGage(lb_handle,LB_SETCURSEL,index,0), //////////////////////////////////////////////////////////////////////////// // LBSetltemData Обертка для сообщения LB_SETITEMDATA //////////////////////////////////////////////////////////////////////////// LRESULT LBSetItemData(HWND lb_handle,WPARAM index,LPARAM dwData) { if(BadLBIndex(index)) return LB_ERR, return SendMeGGage(lb_handle,LB_SETITEMDATA, index,dwData), i //////////////////////////////////////////////////////////////////////////// // LBSetltemHeight Обертка для сообщения LB_SETITEMHEIGHT //////////////////////////////////////////////////////////////////////////// LRESULT LBSetltemHeight(HWND lb_handle,WPARAM index,LPARAM cyltem) { if(BadLBIndex(mdex)) return LB_ERR. return SendMeGGage(lb_handle,LB_SETITEMHEIGHT,index,cyltem), } //////////////////////////////////////////////////////////////////////////// // LBSetTopIndex Обертка для сообщения LB_SETCTOPINDEX //////////////////////////////////////////////////////////////////////////// LRESULT LBSetTopIndex(HWND lb_handle,WPARAM index) { if(BadLBIndex(index)) retuin LB_ERR return SendMeccage(lb_handle,LB_SETTOPINDEX, index, 0),
604 Приложение С 1/1//////I I/(///1//1/I/111/!///!//////II1/1!/!//1/1II//1/1/1 III//////((///// IIllllllllIII/III II//11/11llllIllllllllllll/I III I/III III/IIII/III II//IIII III I/ Обертки для графических функций //////////////////////////////////////////////////////////////////////////// //I//////(//////I I//////////II///!//////II///(/(//////(//1////((/1////////// IIllllllIII/I III/III III III/III Il/ll/lII III IIIl/llll/l/ll/III/I I/I III III/llll I/ uArc Обертка для функции Ate II III III IIIII III 11!III/1/1/I II/III IIIlir/l/Ill/Ill/III I//IIIllllllII III/I III BOOL uArc(HDC hdc.int nLeftRect.int nTopRect.int nRightRect.mt nBottomRect, mt nXStartArc.mt nYStartArc, mt nXEndArc.int nYEndArc) { if(BadGDICoordc(nLeftRect,nTopRect,nRightRect,nBottomRect) || BadGDICoordc(nXStartArc.nYStartArc,nXEndArc.nYEndArc) || (nLeftRect + nRightRect > 32767) || (nTopRect + nBottomRect > 32767)) return FALSE, return Arc(hdc,nLeftRect,nTopRect,nRightRect, nBottomRect, nXStartArc nYStartArc,nXEndArc.nYEndArc), IIllllllI III//1П11l/l/ll//II//II/fill/Ill/1/1/III/IIlll/l/lllllllI/l/l/lIII I/ uBitBlt Обертка для функции BitBlt llll//III/III/llllll/l/lll//!ll//l!l!l/!!l!ll!lll/ll/l/llllll!llll!l/!/ll/!/ BOOL uBitBlt(HDC hdcDeot.int nXDect.mt nYDect.int nWidth.int nHeight. HDC hdcSrc.mt nXSrcint nYSrc. DWORD dwRop) { if(BadGDICoords(nXDest,nYDest,nWidth,nHeight,nXSrc.nYSrc)) { SetLastError(ERROR_INVALID_PARAMETER), return NULL, return BitBlt(hdcDest,nXDeot,nYDect,nWidth,nHeight,hdcSrc, nXSrc, nYSrc,dwRop), )/II11II/I I III I/III II/llll/IllllllllIIII III IIIlllll/l/llIII I III II III IIIl/l/l /I uChord Обертка для функции Chord IIIl/llllllllIll/Ill II/IIIllllllllllIII/IIIll/llII///III II/III/lllllllllllII BOOL uChord(HDC hdc.mt nLeftRect, mt nTopRect.int nRightRect, mt nBottomRect, mt nXRadiall.mt nYRadiall, mt nXRadial2. mt nYRadial2) { if(BadGDICoordG(nLeftRect.nTopRect,nRightRect,nBottomRect) || BadGDICoordG(nXRadial1.nYRadiall.nXRadial2,nYRadial2) || (nLeftRect + nTopRect + nRightRect + nBottomRect > 32767) || (nLeftRect + nRightRect > 32767) || (nTopRect + nBottomRect > 32767)) { SetLactError(ERROR_INVALID_PARAMETER), return FALSE, return Chord(hdc,nLeftRect,nTopRect,nRightRect,nBottomRect,nXRadiall, nYRadiall.nXRadial2,nYRadial2). )ll/l/IIIII/ll/lll/l/lIII/IIII Ill/Ill(IllllllIlllllI/III/III III/I III IIIllll/ II uCreateEllipticRgn Обертка для функции CreateEllipticRgn III III I/lllll11!/I/1!11II11!IIllllllIII IIII III III!IIIllllII/III III IIIlllllII HRGN uCreateEllipticRgn(mt nLeftRect,int nTopRect.int nRightRect,int nBottomRect) { if(BadGDICoordc(nLeftRect,nTopRect,nRightRect,nBottomRect)) return NULL, return CreateEllipticRgn(nLeftRect,nTopRect,nRightRect,nBottomRect),
Библиотека Win32u 605 )l/II IIIII//I/I//I/IIIIUI/I///IIIIIII/IIIIII/Ill/IllllllIllll/lllllll/l/l/I /I uCreateEllipticRgnlndirect Обертка для функции CreateEllipticRgnlndirect //////////////////////////////////////////////////////////////////////////// HRGN uCreateEllipticRgnIndirect(CONST RECT *lprc) { if((lprc •= NULL) && BadGDICoordG(lprc->left,lprc->top,lprc->right, lprc->bottom)) return NULL // Замечание если lprc равен NULL, мы все-равно передаем его дальше1 retur n CreateEllipticRgnlndirect(lprc), //////////////У///////////////////////////////////////////////////////////// // uCreatePolygonRgn Обертка для функции CreatePolygonRgn II III III/l/l/llll/llllIII III IIll/lll/III III III Ill/Ill III III III III III II III III HRGN uCreatePolygonRgn(CONST POINT *lppt,int cPointG.int fnPolyFiUMode) { if(Ippt '= NULL) for(int 1=0, l < cPointc, i++) if(BadGDICoords(lppt[i] x,lppt[i] y)) return NULL, // Замечание если Ippt равен NULL, мы все равно передаем его дальше1 return CreatePolygonRgnQppt, cPoints, fnPolyFiUMode), i /Illll11III/IIIIIIIIIII III/I/I III11IIIIIIII/IIIIIIIII III//IIllll/l/ll/III III // uCreatePolyPolygonRgn Обертка для функции CreatePolyPolygonRgn //I////////////!I///It ft///////1//l/lIf(//I//1/!(//((/Iff///(//(((/(/((/(I// HRGN uCreatePolyPolygonRgn(CONST POINT *lppt,CONST INT *lpPolyCountG, int nCount.int fnPolyFiUMode) i \ ff Подсчитываем общее число точек в ломаных, а затем проверяем каждую // пару координат при помощи BadGDICoordG() lfdppt ' = NULL) { int total_pointc = 0, l, for(i = 0, l < nCount, i++) total_pointG += lpPolyCountcfi], for(i = 0. l < total_pointG, i++) if(BadGDICoordG(lppt[i] x,lppt[i] y)) return FALSE, // Замечание если Ippt равен NULL, мы все равно передаем его дальше1 return CreatePolyPolygonRgn(lppt,lpPolyCountc,nCount,fnPolyFiUMode), i )l/II I//III/III/I////I/IIII/II/I//III IIIII/111III//III II/IIIIIIII III IIIIIIII If uCreateRectRgn Обертка для функции CreateRectRgn /I I/I III/IIIIIIIIII III III11 III III III III III III I llll IIIIII/IIШI Ш/l III IIIII HRGN uCreateRectRgn(int nLeftRect,int nTopRect.int nRightRect,int nBottomRect) { if(BadGDICoordG(nLeftRect,nTopRect,nRightRect,nBottomRect)) return ERROR return CreateRectRgn(nLeftRect,nTopRect,nRightRect,nBottomRect), //////////////////////////////////////////////////////////////////////////// // uCreateRectRgnlndirect Обертка для функции CreateRectRgnlndirect //////////////////////////////////////////////////////////////////////////// HRGN uCreateRectRgnIndirect(CONST RECT *lprc)
606 Приложение с { if((lprc '= NULL) && BadGDICoordG(lprc->left, lprc-Пор lprc->right, lprc-'bottom)) retutn NULL, // Замечание если Iprc равен NULL, мы все равно передаем его дальше1 return CreateRectRgnlndu ect(lprc) I/ uCfeateRoundRectRgn Обертка для функции CreateRoundRectRgn //////////////////////////////////////////////////////////////////////////// HRGN uCreateRoundRectRgn(int nLeft.int nTop,int nRight.int nBottom, mt nWidth, mt nHeight) if(BadGDICoordc(nLeft,nTop.nRight,nBottom,nWidth nHeight)) return NULL, return CreateRoundRectRgn(nLeft,nTop.nRight.nBottom, nWidth, nHeight), )lI IIIlllllllllllllll/lIIlllllllllllllllllllllllllll/lllllllllllllllllllllll // uDPtoLP Обертка для функции DPtoLP I//IIII//IIIII///II/II//II//II/II//IIIIlilllllllll/lllllllll/llII/III Ill/Ill BOOL uDPtoLP(HDC hdc LPPOINT lpPomtc mt nCount) { lfdpPointG '■= NULL) for(int i=0, i ' nCount i++) if(BadGDICoordG(lpPointG[i] x lpPointo[i] y)) return ^ALSE // Замечание если lpPointo равен NULL, мы все равно передаем его дальше1 return DPtoLP(hdc lpPomtc,nCount). )//II/IIIII/I,'///IIIIII И///////////!////////1//////////////////////(!/(//// II uEllipce Обертка для функции Ellipse //////////////////////////////////////////////////////////////////////////// BOOL uEllipse(HDC hdc int nLeftRect, mt nTopRect.int nRightRect, int nBottoinRect) { if(BadGDICoordG(nLeftRect.nTopRect nRightRect, nBottoinRect) || (nLeftRect + nTopRect + nRightRect + nBottoinRect > 32767) || (nLeftRect + nRightRect > 32767) || (nTopRect + nBottoinRect > 32767)) SetLactError(ERROR_INVALID_PARAMETER), return FALSE, return Ellipce(hdc,nLeftRect.nTopRect,nRightRect,nBottoinRect), //////////////////////////////////////////////////////////////////////////// // uExcludeClipRect Обертка для функции ExcludeClipRect //////////////////////////////////////////////////////////////////////////// mt uExcludeClipfiect(HDC ndc.int nLeftRect int nTopRect,int nRightRect int nBottoinRect) { if(BadGDICoordG(nLeftRect,nTopRect nRightRect,nBottoinRect)) return ERROR, return ExcludeClipRect(hdc,nLeftRect.nTopRect,nRightRect,nBottoinRect), lllllIIIlll/l/If Illlll/lll/llllllllllllllllllll/l/Ill/Ill IIl/ll/ll/lllllllII II uExtFloodFill Обертка для функции ExtFloodFill III IIIlllllIII/II/I/III///IIIIIII/I//I/I/IIII/II/I/IIIIII/IIIIIII/II/I///I/I
Библиотека Win32u 607 BOOL uExtFloodFill(HDC hdc.mt nXStart.mt nYStart, COLORREF crColor.UINT fuFillType) if(BadGDICoordc(nXStart.nYStart)) SetLaGtError(ERROR_INVALID_PARAMETER), return FALSE, return ExtFloodFill(hdc,nXStart,nYStart, crColor, fuFillType), i /tltltllltllllllllllllllllllllllllllltllllllllllllllllllllllllllllllllltllll // uExtTextOut Обертка для функции ExtTextOut //////////////////////////////////////////////////////////////////////////// BOOL uExtTextOut(HDC hdc.int X,int Y.UINT fuOptiono,CONST RECT *lprc. LPCTSTR IpString,UINT cbCount CONST INT * IpDx) inBadGDICoords(X Y) | | ((Iprc •= NULL) && BadGDICoordo(lprc->left,lprc-^top. lprc->right. lprc->bottom))) SetLaGtError(ERROR_INVALID_PARAMETER), return FALSE, i // Замечание- если Iprc равен NULL, мы все равно передаем его дальше' return ExtTextOut(hdc,X,Y,fuOptiono,Iprc.IpString, cbCount, IpDx), llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll // uFloodFill Обертка для функции FloodFill //////////////////////////////////////////////////////////////////////////// BOOL uFloodFilKHDC hdcmt nXStart.mt nYStart, COLORREF crFill) { if(BadGDICoordG(nXStart,nYStart)) { SetLaGtError(ERROR_INVALID_PARAMETER), return FALSE. return FloodFill(hdc,nXStart,nYStart, crFill), llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll // uFrameRgn Обертка для функции FraineRgn ////У/////////////////////////////////////////////////////////////////////// BOOL uFraineRgn(HDC hdc HRGN hrgn.HBRUSH hbr.int nWidth.int nHeight) { if(BadGDICoordc(nWidth,nHeight)) return FALSE. return FrameRgn(hdc.hrgn,hbr,nWidth, nHeight), )///////////I/I//////////////I///I/I/l/l//////II///!/!//!/I/////////I/////// // uGetPixel Обертка для функции GetPixel II///////It//1 III III///II11/III/111/I II/IIIII11//I/I Il/llII/III/III/I II I/II/ COLORREF uGetPixel(HDC hdc.int nXPoc.mt nYPoG) i if(BadGDICoordo(nXPoG,nYPoG)) return CLR_INVALID, return GetPixeKhdc, nXPoG. nYPoo), )//II III IIllltltlilt Ill/It Ill/Ill lit/lllllllllltIII IIII till till IIll/llllllll // ulnteroectClipRect Обертка для функции InterGectClipRect
608 Приложение с //////////////////////////////////////////////////////////////////////////// mt uInterGectClipRect(HDC hdc.mt nLeftRect, mt nTopRect, mt nRightRect, int nBottoinRect) if(BadGDICoordG(nLeftRect,nTopRect,nRightRect,nBottoinRect)) return ERROR, return InterGectClipRect(hdc,nLeftRect,nTopRect,nRightRect,nBottoinRect), j. //////////////////////////////////////////////////////////////////////////// // uLineTo Обертка для функции LineTo //////////////////////////////////////////////////////////////////////////// BOOL uLineTo(HDC hdc.int nXEnd.int nYEnd) { if(BadGDICoordG(nXEnd,nYEnd)) return FALSE, return LineTo(hdc,nXEnd,nYEnd), V )/////////I//////II//I///////////I/I//////III/////////////////////////////// // uLPtoDP Обертка для функции LPtoDP //////////////////////////////////////////////////////////////////////////// BOOL uLPtoDP(HDC hdc.LPPOINT lpPomtG.int nCount) { lfdpPointc •= NULL) for(int 1=0, l < nCount. i++) if(BadGDICoordG(lpPomtG[i1 x lpPomtc[i] y)) return FALSE, // Замечание если IpPomtc равен NULL, мы все равно передаем его дальше1 return LPtoDP(hdc,lpPointG,nCount), llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll // uMackBlt Обертка для функции MackBlt //////////////////////////////////////////////////////////////////////////// BOOL uMaGkBlt(HDC hdcDeGt.int nXDeGt.int nYDeGt.int nWidth.int nHeight. HOC hdcSrc.int nXSrc.int nYSrc,HBITMAP hbinMask.int xMack.int yMaGk, DWORD dwRop) { if(BadGDICoordG(nXDeGt,nYDeGt,nWidth.nHeight,nXSrc,nYSrc) || BadGDICoordG(xMaGk yMack)) { SetLaGtError(ERROR_INVALID_PARAMETER), return FALSE, return MaGkBlt(hdcDeGt,nXDeGt,nYDeGt,nWidth,nHeight,hdcSrc.nXSrc nYSrc, hbmMack, xMask. yMaGk, dwRop), '//////////////////////////////////////////////////////////////////////////// // uMoveToEx Обертка для функции MoveToEx //////////////////////////////////////////////////////////////////////////// BOOL jMoveToEx(HDC hdc.int X.int V.LPPOINT lpPomt) { if(BadGDICoordG(X,Y)) return FALSE, return MoveToEx(hdc,X, Y, lpPoint), i )//IIIII I//II11/III//III/11//IIIIIII11/1II III/IIIII III II/I/III 11/I/I/I/III II II uOffcetClipRgn Обертка для функции OffcetClipRgn //////////////////////Ч///////1'HI'/////////'//////'1711'////71'//////'III'////////
Библиотека Win32u 609 int uOffsetClipRgn(HDC hdc.int nXOffset.int nYOffset) { if(BadGDICoordc(nXOffeet,nYOffset)) return ERROR, return OffsetClipRgn(hdc,nXOffcet, nYOffset), )/llllllll/ll/l/lllllllll/llllllll/lllllllllllllllIllll/llllllllll/lllllllll II uOffsetRgn Обертка для функции Off^etRgn /I III III II III/IIIIIIIIIIIIIIIII/II//////I/IIII III/IIIIIIllllllllll/II/Illll/ mt uOffGetRgn(HRGN hrgn.int nXOffset.int nYOffset) { if(BadGDICoords(nXOffset.nYOffset)) return ERROR, return OffGetRgn(hrgn,nXOffset,nYOffGet), i )lI III II I/1 III II11/III III III III I/III III IIIIIIIIIII/11 III IIllllI/I III IIIIIIII II uOffsetViewportOrgEx1 Обертка для функции OffsetViewportOrgEx III/I I/III/II/III IIIIIIIIIllllllllllIII III II/1 III//III IIIIllllIII//III//I/II BOOL uOffGetViewportOrgEx(HDC hdc.int nXOffset.int nYOffset,LPPOINT lpPoint) { if(BadGDICoords(n/Offset,nYOffset)) return FALSE, return OffsetViewportOrgEx(hdc,nXOffset, nYOffset.lpPoint), \ /IllllIIIIIIIIIIIIIIII//I Il/llllll/l/l/lIIlllllllllIIIIIllllllllll/III/Illll I/ uOffsetWindowOrgEx Обертка для функции OffsetWindowOrgEx II III/1/1IIllllllllll/ll//I////II//II/IIll/lllllIlllll/l/llll/ll/l/ll/I/IIII BOOL uOffsetWmdowOrgEx(HDC hdc.int nXOffset.int nYOffset,LPPOINT lpPoint) { if(BadGDICoords(nXOffset,nYOffset)) return FALSE, return OffsetWindowOrgEx(hdc,nXOffset,nYOffset,lpPoint); i )/lIII/I I//III II/I II/I I III I IIIIIIIIIIII III IIIIIIIIIII/IIIII III/II III 11 III III /I uPatBlt Обертка для функции PatBlt IIIII/IIIII/I/IIIIllllIIIIIII III 11lllllllllIIIII/IIIIIII I/II III/111II III/1II BOOL uPatBlt(HDC hdc.int nXLeft.int nYLeft.int nWidth.mt nHeight,DWORD dwPop) { if(BadGDICoords(nXLeft,nYLeft,nWidth, nHeight)) { SetLastError(ERROR_INVALID_PARAMETER), return FALSE, \ return PatBlt(hdc,nXLeft,nYLeft,nWidth,nHeight,dwRop), i )llllllllllllllll/lll/ll/lllll/lllllllllllllllll/lllllllllllllllllII/II/l/ll II uPie1 Обертка для функции Pie //////////////////////////////////////////////////////////////////////////// BOOL uPie(HDC hdc.int nLeftRect,int nTopRect.int nRightRect,int nBottoinRect, int nXRadiall.int nYRadiall.int nXRadial2,int nYRadial2) { if(BadGDICoords(nLeftRect,nTopRect.nRightRect.nBottomRect) || BadGDICoords(nXRadial1.nYRadiall.nXRadial2,nYRadial2) || (nLeftRect + nTopRect + nRightRect + nBottomRect > 32767)) SetLastError(ERROR_INVALID_PARAMETER), return FALSE;
610 Приложение с return Pie(hac,nLeftRect.nTopRect,nRightRect,nBottomRect, nXRadiall,nYRadiall,nXRadial2,nYRadial2), )///111//I/1//I/11/III/IIIllll/llI////////////////////////////////////////// // uPolyBezier Обертка для функции PolyBezier //////////////////////////////////////////////////////////////////////////// BOOL uPolyBezier(HDC hdc,CONST POINT *lppt,DWORD cPointc) { if(lppt ■= NULL) for(DWORD i=0, l < cPoints; i++) if(BadGDICoordc(lppt[i] x,lppt[i].y)) return FALSE, // Замечание если lppt равен NULL, мы все равно передаем его дальше1 return PolyBezier(hdc,lppt,cPointc), i llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll // uPolvBezierTo Обертка для функции PolyBezierTo ////'/'///////////////////////////////////////////////////////////////////// BOOL uPolyBezierTo(HDC hdc.CONST POINT *lppt,DWORD cCount) { if(lppt != NULL) for(DW0RD 1=0. l v cCount; i++) if(BadGDICoordG(lppt[i].x,lppt[i].y)) return FALSE; // Замечание если lppt равен NULL, мы все равно передаем его дальше! return PolyBezierTo(hdc.lppt,cCount); )/II//IIII I/111/III I/I I/I III/I/II III III IIIIIIIIIIIIIIIII/III III III IIIIII/I II // uPolygon Обертка для функции Polygon //////////////////////////////////////////////////////////////////////////// BOOL uPolygon(HDC hdc,CONST P0IN1 *lpPointc,int nCount) { if(lpPointc '= NULL) for(int 1=0; l < nCount, i++) if(BadGDICoordc(lpPomts[i].x,lpPoints[i].y)) i i SetLactError(ERROR_INVALID_PARAMETER); return FALSE, // Замечание если lpPomts равен NULL, мы все равно передаем его дальше1 return Polygon(hdc,lpPointc.nCount), )/1II////////////////////////////1IIIIIl/llII IIIlllll/IIIIIII/l/llI III/1/1II I/ uPolvline Обертка для функции Polyline //////////////////////////////////////////////////////////////////////////// BOOL uPolyline(HDC hdc,CONST POINT *lppt,int cPointc) { if(lppt '= NULL) for(int i=0, l < cPointc; i++) if(BadGDICoordc(lppt[i].x,lppt[i].y)) return NULL; // Замечание если lppt равен NULL, мы все равно передаем его дальше1 return Polyline(hdc,lppt,cPoints); )//////////////////I///IIl/l/ll/l/ll/llllllllllll/llll/lllllllllllllllllllll // uPolylineTo- Обертка для функции PolylineTo
Библиотека Win32u 611 /llllI Ill/Ill III Illl/ll/ll/ll/lll/l/ll//IIIIIIIIIII III IIII/I/I//II//III/I/II BOOL uPolylmeTo(HDC hdc, CONST POINT *lppt. DWORD cCount) { lfdppt '= NULL) for(DWORD l - 0, l cCount i++) if(BadGDICoordc(lppt[i] x,lppt[i] y)) retain NULL /7 Замечание, если Ippt равен NULL, мы все равно передаем его дальше1 return PolylineTo(hdc,Ippt,cCount) )lIII III/IIllll11IIIIIIIII III IIIII III IIIII III III11 IIIllllIII III III 11/11//III I/ uPolyPolygon: Обертка для функции PolyPolygon III IIIIIIIIIIllllIII III IIIIIIIIIIIIIIII IIIllllIIIII IIIIIIIIIIIIIIIII III/III I BOOL uPolyPolygon(HDC hdc,CONST POINT *lpPoints,LPINT lpPolyCountG,int nCount) { // Подсчитываем общее число точек в ломаных, а затем проверяем каждую // пару координат при помощи BadGDICoordG() lfdpPointc •= NULL) { int total_pointc = 0, i; for(i =0, l ' nCount, i++) total_pomtc += lpPolyCounts[i], . * , for(i =0; l - total_pointG, i++) t^ if(BadGDICoordo(lpPointG[i].x,lpPointG[i].y)) return FALSE, // Замечание, если IpPointc равен NULL, мы все равно передаем его дальше1 return PolyPolygon(hdc,IpPointc,lpPolyCountG,nCount), )l/II/llllI/III I/IIIIIIIIIIIII/III III II/II III/II/Ill/Ill I III///II/III III/III // uPolyPolyline Обертка для функции PolyPolyline /////У////////////////////////////////////////////////////////////////////// BOOL uPolyPolyline(HDC hdc,CONST POINT *lppt.CONST DWORD *lpdwPolyPointc, DWORD cCount) { // Подсчитываем общее число точек в ломаных, а затем проверяем каждую // пару координат при помощи BadGDICoordcO if(Ippt >= NULL) { DWORD total_pointc = 0, i; for(i = 0; l < cCount, i++) total_pointG += lpdwPolyPointc[i], for(i = 0; l < total_pomtG; i++) if(BadGDICoordG(lppt[i] x.lppt[i] y)) return FALSE, // Замечание если Ippt равен NULL, мы все равно передаем его дальше1 return PolyPolyline(hdc,Ippt,lpdwPolyPoints,cCount). i iii/i ii iiiiiiiii/i/iiiiiiii/iiiii/iiiiiiiiiiiiiiiiiiiiiiiimni/iiiiiiiiii II uPtlnRegion- Обертка для функции PtlnRegion /I/IIIl/ll/I IIIIIlllll/llllllll/l//II/IIIIIIIIII//1IIII/IIIllllllIIIII/1//I/ BOOL uPtInRegion(HRGN hrgn.mt X, mt Y) { if(BadGDICoordc(X.Y)) return FALSE, return PtInRegion(hrgn,X,Y),
612 Приложение С '//У///////////////////////////////////////////////////////////////////////// /I uPtVicible* Обертка для функции PtVioible //////////////////////////////////////////////////////////////////////////// BOOL uPtVisible(HDC hdcint X.int Y) { if(BadGDICoordc(X,Y)) return FALSE, return PtViGible(hdc,X,Y), //////////////////////////////////////////////////////////////////////////// // uRectangle Обертка для функции Rectangle //////////////////////////////////////////////////////////////////////////// BOOL uRectangle(HDC hdcint nLeftRect, int nTopRect.int nRightRect. mt nBottomRect) { if(BadGDICoords(nLeftRect.nTopRect,nRightRect.nBottomRect)) { SetLastError(ERROR_INVALID_PARAMETER); -eturn FALSE, return Rectangle(hdc,nLeftRect,nTopRect,nRightRect,nBottomRect). //////////////////////////////////////////////////////////////////////////// // uRectlnRegion: Обертка для функции RectlnRegion //////////////////////////////////////////////////////////////////////////// BOOL uRectInRegion(HRGN hrgn.CONST RECT *lprc) { if((lprc '= NULL) && BadGDICoords(lprc->left,lprc->top.lprc->right,lprc->bottom)) return NULL; // Замечание: если lprc равен NULL, мы все равно передаем его дальше1 return RectInRegion(hrgn.lprc), //////////////////////////////////////////////////////////////////////////// // uRectVioible: Обертка для функции RectVisible //////////////////////////////////////////////////////////////////////////// BOOL uRectVisible(HDC hdcCONST RECT *lprc) if((lprc '= NULL) && BadGDICoordG(lprc->left,lprc->top,lprc->right,lprc->bottom)) return NULL; // Замечание1 если lprc равен NULL, мы все равно передаем его дальше! return RectViGible(hdc,lprc): //////////////////////////////////////////////////////////////////////////// // uRoundRect Обертка для функции RoundRect //////////////////////////////////////////////////У///////////////////////// BOOL uRoundRect(HDC hdc.int nLeftRect,mt nTopRect.int nRightRect, int nBottomRect,int nWidth.int nHeight) { if(BadGDICoordG(nLeftRect,nTopRect,nRightRect.nBottomRect.nWidth,nHeight)) { SetLactError(ERROR_INVALID_PARAMETER); return FALSE, return RoundRect(hdc,nLeftRect,nTopRect,nRightRect,nBottomRect,nWidth, nHeight),
Библиотека Win32u 613 )ш///////////////////I////1 ft//////1////////////////////////1 III////////// // uScaleWmdowExtEx. Обертка для функции ScaleWIndowExtEx //////////////////////////////////////////////////////////////////////////// BOOL uScaleWindowExtEx(HDC hdc,int Xnum,int Xdenom,int Ynum,int Ydenom, LPSIZE IpSize) { if(BadGDICoords(Xnuin,Xdenoin, Ynum, Ydenom)) return FALSE; return ScaleWindowExtEx(hdc,Xnuin,Xdenom,Ynum, Ydenom, IpSize); )///////////tt//t//ttt//tll///////l//////l///t////////////////////////////I/ // uScaleViewportExtEx- Обертка для функции ScaleViewportExtEx //////////////////////////////////////////////////////////////////////////// BOOL uScaleViewportExtEx(HDC hdc,int Xnum, int Xdenom, int Ynum,int Ydenom, LPSIZE IpSize) { if(BadGDICoords(Xnum,Xdenom,Ynum,Ydenom)) return FALSE, return ScaleViewportExtEx(hdc,Xnum,Xdenom,Ynum,Ydenom,IpSize), } //////////////////////////////////////////////////////////////////////////// // uSetBoundsRect Обертка для функции SetBoundsRect //////////////////////////////////////////////////////////////////////////// UINT uSetBoundsRect(HDC hdc,CONST RECT *lprcBounds,UINT flags) { if((lprcBoundc ' = NULL) && BadGDICoordG(lprcBoundc->left,lprcBounds->top,lprcBound3->right, lprcBounds->bottom)) return 0; return SetBoundGRect(hdc.lprcBounds,flags), )/111/1///III I/I III I/111llllllIII/IIIIIIIIIII III II III III III III II/1/1/III/1II II uSetBrushOrgEx. Обертка для функции SetBrushOrgEx //////////////////////////////////////////////////////////////////////////// BOOL uSetBrushOrgEx(HDC hdc,int nXOrg.int nYOrg,LPPOINT lppt) { if(BadGDICoords(nXOrg,nYOrg)) { SetLastError(ERROR_INVALID_PARAMETER); return FALSE; return SetBrushOrgEx(hdc,nXOrg,nYOrg,lppt); )////////////I//////////////I/////////////////////////////////////////////// I/ uSetPixelV Обертка для функции SetPixelV //////////////////////////////////////////////////////////////////////////// BOOL uSetPixelV(HDC hdc,int X,mt Y.COLORREF crColor) { if(BadGDICoordc(X,Y)) { SetLactError(ERROR_INVALID_PARAMETER); return FALSE, return SetPixelV(hdc,X,Y,crColor), } ////////////////////////////////////////////////////////////////////////////
614 Приложение с // uSetRectRgn Обертка для функции SetRectRgn //////////////////////////////////////////////////////////////////////////// BOOL uSetRectRgn(HRGN hrgn.int nLeftRect, int nTopRect.mt nRightRect, int nBottomRect) { if(BadGDICoords(nLeftRect,nTopRect,nRightRect,nBottomRect)) return FALSE return SetRectRgn(hrgn.nLeftRect.nTopflect,nRightRect,nBottomRect), //////////////////////////////////////////////////////////////////////////// // uSetViewportExtEx Обертка для функции SetViewportExtEx III III 11IIIIIIII Ill/Ill III II I//IIllll/I III III Ill/Ill/1 III II/III III IIIIIIIIII BOOL uSetViewportExtEx(HDC hdc.int nXExtent,int nYExtent.LPSIZE IpSize) { if(BadGDICoordo(nXExtent,nYExtent)) return FALSE, return SetViewportExtEx(hdc,nXExtent,nYExtent,IpSize), //////////////////////////////////////////////////////////////////////////// // uSetViewportOrgEx Обертка для функции SetViewportOrgEx /l/llll/lll/lIIIl/llllllllIlllllllllllllllll/l/lllllllll/l////////////////// BOOL uSetViewportOrgEx(HDC hdc.int X.int Y, LPPOINT lpPoint) { if(BadGDICoordc(X,Y)) return FALSE; return SetViewportOrgEx(hdc,X,Y,lpPoint); } /Ill/Ill/llll/III/I III IIIIllllllllII III III////////////////////////////////// /I uSetWindowExtEx- Обертка для функции SetWindowExtEx //////////////////////////////////////////////////////////////////////////// BOOL uSetWindowExtEx(HDC hdc.int nXExtent,int nYExtent,LPSIZE IpSize) { if(BadGDICoordG(nXExtent.nYExtent)) return FALSE return SetWindowExtEx(hdc,nXExtent,nYExtent,IpSize), llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll // uSetWindowOrgEx Обертка для функции SetWindowOrgEx //////////////////////////////////////////////////////////////////////////// BOOL uSetWmdowOrgEx(HDC hdc.int X,int Y, LPPOINT lpPoint) { if(BadGDICoordG(X,Y)) return FALSE, return SetWindowOrgEx(hdc,X,Y,lpPoint), i //////////////////////////////////////////////////////////////////////////// /7 uStretchBlt Обертка для функции StretchBlt //////////////////////////////////////////////////////////////////////////// BOOL uStretchBlt(HDC hdcDest.mt nXOriginDest,int nYOriginDest, int nWidthDest, mt nHeightDest.HDC hdcSrc.int nXOnginSrc, int nYOrigmSrc, int nWidthSrc. mt nHeightSre,DWORD dwRop) { if(BadGDICoordc(nX0nginDest,nYOriginDest,nWidthDest, nHeightDest) 11 BadGDICoords(nXOriginSrc,nYOriginSrc,nWidthSrc,nHeightSrc)) {
Библиотека Win32u 615 SetLaGtError(ERROR_INVALID_PARAMETER), return FALSE; return StretchBlt(hdcDeGt, nXOngmDest, nYOriginDect, nWidthDeGt, nHeightDect, hdcSrc nXOriginSrc,nYOriginSrc,nWidthSrc,nHeightSrc.dwRop); )///////I/////////////////////////////////////////////////////////////////// // uTextOut Обертка для функции TextOut //////////////////////////////////////////////////////////////////////////// BOOL uTextOut(HDC hdcint nXStart.int nYStart,LPCTSTR lpStnng.int cbString) { if(BadGDICoordo(nXStart,nYStart)) { SetLastError(ERROR_INVALID_PARAMETER), return FALSE, return TextOut(hdc,nXStart.nYStart,lpString,cbStnng), )///////////I////////////////////I////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// // Вспомогательные обертки и другие функции //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// // uEMGetLineCount() Возвращает количество строк текста в многострочном поле /,/ редактирования Специально обрабатывает тот случай, когда сообщение // EM_GETLINECOUNT считает, что полностью пустое поле редактирования содержит // одну строку текста // // Возвращает количество строк текста в поле редактирования // // Проверка параметров: отсутствует //////////////////////////////////////////////////////////////////////////// DWORD uEMGetLineCount(HWND edit_handle) { if((linec = SendMecGage(edit_handle,EM_GETLINECOUNT, 0,0)) > 1) return lines, else { int Gtart_char = SendMeccage(edit_handle, EM_LINEINDEX,0,0). if(SendMeGGage(edit_handle,EM_LINELENGTH, Gtart_char,0) == 0) return 0 elGe return 1. i V /it ii i ii ii i in i/i/ii/ii ii i iiiii 11 iiiiiii nil/1 ii ii ii i/ii/i ii i in i и in//in // Унифицирующая обертка для функции GetShortPathName() Возвращает 0 и // устанавливает подходящий расширенный код ошибки, если заданное имя не // соответствует существующему файлу или каталогу В противном случае // возвращается результат вызова функции GetShortPathNaine() // // Проверка параметров // все нулевые указатели отвергаются
616 Приложение с //////////////////////////////////////////////////////////////////////////// DWORD uGetShortPathName(LPCTSTR long_path,LPTSTR buffer,DWORD buffer_len) { tfifdef .DEBUG if((long_path == NULL) || (buffer == NULL)) OutputDebugString('"\n*** uGetShortPathName: Обнаружен недопустимый параметр' ***\n\n ), #endif /7 Одновременно отлавливаются строки-нулевой длины и нулевой указатель if(uFileExists(long_path) || uDirExisto(long_path)) { SetLastError(O), return GetShortPathName(long_path,buffer,buffer_len); i else { if((long_path == NULL) || (strlen(long_path) == 0)) SetLastError(ERROR_BAD_PATHNAME), else SetLa3tError(ERROR_PATH_NOT_FOUND); return 0, i i /////////////////////////////////////////////////////////////////////////// /7 Возвращает TRUE, если заданное имя соответствует существующему файлу // (но не каталогу). В противном случае возвращает FALSE. // // Проверка параметров. // нулевой указатель и строка нулевой длины отвергаются //////////////////////////////////////////////////////////////////////////// BOOL uFileExists(const char *fn) { #ifdef _DEBUG if((fn == NULL) || (strlen(fn) == 0)) OutputDebugStringC \n*** uFileExists Обнаружен недопустимый параметр1 ***\n\n"); #endif if(fn == NULL M (strlen(fn)) == 0) return FALSE; DWORD dwFA = GetFileAttnbuteG(fn); if(dwFA == OxFFFFFFFF) return FALSE; else return ((dwFA & FILE_ATTRIBUTE_DIRECTORY) '= FILE_ATTRIBUTE_DIRECTORY), v /////////////////////////////////////////////////////////////////////////// // Возвращает TRUE, если заданное имя соответствует существующему каталогу // В противном случае возвращает FALSE // // Проверка параметров // нулевой указатель и строка нулевой длины отвергаются //////////////////////////////////////////////////////////////////////////// BOOL uDirExists(const char *dn) { #ifdef .DEBUG if((dn == NULL) || (strlen(dn) == 0)) OutputDebugString("\n*** DirExists: Обнаружен недопустимый параметр! ***\n\n"); #endif if(dn == NULL || (strlen(dn)) == 0)
iii и ii 1111 iii 111 iii и i и/и til iiu/i пи/1 ii/iiui nui i и i m/i i in и и/// эиэфс!эл.ни сморитм tsutr И1чныяаис1и и инндоЯЛ aauog всПк^ваиьэиоэдо // '()}uiuia}l6iQ}99 ииПхнЛф ы/tr B»±dago ввняивнинии •( )iuiuia}i6xo}a9n // ///////////////////I/////////////////////////////////////I////////////////// •(НаЗ~8П =i (pu9 4Jcac'X339NVdW31I13S"81'QI)36cGcawpu9S) ujn^gj duiaq. = pua 'риэ = q.jcq.c 'ipcas = diiia} }ui } (риЭ < }IC1C)^T •BMHC±0Ud808 a»tfBdOU // g Boqieactradau инжиой" нэяэАни 'dogna оииаонгиэЛ X339NVUW3iI13S"81 // эинэйдооэ iqgoiK 'ojoj. Bi/tf •аоояэй'ни »otfBdou ипнчииавйи иэсаиьэиээдо // 'УУЗ~81 ujfuaj ((риэ)хэри1дпрва II (iJCic)x8puiaipea)^i i (риэ }ит '}jb}c }ut 'qx aNMH)96ucyui9HX9sain 1008 //////////////////////////////////////////////////////////////////////////// BMHcegdgo сионжомеоа xawtfadu ен BOioiBdaaodu мояэЯни // .aod±9wcdcu ENdaaodu // // •аоо»э*Гни »otfBdou qj_Eaodm_>iaddc»i // илэомиИохдоэн ю BEtooiEtftKogoaoo и BMHEeadgo ю иояэйни BEtoUBHBdxotfadu // 'X339NVyW31I13S~8"l винэшдооэ «irt/ Bx±dago всньиюеь ()96ucyui9an9S81n // 11//in///////mi/i/1/i/1//i/mm/////////////1///ii/it/mm mi mm •(УУЗ~81 =. (pu9UJt?ac'X339Mvyw31I13S"8"i'qT)96BGGawpu9S) turuaj 'duiaa = pua lpua = ijcic Iijeis s duiai iut } (pua > }JEac)iT •винсаядЛ a»tfBdou // a Boq±E3Etf9d9u инжиоУ пэяэИни 'dogna oi/инэуш) X339NVyW31I13S~81 // эинэйдооэ wgo±h 'ojoi bis\j -доэяэИни »ot/Bdou ипняииавЬи иэваиьэиээдо // •УУЗ"81 ujnaai ((pua)xapuiaipeg || (iJBic)xapuigipca):iT } (pua аит 'ijeic iut 'qy aNMH)96ucyuia^ixacaaain Ю08 //////////////////////////////////////////////////////////////////////////// винвеэйдо о-юнжоиеоа ±awVadu вн BoxoiBdaaodu яояэгГни // •aodiawBdBu Bxdaaodu // // аоэмэтГни *otfBdou q±Eaodm_>iaddo>i // и±ооми1Гохдоэн ю ввйавИжодоаэо и BHHBeadgo ю иэяэг/ни BBtooiBHEdxotfadu // 'X339NVyW31I~13S~81 винэйдооо Bi/tf Exidago ввньиховь •()9бивушэ;1хэсэаа1п // /////////////////////////////////////////////////////////////////////////// '(Ad0i03dia"3inaiyiiv"3iid == (Ayoi03yia~3inaidiiv~3~iid * vd^p)) ujnaaj эсхэ '3S1V3 u-iruaj (dddddddd*0 == VdMP)^i '(up)caanqu5.;v9lidl90 = vd^p ОУОМО :3S1Vd tuniaj Z19 nZ€u!M еиэюиудид
618 Приложение с BOOL uGetDlgItemInt(HWND hDlg.int nIDDlgltein, UINT* lpValue.BOOL bSigned) { BOOL re, DWORD value, value = GetDlgIteinInt(hDlg,nIDDlgItein,&rc,bSigned); if(rc) { «lpValue = value return TRUE. else return FALSE, //////////////////////////////////////////////////////////////////////////// // uGetSycColor() Минимальная обертка для функции GetDlgltemlnt(), ,// отсеивающая коды цветов, не поддерживаемые под Win32c // // Возвращает TRUE, если код был допустим и соответствующий цвет был успешно // определен. В противном случае возвращает FALSE. // // Обратите внимание на зависимость от версии' Эта функция предполагает, что // Win32s не будет иметь номер версии, превышающий 3 51, до тех пор, пока она // не поддержит новые коды цветов (специфичные только для Windows 95) Кроме // того, она предполагает, что самое большое возможное значение кода (согласно // определениям из WINUSER Н) равно C0L0R.INF0BK (24) под Windows 95 и // COLOR.BTNHIGHLIGHT (20) - в предшественницах Windows 95. //////////////////////////////////////////////////////////////////////////// BOOL uGetSysColor(int nlndex.DWORD *color) { if(((IsVer3510rLater()) && ((nlndex < 0) || (nlndex > C0L0R_INFOBK)) ) || (■IsVer3510rLater()) && ((nlndex < 0) || (nlndex > COLOR_BTNHIGHLIGHT))) return FALSE, else { ♦color = GetSysColor(nlndex) return TRUE. //////////////////////////////////////////////////////////////////////////// /7 uGetPrivateProfileStiing() Обертка для функции GetPrivateProfileStringO, // устраняющая опасность недвусмысленной индикации успеха/неудачи /7 // Возвращает TRUE если задача выполнена полностью, в противном случае // возвращает FALSE //////////////////////////////////////////////////////////////////////////// BOOL uGetPrivateProfileStnng(LPCTSTR lpAppName, LPCTSTR lpKeyNaine, LPCTSTR lpDefault.LPTSTR lpReturnedStnng DWORD nSize,LPCTSTR lpFileName) { // Сначала пытается вызвать исходный API с заданными параметрами. . DWORD resultl = GetPrivatePtofileStringdpAppName,lpKeyName,lpDefault, lpReturnedStnng, nSize, lpFileName), // Если мы столкнулись с одним из двусмысленных результатов, придется // попробовать снова (теперь уже с локальным буфером) и выяснить, получили // ли мы строку целиком Подробности об условиях nSize -1 и nSize - 2 см. // в описании GetPrivateProfileStringO if((((lpAppName == NULL) || (lpKeyName == NULL)) && (resultl == nSize - 2)) ||
Библиотека Win32u 619 (((lpAppName '= NULL) && (lpKeyName •= NULL)) && (resultl == nSize - 1))) { char *local_buffer = (char *)malloc(nSize + 1); if(local_buffer == NULL) return FALSE, // Повторная попытка используем наш собственный буфер и его длину, // остальные параметры без изменения DWORD recult2 = GetPrivateProfileStnng(lpAppName, lpKeyName,IpDefault, local_buffer.nSize + 1,lpFileName), free(local_buffer); // В любом случае буфер нам больше не нужен // Если получаем тот же результат, значит тревога была ложной if(recult2 == resultl) return TRUE, else // Если результат отличается, то оригинальный буфер и в самом деле // оказался слишком коротким return FALSE, // Повезло с первого раза, просто возвращаем TRUE return TRUE,
ЭййВВАОГ Youth, what man's age is like to doth show, We may our ends by our beginnings know. Сэр Джон Деихсм I have seen the juture, and it works. Линкольн Стсффс пс- Мир Windows многолик. Теперь для нас с вами пришло время остановиться и оглянуться вокруг. Пора посмотреть, чем жил этот мир, пока я работал над книгой. У меня есть две причины сделать именно такой завершающий шаг. Первая из них, самая простая и очевидная, состоит в том, что перечисленные ниже факты и даты наверняка развлекут некоторых читателей. Ведь все мы несемся в будущее уже на 133 мегагерцах, и сегодняшние сенсационные новости уже через год-другой превратятся в милые, смешные и причудливые воспоминания. Вторая причина — воспользоваться последней возможностью и еще раз обратить ваше внимание на то, что работа над этой книгой все еще продолжается, и что я хотел бы услышать ваши отзывы о ней. Я пишу эти строки в конце октября 1995 года и уже строю планы насчет второго издания и других проектов. Поэтому не оставайтесь в стороне, о'кей? Ну, а теперь обратимся к тем самым новостям, которые вскоре станут забавными историческими фактами: В IBM покупает Lotus за 3,6 млрд. долларов и позже объявляет, что их программные продукты для настольных систем будут продаваться под именем Lotus. В Продажи Delphi от Borland и интерес разработчиков к этому продукту очень велики. Я получаю по электронной почте многочисленные письма с просьбами дать совет по выбору языка программирования. Многие из этих писем приходят от людей, желающих переключиться с С/ С-н- на что-нибудь другое, и при этом Delphi упоминается в качестве первейшего кандидата. В 14 июля 1995 года: Windows 95 «позолочена» и передана на конвейер, который должен изготовить миллионы гибких дисков и CD к 24 августа. К этому времени Департамент Юстиции США, предположительно, все еще будет изучать вопрос о легальности включения в Windows 95 программного обеспечения для доступа к The Microsoft Network. По всеобщему мнению, Microsoft сильно рискует, приступая к производству дистрибутивов, не дожидаясь решения этого вопроса; если Департамент Юстиции вынесет отрицательный вердикт, Редмонд в один
622 Эпилог миг окажется затоваренным крошечными летающими тарелочками ц подставками для посуды. в 26 июля 1995 года: Borland преподносит всей общественности (в особенности инвесторам) немалый сюрприз, объявляя о получении прибыли в 2.8 млн. долларов, частично благодаря продаже 125 000 копий Delphi в течение первого полного квартала существования этого продукта. В Перед самым выходом в свет Windows 95, Tiger Software выпускает каталог (том V, выпуск 10), в котором говорится буквально следующее: «В отличие от всех остальных операционных систем, Windows 95 действительно позволяет вам выполнять несколько разных задач одновременно -- не ожидая, пока каждая из них закончится». Это заявление становится ошеломляющим открытием для миллионов людей (включая вашего покорного слугу), которые годами работали в многозадачном режиме под Windows 3.1, Windows NT, OS/2, Unix и на мэйнфреймах. По-видимому, до прихода Windows 95 все мы делали что-то невозможное, нарушая все физические законы. Надо же! В 24 августа 1995 года: начались всеобщие продажи Windows 95. В Intel объявляет, что следующий представитель чипов семейства Pentium (до этого момента известный под кодовым названием Р6) будет носить название Pentium Pro. Судя по всему, все нынешние владельцы старых обыкновенных процессоров Pentium в действительности приобрели Pentium Amateur1. В Становится общеизвестным тот факт, что скорость выполнения процессором Р6 (он же Pentium Pro) 16-разрядного кода будет намного ниже, чем скорость выполнения 32-разрядного кода. В качестве основных причин указываются перезагрузки сегментов и частичные регистровые операции, которые свойственны 16-разрядному коду и болезненны для сильно «трубопроводной» архитектуры нового процессора. В 11 сентября 1995 года: InfoWorld цитирует слова Джима Оллчина (вице-президента отдела Microsoft, ответственного за Windows NT): «Слияния Windows NT и Windows 95 не произойдет, поскольку нельзя одним продуктом удовлетворить требования каждого клиента». Там же опубликована следующая цитата: «Мы ожидаем расширения использования Windows 95 и ее преемников на потребительском рынке. А перспектива NT — превратиться в стандарт настольной и серверной 1. Буквальный перевод «Pentium Pro» мог вы звучать как «Pentium для профессионалов»; а «Pentium Amateur» — соответственно, как «Pentium для любителем'!» [Примечание переводчики ]
Эпилог 623 системы для корпораций». До этого объявления предполагалось, что Microsoft планирует объединить Windows 95 и Windows NT в единый продукт (спустя одну-две версии, следующие за Windows 95). Если же Microsoft действительно будет придерживаться нового плана, то это значит, что мы только что стали свидетелями еще одного серьезного изменения в мире Windows. Что ж, нам уже не привыкать к подобным поворотам. В В конце сентября 1995 года Верховный Суд США соглашается принять к рассмотрению спор между Lotus и Borland по вопросам пользовательского интерфейса. Вне зависимости от исхода этой тяжбы, решение суда скорее всего будет иметь весьма серьезные последствия для всей программной индустрии.
Алфавитный указатель 2000 год, реакция программ 75 32-разрядная архитектура Windows 95 378 DoLockUnlockO 366 DragAcceptFilesO 241 AddPlatformO 206 AliasToLFNO 132, AiigleArcO 502 AppendSlashO 120, Arc() 502 ASSERTO 182 546 Barney() 49 BeginPathO 502 BringWindowToTopO 250 BuildFNQ 307 CalcMeanO 320 ChordO 502 chunks 69 ComplainlfNotO 206 CopyFileO 129 CopyFileForceRWO 129, 329 CRC 266 CWnd OnCtlColorO 386 a DirExistsO 130 DoGSPNO 489 DoItO 206 EllipseO 502 EXCEL5.INI 288 ExtTextOutO 502 JfdopenO 396 JfsplitpathO 480 FAT 69 FAT, раз 69 fileExecutableO 413 FileExistsO 81, 82, 130 FindCRCO 275 FindFirstFileO 163 FormatMessageO 338, 348 FredO 81, 594 FredExO 81 FreeLibraryO 213 a GetBaseDirectoryO 306 GetClientRectO 162 GetCommandLineO 435 GetDlgltemlntO 56, 352 GetFreeSystemResourcesO 142, 417 GetFullPathNameO 134 GetLastErrorO 54, 111, 482 GetLongPathNameO 163 GetModuleFileNameO 275 GetOpenFileNameO 81, 101, 411 GetPrivateProfileStringO 288, ,.41
626 Алфавитный указатель GetShortPathNameO 82, 111, 163, 32: 486 GetSysColorO 345, 505 GetVersionO 229 GetWindowsDirectoryO 307 GlobalMemoryStatusO 417 GoodCRCO 152, 274, 275 GotHelloO 231 I INI-файлы 288 InitApplicationO 242 InitlnstanceO 515 InvalidateRectO 250 IsChildO 118 IsGDIObjectO 408 IsIconicO 118 IsOurFileO 307 IssueWarningO 320 IsWindowsO 118 £ JcreatO 396 JopenO 163, 396 lfnDeleteFileO 395 LFNToAliasO 132, 447 LineToO 502 LoadConfigDataO 306 LoadHelloO 230 LoadLibraryO 54, 209 LoadModuleO 227 LooksLikeCISIDO 118, 135 ipszFileSpec 459 м memsetO 53 MessageBoxO 308 MFC, выкрутасы 260 MoveFileExO 510 MoveToExO 502 MSDOS.SYS 290 N NORMAL DOT 285 О OnCreateO 366 OnDropFilesO 249 OnlnitDialogO 500 OnLockUnlockO 366 OnSelfCheckO 274 OnSysCommandO 251 OutputDebugStringO 114, 306 OWL 186 P PageMaker 5 0, справка 56 PieO 502 PolyBezierO 502 PolyBezierToO 502 PolyDrawO 460, 502 PolygonO 502 PolylineO 502 PolylineToO 502 PolyPolygonO 502 PolyPolylineO 502 PrependCurrentDirO 134 Q QTJThunkO 402 II ReadFileO 116 RectangleO 409, 502 RedrawWindowO 250 RegisterClassO 399, 410 registry 291 RegQueryValueExO 68, 348, 493 RetrieveLockedStateO 364 RoundRectO 502 rtPathNameO 489 S SaveConfigDataO 306 SaveLockedStateO 364 SayHelloO 213 SendMessageO 478 SetFocusO 250
Алфавитный указатель 627 SetForeGroundWindow() 415 SetForegroundWindowO 249, 250 SetLastErrorO 327 SetWindowLongO 398 SetWindowPosO 250 SetWindowsPosO 398 sizeofO 340 SomeWin32API() 350 strcmpO 335 stripLeadingO 121 stripLTO 121 stripTrailingO 121 strlcatO 120 strnepyO 123 Г TextOutO 502 thunk 400 TRACEO 182 u uGetDlgltemlntO 327 LiGetPrivateProfileStringO 343 uGetShortPathName( 82 LiGetShortPathNameO 82, 324 uLBDeselltemRangeO 339 uLBSelltemRangeO 339 UnloadHelloO 231 UpdateCRCO 281 UpdatcWindowO 250 w Winl61ock 379 Winl6mutex 380 Win32 API, документация 352 WinMainO 358 WNetGetUniversalNameO 494 Word for Windows, настройки 285 д абстрагирование, три степени 326 Б баланс 72 быстрая разработка приложений 107 в вандализм 263 вежливость 88 г гизмонавты 147 А данные защита 356 постоянное хранение 284 диалог загрузка перечня слов 61 кнопка Help 55 длинные имена, в 16-разрядных программах 395 документирование 80 з защита данных 356 программ 265 значки 239 И идиома локальной функции 201 имена файлов, длинные 375 имя файла, локальное 430 инструмент программирования 148 рекомендации по выбору 150 к кластер, размер в FAT 69 код, тестирование 97 А локальная функция, идиома 201
628 Алфавитный указатель локальные функции 195 м макропроблемы 47 микропроблемы 105 минимизированные программы, разновидности 240 многопоточность 376 И нео-луддиты 147 неопределенное поведение 52 о обертка 326 оборонительное программирование 109 ООП 152 ошибки процессора 37 пользователей 37 0 Пасхальное яйцо Windows 95 87, 89 перенос кода 387 стратегия 388 переходник 400 поставщики 156 приватные программы 33 придирки 57 Проводник, перетаскивание файлов 61 производительность 92 публичность 34 публичные программы 34 р разумность 53 реестр Windows 291 рекомендации макроуровневые 63 микроуровневые 106 С самодокументированный код 82 самопроверка, реализация в программе 276 ел оптимизация 78 системный реестр 291 стиль программирования 47 стоимость проекта, оценка 74 т типы, изменение размеров 385 у услужливость 55 Ф файлы данных 284 умные 138 бинарные 293 3 Этика 35
I v 1^^Ш_УКШЗВ-flBEiMilifV- 'I Чтобы мы знали, как сообщить Вам о новых книгах нашего издательства, заполните, пожалуйста, анкету и вышлите ее по адресу: 190031, Санкт-Петербург, а/я 418 Вы можете запросить анкету и в электронной форме. Для этого пошлите письмо по адресу news@symbol.ru, содержащее строчку: GET AN КЕТА. Итак, Вы купили и прочитали книгу Философия программирования для Windows 95/NT 1. Как бы Вы оценили общее качество этой книги: G Отлично G Очень хорошо □ Хорошо G Посредственно Q Плохо 2. Что Вам понравилось в этой книге (укажите все, что считаете нужным): G Формат Q Стиль изложения Q Примеры G Оглавление Q Предметный указатель □ Цена □ Иллюстрации G Шрифтовое оформление G Обложка G Глубина описания G Пометки на полях 3. Что Вам не понравилось в этой книге G Формат G Стиль изложения Gl Примеры G Оглавление Q Предметный указатель G Цена G Иллюстрации G Шрифтовое оформление 3 Обложка G Глубина описания G Заметки на полях 4. Где Вы купили эту книгу: Q В обычном книжном магазине Q В магазине тех. литературы □ ъ лотка G Другое 5. Почему вы купили эту книгу: Q По рекомендации друзей Q По рекомендации продавца Q Видел рекламу в G Увидел рекламу в магазине G Из-за репутации автора Q Из-за репутации издательства Q Читал обзор книги в G Другое 6. Вы купили эту книгу: G За свой счет Q За счет фирмы Q По цене G Получили эту книгу в подарок 7. Каков Ваш уровень знакомства с предметом, описанным в данной книге: G Начальный Средний G Высокий 8. Как давно Вы работаете на компьютере: G лет G месяцев
9. Ваше образование: Q Среднее Q Незаконченное высшее Q Высшее 10. Где Вы больше всего пользуетесь компьютером: Q Дома Q На работе Q И там, и там □ Другое 11. Какое компьютерное оборудование у Вас имеется: Q IBM PC совместимый компьютер □ Лаптоп (ноутбук) □ CD ROM □ Звуковая карта (Sound Blaster) Ul Факс-модем Q Модем Q Сканер Q Лазерный принтер □ Струйный цветной принтер 12. Какими прикладными программами Вы обычно пользуетесь (укажите названия): □ Бухгалтерия Q Базы данных Q Сетевое ПО Q Издательские системы □ Электронные таблицы Q Системы проектирования Q Игры □ Текстовые процессоры Q Телекоммуникационное ПО Q Банковские системы 16. Есть ли у Вас какие-либо дополнительные комментарии относительно данной книги или книг издательства «Символ-Плюс» (Вы можете изложить их в произвольной форме): 17. Оставьте, пожалуйста, информацию о себе: Фамилия: Почтовый адрес: E-mail: Если Вы чувствуете в себе силы сотрудничать с нами как автор или переводчик, то просим Вас обращаться в письменной форме или по электронной почте с конкретными предложениями. 13. Какой операционной системой Вы' пользуетесь: □ DOS □ OS/2 □ Windows 3.1 □ Windows 95 □ Windows NT □ UNIX □ Другое 14. К какой группе Вы бы отнесли свою должность: □ Директор Q Руководитель отдела Q Секретарь-референт Q Инженер/программист □ Менеджер □ Другое 15. По какой теме Вам больше всего хотелось бы иметь книгу:
Издательский план на 1997 год В 1997 году издательство «Символ-Плюс» продолжает выпуск литературы, рассчитанной на читателя, привыкшего думать над прочитанным и способного оценить хороший стиль русского языка даже в специальной, компьютерной литературе. У нас в планах как переиздание хорошо известных книг, так и выпуск книг в новых сериях. Кроме того, в дополнение к технической литературе мы планируем начать издавать и литературу учебную. Д. Кирсанов Факс-модем: от покупки и подключения до выхода в Интернет, 2 издание ISBN 5-89051-006-1 300 стр., тираж 10 000, выходит в мае Вышедшая в прошлом году книга Дмитрия Кирсанова «Факс-модем: от покупки и подключения до выхода в Интернет» пришлась по душе многим нашим читателям. Вот лишь некоторые их отзывы: «...Книга полностью соответствует тому, что заявлено в предисловии, что приятно, ибо бывает нечасто...» «...Хотя книга [...] вышла достаточно давно, в Одессе она появилась недавно. Ну что сказать —потрясающе!!! Единственная вещь, которая помогла более-менее разобраться в этой «железке». Очень удачная книга, очень понятная для среднего пользователя» «...Даже не имея модема, я заполнил пробел в этой области. На сто моих знакомых лишь 2-4 человека имеют знания уровня этой книги. ..» «Большое спасибо! Ваша книга [...] действительно открывает новый мир телекоммуникаций... Книга выполнила поставленную перед ней цель на 100%» %имвэ\
А вот мнение Андрея Борзенко, сотрудника журнала «Компьютер-Пресс» (№3/1997): «...считаю ее самой толковой из виденных мною книг о модемах для пользователей. Более того, эту книгу рекомендует для изучения и крупнейший российский поставщик услуг Internet АО «Релком» Конечно, за год в области телекоммуникаций произошли серьезные изменения Поэтому в новом издании вы найдете описание и новых, скоростных способов соединения с Интернетом, такие рсак, например, линии ISDN. Книга содержит: • подробное описание принципов работы, назначения и применения модемов и факс-модемов для персональных компьютеров; • подробные рекомендации по выбору, установке и настройке модемов и программного обеспечения для работы с ними; • основы работы с коммуникационными программами различного назначения для основных операционных систем; • основы работы в сети Интернет — как через простое почтовое подключение, так и в режиме online. Серия «Профессионально!» Предназначена исключительно для специалистов. Все книги этой серии написаны профессионалами в своих областях. А переводы зарубежных изданий выполнены специалистами соответствующего профиля. Кроме книги, которую Вы держите в руках, в этой серии к изданию запланированы книги по администрированию Windows NT 4.0 Server, а также: Д. Кирсанов Профессиональный Web-дизайн ISBN 5-89051-017-7, 500 стр. + CD-ROM, выходит в августе Книга представляет собой подробное руководство по Web-дизайну, работе с графикой и программированию для World Wide Web. Детально рассмотрены язык HTML, работа с графикой для Web, интерактивные и мультимедийные расширения. Обсуждаются вопросы дизайна и композиции, организации содержимого сервера, построения навигационного интерфейса. Описаны лучшие из существующих программных средств Web-дизайнера. К книге прилагается диск CD-ROM с богатой подборкой справочной информации и программ. .кмвэ\
Серия «БиблиотеНа вебмастера» В этой серии к изданию запланированы книги на следующие темы: • Разработка сетей интранет • Разработка баз данных для World Wide Web • Публикация баз данных в Интернете/Интранете • Создание UNIX Web-Сервера Серия «БиблиотеНа дизайнера» Эта серия предлагает отличный набор книг для художника, дизайнера, да и просто любого человека, стремящегося выразить свои творческие способности современными изобразительными средствами. Компьютерщики со стажем, которым постоянно приходится заниматься несвойственным им делом — рисованием и дизайном, — без лишних рассуждений о меню и без того хорошо знакомых программ наконец-то узнают главное: как сделать это красиво! Английский языН Джина English for Russians, или Английский для наших ...которые учили-учили его, да так и не выучили Серия, первая книга (300 стр.) выходит в августе Мы привыкли к мысли, что выучить иностранный язык — дело трудное, почти невозможное. Для этого нужны горы учебников, уйма времени, особый природный дар и Бог знает что еще. Но вот пришло время разрушить этот жестокий миф. Автор книг данной серии получила высшее филологическое образование в США, но осталась русской по национальности. Она использует принципиально новый подход к обучению, мастерски сочетая глубину изложения теоретического материала с непринужденностью практических занятий. е
Книжный интернет-магазин Вы можете заказать в интернет-магазине все книги нашего издательства, а также многие другие книги — компьютерные и не только. Наряду с онлайн-версией, которая находится на web- сервере по адресу http://www.symbol.ru, вы можете воспользоваться и почтовой версией, которую обслуживает почтовый сервер news@symbol.ru. Чтобы вам было удобнее начать с ним общение, ниже мы поместили описание доступных команд. Если же у вас совсем нет доступа к Интернету, вы можете послать заявку и обычной почтой: 190031, Санкт-Петербург, а/я 418. ВОПРОСЫ И ОТВЕТЫ В этом небольшом разделе мы приводим ответы на наиболее часто встречающиеся вопросы по работе нашего интернет-магазина. В: Наверное, книги в вашем магазине стоят дорого? О* Вовсе нет. Цены — практически такие же, как в обычном книжном магазине Петербурга. Но уже с учетом почтовых расходов] В: Отправишь вам деньги... А вдруг вы книг не пришлете? О: Вы можете заказать книги наложенным платежом и тогда оплатите их только при получении на почте. Однако это выйдет на 20% (почтовый сбор) дороже. В. У нас сейчас все так плохо работает... Наверное и книг придется ждать очень долго? О* Как это ни странно, но почтовая служба России работает весьма неплохо. Среднее время доставки — 10 дней. За два года работы претензий от покупателей еще ни разу не поступало. В- Кто обращается в ваш магазин? О: Наши покупатели — жители самых разных частей России, а также стран ближнего зарубежья. Однако и москвичи (петербуржцы), которым проще (и нередко дешевле) заказать книги у нас, чем ехать в центр или на тушинский (автовский) рынок, нередко обращаются к нам. Кроме того, благодаря нашим скидкам, у нас заказывают книги мелкооптовые торговцы, небольшие книжные магазины на периферии, а также различные фирмы, библиотеки и учебные организации. В: Ваш магазин находится в Петербурге, и я тоже живу здесь. Зачем мне заказывать книги по почте? О' Для Петербурга у нас действуют специальные цены. В случае, если вы закажете книги на сумму свыше 250 000 руб., мы доставим их сами. В: Чем ваш магазин лучше других? О: Наш девиз: «Все заказы выполняются исправно и в срок!» А кроме того: • три способа оплаты (предоплата по безналичному расчету, почтовым переводом, а также наложенным платежом) и скидки уже при покупке 2 книг; • прайс-лист с окончательной ценой, учитывающей все дополнительные расходы; • готовый счет (в случае оплаты по безналичному расчету), который вы можете распечатать прямо с экрана интернет-магазина, заказать по факсу или e-mail; • подробная информация по каждому изданию; • единственная в стране полностью автоматизированная почтовая (e-mail) версия магазина, не требующая сеансного подключения к Интернету; ммаол
• возможность подписки на автоматическое получение всей имеющейся информации: прайс-листа для нужного вам региона, списка новых поступлений и т.д. В: Какие страны вы обслуживаете? О: Россия, СНГ, Германия, Финляндия, Израиль. В: Почему вы говорите, что ваш магазин — первый в России, когда я точно знаю (где-то слышал/своими глазами читал), что это не так? О: Действительно, если быть совершенно точными, то он — один из трех, возникших примерно в одно время. Однако, мы — первые, кто специально создал узел для торговли книгами в России, а не для продажи их за границу. В: Я — вебмастер и хочу добавить на свою страницу ссылку на ваш сервер. Как это лучше сделать? О: Адрес нашего сервера http://www.symbol.ru Картинки для ссылок (регистр важен): http://www. symbol.ru/Banner.jpg http://www. symbol.ru/Ad 1 .gif http://www.symbol.ru/Ad2.gif РАБОТА С ПОЧТОВЫМ СЕРВЕРОМ Обращением к серверу считается любое письмо, пришедшее по адресу news@symbol.ru. Тема письма (subject) игнорируется, а строки письма интерпретируются как команды серверу. Формат письма: <команда1> <параметр1_1> <параметр1_2> ... <команда2> <параметр2_1> <параметр2_2> ... <командаЗ> <параметрЗ_1> <параметрЗ_2> ... Команды отделяются друг от друга переводом строки, а параметры команд — пробелами или табуляцией. В настоящее время сервер понимает следующие команды (регистр букв в письме значения не имеет): MODE <параметр> устанавливает режим получения информации, запрошенной во всех следующих за этой командах. Эта команда должна даваться ранее всех остальных. Параметры команды MODE: a s с i i информация высылается в виде текста (этот режим установлен по умолчанию) uuencode информация высылается в закодированном с помощью UUENCODE виде HELP высылает на адрес пришедшего письма справку по работе с системой LIST высылает на адрес пришедшего письма список доступных команд с пояснениями GET <параметры> высылает на адрес пришедшего письма информацию, запрошенную в параметрах. Каждому параметру соответствует свой файл, который высылается отдельным письмом. Количество параметров может быть любым. Параметры команды GET и соответствующая им информация: pricelist текущий прайс-лист по всем жанрам для России. рпсе1л.з1:_<суффикс> где суффикс определяет регион (способ доставки) значения суффикса: spb Санкт-Петербург avi а регионы России без ж/д сообщения (Якутия и некоторые другие) ^имда\
CIS fl de ll страны СНГ Финляндия Германия Израиль (Например- pi icelist_spb — текущий прайс-лист по всем книжным жанрам для Санкт-Петербурга) book_# зннотация к книге, где # — год книги из прайс-листа pub 11 sh^ri список изданных книг catalog информация о готовящихся изданиях, а также издательский план на год an k e t а анкета читателя test тестовый английский текст (для тех, кто хочет попробовать себя в качестве переводчика) SUBSCRIBE <параметры> производит подписку на регулярное получение информации, запрошенной в параметрах (их количество может быть любым). Информация высыпается на адрес пришедшего письма. Параметры команды SUBSCRIBE и соответствующая им информация allprice подписка на прайс-лист по всем книжным жанрам для России а11ргл_се_<суффикс> суффикс определяет регион или способ доставки (см. команду GET) с ompp rice подписка на прайс-лист по компьютерным книгам для России comppnce <суффикс> суффикс определяет регион или способ доставки (см. команду GET) newbooks подписка па информацию о готовящихся в данный момент к печати изданиях (начале приема иредварителных заявок) После выполнения команды SUBSCRIBE для каждой из запрошенных подписок высылается уведомление об успешном и 'ш неуспешном выполнении этой команды. UNSUBSCRIBE <параметры> производит отказ от подписок, перечисленных в параметрах (их количество может быть любым). Параметры команды UNSUBSCRIBE и соответствующие им действия: allprice отказ от подписки на прайс-лист по всем книжным жанрам а11рг1се_<суффикс> суффикс определяет регион или способ доставки (см. команду GET) compprice отказ от подписки на прайс-лист по компьютерным книгам сотррг1се_<гуффикс> суффикс определяет регион или способ доставки (см. команду GET) newbooks отказ от подписки на информацию о готовящихся в данный момент к печати изданиях all отказ от всех ранее заказанных подписок После выполнения команды UNSUBSCRIBE для каждой из подписок, от которых Вы решили отказаться, высылается уведомление об успешном или неуспешном выполнении этой команды. CHANGE <старый_адрес> <новый__адрес> производит замен}- в базе данных информационного сервера старого адреса подписки на новый Параметры команды CHANGE: <старый_адрес> адрес, по которому Вы ранее получали подписку <новый_адрес> адрес, по которому Вы желаете получать подписку теперь После выполнения команды CHANGE высылается уведомление об успешном или неуспешном выполнении этой команды. QUIT игнорирует последующие строки письма
Интеграция сетей, От консультации до сопровождения. Предоставление канала связи с гарантированной пропускной способностью, ISDN-качественно новая возможность иметь коммутируемый доступ в Интернет 128 кб/с WWW server Создание и поддержка WWW-сёрверрв клиентов Создание и поддержка корпоративных сайтов Реклама в Интернет Студия дизайна 325 65 07 цифрр^ каналы круглосуточная техническая поддержка, Вас доброжелательно проконсультируют в любое время. ,**** (812)325 65 07 Серпуховская ул., 9 (812)315 9473 Гороховая 16, магазин Технофон INTERNET On-Llne Около Швходных линий гарантируют высококачественный^ доступ в ИНТЕРНЕТ. Ваша домашняя страница и электронная почта сделают Вашу жизнь в Интернет насыщенной Седоуховсмая, 9 Наш офис находится в трех минутах ходьбы от метро "Технологический институт"
-Ч INTERNET Подключение частных лиц, организаций, льных сетей корпораций ко всем ресурсам eW Relcom/Internet, включая электронную почту и World Wide Web. Цифровые телефонные линии. Выделенные каналы связи. НЕВАЛИНК Четыре года на рынке С-Петербурга. Более 2000 абонентов, http://www.spb.su - старейший и один из самых популярных серверов России. Более * 100 000 обращений в день. On-line доступ к :д;.^! телефонным справочникам. >Сч^ ВЫ В INTERNET Размещение информации на World Wide Web. Профессиональный Web-дизайн: от простых и бесплатных индивидуальных страниц до; Internet-представительств с использованием« новейших технологий (аудио, видео, анШ£ф(${Ц базы данных). Электронные версии изданий^'' СаН1№П^тЩбург 191023, Канал Грибоедова 36 , .;;Щ®Л ЩЩ) 31Ш$ЩШ^9951, 310-9052
ПОЛНЫЙ КОМПЛЕКС ИНТЕРНЕТ УСЛУГ семинлры, консультации CONVEY Internet Oft) Services http://www.convey.ru ЦЕНЫ НИЖЕ, Uftfl УИЯЩИХ^» СТУДЕНТОВ К НЙУЧНЫХ РПБСРВНККОВ й и rose & ш и f фесишг >■' Mi ' ""-» а . 1812)1123713 т/ф (812)1644236 САНКТ-ПЕТЕРБУРГ, ТРАНСПОРТНЫЙ ПЕРЕУЛОК, Д0М1 предъявите эту книгу и вы получите 10 часов в ИНТЕРНЕТ БЕСПЛАТНО
«Лу Гринзоу копну i Windows 95 так глубоко, что вы почта слышите грохот вращающихся вокруг вас шестеренок. Кроме того, он и C++ знает вдо w и поперек. От этого сочетания у вас го юва пойдет кругом.* Джефф Дантеманн «PC TECHNIQUES» ПРОФЕ^Ъ ИОНАЛЬНО ФИЛОСОФИЯ ПРОГРАММИРОВАНИЯ WINDOWS 95/NI Внутри вы найдете: • Сотни методов, специально разработанных для того, чтобы помочь вам проникнуть в тайну «Большого Каньона», существующего между Windows 95, Windows NT, Win32 и Windows 3.1 • Инструкции и советы по разработке и тестированию коммерческих 32-битных приложений • Высокоэффективный 32-бит- ный код на C++ для всех типов проектов для Windows 95 Онлайн-ресурсы (www.symbol.ru): • Все проект ы для Windows 95, описанные в книге, включая все файлы с кодом и проектные файлы • Условно бесплатные версии самых полезных инструментов для разработки программ для Windows 95 • Специальная версия популярной программы Stickles! для Windows 95 в авторском исполнении 190000, С. Петербург Черноморский пер., 7 В книге «Философия программирования для Windows 95/NT» вы найдете безукоризненный план, который поможет вам овладеть искусством создания 32-разрядных приложений для Windows. Профессиональный разработчик программного обеспечения и писатель Лу Гринзоу делится с читателем опытом, заработанным упорным трудом, и советами, как обойти сложности и не попасться в ловушки на нелегком пути создания программ для 32-разрядной платформы Microsoft. Никакая другая книга по программированию не расскажет столько о секретах, подвохах и преимуществах освоен я 32-разрядной Вселенной! Об авторе Л у Грин юу один из ведущих специалистов в области нрохрам- мирования для Windows. Профессиональный подход и способность проникнуть в суть дела часто делают его публикации-центральной темой гаких изданий, как PC WEEK, PC TECHNIQUES, Dr. Dobbs Journal, Softwaic Development и Windows Tech Journal. Он также является созданием программы Stickies! весьма популярной комбинации деловой организационной программы и базы данных для Windows.