Интерфейс пользователя + многопоточный апартмент = смерть

Это перевод User interface code + multi-threaded apartment = death. Автор: Реймонд Чен.

Существуют однопоточные апартменты (single-threaded apartment) и многопоточные апартменты (multi-threaded apartment). Ну, сначала существовали только однопоточные… Нет, давайте начнём по-другому.

Сначала в приложениях был только один поток. Вспомните, в 16-битной Windows не было потоков. Каждый процесс, программа и были тем, что сегодня мы называем потоком — тут и сказочке конец. Совместимость с этой древней моделью существует и сегодня — в виде потоковой модели «main». И чем меньше мы про неё будем говорить, тем лучше.

Поэтому поехали дальше: OLE был разработан в те же времена, поэтому он использовал оконные сообщения, чтобы передавать информацию между процессами. В те времена не было никакого иного межпроцессного механизма взаимодействия (IPC). Когда вы инициализировали OLE, он создавал секретное окно OleMainThreadWnd, эти окна использовались для взаимодействия между процессами (а в Win32 — потоками). Как мы узнали ранее, оконные описатели имеют привязку к потоку, что означает, что эти секретные окна для коммуникации также имеют привязку к потоку, что означает, что OLE имеет привязку к потоку. Когда вы делаете вызов объекта из другого апартмента, OLE отправляет сообщение в секретное окно OleMainThreadWnd потока-владельца, сообщая, что нужно сделать объекту, а затем входит в цикл обработки сообщений, ожидая завершения работы (выполняемой потоком-владельцем) и обратной отправки результатов.

В это время команда разработки OLE осознала, что они в действительности делали сразу две вещи. Во-первых, они сделали низкоуровневое управление объектами и интерфейсами (IUnknown, CoMarshalInterThreadInterfaceInStream и т.п.), во-вторых, они сделали высокоуровневое связывание и внедрение объектов (IOleWindow, IOleDocument и т.п.) — что и было изначальной задачей для OLE. Поэтому низкоуровневый слой был отделён в отдельную технологию — COM, а весь высокоуровневый код сохранил имя OLE.

Разделение всего кода на два уровня позволило использовать низкоуровневый код невизуальным программам, которые усердно присматривали себе функциональность по управлению объектами. В результате COM вырастил две «личности»: одна направлена на клиентов GUI, а вторая — на не-GUI. Для не-GUI программ в COM добавили дополнительную функциональность, например, многопоточные апартменты. Поскольку эти программы не обрабатывали GUI, то многопоточные апартменты не были стеснены ограничениями и правилами GUI. Они не отправляли сообщения для взаимодействия друг с другом; они использовали объекты IPC ядра и вызывали WaitForSingleObject. Все только выиграли, или нет?

Ну, да, все выиграли, но вам лучше бы знать, с какой стороны намазан ваш бутерброд. Если вы инициализируете GUI-поток как многопоточный апартмент, то вы нарушаете предположения, для которых многопоточные апартменты были созданы изначально! Многопоточные апартменты предполагают, что вы не запускаете их в GUI-потоках, поскольку эти апартменты не выполняют цикл прокачки оконных сообщений; они просто ждут через WaitForSingleObject. Это не только тормозит широковещательные рассылки, но также блокирует интерфейс вашей программы. Поток, который владеет объектом, может попытаться отправить вашему потоку сообщение, но ваш поток не сможет его обработать, поскольку вместо цикла прокачки сообщений он сидит внутри WaitForSingleObject.

Вот почему COM-объекты, которые связаны с интерфейсом пользователя, почти всегда требуют однопоточный апартмент. И вот почему OleInitialize инициализирует однопоточный апартмент. Потому что многопоточные апартменты были созданы в предположении, что в потоке нет интерфейса пользователя. Как только вы начинаете обрабатывать интерфейс пользователя, вам нужно использовать однопоточный апартмент.

Читать на сайте автора.

Что если две программы сделают это? Практический экзамен

Это перевод What if two programs did this? Practical exam. Автор: Реймонд Чен.

У клиента, который не читает этот блог, есть следующий вопрос.

У нашего приложения есть требование: оно должно быть поверх всех других окон, даже если показываются окна со стилем topmost. Для этого в наших сообщениях WM_KILLFOCUS и WM_PAINT мы делаем наше окно topmost, а затем вызываем SetWindowPos, чтобы вынести наше окно поверх остальных, и дополнительно мы вызываем SetForegroundWindow — просто на всякий случай.

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

