воскресенье, 28 февраля 2010 г.

Ограничения элемента управления "анимация" оболочки

Это перевод Limitations of the shell animation control. Автор: Реймонд Чен.

Элемент управления анимацией в библиотеке общих элементов управления оболочки поддерживает только очень ограниченный класс AVI-файлов.

  • AVI не должен быть interleaved.
  • AVI должен иметь ровно один поток видео.
  • AVI не может иметь аудио-поток.
  • AVI не может менять палитру.
  • AVI должен быть несжатым или сжатым по BI_RLE8.
Почему введены все эти ограничения?

Потому что целью элемента управления анимацией была возможность показывать простую анимацию. Если вам нужна сложная анимация, то вам нужно использовать другие, более продвинутые способы.

Нет никакого смысла добавлять все эти дополнительные возможности в элемент управления оболочки. В результате получился бы просто никому не нужный клон других продвинутых элементов.

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

суббота, 27 февраля 2010 г.

Вы не можете зарезервировать пользовательское адресное пространство глобально

Это перевод You cannot globally reserve user-mode address space. Автор: Реймонд Чен.

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

Это, очевидно, невозможно.

Почему очевидно? Ну, представьте, если бы это было бы возможным.

"Представьте, если бы это было возможным" - это один из логических тестов, который вы можете применить к теории или идее, чтобы посмотреть, возможно/сработает ли это. Сейчас мы используем его, чтобы определить, возможно ли предлагаемое поведение (есть и связанный с этим другой мысленный эксперимент: "представьте, что было бы, если бы всё действительно работало так").

Каковы последствия возможности глобального выделения пользовательского адресного пространства?

Ну, для начала, нет гарантий, что к моменту вашего резервирования в вашей системе вообще будет свободные для этого адреса. Возьмите в пример программу, которая использует практически всё своё адресное пространство (ей не обязательно использовать для этого реальную память - она может использовать VirtualAlloc(MEM_RESERVE)). Запустите такую программу и ни одно выделение глобальной памяти не будет успешным, пока программа не закроется.

Поэтому, даже если бы это было бы возможным - это не было бы надёжным. Ваша программа должна была бы быть подготовленной к ситуациям, когда не оставалось бы глобального свободного пространства. Поскольку вам всё равно придётся писать код отката (fallback code), то вы не сэкономили себе никакой работы.

Далее, положим, что это возможно, что у нас есть некая воображаемая функция GlobalVirtualAlloc. Ну, тогда я могу написать код, который вызывает эту воображаемую функцию в цикле, засасывая всё доступное глобальное адресное пространство, и запущу её под обычным пользователем (не администратором).

Я только что нарушил правила безопасности (общий принцип: обычный пользователь не должен влиять на других пользователей или систему. Мы уже видели сценарий, когда не-администратор может сломать программу, запущенную из под администратора, из-за небезопасного использования общей памяти).

Моя воображаемая программа забрала себе всё глобальное адресное пространство, уменьшая адресное пространство всех других программ - в том числе, работающих под администратором. Если в системе не работает много программ, то моя воображаемая программа, скорее всего, сможет выделить очень много адресного пространства таким образом. Что приведёт к эквивалентному уменьшению свободного адресного пространства в администраторских (и системных) программах. Поэтому, я могу вызывать исчерпание адресного пространства у этих программ, что приведёт к их завершению (отказ в обслуживание - denial of service).

Да, вы можете ограничить выделение глобального адресного пространства только для администраторов, но это никак не поможет ленивому программисту, поскольку программа не станет работать под обычным пользователем - и вам всё равно придётся писать код отката. Или вы можете ограничить "глобальное" выделение только пользователями в рамках той же сессии, с тем же токеном безопасности, но, опять-таки, вам всё равно придётся писать код отката, если закончится место в глобальном пространстве; вы не получили никакого выигрыша.

Мораль истории в том, что каждый процесс имеет своё собственное адресное пространство, и каждый процесс управляет им независимо от остальных процессов (конечно же, процесс может дать пользователю право PROCESS_VM_OPERATION, что позволит пользователю делать что угодно с адресным пространством процесса. Но изменения в адресном пространстве этого процесса никак не затронут остальные процесса).

LoadLibraryEx(DONT_RESOLVE_DLL_REFERENCES) имеет фундаментальный изъян

Это перевод LoadLibraryEx(DONT_RESOLVE_DLL_REFERENCES) is fundamentally flawed. Автор: Реймонд Чен.

У функции LoadLibraryEx есть флаг, называемый DONT_RESOLVE_DLL_REFERENCES. В документации сказано:
Если это значение задано, а выполняемый модуль является DLL, то система не будет вызывать её DllMain для инициализации и завершения процесса и потоков. Также система не будет загружать (дополнительные) библиотеки, на которые ссылается данный модуль.

Если вы планируете только получать доступ к данным или ресурсам этой DLL, то лучше использовать флаг LOAD_LIBRARY_AS_DATAFILE.
По-моему, текст выше составлен так, что он "предполагает", что флаг LOAD_LIBRARY_AS_DATAFILE недостаточно сильный.

DONT_RESOLVE_DLL_REFERENCES - это бомба замедленного действия.

пятница, 26 февраля 2010 г.

В не оконных элементах управления нет ничего волшебного

Это перевод Windowless controls are not magic. Автор: Реймонд Чен.

Похоже, что когда люди замечают, что движок рендеринга Internet Explorer не использует HWND для экранных элементов, они думают, что Internet Explorer как-то "читерит", делает что-то "недокументированное" и имеет "нечестное преимущество".

четверг, 25 февраля 2010 г.

Почему все свёрнутые окна имеют размер 160x31?

Это перевод Why do minimized windows have an apparent size of 160x31? Автор: Реймонд Чен.

Мы обсуждали некоторое время назад, куда сворачивались окна до изобретения панели задач. В мире же современной панели задач свёрнутые окна имеют кажущийся размер 160x31 (вы, конечно, не можете видеть свёрнутое окно, но если вы посмотрите его размеры в программе типа Spy++, то вы увидите, что свёрнутое окно имеет размер 160x31). Почему?

