воскресенье, 26 апреля 2009 г.

Почему папка Ссылки (Links) всё время себе пере-создаёт?

Это перевод Why does the Links folder keep re-creating itself? Автор: Реймонд Чен.

Те из вас, кому не нравится папка Ссылки в избранном, уже, вероятно, пробовали её удалять, но это привело только к тому, что вы обнаружили, что папка постоянно возвращается обратно. Почему это?

Это Internet Explorer пытается делать само-починку (auto-repair). Он замечает, что папки Ссылки нет и думает: "чёрт, наверное её кто-то повредил! Лучше бы мне исправить эту проблему, пересоздав её".

Люди жалуются, что их компьютеры не могут починить сами себя, но когда программы пытаются это делать, людям это не нравится и это тоже: "но я хочу, чтобы это оставалось сломанным!" Вы просто не можете выиграть.

Способ указать "Да, я знаю про папку Ссылки, но я не хочу использовать её" - это скрыть её.

Это весьма похоже на проблему, с которой встречаются некоторые люди в Диспетчере устройств (Device Manager). Они не хотят, чтобы Windows использовала какую-то железку, поэтому они открывают Диспетчер устройств и удаляют её. А потом она опознаётся заново и добавляется обратно.

Это потому, что когда вы удаляете устройство, вы говорите: "забудь всё, что ты знаешь об этом девайсе". Потом, когда устройство обнаруживается, Windows говорит: "дьявол, у меня тут железка, которую я никогда не видела раньше! Наверное, пользователь недавно её купил. Давай-ка я добавлю её в список устройств, чтобы пользователь смог воспользоваться ей".

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

Если вы хотите, чтобы Windows не использовала какое-то устройство, то откройте Диспетчер устройств и просто отключите его, а не удаляйте. Это означает: "Да, я знаю, что у меня это есть, но я не хочу его использовать".

В чём разница между потоко-безопасностью (thread-safety) и повторной входимостью (re-entrancy)?

Это перевод The difference between thread-safety and re-entrancy. Автор: Реймонд Чен.

Это две связанные, но не идентичные концепции.

Операция является "потоко-безопасной", если она может быть безопасно вызывана из многих потоков, даже если вызов получился одновременных в двух разных потоках.

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

Рассмотрим такую функцию:
var
S: PChar = nil;
Len: Integer = 0;

// Примечание: т.к. строки заканчиваются нулём,
// то если мы хотим добавить 0, мы записываем его
// как '\'#0, а сам '\' записываем как '\\'.

// ВНИМАНИЕ! Этот код - бажный, не используйте!