В настоящее время мы пишем DLL, которая установит ловушку на все оконные сообщения и [дальше идёт перечисление ещё более сумасшедших идей, которые не стоят того, чтобы тратить на них время], но сейчас это приводит к вылету других приложений. Мы также рассматриваем возможность установки таймера и вызова SetWindowPos по таймеру, но это кажется недостаточным.

Этот клиент не провёл мысленный эксперимент Что если две программы сделают это?; он просто взял и сделал это! Результат был предсказуем: две программы стали бороться друг с другом. Никто не выиграл, обе проиграли — и особенно досталось пользователю.

Читать на сайте автора.

В чём разница между каталогами и папками?

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

В Windows 95 появился Проводник Windows (Windows Explorer) и вместе ним — новый термин «папка» (folder). Чем папка отличается от каталога?

Кто-то считает, что Windows 95 просто переименовала каталоги в папки, но это не совсем так.

Проводник Windows позволяет вам просматривать папки, которые являются контейнерами в пространстве имён Оболочки (Shell namespace). Каталоги — это один из видов папок, а именно — папки, которые соответствуют путям в файловой системе. Есть и другие типы папок — например, Панель Управления, Сетевое Окружение или Принтеры. Эти папки представлены объектами в пространстве имён Оболочки, которые не соответствуют никаким файлам в файловой системе. Вообще же, термин «виртуальная папка» применяется для обозначения тех папок, которые не являются каталогами. Другими словами, имеет место такая диаграмма:

Папки

Каталоги
Виртуальные папки = Папки − Каталоги

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

Читать на сайте автора.

Проклятие текущего каталога

Это перевод The curse of the current directory. Автор: Реймонд Чен.

Текущий каталог — это и благо и проклятие. Это благо, потому что экономит вам набор текста (путём использования относительных путей). Это проклятие по всем остальным причинам.

Корнем многих зол является то, что в семейство операционных систем Windows NT держит открытым описатель текущего каталога каждого процесса (упреждающий комментарий от Yuhong Bao: семейство систем Windows 95, напротив, не держало описатель открытым, что порождало кучу других проблем, что, впрочем, никак не связано с темой этого поста).

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

Я пытаюсь удалить каталог X, но при попытке его удалить, я получаю сообщение об ошибке: «Процесс не может получить доступ к файлу, поскольку он открыт в другой программе»… После долгих часов, я нашёл виновника: оказывается, каталог X держится программой someapp.exe. Какого чёрта ей вообще надо в моём каталоге? Как мне удалить каталог?

Конкретное приложение someapp.exe не имеет значения, оно каждый раз разное. Когда такое происходит, пользователи винят someapp.exe.

Но почти во всех случаях, someapp.exe — просто невинная жертва текущего каталога.

Во-первых, рассмотрим отдельно случай, когда someapp.exe — это explorer.exe. Почему текущий каталог Проводника может быть равен этому каталогу?

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

Заметьте, что даже если расширение оболочки попытается сделать всё правильно, то это может не сработать:

GetCurrentDirectory(Old) // возвращает C:Previous
SetCurrentDirectory(New) // меняет на C:Victim
.. что-то делаем ..
SetCurrentDirectory(Old) // меняем на C:Previous - неудачно?!

Второй вызов SetCurrentDirectory может завершиться неудачей, если, к примеру, каталог C:Previous был удалён или переименован, пока расширение оболочки делало свою работу. Расширение Оболочки не может вернуть текущий каталог, так что оно оставляет C:Victim текущим каталогом, и теперь вы не можете удалить C:Victim, потому что он стал текущим каталогом Проводника.

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

Заметьте, что если сделать текущий каталог настройкой потока (перевод поста), а не процесса, то это также не решит проблему (полностью), потому что текущий каталог потока (если бы такая вещь существовала бы) всё равно держал бы открытым описатель текущего каталога. Но если бы текущий каталог был бы настройкой потока, то, по крайней мере, если бы этот поток был ассоциирован с окном, то вы могли бы закрыть окно и, таким образом, завершить поток — что, в свою очередь, освободило бы открытый каталог. Ну если только вы не вызвали Terminate­Thread — в последнем случае описатель текущего каталога просто утёк бы, и ваша «попытка» отпустить каталог только что прогарантировала, что этого никогда не случится (заметка для технологических ипохондриков: весь этот параграф был гипотетическим и поэтому совершенно не годится для решения вашей проблемы).

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