среда, 24 февраля 2010 г.

Опасности фильтрования сообщений

Это перевод The dangers of filtering window messages. Автор: Реймонд Чен.

Функции GetMessage и PeekMessage позволяют вам указать фильтр, ограничивающий обрабатываемые сообщения по описателю окна или диапазону кодов сообщений, которые функции будут получать из очереди. Хотя это вполне нормально - использовать их, но вы только убедитесь, что время от времени вы позволяете проходить любому (нефильтрованному) сообщению.

Частой ошибкой бывает использование GetMessage с фильтром по окну в вашем цикле выборки сообщений:
  while GetMessage(Msg, Wnd, 0, 0) do // Неверно!
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
Хотя ваша программа может создавать всего одно окно, этот код, тем не менее, неверен.

"Как это может быть?" - спросите вы, - "Моя программа имеет только одно окно. Почему тут могут быть какие-то ещё сообщения для других окон? Фильтр, хотя и избыточен, но ведь безвреден, правда?"

Многие системные службы создают окна для вас. Например, если включено редактирование метода ввода, то редактор метода может создать вспомогательные окна для содействия вводу символов. Если вы инициализируете COM, то тогда COM может решить создать вспомогательное окно для поддержки межпоточного маршаллинга. Если вы используете только фильтрованный GetMessage, то сообщения для этих окон никогда не будут обработаны, а вы останетесь сидеть и почёсывать голову, соображая, почему ваша программа иногда зависает, когда вы, например, пытаетесь сделать drag&drop.

Мораль истории: убедитесь, что ваши циклы сообщений рано или поздно выбирают все сообщения из очереди, так что дополнительные службы смогут функционировать верно.

вторник, 23 февраля 2010 г.

Какие ещё эффекты имеет флаг DS_SHELLFONT на страницы свойств?

Это перевод What other effects does DS_SHELLFONT have on property sheet pages? Автор: Реймонд Чен.

Как только вы изобретёте новый флаг, вы можете использовать его, чтобы исправить старые баги, без ломания обратной совместимости (прим.пер.: как мы помним, не все баги могут быть исправлены, потому что тогда перестанут работать программы).

Одной из ошибок прошлого было то, что размеры страницы свойств брались относительно шрифта "MS Sans Serif", даже если страница использовала другой шрифт (прим.пер.: речь идёт о диалогах на WinAPI).
DLG_SAMPLE DIALOGEX 32, 32, 212, 188
CAPTION "Caption"
FONT "Lucida Sans Unicode"
...
Этот пример диалогового шаблона говорит, что он имеет длину 212dlu и высоту 188dlu (dlu - диалоговых единиц). Если шаблон диалога использовался для автономного диалога, то эти значения DLU вычислялись бы относительно шрифта диалога, а именно - Lucida Sans Unicode.

Однако, если диалоговый шаблон использовался в странице свойств, то ранние версии Windows интерпретировали значения 212 и 188 относительно шрифта фрейма диалога свойств (обычно - MS Sans Serif), а не относительно шрифта, ассоциированного с самой страницей. Многие люди обошли это проблему, указывая "исправленные" (pre-adjusted) размеры, так что когда Windows измеряла диалог относительно MS Sans Serif, то поправки к размерам отменяли баг.

Другими словами, предположим, что Lucida Sans Unicode на 25% шире чем MS Sans Serif (я беру от балды). Тогда чтобы получить диалог шириной в 212dlu относительно Lucida Sans Unicode, диалоговый шаблон должен указывать ширину в 212dlu + 25% = 265dlu.

Поскольку люди опираются на это поведение (и уже написали несчётное множество программ), вы не можете изменить его. Если вы "исправите" его, то все эти уже написанные диалоги будут иметь неверный размер (так что ваше "исправление" в действительности будет привнесением нового бага).

Ах, но теперь у нас есть новый флаг - DS_SHELLFONT. Начиная с Windows 2000, если вы указываете в стиле диалога DS_SHELLFONT в вашем шаблоне типа DIALOGEX, то диалоговые размеры будут высчитываться относительно шрифта шаблона (что вы и хотели изначально), а не шрифта фрейма. Если же вы опустите этот флаг (как и делают старые/уже написанные программы), то все вычисления размеров будут выполняться по старой модели, баг-к-багу совместимой с предыдущими версиями системы.

Примечание переводчика: а вот ещё по теме от Джоэля Спольски. Купите книгу, если хотите читать на русском ;) Она того стоит.

понедельник, 22 февраля 2010 г.

Почему DS_SHELLFONT = DS_FIXEDSYS or DS_SETFONT?

Это перевод Why does DS_SHELLFONT = DS_FIXEDSYS | DS_SETFONT? Автор: Реймонд Чен.

Вы могли заметить, что числовое значение флага DS_SHELLFONT равно комбинации DS_FIXEDSYS or DS_SETFONT.
const
DS_SETFONT = $0040;
DS_FIXEDSYS = $0008;
DS_SHELLFONT = DS_SETFONT or DS_FIXEDSYS;
Разумеется, это не простое совпадение.

Значение флага DS_SHELLFONT было выбрано так, чтобы старые операционные системы (Windows 95, 98, NT 4) принимали бы флаг, но при этом игнорировали. Это позволило бы людям писать одну программу, которая получала бы стиль "Windows 2000", будучи запущенной на Windows 2000, и получала "классический" вид, работая на старых системах (если бы вы заставили людей писать две версии программы, одна из которых работает на всех системах, а вторая - только на новой, но выглядит чуточку лучше, то они просто не стали бы писать второй вариант).

Флаг DS_FIXEDSYS удовлетворяет этим условиям. Старые системы принимают флаг, поскольку это допустимый для них флаг, но они также игнорируют его, потому что флаг DS_SETFONT имеет приоритет.

Это одно из этих небольших упражнений по обратной совместимости: как вы спроектируете новое нечто, чтобы было возможным написать программу с использованием новых возможностей, но чтобы при этом она же работала бы и на старой системе (с деградацией возможностей, конечно же)?