procedure AddToString(const ch: Char);
var
newString: PChar;
begin
EnterCriticalSection(someCriticalSection);
try
// +1 для добавляемого символа
// +1 для терминатора
newString := ReallocMem(S, (Len + 1) * SizeOf(Char));
if (ch = #0) or (ch = '\') then
AddToString('\'); // escape-префикс
newString[Len] := ch;
Inc(Len);
newString[Len] := #0;
S := newString;
finally
LeaveCriticalSection(someCriticalSection);
end;
end;
Эта функция является потоко-безопасной, потому что критическая секция не даёт двум потокам одновременно добавлять символ в строку. Однако, она не является повторно-входимой.

Внутренний вызов AddToString происходит в тот момент, когда структуры данных находятся в нестабильном состоянии. В момент вызова мы можем повторно войти в функцию AddToString (когда ch равен #0 или '\'), и хотя критическая секция даст нам это сделать, но в этот раз мы попытаемся вызвать ReallocMem для указателя S, который сейчас уже неверен - он устарел в момент вызова ReallocMem в вызывающем (первом вызове AddToString).

Когда SHLoadInProc выгружает DLL?

Это перевод When does SHLoadInProc unload a DLL? Автор: Реймонд Чен.

Функция SHLoadInProc просит оболочку (Explorer) загрузить экземпляр конкретного CLSID. Это приводит к загрузке DLL, ответственной за этот CLSID.

Но когда эта DLL выгружается? (прим. пер.: ок, сейчас в MSDN уже добавили ответ на этот вопрос, но раньше его, видимо, не было)

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

Посмотрите: объект создаётся вызовом CoCreateInstance и сразу же освобождается. Вот и всё. Explorer теперь совершенно не волнует ваша DLL.

Можете ли вы, теперь с такой подсказкой, ответить на вопрос: когда выгружается DLL?

Всё ещё не знаете? Вот другая подсказка: теперь этот вопрос уже не в компетенции оболочки. Теперь это вопрос COM.
Когда выгружается любая DLL, загруженная CoCreateInstance?


Ответ: DLL периодически опрашивается, безопасно ли её сейчас выгружать. Как только DLL ответит утвердительно (с помощью S_OK), COM выгрузит её.

Какой смысл у параметра hPrevInstance в WinMain?

Это перевод What was the purpose of the hPrevInstance parameter to WinMain? Автор: Реймонд Чен.

Когда запускается ваша GUI-программа, её выполнение начинается с функции WinMain. Второй параметр, hPrevInstance, всегда равен нулю в Win32. Наверное, у него когда-то тоже был смысл?

Ну конечно же был.

В 16-ти битных Windows была функция GetInstanceData. Эта функция принимала аргументы типа HINSTANCE, указатель и размер буфера и копировала память из указанного экземпляра в ваш экземпляр (в какой-то мере, это 16-ти битный эквивалент функции ReadProcessMemory, только с ограничением, что второй и третий параметр должны быть одинаковыми).

Поскольку в 16-ти битных Windows все приложения выполнялись в единственном адресном пространстве, то функция GetInstanceData была просто обёрткой к hmemcpy, и многие программы использовали это и просто вызывали hmemcpy напрямую, вместо использования документированных API. На самом деле, Win16 был спроектирован с возможностью навязывания раздельного адресного пространства в будущих версиях Windows (обратите внимание на флаг GMEM_SHARED), но повсеместное злоупотребление трюками типа прямого вызова hmemcpy свело эти возможности к нулю.

В этом была причина аргумента hPrevInstance в WinMain. Если hPrevInstance был не равен нулю (non-NULL), то это был описатель экземпляра (instance handle) уже запущенной копии вашей программы. Вы могли использовать функцию GetInstanceData для копирования с него данных, и быстрее запуститься. Например, вы могли скопировать описатель главного окна с предыдущего экземпляра, так, что вы могли взаимодействовать с ним.

Когда hPrevInstance был равен нулю (NULL) - это говорило вам о том, что вы запущены первыми. В 16-ти битных Windows только первый экземпляр программы регистрировал свои оконные классы; второй и последующие использовали классы, которые уже были зарегистрированы первым экземпляром (в самом деле, если бы они попробовали бы провести повторную регистрацию - она бы не удалась, т.к. такие классы уже существовали). Поэтому все 16-ти битные программы Windows пропускали регистрацию классов, если параметр hPrevInstance бы отличен от нуля.

Люди, которые проектировали Win32, встали перед проблемой, когда пришло время портировать WinMain: что передавать в hPrevInstance? В конце концов, вся эта путаница с module/instance более не существовала в Win32, а раздельные адресные пространства означали, что программы не могут пропускать инициализацию, иначе они не будут работать. Поэтому в Win32 стали всегда передавать NULL, заставляя все программы верить, что они - первый экземпляр.

И, удивительно, но это на самом деле сработало.

суббота, 18 апреля 2009 г.

Разработка управляемая исключениями

Это перевод Exception-Driven Development. Автор: Jeff Atwood.

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


пятница, 17 апреля 2009 г.

В чём разница между HINSTANCE и HMODULE?

Это перевод What is the difference between HINSTANCE and HMODULE? Автор: Реймон Чен.

Сегодня это одно и то же, но когда-то они обозначали разные вещи.

Это пришло к нам из 16-ти битных Windows.

В те дни, "модуль" (module) представлял файл на диске, который был загружен в память, и этот модуль имел "описатель" (handle), который указывал на структуру данных, описывающую части файла, откуда они взялись, откуда были загружены в память (если были загружены). С другой стороны, "экземпляр" (instance) представлял собой "набор переменных".

Аналогия, которая может пролить свет на это дело (или нет?): "модуль" - это как-бы код для класса в языке программирования; он описывает, как создать объект, как реализованы его методы, он описывает, как ведут себя объекты этого класса. С другой стороны, "экземпляр" - это как-бы конкретный объект, который является объектом какого-то класса; он описывает состояние конкретного экземпляра этого класса.

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

Вот диаграмма (вспомните, что мы уже обсуждали 16-ти битный HRSRC ранее):

USER32 HMODULEUSER32 HINSTANCE
дескриптор сегмента кодаКод USER32...Данные USER32...
дескриптор сегмента кода(не в памяти)
дескриптор сегмента кодаКод USER32...
дескриптор сегмента данных
HRSRC(не в памяти)
HRSRCРесурсы USER32...
HRSRC(не в памяти)
таблица экспорта

В 16-ти битных Windows, все программы запускались в единственном адресном пространстве, и если DLL использовалась пятью программами, то она загружалась в память только один раз. В частности, она имела только одну копию своего сегмента данных в памяти (в терминах языка программирования, DLL была синглтоном).

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

В терминах гиков: был только один "экземпляр" (instance) DLL в системе.

С другой стороны, если вы запускали две копии Блокнота, каждая из них получала свой собственный набор переменных - это были два разных "экземпляра":

HMODULE БлокнотаHINSTANCE
дескриптор сегмента кодаКод Блокнота...Данные Блокнота...
дескриптор сегмента кода(не в памяти)
дескриптор сегмента данныхHINSTANCE
HRSRC(не в памяти)Данные Блокнота...
HRSRCРесурсы Блокнота...

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

Дескрипторы "экземпляров" (instance) в диаграммах выше являются сегментами данных.

Запущенные программы идентифицировались по их instance handle. Вы не могли использовать module handle, потому что две копии Блокнота имели один и тот же module handle (поскольку один и тот же код выполняется в обоих копиях). Штука, которая их отличала друг от друга - это их собственный набор переменных.

Вот почему функции WinExec и ShellExecute возвращают HINSTANCE: это рудимент 16-ти битных Windows, где HINSTANCE использовались для идентификации запущенных программ.

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

Когда пришло время проектировать Win32, возник вопрос: "что нам делать с HINSTANCE и HMODULE для Win32?". Поскольку программы теперь выполняются каждая в своём адресном пространстве, вам не нужно делать instance handle видимыми между процессами. Поэтому дизайнеры сделали единственную вещь, которая у них была: базовый адрес модуля. Это аналог HMODULE, поскольку файловый заголовок описывает содержимое файла и его структуру. Но он же является аналогом HINSTANCE, потому что данные хранятся в сегменте данных, который теперь проецируется в адресное пространство процесса напрямую.

Поэтому в Win32, HINSTANCE и HMODULE являются просто базовыми адресами модуля.

Завтра, я поговорю об этом загадочном параметре hinstPrev в WinMain.

Скрытое влияние на производительность "фигурных" окон

Это перевод A hidden performance cost of regional windows. Автор: Реймонд Чен.

Фигурные окна (т.е. окна, полученные с помощью регионов; regional windows) довольно приятны, но это не достаётся за бесплатно. Большинство расходов вы можете видеть явно. Например, постоянное изменение региона очевидно создаёт нагрузку, потому что вы должны сидеть и постоянного создавать новые регионы.

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

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

На самом деле, довольно значительно.

Проверка на входимость (hit-testing) - это одна из самых часто выполняемых оконным менеджером операций. По заданной точке на экране нужно найти окно, которому она соответствует. Чтобы ускорить этот процесс, для быстрого отсекания окон используются прямоугольники окон. Например, если прямоугольник окна будет иметь координаты (0,0)-(100,100), то тогда точка (200,10) лежит, очевидно, вне окна, т.к. её координаты не вписываются в прямоугольник. Такие проверки с прямоугольниками очень быстры.

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

Но Windows использует оконный прямоугольник для быстрого теста, чтобы отсеять ненужные окна и убедиться, что необходимо проверять именно регион. Если точка лежит вне прямоугольника, тогда менеджер окон не будет беспокоиться о проверках региона. Это хорошо, потому что проверки принадлежности точки региону относительно медленны.

Другими словами, псевдо-код для hit-testing делает примерно следующее:
  if точка лежит вне прямоугольника окна then
Result := нет-вхождения
else
if у окна нет региона then
Result := вхождение
else
if точка лежит внутри региона then
Result := вхождение
else
Result := нет-вхождения;
Так что, если вы создадите огромное окно с крошечным регионом, менеджер окон не сможет применять (быстрый) тест прямоугольников. Ему придётся пойти по третьей ветке - (медленной) проверке региона.

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

Ускоряем добавление элементов в ComboBox или ListBox

Это перевод Speeding up adding items to a combobox or listbox. Автор: Реймонд Чен.

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

Во-первых, вы можете отключить перерисовку, пока вы добавляете элементы (вообще-то, это работает для любых элементов управления):
  SetWindowRedraw(Handle, FALSE);
try
... добавляем элементы ...
finally
SetWindowRedraw(Handle, True);
InvalidateRect(Handle, nil, True);
end;
Это подавит обновление контрола при каждом добавлении нового элемента (*). Но есть и ещё кое-что, что можно сделать:

  SendMessage(hwndCombo, CB_INITSTORAGE, cItems, cbStrings);
// для ListBox используйте LB_INITSTORAGE
... добавляем элементы ...
cItems - это число элементов, которые вы собираетесь добавить, а cbStrings это суммарное кол-во памяти (в байтах), необходимое для хранения всех строк, что вы хотите вставить.

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

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

Очевидное ограничение LockWindowUpdate: только одно окно может быть заблокировано в каждый момент времени. Так что, если два окна попытаются одновременно использовать трюк с LockWindowUpdate, то получится это только у одного из них. Это не слишком-то надёжно, не так ли?

LockWindowUpdate была спроектирована, чтобы помогать при перерисовке во время операций drag/drop. Если вы рисуете курсор для операции drag/drop, вы не хотите, чтобы окно под курсором начало рисоваться (и портить ваш прекрасный курсор). Поэтому вы блокируете перерисовку окна, пока вы рисуете курсор и разблокируете его, когда курсор покидает окно.

Вот почему может быть только одно заблокированное окно: в каждый момент времени может быть только одна drag/drop операция, потому что у нас есть только одна мышь.

Примечания переводчика:
(*) В Delphi, если вы используете VCL, то эта функциональность уже заключена в базовые классы. Делается это так:
  ListBox.Items.BeginUpdate;
try
... добавляем элементы ...
finally
ListBox.Items.EndUpdate;
end;

Когда поток может принимать сообщения?

Это перевод When can a thread receive window messages? Автор: Реймонд Чен.

Каждый, кто игрался с оконными сообщениями, знают, что функции GetMessage и PeekMessage получают сообщения с очереди, которые распределяются (dispatch) окнам через DispatchMessage (прим. пер.: исправлен перевод "dispatch" с "отправки" на "распределение" по совету odysseos, старый вариант перевода был просто ужасен, т.к. вносит путаницу с "отправка"/"send" и "посылка"/"post", но другого варианта мне тогда в голову не пришло).

Большинство людей также знают, что GetMessage и PeekMessage также распределяют (dispatch) сообщения не из очереди (все ожидающие (pending) сообщения вне очереди отправляются, когда возвращается первое сообщение из очереди).

Но, видимо, не много людей осознают, что SendMessage также распределяет (dispatch) сообщения!

Если какой-то поток T1 посылает (send) сообщение окну, которое принадлежит другому потоку T2, отправляющий поток T1 засыпает, пока принимающий поток не ответит на сообщение. Но если кто-то ещё отправит (send) сообщение потоку T1, то поток T1 проснётся для обработки этого сообщения, а затем снова заснёт.

Почему так происходит?

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

Например, поток T1 может отправить (send) сообщение, говорящее: "Ну-ка, перечисли мне все X, которые ты знаешь". Тогда поток T2 отправит (send) сообщение потоку T1, говоря: "Вот мой X", а затем ещё одно: "А вот ещё один X", и т.д., пока он не перечислит все X. И только после этого он закончит обработку сообщения и вернёт управление.

Поток T1 теперь знает, что, когда ему вернули управление после обработки исходного сообщения, он уже имеет полный список X-ов от потока T2.

Этот механизм вперёд-назад (back-and-forth) является основой для обнаружения службы DDE (DDE service discovery; прим. пер.: не понимаю в DDE, поэтому мог перевести неверно).

Другой пример: поток T1 отправляет сообщение потоку T2, а потоку T2 нужно спросить дополнительную информацию у потока T1, прежде чем вернуть ответ. Это не так странно, как может сперва показаться. Вы наверняка делали что-то похожее, хоть и не осознавали этого, когда вы отвечали на сообщение WM_NOTIFY отправкой сообщения элементу управления, который отправил исходное сообщение (например, вы можете ответить на LVN_ITEMACTIVATE ответной отправкой LVM_GETITEM для получении информации об элементе, который был активирован).

Поэтому помните: каждый раз, когда вы отправляете (send) сообщение, у вас появляется потенциальная возможность повторной входимости (re-entrancy).

Примечание переводчика: после изменения перевода слова dispatch, этот пост воспринимается полегче, но я всё равно решил оставить старый поясняющий пример:
// что-то делаем...
SendMessage(...); 
// что-то делаем ещё
Суть в том, что в вышеприведённом куске кода вы не можете гарантировать, что ваш поток выполнит строго написанные действия: что-то + SendMessage + ещё что-то. В момент, когда вы вызвали SendMessage и ждёте ответа, вам могут быть отправлены сообщения, которые будут обработаны внутри этого же вызова SendMessage! (речь идёт только об обратной отправке тоже через SendMessage, сообщение от PostMessage уйдёт в очередь). Иными словами, вышеприведённый код является в каком-то смысле аналогом такого кода (это очень грубая аналогия, поскольку Send <> Post):
// что-то делаем
PostMessage(...);
while not MessageProcessingCompleted do // пока сообщение, отправленное PostMessage, не обработано...
Application.ProcessMessages;          // обрабатываем входящие сообщения
// что-то делаем ещё

понедельник, 13 апреля 2009 г.

Не недооценивайте игру Deer Hunter

Это перевод Do not underestimate the power of the game Deer Hunter. Автор: Реймонд Чен.

В процессе подготовки Windows XP Service Pack 2 Beta в декабре прошлого года, был составлен список из пяти багов, которые команда подготовки релиза сочла настолько серьёзными, что они хотели не выпускать бету, пока эти баги не будут исправлены.

воскресенье, 12 апреля 2009 г.

Страшная и ужасная потоковая модель "main"

Это перевод The dreaded "main" threading model. Автор: Реймонд Чен.

При отсутствии явного указания потоковой модели для вашего COM-объекта, вы получите модель по-умолчанию - "main". Потоковая модель "main" мало известна, и это хорошо. Это пережиток прошлого.

Что означает SHGFI_USEFILEATTRIBUTES?

Это перевод What does SHGFI_USEFILEATTRIBUTES mean? Автор: Реймонд Чен.

В функцию SHGetFileInfo одним из флагов вы можете передать SHGFI_USEFILEATTRIBUTES. Что означает этот флаг?

Он означает: "не трогай диск. Представь, что файл/каталог существует и имеет те атрибуты, что я передал в параметре dwFileAttributes. Делай это, не проверяя, существует ли файл на самом деле".

Вы можете использовать этот флаг для получения иконки типа файла. Например, чтобы получить иконку для текстовых файлов, передайте в имени файла "x.txt", флаг SHGFI_USEFILEATTRIBUTES в uFlags и укажите FILE_ATTRIBUTE_NORMAL в файловых атрибутах.

Почему имя файла "x.txt"? Потому что вам нужен файл с расширением ".txt", и имя файла должно быть допустимым (не передавайте неверных имён файлов типа "???.txt").

Почему файловые атрибуты равны FILE_ATTRIBUTE_NORMAL? Потому что вы хотите, чтобы SHGetFileInfo притворилась, что это обычный файл (если бы вы передали, скажем, FILE_ATTRIBUTE_DIRECTORY, то вы получили бы иконку папки - потому что вы сказали функции представить, что вы получаете информацию о папке с именем "x.txt").

Заметьте, что поскольку вы просите функцию SHGetFileInfo "притворяться", то этим вы теряете некоторые возможности.

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

четверг, 9 апреля 2009 г.

Моя первая угроза смерти

Это перевод My first death threat. Автор: Реймонд Чен. Входит в книгу The Old New Thing.

Вот реальное сообщение, отправленное нам через web-сайт Microsoft много лет назад.

Удалён, но не забыт

Это перевод Deleted but not yet forgotten. Автор: Реймонд Чен.

Ранее я обсуждал взаимодействие различных флагов FILE_SHARE_*, потом Ларри Остерман вдохновился этим и написал историю этих флагов.

Если файл был открыт с разрешением на удаление, и вы удаляете файл, то файл на самом деле не пропадает из каталога, пока его дескриптор не будет закрыт. В это время файл болтается как зомби (zombie) (в Unix удалённый файл с открытыми дескрипторами удаляется из каталога и просто плавает в счастливом море безымянных inode-ов).

Почему же в Windows файл не исчезает?

Ну, первая причина: драйвер может запросить имя файла по открытому дескриптору (прим. пер.: а в Vista даже появилась аналогичная функция для приложений: GetFileInformationByHandleEx, даже для предыдущих версий Windows есть официальный путь получения имени: Obtaining a File Name From a File Handle). Если бы запись в каталоге была бы удалена, тогда не было бы никакого имени, которое можно было бы вернуть! (Что вы ожидали бы получить от GetModuleFileName, если модуль был бы удалён? Возможно ли для GetModuleFileName проваливаться с ошибкой ERROR_FILE_NOT_FOUND?)

Вторая причина - если бы питание компьютера отключилось бы в тот момент, когда у вас есть "удалённый, но не забытый" файл, то его кластеры на диске оказались бы забытыми.

И последняя причина: "ожидающий удаления" файл вовсе не исчезает безусловно. Драйвер может "анти-удалить" (undelete) файл, если он очистит флаг удалить-при-закрытии!

среда, 8 апреля 2009 г.

Но почему биты называются FILE_SHARE_READ и FILE_SHARE_WRITE?

Это перевод Why is it FILE_SHARE_READ and FILE_SHARE_WRITE anyway? Автор: Ларри Остерман.

Пост Реймонда о битах FILE_SHARE_* напомнил мне историю о том, откуда вообще взялись биты типа FILE_SHARE_READ.

В MS-DOS была такая же семантика разделения файлов (file sharing semantics) как и в NT (ok, NT добавляет ещё флаг FILE_SHARE_DELETE - подробнее об этом позже). Но на MS-DOS семантика разделения файлов опциональна - вы должны были запустить утилиту share.com, чтобы использовать её. Это объясняется тем, что на однозадачной операционной системе всегда работает только одно приложение, поэтому семантика разделения файлов не нужна. Если только вы не использовали файловый сервер - в этом случае Microsoft настоятельно рекомендовала запускать утилиту.

В MS-DOS режим разделения контролировался тремя битами "режим разделения" (“sharing mode”). Документированными значениями для "режима разделения" были:
000 – Режим совместимости. Любой процесс может открыть файл с этим режимом любое кол-во раз. Открытие файла будет неудачно, если файл уже был открыт в любом другом режиме.
001 – Запретить всё. Открытие будет неудачно, если файл был открыт в режиме совместимости, для чтения или для записи. Даже если файл был открыт текущим-же процессом.
010 – Запретить запись. Открытие будет неудачным, если файл был открыт в режиме совместимости или для записи любым процессом.
011 – Запретить чтение. Открытие будет неудачным, если файл был открыт в режиме совместимости или для чтения любым процессом.
100 – Ничего не запрещать. Открытие будет неудачным, если файл был открыт в режиме совместимости любым процессом.
Вместе с битами "режима разделения" пристыковывались биты "кода доступа" (“access code”). Для них было всего три законных значения: Чтение, Запись и оба сразу (Чтение/Запись).

Дизайнеры набора Win32 API (в частности, дизайнер подсистемы ввода-вывода) как только взглянули на эти флаги - так сразу же вскинули руки в отвращении. По его мнению, с этими определениями есть две большие проблемы:
  1. Из-за того, что биты разделения определяются как отрицание чего-либо, то становится очень сложно определить, что же в итоге будет разрешено и запрещено. Если вы открываете файл для записи в режиме запрета чтения: что при этом произойдёт? Как насчёт режима запрета записи – он допускает чтение или нет?
  2. Из-за того, что режимом по-умолчанию является режим “совместимости”, это означает, что по-умолчанию большинство приложений не могут гарантировать согласованность своих данных. Вместо того, чтобы быть защищённым по-умолчанию, вам нужно делать ещё какие-то доп. действия, чтобы гарантировать, что никто больше не трогает ваши данные.
Поэтому дизайнер подсистемы ввода-вывода предложил инвертировать семантику битов разделения доступа. Вместо того, чтобы биты запрещали доступ, они теперь РАЗРЕШАЮТ доступ. Вместо того, чтобы по-умолчанию разрешать доступ, теперь по-умолчанию доступ будет запрещён. Приложению нужно явно указывать, что оно хочет дать доступ к файлу и другим, пока оно работает с ним.

Это изменение красиво решает кучу проблем, существовавших при одновременном запуске нескольких приложений MS-DOS – если одно приложение работало; другое приложение могло втихую испортить данные, с которыми работало первое.

Итак, мы можем объяснить, что флаги FILE_SHARE_READ и FILE_SHARE_WRITE являются более понятной и безопасной версией разрешений доступа DOS. Но что насчёт FILE_SHARE_DELETE? Откуда, чёрт возьми, он взялся? Ну, он был добавлен для Posix-совместимости. В подсистеме Posix, так же, как и в *nix, файл может быть удалён (unlinked), пока он всё ещё открыт. Кроме того, когда вы переименовываете файл в NT, операция переименования открывает исходный файл для доступа удаления (в конце концов, операция переименования - это создание нового файла в каталоге-назначении и удалении файла из старого места).

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

Загрузчик NT пользуется этим флагом – когда он открывает DLL или приложение для выполнения, он указывает FILE_SHARE_DELETE. Это означает, что вы можете переименовать исполняемый файл запущенного приложения (или загруженной DLL). Это может оказаться полезным, если вы хотите обновить DLL, которая сейчас используется приложением (почему нельзя использовать для этого удаление файла). Я всегда так делаю, когда работаю с winmm.dll. Т.к. winmm.dll используется кучей процессов в системе (включая те, которые нельзя остановить), я не могу просто закрыть все процессы, которые её используют, поэтому, вместо этого, когда мне нужно протестировать новый экземпляр winmm, я просто переименовывая winmm.dll в winmm.old, копирую новый экземпляр и перезагружаю машину (прим. пер.: если проверять нужно в новом приложении, то перезагружать не обязательно - LoadLibrary загрузит именно новую копию).

Читать далее.

BOOL, Boolean и Integer

Это перевод BOOL, Boolean and Integer. Автор: Christian Wimmer.

Кто-то может подумать, что типы BOOL и Boolean являются одним и тем же типом в Delphi, т.к. они могут содержать только true и false. Верно ведь? И да и нет. Они кажутся одинаковыми, но есть небольшая разница в трактовке этих типов при присваивании и сравнении. Чтобы увидеть разницу, нам нужно открыть окно CPU-отладчика и копнуть ассемблерный код. Не закрывайте страничку - это действительно не так сложно понять.

вторник, 7 апреля 2009 г.

Почему 8-ми битовая страница по-умолчанию называется "ANSI"?

Это перевод Why is the default 8-bit codepage called "ANSI"? Автор: Реймонд Чен.

Ben Hutchings хотел узнать, почему 8-ми битовая кодовая страница по-умолчанию называется "ANSI", хотя в действительности она не является ANSI.

Сортировка строк сегодня больше не выполняется по ASCII-кодам

Это перевод String sorting is not done by ASCII code any more. Автор: Реймонд Чен.

Если вы наизусть помните ASCII-таблицу - это ещё не означает, что вы знаете, как работает сортировка.

Как биты FILE_SHARE_* взаимодействуют с запрашиваемыми битами доступа?

Это перевод How do the FILE_SHARE_* bits interact with the desired access bits? Автор: Реймонд Чен.

На самом деле, это не так сложно. Если вы вставляете, скажем, FILE_SHARE_READ, то вы говорите: "Я не возражаю, если другие люди будут читать этот файл, пока я его держу открытым". И если вы не устанавливаете этот флаг, тогда вы говорите: "Я не хочу, чтобы другие люди читали данный файл, пока я держу его открытым".

Теперь все, что осталось сделать - это понять, что это означает.

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

Вот и все.

Конечно, если файл уже был открыт, то соответствующая проверка проводится между запрашиваемым вами доступом и режимом совместного использования файлов от тех людей, кто уже открыл его. Например, если кто-то уже открыл файл с запретом чтения, а вы пытаетесь открыть его для чтения, то вы получите sharing violation.

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

Повторите вышеуказанную логику для разрешений на "delete" и "write" - и, в двух словах, это всё.

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

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

Заметьте, что задаваемые вами биты маски общего доступа не обязаны совпадать с битами запрашиваемого доступа к файлу. Биты маски общего доступа (file share bits) указывают на то, что вы хотите позволить делать другим людям. Биты маски запроса доступа (access bits) указывают, что вы хотите сделать сами.

Читать далее.

Есть два типа полос прокрутки

Это перевод There are two types of scrollbars. Автор: Реймонд Чен.

Не забывайте, что есть два типа полос прокрутки.

Первый тип - это автономный элемент управления полосой прокрутки (standalone scrollbar control). Он имеет свой собственный оконный дескриптор (window handle), и, следовательно, ему можно передать фокус и вообще делать кучу забавных вещей, которые вы можете делать с обычными оконными описателями. Для управления им - передайте его собственный описатель в соответствующую функцию управления (например, в SetScrollInfo) и укажите SB_CTL в параметре nBar, чтобы указать, что у вас есть элемент управления "полоса прокрутки".

Второй тип - это горизонтальная или вертикальная полоса прокрутки (или обе), прикреплённые к окну с помощью установки ему стиля WS_HSCROLL и/или WS_VSCROLL. Эти полосы прокрутки в не клиентской области окна не являются элементами управления (controls). Это просто дополнительная декорация для какого-то другого окна. Вы не можете передать им фокус, потому что они не являются самостоятельными окнами. Чтобы управлять ими - передавайте описатель окна, которое содержит их, в соответствующую функцию управления и указывайте SB_HORZ или SB_VERT в параметре nBar, чтобы указать, что вы хотите управлять не клиентской горизонтальной или вертикальной полосой прокрутки.

Я пишу это потому, что многие люди, похоже, не делают различий между двумя этими типами полос прокрутки.