Бонус: Привет, люди. «История ещё не закончена». Воздержитесь от спекуляций в комментариях.

Читать на сайте автора.

Технологические ипохондрики

Это перевод Technology hypochondriacs. Автор: Реймонд Чен.

Я часто замечаю один феномен, который я буду называть «технологическая ипохондрия» — вера в то, что вы страдаете от проблемы (перевод поста), о которой только что прочитали (перевод поста). Для этого есть бородатый анекдот:

К доктору пришёл один мужчина: «Доктор», — сказал он. — «Я просто уверен, что у меня именно эта болезнь. Совпадают все симптомы: у меня усталость, раздражительность и потеря памяти.»

«Вы знаете», — ответил доктор. — «Я же, напротив, просто уверен, что вы не страдаете от менопаузы.»


Один из моих знакомых, врач в обычной больнице, сказал мне, что, исходя из его наблюдений, вам никогда не следует верить диагнозу студентов: «Мы приводим их сюда, показываем пациента и спрашиваем, чем, по их мнению, он болеет. И знаешь что? Они всегда называют то, про что им читали на последней неделе на лекциях».

Если я пишу про то, как программа может зависнуть, это совсем не означает, что именно в этом кроется причина зависания вашей программы. Программа может зависнуть из-за миллиона причин, большинство из которых имеют один и тот же симптом: «Когда я щёлкаю по программе, ничего не происходит». Этого не достаточно, чтобы сделать диагноз. А чтобы сделать диагноз, вам нужно взять отладочный инструмент, подключить его к программе и проанализировать, почему поток, ответственный за UI (как правило — главный), не обрабатывает сообщения (вот, к примеру, как это сделал Марк Руссинович при диагностике задержек запуска процесса; этот случай даже я вижу в первый раз). Прим.пер.: а вот как это делали мы.

Если вы задаёте в комментариях к моим постам вопрос: «Это поэтому моя программа тоже так делает?», то не ждите от меня ответа. Это всё равно что писать письмо в газету для колонки «Здоровье»: «У меня усталость и потеря аппетита. У меня СПИД?».

Читать на сайте автора.

Что это за странные переменные окружения с =C:?

Это перевод What are these strange =C: environment variables? Автор: Реймонд Чен.

Вы не увидите их при выполнении команды SET, но вы увидите их, если получите список переменных окружения сами, программным путём. К примеру, если вы напишете программу, которая получает список всех переменных окружения и выводит их на консоль, и запустите её из командной строки, то увидите странные переменные с именами вроде =C: и значениями, соответствующими каталогам на этих дисках. Что это такое?

Эти переменные являются частью командного интерпретатора cmd.exe. Вот почему я добавил «и запустите её из командной строки» выше — потому что если запустить программу из Проводника или диалога «Выполнить», то вы не увидите эти переменные. Если cmd.exe не будет среди тех, кто изменял ваш блок переменных окружения, то вы не увидите переменные от cmd.exe.

Окей, командный процессор устанавливает эти переменные. Но зачем? Это остатки от попыток командного процессора имитировать старый способ, которым MS-DOS работала с дисками и каталогами. В Win32 существует только один текущий каталог, но в MS-DOS у каждого диска был свой текущий каталог. Посмотрите на такую последовательность команд:

A> CD SUBDIR
// текущим каталогом для диска A: является A:SUBDIR
A> B:
B> CD TWO
// текущим каталогом для диска B: является B:TWO
B> A:
A> DIR
// показывает содержимое каталога A:SUBDIR

В этой последовательности команд мы начинаем с A: (текущего диска) и устанавливаем его текущий каталог в A:SUBDIR. Далее, мы меняем текущий диск на B: и ставим B:TWO его текущим каталогом. Наконец, мы возвращаемся на диск A: и когда мы просим показать текущий каталог, нам выводят содержимое A:SUBDIR — потому что именно этот каталог является текущим на текущем диске.

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

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

A> CD SUBDIR
// Переменная окружения с именем =A: устанавливается равной A:SUBDIR
// Текущий каталог Win32 устанавливается в A:SUBDIR
A> B:
B> CD TWO
// Переменная окружения с именем =B: устанавливается равной B:TWO
// Текущий каталог Win32 устанавливается в B:TWO
B> A:
// Текущий каталог Win32 устанавливается в A:SUBDIR
A> DIR
// Показывает содержимое A:SUBDIR