воскресенье, 21 февраля 2010 г.

А что за дела с флагом DS_SHELLFONT?

Это перевод What's the deal with the DS_SHELLFONT flag? Автор: Реймонд Чен.

Он указывает, что вы хотите получить шрифт оболочки Windows 2000 по-умолчанию. Но это не означает, что вы его действительно получите.

Чтобы указать, что вы хотите получить вид "Windows 2000" для вашего диалога (прим. пер.: речь идёт о диалогах на WinAPI), вам нужно сделать три вещи и надеяться на четвёртую:
  1. Использовать шаблон DIALOGEX вместо шаблона DIALOG.
  2. Установить флаг DS_SHELLFONT в стиле диалога.
  3. Установить шрифт диалога в "MS Shell Dlg".
  4. Надеяться, что вы запущены на Windows 2000 или старшей, и при этом на системе включен новый шрифт Windows 2000.
Если все четыре условия выполнены, то ваш диалог получает новый вид "Windows 2000".

Если любое условие не выполняется, то вы получаете "классический" диалоговый шрифт. Заметьте, что четвёртое условие не в вашей юрисдикции. Соответственно, вы обязаны делать диалоги так, чтобы они выглядели хорошо и в новом стиле и в классическом.

Однако, для закладок свойств (property sheet) всё становится более сложным.

Было бы очень плохо, если бы закладки выполнялись в различных визуальных стилях. Вы бы не хотели иметь кнопку "Дополнительно" выполненную шрифтом MS Sans Serif, но при этом кнопка "Применить" была бы с Tahoma. Для избежания этой проблемы менеджер закладок свойств пробегает по всем страничкам. Если все страницы используют вид "Windows 2000", то весь диалог целиком также получает вид "Windows 2000". Но если хотя бы одна страница не поддерживает новый стиль, то менеджер решает "плыть по течению" и создаёт диалог в классическом виде, он также конвертирует все страницы с видом "Windows 2000" в классический вид.

Таким способом, все страницы диалога будут иметь классический вид, вместо помеси стилей, когда некоторые страницы имеют классический стиль, а другие - стиль Windows 2000.

Вот почему вы иногда можете видеть, как диалоги свойств оболочки имеют классический вид. Некоторые расширения оболочки добавляют страницу, которая не поддерживает вид "Windows 2000", так что ради визуального единообразия менеджер закладок свойств вынужден использовать классический стиль.

Это ещё одна причина для проверки ваших вкладок свойств в обоих стилях.

пятница, 19 февраля 2010 г.

Ширина иногда лучше чем глубина

Это перевод Breadth is sometimes better than depth. Автор: Эрик Липперт.

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

четверг, 18 февраля 2010 г.

Как программно определить, запущены ли мы на 64-разрядной Windows?

Это перевод How to detect programmatically whether you are running on 64-bit Windows. Автор: Реймонд Чен.

Чтобы программно определить, запущены ли вы на 64-разрядной Windows, вы можете использовать функцию IsWow64Process, которая показывает, не запущен ли ваш 32-разрядный процесс в режиме эмуляции.

среда, 17 февраля 2010 г.

Почему команда Win64 выбрала модель LLP64?

Это перевод Why did the Win64 team choose the LLP64 model? Автор: Реймонд Чен.

Участник Beer28 написал на Channel 9: "Я не могу себе представить, что у многих программ могут быть проблемы с изменением размерности типов". Я усмехнулся и сделал пометку, чтобы написать пост о модели данных Win64.

Команда Win64 выбрала модель данных LLP64, в которой все целочисленные типы остаются 32-х разрядными и только указатели расширяются до 64-х бит. Почему?

В дополнении к причинам, данными на той странице, другой причиной было сохранение совместимости хранимых (persistence) форматов данных. Например, часть заголовка bitmap-файла определяется такой записью:
type
PBitmapInfoHeader = ^TBitmapInfoHeader;
{$EXTERNALSYM tagBITMAPINFOHEADER}
tagBITMAPINFOHEADER = packed record
biSize: DWORD;
biWidth: Longint;
biHeight: Longint;
biPlanes: Word;
biBitCount: Word;
biCompression: DWORD;
biSizeImage: DWORD;
biXPelsPerMeter: Longint;
biYPelsPerMeter: Longint;
biClrUsed: DWORD;
biClrImportant: DWORD;
end;
TBitmapInfoHeader = tagBITMAPINFOHEADER;
{$EXTERNALSYM BITMAPINFOHEADER}
BITMAPINFOHEADER = tagBITMAPINFOHEADER;
Если бы Longint расширился с 32-х бит до 64-х, то для 64-х разрядных программ стало бы невозможным чтение bitmap-файлов (прим.пер.: напомню, пост идёт о Windows; в Delphi мы пока не имеем 64-х разрядного компилятора, хотя всё идёт к тому, что в Delphi будет так же).

Кроме файлов у нас есть и других форматы. В дополнение к очевидным вещам типа RPC и DCOM, для передачи информации между процессами могут также использоваться двоичные BLOB реестра и блоки разделяемой памяти. Если процесс-источник и процесс-назначение будут различной разрядности, то любое изменение размерности целых приведёт к несовпадению форматов.

Заметьте, что в этих сценариях с обменом информации между процессами, нам не нужно волноваться об изменившемся размере указателя (Pointer). Никто в здравом уме на станет передавать указатель через границы процессов: раздельные адресные пространства означают, что значение указателя из чужого процесса бесполезно в нашем и наоборот - так зачем же тогда их передавать?

вторник, 16 февраля 2010 г.

Почему файлы и каталоги без даты и времени не сортируются правильно Проводником?

Это перевод Why do files and directories with no time/date mess up sorting in Explorer? Автор: Реймонд Чен.

Если у вас есть файл или каталог, у которого нет времени последнего изменения, вы можете обнаружить, что Проводник не умеет сортировать такие элементы (как вы можете создать файл или папку без даты последнего изменения? Ну, это тяжело сделать; вам потребуется помощь внешней файловой системы). Почему это так?

Почему описатели ядра всегда кратны четырём?

Это перевод Why are kernel HANDLEs always a multiple of four?. Автор: Реймонд Чен.

Не слишком хорошо известно, что два младших бита любого описателя ядра (также: дескриптора или HANDLE) всегда равны нулю; другими словами, их численное представление всегда кратно четырём. Заметьте, что это применимо только к описателям объектов ядра; это не так для псевдо-описателей (типа GetCurrentProcess) или описателей других типов (описатели USER или GDI, мультимедийные дескрипторы...). Описателями ядра являются те вещи, которые вы можете передать в функцию CloseHandle.

Доступность младших двух битов кроется в заголовочном файле ntdef.h:
//
// Low order two bits of a handle are ignored by the system and available
// for use by application code as tag bits. The remaining bits are opaque
// and used to store a serial number and table index.
//

#define OBJ_HANDLE_TAGBITS 0x00000003L
То, что как минимум один младший бита всегда равен нулю, также неявно указывается функцией GetQueuedCompletionStatus, описание которой говорит, что вы можете установить младший бит описателя события для подавления уведомления порта. Чтобы это работало, необходимо, чтобы младший бит произвольного описателя изначально всегда был равен нулю.

Эта информация бесполезна для большинства программистов, которые должны трактовать описатели как непрозрачные значения. Но эта информация может заинтересовать людей, которые реализуют низкоуровневые библиотеки классов и оборачивают объекты ядра для использования в такой библиотеке.

воскресенье, 14 февраля 2010 г.

CreateProcess не ждёт, пока процесс запустится

Это перевод CreateProcess does not wait for the process to start. Автор: Реймонд Чен.

Функция CreateProcess создаёт новый процесс, но она не ждёт, пока процесс "раскрутится" до возврата управления. Она просто создаёт объект ядра процесс и возвращает управление, позволяя процессу заниматься своими делами.

суббота, 13 февраля 2010 г.

Важность обратной совместимости кодов ошибок

Это перевод The importance of error code backwards compatibility. Автор: Реймонд Чен.

Я помню баг-отчёт по одной старой программе MS-DOS (от компании, которая ещё ведёт дела, так что не просите меня назвать их) о том, что она пыталась открыть файл "". Это файл без имени.

Эта попытка возвращала ошибку номер 2 (file not found - файл не найден). Но программа не проверяла этот код ошибки и считала, что это был описатель файла (при. пер.: как указано в предыдущем посте, функции MS-DOS возвращали коды ошибки так же, как и результат выполнения функции: в регистре AX; признак ошибки устанавливался отдельно, флагом CF). Поэтому дальше она продолжала писать данные в дескриптор 2. В итоге данные оказывались выведенными на экран, потому что дескриптор 2 является описателем стандартного вывода ошибок, который по-умолчанию идёт на экран.

Так получалось, что эта программа изначально хотела выводить на экран при пустом имени файла.

Другими словами, она работала исключительно случайно.

Из-за различных внутренних изменений файловой системы в Windows 95, код ошибки при открытии файла без имени поменялся с 2 (file not found) на 3 (path not found - путь не найден).

Посмотрим, что произошло.

Программа пыталась открыть файл "". Теперь она получала 3, как результат вызова функции открытия файла. Она ошибочно принимала его за файловый описатель и пыталась в него писать.

Но что такое описатель 3?

В MS-DOS есть следующие предопределённые описатели файлов:
ОписательИмяЗначение
0stdinстандартный ввод
1stdoutстандартный вывод
2stderrстандартный вывод ошибок
3stdauxстандартный последовательный порт
4stdprnстандартный принтер
Что происходило, когда программа пыталась записать данные на описатель 3?

Она пыталась записать в последовательный порт.

Большинство машин не имеют ничего присоединённого на последовательный порт. Запись останавливалась.

Результат: зависшая программа.

Ребятам, ответственным за файловую систему, пришлось специально менять условия обработки, чтобы функции продолжали возвращать 2 в таких случаях.

Как MS-DOS сообщала о кодах ошибок?

Это перевод How did MS-DOS report error codes?. Автор: Реймонд Чен.

Вызовы функций MS-DOS (ах, этот int 21h) обычно сообщали об ошибке установкой флага CF и записью кода ошибки в регистр AX. Эти коды ошибок выглядят ужасно знакомыми сегодня: это те же самые коды ошибок, что использует Windows. Все эти малозначные коды типа ERROR_FILE_NOT_FOUND ведут свою историю ещё из MS-DOS (и, вероятно, даже глубже в прошлое).

Коды ошибок являются одной из главных проблем совместимости, потому что вы не можете просто добавить новые коды без ломания существующих программ. Например, стал широко известным тот факт, что "Единственными возможными кодами ошибок, возвращаемыми OpenFile, могли быть 3 (path not found), 4 (too many open files) и 5 (access denied)". Если бы MS-DOS попробовала вернуть код ошибки не из этого списка, программы бы вылетали, потому что они использовали возвращаемое значение как индекс в таблице (массиве) без проверки диапазона. Возврат новой ошибки типа 32 (sharing violation) означало бы, что эти программы переводили бы управление на мусорный адрес и вылетали.

Ещё о совместимости кодов ошибок - в другой раз.

Когда заходит речь о добавлении новых кодов ошибок, совместимость диктует, чтобы вы не меняли коды, уже возвращаемые функциями. Поэтому, если происходит новый тип ошибки (например, a sharing violation), то выбирается один из предыдущих "хорошо-известных" кодов ошибок, который больше всего похож по смыслу на новый код (для "sharing violation" наилучшим совпадением будет, вероятно, "access denied"). Программы, которые были в курсе про новые коды ошибок, могли вызвать новую функцию "получить расширенный код ошибки", которая возвращала один из новых кодов ошибок (в нашем случае - 32 для sharing violation).

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

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

К сожалению, этот тип расширенной информации был потерян, когда мы покинули мир 16-ти разрядного программирования. Теперь у вас есть либо код ошибки, либо исключение и вам лучше бы заранее знать, как их следует обрабатывать. Например, если вы вызываете некоторую функцию и она возвращает какой-то код ошибки, то как вы узнаете была ли эта ошибка из-за логики в вашей программе (скажем, использование описателя после закрытия) или же это было какое-то внешнее условие (например, ошибка подключения к удалённому серверу)? Никак.