Когда мы переключаемся обратно на диск A:, командный процессор спрашивает: «Эй, каким был текущий каталог на диске A:, когда я последний раз был на нём?». Для этого он пытается прочитать переменную окружения с именем =A:, которая говорит ему: «Ах, да, это был A:SUBDIR«. И именно этот каталог он устанавливает в качестве текущего.

Но почему эти внутренние переменные выносятся в переменные окружения? Разве нельзя их сделать обычными переменными внутри процесса cmd.exe?

Эти переменные экспортируются в переменные окружения, поскольку вы хотели бы, чтобы эти «подложные текущие каталоги дисков» наследовались бы дочерними процессами. К примеру, представьте себе, что сидите вы в командной строке, запускаете emacs, а из emacs открываете ещё одну командную строку. Вы бы ожидали все те же текущие каталоги дисков в этой командной строке.

C:SUBDIR> D:
D:> emacs
M-x shell
D:> C:
C:SUBDIR>
// "Текущий каталог на диске C" был сохранён - как и ожидает пользователь

Что вам тогда нужно делать с этими переменными окружения?

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

Читать на сайте автора.

Подключение устройств Android Wear

В данной статье я расскажу, как подключить для отладки устройство Sony SmartWatch 3, на борту которых используется операционная система Android Wear.

Для открытия меню разработчиков перейдите в раздел Настройки — >Об устройстве и кликните несколько раз по пункту Номер сборки.

device-2015-01-12-133225     device-2015-01-12-133243

После этого перейдите в меню Для разработчиков и включите отладку.

device-2015-01-12-133315     device-2015-01-12-133330

Можно включить отладку по Bluetooth, но на моей практике она работает очень плохо, поэтому подключаю через кабель.

Далее нам нужно настроить драйвер в нашей ОС Windows7.

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

Зайдем в Диспетчер устройств.

2015-01-03 23-17-45 Диспетчер устройств
Нажмем Обновление/Установка драйвера.

2015-01-03 23-18-40 Обновление драйверов - SmartWatch 3Нажмем Выбрать драйвер из списка уже установленных.

2015-01-03 23-18-57 Обновление драйверов - SmartWatch 3 Выберем Android Device.2015-01-03 23-19-12 Обновление драйверов - SmartWatch 3
Выберем Android ADB Interface.2015-01-03 23-19-28 Обновление драйверов - SmartWatch 3Нажмем Да

2015-01-03 23-19-40 Предупреждение при обновлении драйвераТеперь все хорошо.2015-01-03 23-19-53 Обновление драйверов - Android ADB Interface
Перепроверим.2015-01-03 23-20-12 Диспетчер устройств

При подключении к ПК нужно будет в всплывающем окне на смартфоне дать разрешение на отладку.

Теперь ищем устройство в DDMS.

Снимок Наконец можно нормально работать.

Читать на сайте автора.

Что на самом деле делает флаг SEE_MASK_UNICODE в ShellExecuteEx?

Это перевод What does the SEE_MASK_UNICODE flag in ShellExecuteEx actually do? Автор: Реймонд Чен.

Кое-кто с невежливым ником хотел узнать, что делает флаг SEE_MASK_UNICODE.

А ничего он не делает.

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

Одной из вещей, которые было необходимо сделать во время переноса, было создание Unicode-вариантов всех ANSI-функций, добавленных в Windows 95. Это можно было делать несколькими способами. Иногда создавали два варианта одной функции: с A и W суффиксами. Иногда это достигалось A и W вариантами одного интерфейса. Иногда это достигалось добавлением в запись флага, говорящего нужно ли использовать/трактовать данные как ANSI или как Unicode.

Я подозреваю, что изначально для Shell­Execute­Ex планировалось использовать третью модель (с флагом). Вероятно, потом разработчики изменили решение и вместо этого сделали A и W варианты самой записи SHELL­EXECUTE­INFO (SHELL­EXECUTE­INFOA и SHELL­EXECUTE­INFOW) и функции Shell­Execute­Ex (Shell­Execute­ExA и Shell­Execute­ExW). Но когда они переключились к другой модели, они оставили флаг нетронутым — вероятно, с целью убрать его позднее, когда будет исправлен весь код, который уже передаёт этот флаг. Однако эта задача так и не была выполнена.