Это в особенности ужасно для программирования на исключениях. Когда вы ловите исключение, вы не можете сказать, глядя на него, либо это что-то, что должно привести к вылету вашей программы (ошибка во внутренней логике типа разыменования nil) или же что-то, что не представляет ошибки в вашей программе, а было вызвано внешними условиями (ошибка соединения, файл не найден и т.п.).

Примечания переводчика:
Видимо, Реймонд говорит только о системном механизме исключений. Напомню, что на системном уровне данные исключения - это запись TExceptionRecord. У неё есть, конечно же, адрес инструкции, вызвавшей исключение, а также числовой код (аналог кода ошибки), флаги (свойства) и ограниченное кол-во пользовательской информации. В такой реализации исключения, конечно страшны, поскольку они имеют все недостатки кодов ошибок (которые только усугубляются способностью исключений уводить выполнение от места возникновения проблемы к её обработчику).

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

Во-первых, из-за наследования вы спокойно можете расширять пространство ошибок: вводя новые классы, дочерние к уже существующим классам исключений. Вы не поломаете при этом никакой код, поскольку он как обрабатывал исключения определённых классов, так и продолжит это делать - теперь уже включая и ваши новые классы (и всё благодаря наследованию). Например, если код реагирует на ELoadConfigError, то вы можете ввести новое исключение вида ELoadConfigFileReadingError = class(ELoadConfigError) - и старый код по-прежнему будет корректно реагировать на все ошибки. А те, кто в курсе про новые классы (ELoadConfigFileReadingError), смогут реагировать только на них. Таким образом все стороны оказываются удовлетворены.

Во-вторых, вы можете передавать произвольную информацию вместе с исключением. Кроме того, из-за предыдущего пункта, исключения используются несколько иным способом, чем коды ошибок. Имеется ввиду, что разные функции возвращают одинаковый код ошибки - в первую очередь из-за сложностей расширения поля кодов ошибок. С другой стороны, исключения обычно создаются в привязке к вызывающему (пример: EListError, EStreamError). Что делает идентификацию проблемы тривиальной, и отпадает необходимость в чём-то типа дополнительной мета-информации, описанной выше. Разумеется, речь идёт только о корректном программирования на исключениях, а не когда вы используете только Exception для всех задач.

четверг, 11 февраля 2010 г.

Чище, элегантнее и тяжелее опознать

Это перевод Cleaner, more elegant, and harder to recognize. Автор: Реймонд Чен.

Похоже, что некоторые люди проинтерпретировали заголовок одного моего поста "Чище, элегантнее и неверно", как говорящего об исключениях вообще (см. библиографическую ссылку [35]; заметьте, что цитирующий даже изменил название моего поста!).

Этот заголовок был ссылкой только на конкретный кусок кода, который я скопировал из книжки, где автор утверждал, что представленный кусок кода "чище и элегантнее". Я лишь указал, что этот фрагмент кода не только чище и элегантнее, но и просто неверен.

Вы можете писать корректный код с исключениями.

Заметьте, это тяжело.

С другой стороны, только потому, что что-то является тяжёлым, не значит, что его не нужно делать.

среда, 10 февраля 2010 г.

Проектировка интерфейса пользователя для внутреннего дверного замка

Это перевод User interface design for interior door locks. Автор: Реймонд Чен. Входит в книгу The Old New Thing.

Насколько сложным может быть разработка интерфейса для замка внутренней двери?

вторник, 9 февраля 2010 г.

Почему \ \ не пытается дополнить имя машины?

Это перевод Why doesn't \\ autocomplete to all the computers on the network?. Автор: Реймонд Чен.

Wes Haggard хочет, чтобы ввод \\ автоматически предлагал бы автодополнение имени всех машин в сети. Ранняя бета версия Windows 95 вообще-то делала что-то такое похожее, показывая все машины в сети, когда вы открывали папку "Сетевое окружение". Но эта возможность была вскоре убрана.

Почему?

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

Сетевые администраторы ясно и недвусмысленно указали, что такая возможность Windows была абсолютно неприемлемой.

Потребности в корпоративной среде очень отличается от домашней сети, и Windows должна работать в обоих мирах.

Примечание переводчика: к сожалению, этот пост имеет проблемы с delphifeeds.ru.

понедельник, 8 февраля 2010 г.

Подсказки области уведомления не штрафуют вас за отсутствие у клавиатуры

Это перевод Taskbar notification balloon tips don't penalize you for being away from the keyboard. Автор: Реймонд Чен.

Функция Shell_NotifyIcon используется для различных вещей. Среди них - показ пользователю balloon-сообщений. Как обсуждается в документации записи NOTIFYICONDATA, поле uTimeout указывает, как долго должена показываться подсказка.

Но что если пользователь не у компьютера, когда вы её показываете? После 30 секунд подсказка исчезнет, а пользователь упустит ваше важное сообщение!

Не бойтесь. Панель задач отслеживает активность пользователя (с помощью функции GetLastInputInfo) и не запускает таймер, если ей кажется, что пользователя за машиной нет. А вы получите свои 30 секунд "с глазу на глаз".

Но что если вы хотите учитывать даже то время, когда пользователь отошёл?

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

Но что если вы хотите показывать ваше сообщение дольше 30 секунд?

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

воскресенье, 7 февраля 2010 г.

Как может код для предотвращения переполнения буфера в итоге вызывать его?

Это перевод How can code that tries to prevent a buffer overflow end up causing one? Автор: Реймонд Чен.

Если вы прочтёте спецификацию языка C, то увидите, что функции ...ncpy имеют очень странную семантику.

Функция strncpy копирует первые count символов из strSource в strDest и возвращает strDest. Если count меньше или равно длине strSource, то к строке не добавляется нулевой символ. Если же count больше длинны строки strSource, то целевая строка заполняется нулевыми символами до своего конца.
На рисунках ниже показаны различные сценарии копирования строк:
strncpy(strDest, strSrc, 5)
strSource
Welcome\0
strDest
Welco
заметьте: нет терминатора
 
strncpy(strDest, strSrc, 5)
strSource
Hello\0
strDest
Hello
заметьте: нет терминатора
 
strncpy(strDest, strSrc, 5)
strSource
Hi\0
strDest
Hi\0\0\0
заметьте: заполнение нулями до конца strDest
Почему же эти функции имеют такое странное поведение?

Вернитесь назад во времени в ранние дни UNIX. Лично я заведу свою машину времени на времена System V. В System V имена файлов могли иметь длину до 14 символов. Любые имена длиннее усекались до 14-ти символов. А поле для хранения имени файла на диске было ровно 14 байт. Не 15. Терминатор только подразумевался. Это экономило один байт.

Вот несколько имён файлов и их представления в каталоге:
passwd
passwd\0\0\0\0\0\0\0\0
newsgroups.old
newsgroups.old
newsgroups.old.backup
newsgroups.old
Заметьте, что newsgroups.old и newsgroups.old.backup фактически являются одинаковыми файлами из-за усечения. Длинные имена усекались автоматически, втихую, без какого-либо сообщения об ошибке. Это исторически было источником множества ошибок непреднамеренной потери данных.

Функция strncpy использовалась системой для хранения имён файлов в каталоге на диске. Это объясняет часть странного поведения этой функции, а именно: почему она не добавляет нуль-терминатор к результату. Потому что терминатор неявно подразумевается концом массива символов (это также объясняет молчаливое усекание имён файлов).

Но зачем дополнять нулями короткие имена?

Потому что это упрощает поиск имён (делает его быстрее). Если вы гарантируете, что все "мусорные байты" будут нулями, то вы можете использовать функцию memcmp для их сравнения.

По соображениям совместимости, комитет языка C решил сохранить это причудливое поведение strncpy.

Так что насчёт заголовка сегодняшнего поста? Как может код для предотвращения переполнения буфера в итоге вызывать его?

Вот один пример (к сожалению, я не понимаю японский, так что я действую, исходя только из кода). Заметьте, что он использует _tcsncpy для заполнения lpstrFile и lpstrFileTitle, при этом осторожно следя за размерами буферов. Это прекрасно, но это также приводит к отсутствию нуль-терминатора, если строка слишком длинна. Вызывающий вполне может потом скопировать этот буфер в какой-то другой. Но в lstrFile отсутствует терминатор, поэтому он превышает длину, указанную вызывающим. Результат: переполнение другого буфера.

А вот ещё один пример. Заметьте, что функция использует _tcsncpy для копирования результата в выходной буфер. Автор был осведомлён о причудливом поведении семейства функций strncpy, поэтому он вручную добавил терминатор в конец буфера.

Но что если ccTextMax = 0? Тогда попытка записи терминатора обратится к символам до начала массива, затирая случайный символ.

Какой можно сделать вывод из всего этого?
Ну, лично мой вывод такой: избегать strncpy и всех её друзей, если вы работаете с нуль-терминированными строками. Несмотря на наличие в их имени "str" - эти функции не создают нуль-терминированные строки. Они конвертируют нуль-терминированную строку в символьный raw-буфер. Использование их там, где на выходе ожидается нуль-терминированная строка, просто неверно. Вы не только не получите терминатора в конце строки, но вы также получите бесполезное для вас дополнение нулями, если строка слишком коротка.

суббота, 6 февраля 2010 г.

PulseEvent имеет фундаментальный изъян

Это перевод PulseEvent is fundamentally flawed. Автор: Реймонд Чен.

Функция PulseEvent освобождает один поток (или все потоки, если вы используете событие с ручным сбросом), который ждал событие, после чего возвращает событие в сброшенное (unset) состояние. Если ни один поток не ждал событие, то ничего не происходит.

И в этом-то и есть проблема.

вторник, 2 февраля 2010 г.

Использование волокон для упрощения enumerator-ов, часть 2: когда жизнь проще для вызывающего

Это перевод Using fibers to simplify enumerators, part 2: When life is easier for the caller. Автор: Реймонд Чен.

В последний раз мы увидели, как могла бы быть написанной функция перечисления каталога, если бы спецификацию на неё проектировал человек, пишущий перечислитель. Давайте теперь посмотрим, во что она превратится, если спецификацию будет писать человек, её вызывающий.
type
TEnumFound = (
fefFile, // нашли файл
fefDir, // нашли каталог
fefLeaveDir, // вышли из каталога
fefDone // закончили
);

TDirectoryTreeEnumerator = class
private
// ... тут реализация ...
public
constructor Create(const ADir: String);

function Next: TEnumFound;
procedure Skip;

property CurDir: String read GetCurDir;
property CurPath: String read FPath;
property CurFindData: TSearchRec read GetCurFindData;
end;
С этим дизайном, перечислитель выплёвывает файлы, а вызывающий контролирует его, говоря, когда идти дальше, опционально указывая, что какой-то каталог надо бы пропустить.

Заметьте, что у нас теперь нет аналога кода ferStop. Если вызывающий хочет остановить перечисление - он просто прекращает вызывать метод Next.

С этим дизайном наша тестовая функция будет довольно простой:
procedure TForm1.Button1Click(Sender: TObject);

function TestWalk(const AEnum: TDirectoryTreeEnumerator): UInt64;
var
SizeSelf, SizeAll: UInt64;
Found: TEnumFound;
begin
SizeSelf := 0;
SizeAll := 0;
Result := 0;
repeat
Found := AEnum.Next;
case Found of
fefFile:
SizeSelf := SizeSelf + AEnum.CurFindData.Size;

fefDir:
SizeAll := SizeAll + TestWalk(AEnum);

fefLeaveDir:
begin
SizeAll := SizeAll + SizeSelf;
Memo1.Lines.Add(Format('Size of %s is %u (%u)', [AEnum.CurDir, SizeSelf, SizeAll]));
Result := SizeAll;
end;