Итак, сегодня этот флаг всё ещё находится в заголовочных файлах Windows, хотя на него никто не обращает внимания.

Читать на сайте автора.

Что за дела с сообщением EM_SETHILITE?

Это перевод What’s the deal with the EM_SETHILITE message? Автор: Реймонд Чен.

Если вы посмотрите на документацию по сообщениям EM_SETHILITE и EM_GETHILITE, то увидите просто «not implemented» («не реализовано»). Что тут происходит?

Сообщения EM_SETHILITE и EM_GETHILITE были добавлены в 2002 году для элемента управления «breadcrumb bar» (панель типа «хлебные крошки» aka «навигационная цепочка»). В те времена этот элемент управления был не таким, каким вы можете видеть его сегодня в Windows Vista (набор кнопок со стрелочками, каждая из которых представляет уровень иерархии). В 2002 году breadcrumb bar был просто несколько иначе выглядевшей строкой адреса (т.е. элементом управления edit). Вместо кнопки для каждого уровня в иерархии breadcrumb bar подсвечивал часть пути, когда вы наводили на неё мышью (или использовали клавиатуру для выделения). Вместо стрелочек с выпадающими меню breadcrumb bar ждал, пока вы наведёте на часть пути мышь, и автоматически показывал меню с дочерними элементами.

Потом дизайн breadcrumb bar изменился, а вариант с «просто немного иначе выглядящая строка адреса» был заброшен. Функциональность сообщений EM_SETHILITE и EM_GETHILITE была удалена, но номера сообщений остались зарезервированными — чтобы не сломать код, который уже начал отправлять эти сообщения (прим.пер.: речь идёт о коде Оболочке, внутреннем коде Microsoft). Конечно же, сообщения уже ничего не делают, но, по крайней мере, старый код компилируется и не отправляет случайно какое-то другое сообщение. Те, кто до сих пор продолжают использовать эти сообщения в своём коде, могут удалить их по собственному желанию.

Такова реальность разработки одновременно и библиотеки и клиентского приложения к ней. Если вы ответственны за библиотеку и хотите сделать в ней ключевое изменение (breaking change), то вам нужно делать его по частям, чтобы все клиентские приложения могли плавно совершить переход. Вы не можете просто пройтись по коду всех клиентов и исправить его — это не ваш код, у вас нет к нему доступа. Клиенты принадлежат другим командам разработчиков, а иногда — и другим компаниям. Вам нужно скоординироваться с этими разработчиками, сообщить им ваш план перехода, чтобы они могли продолжить использовать существующий код и выделить время на ваш план перехода.

Сообщения EM_SETHILITE и EM_GETHILITE — это просто остатки, которые так и не были вычищены. То, что сейчас находится в заголовочных файлах — это предпоследний шаг плана по переходу. Последним шагом этого плана было бы полное удаление этих сообщений из файлов заголовочников, но, к сожалению, конечный срок «больше не допускается изменение публичных заголовочных файлов» наступил раньше, чем успели сделать этот последний шаг — поэтому теперь эти сообщения продолжают находится в (публичном) заголовочном файле, и им придётся оставаться там вечно. Даже хотя они ничего не делают.

Читать на сайте автора.

Возможность-изгой: пристыковка папки к краю экрана

Это перевод Rogue feature: Docking a folder at the edge of the screen. Автор: Реймонд Чен.

Начиная с Windows 2000 и вплоть до Windows Vista вы могли перетащить папку из Проводника и бросить её на край экрана — как только вы отпускали папку, она пристыковывалась (dock) к краю экрана по аналогии с панелью (toolbar). Один клиент заметил, что это поведение больше не работает в Windows 7, и спросил: «Была ли эта возможность полностью удалена из Windows 7 или есть способ её включить обратно?»

Да, эта функциональность была удалена из Windows 7. Не существует способа её включить, поскольку код, который её реализует, был полностью удалён из продукта. Ну, хорошо, вы можете «включить» её с помощью вашего представителя поддержки, который отправит Design Change Request (запрос на изменение дизайна) с просьбой восстановить этот код в команду Windows Sustained Engineering. Но, я думаю что они будут злодейски смеяться, нажимая ОТКАЗАТЬ. Может быть, для пущего эффекта они даже добавят звуковой эффект.

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

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

Читать на сайте автора.