fefDone:
Result := SizeAll;
end;
until (Found = fefDone) or (Found = fefLeaveDir);
end;

var
Enum: TDirectoryTreeEnumerator;
begin
Memo1.Lines.BeginUpdate;
try
Enum := TDirectoryTreeEnumerator.Create('.');
try
TestWalk(Enum);
finally
FreeAndNil(Enum);
end;
finally
Memo1.Lines.EndUpdate;
end;
end;
Конечно же, этот вариант дизайна нагружает всей реальной работой реализацию перечислителя. Вместо того, чтобы позволить перечислителю пройтись по дереву, вызывая для каждого найденного элемента функцию (callback) - теперь вызывающий постоянно вызывает Next, и каждый раз перечислителю нужно найти следующий файл и вернуть его. Поскольку перечислитель возвращает управление, то он не может хранить своё состояние в стеке; вместо этого ему нужно эмулировать это вручную.
  TDirectoryTreeEnumerator = class
private
type
TEnumState = (
ES_NORMAL,
ES_SKIP,
ES_FIRST
);
PStackEntry = ^TStackEntry;
TStackEntry = record
Next: PStackEntry;
hFind: Boolean;
SR: TSearchRec;
Dir: String;
end;
var
FCur: PStackEntry;
FES: TEnumState;
FPath: String;
function Push(const ADir: String): PStackEntry;
procedure StopDir;
function Stopped: Boolean;
procedure Pop;
function GetCurFindData: TSearchRec;
function GetCurDir: String;
public
constructor Create(const ADir: String);
destructor Destroy; override;

function Next: TEnumFound;
procedure Skip;

property CurDir: String read GetCurDir;
property CurPath: String read FPath;
property CurFindData: TSearchRec read GetCurFindData;
end;

constructor TDirectoryTreeEnumerator.Create(const ADir: String);
begin
Push(ADir);
end;

destructor TDirectoryTreeEnumerator.Destroy;
begin
while Assigned(FCur) do
begin
StopDir;
Pop;
end;
end;

function TDirectoryTreeEnumerator.GetCurDir: String;
begin
Result := FCur^.Dir;
end;

function TDirectoryTreeEnumerator.GetCurFindData: TSearchRec;
begin
Result := FCur^.SR;
end;

function TDirectoryTreeEnumerator.Push(const ADir: String): PStackEntry;
begin
New(Result);
FillChar(Result^, SizeOf(Result^), 0);
FPath := IncludeTrailingPathDelimiter(ADir);
Result^.Dir := FPath;
Result^.hFind := (FindFirst(Result^.Dir + '*.*', faAnyFile, Result^.SR) = 0);
if Result^.hFind then
begin
Result^.Next := FCur;
FES := ES_FIRST;
FCur := Result;
end
else
begin
Dispose(Result);
Result := nil;
end;
end;

procedure TDirectoryTreeEnumerator.Skip;
begin
FES := ES_SKIP;
end;

procedure TDirectoryTreeEnumerator.StopDir;
var
pse: PStackEntry;
begin
pse := FCur;
if pse.hFind then
begin
FindClose(pse.SR);
pse.hFind := False;
end;
end;

function TDirectoryTreeEnumerator.Stopped: Boolean;
begin
Result := not FCur^.hFind;
end;

procedure TDirectoryTreeEnumerator.Pop;
var
pse: PStackEntry;
begin
pse := FCur;
FCur := pse^.Next;
Dispose(pse);
end;

function TDirectoryTreeEnumerator.Next: TEnumFound;
label
Loop;
begin
Loop:
// Есть что-то искать?
if not Assigned(FCur) then
Exit(fefDone);

// Если нам просто выйти - то делаем pop
if Stopped then
begin
Pop;
FES := ES_NORMAL;
end

// Если каталог принят, то входим в него
else
if (FES = ES_NORMAL) and
((FCur^.SR.Attr and faDirectory) <> 0) then
Push(FPath);

// Ещё файлы в этом каталоге?
if (FES <> ES_FIRST) and
(FindNext(FCur.SR) <> 0) then
begin
StopDir;
Exit(fefLeaveDir);
end;

// Не надо заходить в . или ..
if (FCur^.SR.Name = '.') or
(FCur^.SR.Name = '..') then
begin
FES := ES_SKIP;
goto Loop;
end;

FPath := FCur^.Dir + FCur.SR.Name;

// Возвращаем найденный элемент
FES := ES_NORMAL; // Состояние по-умолчанию
if (FCur^.SR.Attr and faDirectory) <> 0 then
Exit(fefDir)
else
Exit(fefFile);
goto Loop;
end;
Тьфу ты, пропасть! Простая рекурсивная функция превратилась в это ужасное месиво кода по управлению состояниями.

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

Мы построим такую штуку в следующий раз.

понедельник, 1 февраля 2010 г.

Использование волокон для упрощения enumerator-ов, часть 1: когда для enumerator-а жизнь проще

Это перевод Using fibers to simplify enumerators, part 1: When life is easier for the enumerator. Автор: Реймонд Чен.

Модель перечисления COM (перечисления объектов) сделана так, чтобы сделать проще жизнь потребителя (вызывающего) и сложнее - жизнь поставщика (вызываемого). Перечисляемый объект (поставщик) должен быть выполнен как автомат с состояниями (state machine), что может быть весьма обременительным для сложных перечислений - например, пробежке по дереву или составного (composite) перечисления.

С другой стороны, модель с обратным вызовом для поставщика (используемая в большинстве функций Win32) склоняется к простой жизни для вызываемого и сложной для потребителя. В этом случае, именно потребителю нужно поддерживать состояния, что только усугубляется, если потребитель делает в своей функции обратного вызова что-то сложное (даже если и нет: вам всё равно нужно создать контекстную структуру, чтобы передавать её от вызывающего через перечислитель в эту функцию обратного вызова).

Например, предположим, что мы хотим написать код, который проходит по структуре каталога, позволяя вызывающему указать, что делать в каждой точке. Давайте сперва спроектируем код с использованием парадигмы обратного вызова:

type
TEnumResult = (
erContinue, // продолжить перечисление
// (если каталог: войти в него)
erSkip, // пропустить этот файл/каталог
erStop // остановить перечисление
);

TEnumOperation = (
eoFile, // нашли файл
eoDir, // нашли каталог
eoLeaveDir // выходим из каталога
);

type
TFileEnumCallback = function(const AOperation: TEnumOperation; const ADir,
APath: String; const ASearchRec: TSearchRec; const AContext: Pointer): TEnumResult;

function EnumDirectoryTree(const ADir: String; const ACallback: TFileEnumCallback;
const AContext: Pointer): TEnumResult;

Здесь дизайн таков: вызывающий вызывает EnumDirectoryTree и передаёт ей функцию обратного вызова, которая уведомляет о каждом найденном файле и каталоге и определяет, что делать дальше.

Такой подход делает жизнь намного проще для реализации EnumDirectoryTree:

function EnumDirectoryTree(const ADir: String; const ACallback: TFileEnumCallback;
const AContext: Pointer): TEnumResult;
var
Path, Name: String;
SR: TSearchRec;
Operation: TEnumOperation;
begin
Result := erContinue;
Path := IncludeTrailingPathDelimiter(ADir);
if FindFirst(Path + '*.*', faAnyFile, SR) = 0 then
try
repeat
if (SR.Name <> '.') and
(SR.Name <> '..') then
begin
if (SR.Attr and faDirectory) <> 0 then
Operation := eoDir
else
Operation := eoFile;
Name := Path + SR.Name;
Result := ACallback(Operation, ADir, Name, SR, AContext);
if Result = erContinue then
begin
if Operation = eoDir then
begin
Result := EnumDirectoryTree(Name + PathDelim, ACallback, AContext);
if Result = erContinue then
Result := ACallback(eoLeaveDir, ADir, Name, SR, AContext);
end;
end
else
if Result = erSkip then
Result := erContinue;
end;
until (FindNext(SR) <> 0) or (Result = erStop);
finally
FindClose(SR);
end;
end;

Примечание: я даже не пытался как-то оптимизировать эту функцию, поскольку смысл статьи не в этом. Например, она очень сильно ест стековую память (что может быть проблемой при перечислении деревьев каталогов с глубокой вложенностью). Также я не забочусь о точках соединения, которые могут вызвать бесконечную рекурсию.

Ну, написать это было не сложно. Но только потому, что мы сделали тяжелее жизнь потребителя (вызывающего). Потребитель будет вынужден поддерживать своё состояние между каждым вызовом callback-а (функции обратного вызова). Например, предположим, что вам нужно построить список каталогов вместе с их размером(с учётом подкаталогов и без).

type
TEnumState = class
public
constructor Create;
destructor Destroy; override;
function Callback(const AOperation: TEnumOperation; const ADir,
APath: String; const ASearchRec: TSearchRec): TEnumResult;
procedure FinishDir(const ADir: String);
private
type
PDirectory = ^TDirectory;
TDirectory = record
Parent: PDirectory;
SizeSelf: UInt64;
SizeAll: UInt64;
end;
var
FDirCur: PDirectory;
function Push: PDirectory;
procedure Pop;
procedure Cleanup;
end;

constructor TEnumState.Create;
begin
inherited;
New(FDirCur);
FillChar(FDirCur^, SizeOf(TDirectory), 0);
end;

destructor TEnumState.Destroy;
begin
Cleanup;
inherited;
end;

function TEnumState.Push: PDirectory;
begin
New(Result);
FillChar(Result^, SizeOf(TDirectory), 0);
Result.Parent := FDirCur;
FDirCur := Result;
end;

procedure TEnumState.Pop;
var
Dir: PDirectory;
begin
Dir := FDirCur.Parent;
Dispose(FDirCur);
FDirCur := Dir;
end;

procedure TEnumState.Cleanup;
begin
while Assigned(FDirCur) do
Pop;
end;

procedure TEnumState.FinishDir(const ADir: String);
begin
FDirCur^.SizeAll := FDirCur^.SizeAll + FDirCur^.SizeSelf;
Form9.Memo1.Lines.Add(Format('Size of %s is %u (%u)', [ADir,
FDirCur^.SizeSelf, FDirCur^.SizeAll]));
end;

function TEnumState.Callback(const AOperation: TEnumOperation; const ADir,
APath: String; const ASearchRec: TSearchRec): TEnumResult;
begin
if not Assigned(FDirCur) then
Exit(erStop);

case AOperation of
eoFile:
begin
FDirCur^.SizeSelf := FDirCur^.SizeSelf + ASearchRec.Size;
Result := erContinue;
end;
eoDir:
begin
Push;
Result := erContinue;
end;
eoLeaveDir:
begin
FinishDir(APath);

// Выталкиваем размер в родителя
FDirCur^.Parent^.SizeAll := FDirCur^.Parent^.SizeAll +
FDirCur^.SizeAll;
Pop;
Result := erContinue;
end
else
Result := erContinue;
end;
end;

function EnumState_Callback(const AOperation: TEnumOperation; const ADir,
APath: String; const ASearchRec: TSearchRec; const AContext: Pointer):
TEnumResult;
begin
Result := TEnumState(AContext).Callback(AOperation, ADir, APath, ASearchRec);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
EnumState: TEnumState;
begin
EnumState := TEnumState.Create;
try
Memo1.Lines.BeginUpdate;
try
if EnumDirectoryTree('.', EnumState_Callback, Pointer(EnumState)) = erContinue then
EnumState.FinishDir('.');
finally
Memo1.Lines.EndUpdate;
end;
finally
FreeAndNil(EnumState);
end;
end;

Мда, не слабо нам тут пришлось печатать. Что еще хуже - вся структура программы скрыта на фоне явного управления состояниями. Я уверен, что тяжело с первого раза сказать, что делает программа, взглянув на этот код. Вместо этого, вам нужно глазеть на класс TEnumState и вкапываться в его код, пытаясь понять, что он делает.

Завтра мы посмотрим, как выглядел бы мир, если бы акценты функции EnumDirectoryTree были смещены в противоположном направлении.