Здравствуйте, Aniskin, Вы писали:
AG>>При этом, код в 7z.dll также как-то связан и с дополнительным потоком?
A>Дополнительный поток создается в 7z.dll, такая у него архитектура. В этом дополнительном потоке 7z.dll вызывает callback функцию для получения файлового потока, которые нужно упаковать.
Вот оно что.
Предполагаю, что для ан-маршаллинга требуется обработать входящий COM-вызов в маршаллящем потоке.
Для такого обычно используют CoWaitForMultipleHandles вместо обычных Wait-функций.
Поскольку 7z.dll не будет использовать CoWaitForMultipleHandles, предлагаю в своём IDropTarget::Drop стартовать свой поток, который уже вызовет ту самую функцию из 7z.dll, и этот свой поток тут же ждать через CoWaitForMultipleHandles.
A>При этом сам дополнительный поток имеет не инициализированный COM, и вызов CoInitialize произвожу я в callback функции.
Ну это ничего, только, разумеется, CoUninitialize следует вызвать до возврата из callback функции, и к моменту этого вызова CoUninitialize не должно быть ссылок на COM интерфейсы, полученные в callback-функции.
Здравствуйте, Aniskin, Вы писали:
A>Первые эксперименты не дали результата. А какие COWAIT_ флаги нужно выставить?
Да вроде просто 0 — в STA всё, что нужно, диспатчится по умолчанию. Ну можно COWAIT_DISPATCH_CALLS | COWAIT_DISPATCH_WINDOW_MESSAGES попробовать.
В принципе, вижу workaround — не маршаллить интерфейс вообще, стартовать поток для 7z.dll-вызова, затем крутить собственный цикл сообщений, в котором обрабатывать запросы из 7z.dll-потока и ждать сообщение о завершении собственного потока. Но вообще должно же работать через маршаллинг COM
Здравствуйте, Aniskin, Вы писали:
A>из-за незнания предметной области
Попробую прояснить в меру своего понимания.
Как работает STA
Для STA-объекта гарантировано, что все вызовы методов данного объекта будут из того же потока, в котором он создан. Если его замаршаллили в другой поток или другой процесс, то там прокси, а в оригинальном STA-потоке есть невидимое окно COM, которое принимает SendMessage вызовы.
То есть, ожидание через CoWaitForMultipleHandles отличается от WaitForMultipleObjectsEx тем, что помимо указанных объектов ждёт ещё сообщения, и диспетчерезирует их. Но не все сообщения! Потому что было бы неожиданно во время COM вызова из обработчика Button1Click получить вызов Button2Click, либо даже повторный вызов Button1Click. Обрабатываются специфические сообщения для того самого секретного COM окна, плюс, возможно, какие-то служебные, вроде WM_COPYGLOBALDATA.
CoWaitForMultipleHandles могла бы быть сделана через MsgWaitForMultipleObjectsEx, и скорее всего, как-то так и сделана. Ну а MsgWaitForMultipleObjectsEx реализована через реализацию WaitForMultipleObjectsEx, в которой к массиву объектов подложили ещё объект, соотвествующий очереди сообщений.
Флаги CoWaitForMultipleHandles. Когда-то были только COWAIT_WAITALL и COWAIT_ALERTABLE, это по сути соотвествующие параметры WaitForMultipleObjectsEx. Смысл остальных трёх понятен из их описания, если знать про MsgWaitForMultipleObjectsEx внутри и невидимое COM окно. Обращаю внимание, что COWAIT_ALERTABLE и COWAIT_INPUTAVAILABLE могут приводить к возврату из ожидания при выполнении соотвествующих флагам условий, в этом случае обычно требуется выполнять соотвествующие действия, и повторно вызывать CoWaitForMultipleHandles. А флаг COWAIT_WAITALL вообще ни к чему хорошему не приведет.
Так как входящий STA COM вызов — это входящее SendMessage сообщение, его можно принять не только CoWaitForMultipleHandles, но и обычным циклом сообщений. В большинстве приложений обычно так и происходит. Из того же цикла сообщений, из которого вызовется Button1Click, обработается и входящий STA COM вызов.
Всякие MessageBox и OpenDialog тоже обработают входящий COM вызов (Button2Click из них не придёт, но не потому, что они обрабатывают не все сообщения, сообщения они как раз обрабатывают все, а потому, что механизм модальных диалогов дизейблит owner-окно).
Исходящий COM-вызов, если он сделан на прокси-объекте (замаршалленом указателе), тоже может диспечеризировать входящие COM вызовы, иначе как придут коллбэки. При этом, все ли COM вызовы он диспечеризирует, и какие сообщения при этом проходят — не знаю.
Это всё было про STA. В MTA проще. Нет никакого скрытого окна, вызовы, сделанные в другом MTA потоке придут из того же MTA потока. В случае маршаллинга, входящие вызовы будут приходить из служебных COM-потоков, эти потоки тоже будут принадлежать MTA. В MTA CoWaitForMultipleHandles это просто WaitForMultipleObjectsEx
Что с этим делать
С точки зрения корректности, трюка с CoWaitForMultipleHandles достаточно. Но могут быть проблемы с производительностью.
Потому что все COM-вызовы будут проходить через SendMessage.
Однозначно, следует вызывать COM из другого потока как можно реже (на всяких AddRef/Release, при этом, можно не экономить, они не транслируются, кроме последнего Release).
Возможно, стоит избежать маршаллинга, поглотить параметры в IDropTarget::Drop и в рабочем потоке обходиться без COM. Особенно если не нужен feedback, или нужен только прогресс без интерактивности. Можно взять producer-consumer очередь, которая будет шустрее SendMessage на каждый чих, а то и впихнуть всё в один параметр рабочего потока.
С другой стороны, попытки избежать маршаллинга могут быть напрасными, если в IDropTarget::Drop приходит уже замаршалленый указатель. Вряд ли такое будет в проводнике, но я не сильно удивлюсь, если так будет вести себя wscript или ещё какой-то клиент.
Здравствуйте, Alexander G, Вы писали:
AG>Я бы проверил, действительно ли это дело перформит заметно хуже архивации без COM. Если да, то можно следующее попробовать: AG>* Если 7z.dll читает мелкими порциями, читать у себя большими, ему уже отдавать мелкими из своего буфера. AG>* Если есть случаи, где вместо замаршаленого IStream можно взять настоящий файл, и их можно обнаружить — воспользоваться прямым чтением из файла.
Изначально у меня было все просто, я работал в потоках, создаваемых Проводником или иным приложением, использовавшим мои COM сервера, принимал только CF_HDROP и CF_FILENAMEW/A, которые содержат ссылки на физические файлы, для которых создавал свою реализацию IStream, которую и отдавал 7z. Но человеки стали жаловаться, мол, а почему это мы не можем упаковывать файлы, которые содержатся в других архивах. Пришлось думать над обработкой CF_FILEDESCRIPTORW/A + CF_FILECONTENTS. А поскольку форматы CF_FILEDESCRIPTOR + CF_FILECONTENTS уж очень универсальные, то пришлось заниматься унификацией под все сценарии, в том числе и под сценарии, когда эти данные созданы не моим приложением, и о специфике которых мне ничего не известно. Сейчас, если в источнике есть CF_HDROP или CF_FILENAME, то я использую старый способ, не требующий маршалинга, а если только CF_FILEDESCRIPTOR + CF_FILECONTENTS, то новый. В целом, упаковка виртуального объекта является не очень частой задачей, поэтому заморачиваться с перфеменсом пока нет смысла.
A>Oh, hey, look at the time. We'll pick up this question next time.
A>Но вот не могу я найти у него статью на эту тему. Может быть кто-нибудь видал или знает ответ?
В моей предыдущей конторе просто перематывали указатель стрима перед анмаршалингом (не знаю на сколько это официально, но работало). В таком случае нужно использовать не CoGetInterfaceAndReleaseStream, а CoUnmarshalInterface.
И вдогонку еще вопрос на эту же тему. Есть у меня такая программка — TC4Shell. Суть — открытие всяких архивов как папок в Проводнике. Все работало чудесно вплоть до момента, когда пользователи нашли ему альтернативное применение. Они решили использовать его в своих vbs скриптах. Сами скрипты имеют такой вид:
With CreateObject("Shell.Application")
Set Items = .NameSpace("e:\1.zip").Items
Items.Filter 73952, "*"
.NameSpace("e:\1.7z").CopyHere Items, 12312
End With
Данный конкретный скрипт копирует содержимое архива 1.zip в архив 1.7z. Но он зависает. Эти же самые действия в Проводнике работаю корректно, а скрипт зависает.
На внутреннем уровне wscript.exe общается с моим NSE примерно следующим образом: он запрашивает у IDataObject у 1.zip, запрашивает IDropTarget у 1.7z, а затем вызывает IDropTarget.Drop. Как бы ничего сложно. Но дело в том, что реальная работа с IDataObject, переданным в IDropTarget.Drop, происходит в дополнительном потоке. Поэтому я использую маршалинг IDataObject силами CoMarshalInterface(..., MSHCTX_INPROC, nil, MSHLFLAGS_TABLESTRONG). Но в дополнительном потоке вызов CoUnmarshalInterface зависает, видимо какой то внутренний deadlock. И ума не приложу, куда копать.
Здравствуйте, Alexander G, Вы писали:
AG>Вопрос о том, вернул ли он из IDropTarget::Drop. А если не вернул, не ждёт ли случайно на каком-то объекте, который сигналится через рабочий поток.
Да, тот поток, в котором был вызван IDropTarget::Drop на момент зависания еще не вышел из IDropTarget::Drop и ждет на каком-то сигнальном объекте дополнительный поток (это происходит не в моем коде, а внутри сторонней библиотеки 7z.dll, сужу по логам).
Здравствуйте, Aniskin, Вы писали:
A>Да, тот поток, в котором был вызван IDropTarget::Drop на момент зависания еще не вышел из IDropTarget::Drop и ждет на каком-то сигнальном объекте дополнительный поток (это происходит не в моем коде, а внутри сторонней библиотеки 7z.dll, сужу по логам).
То есть, в IDropTarget::Drop был сначала маршаллинг переданного IDataObject , а потом какие-то ещё действия, которые приводят к вызову зависающего кода из 7z.dll ?
При этом, код в 7z.dll также как-то связан и с дополнительным потоком?
Здравствуйте, Alexander G, Вы писали:
AG>То есть, в IDropTarget::Drop был сначала маршаллинг переданного IDataObject
Да.
AG>а потом какие-то ещё действия, которые приводят к вызову зависающего кода из 7z.dll?
В IDropTarget::Drop я запускаю процедуру упаковки, код которой находится в 7z.dll.
AG>При этом, код в 7z.dll также как-то связан и с дополнительным потоком?
Дополнительный поток создается в 7z.dll, такая у него архитектура. В этом дополнительном потоке 7z.dll вызывает callback функцию для получения файлового потока, которые нужно упаковать. При этом сам дополнительный поток имеет не инициализированный COM, и вызов CoInitialize произвожу я в callback функции. А файловый поток я извлекаю из IDataObject, который и пытаюсь маршалить.
Здравствуйте, Alexander G, Вы писали:
A>>Первые эксперименты не дали результата. А какие COWAIT_ флаги нужно выставить?
AG>Да вроде просто 0 — в STA всё, что нужно, диспатчится по умолчанию. Ну можно COWAIT_DISPATCH_CALLS | COWAIT_DISPATCH_WINDOW_MESSAGES попробовать.
Каюсь, я баран, из-за незнания предметной области пробовал выставлять различные комбинации флагов, а о простом нуле и не подумал С нулем все прекрасно работает. Спасибо за науку!
Здравствуйте, Alexander G, Вы писали:
AG>Как работает STA
Спасибо за ликбез. У меня реально не хватает знаний о внутренней кухне COM. Может где то есть статейки о COM с таким же детальным описанием?
AG>Возможно, стоит избежать маршаллинга, поглотить параметры в IDropTarget::Drop и в рабочем потоке обходиться без COM.
Я пробовал, но мне приходится извлекать IStream из IDataObject (на основе пары форматов CF_FILEDESCRIPTOR/CF_FILECONTENTS), и в доппотоке я при работе с IStream я натыкался на ошибку (пишу по памяти) "объект был marshaled в другом потоке". Можно конечно IStream маршалить, но какой смысл менять шило на мыло.
AG>С другой стороны, попытки избежать маршаллинга могут быть напрасными, если в IDropTarget::Drop приходит уже замаршалленый указатель. Вряд ли такое будет в проводнике, но я не сильно удивлюсь, если так будет вести себя wscript или ещё какой-то клиент.
Imho, как раз в Проводнике это и является частым случаем. Например, при drag&drop или copy&paste из источника данных, который может быть вообще в другом процессе. И именно из-за Проводника я и стал городить всю эту лабуду с маршалингом. А в wscript все однопоточно и просто, все бы работало и без маршалинга, wscript просто стал жертвой унификации моего кода, который должен уметь обрабатывать наиболее сложные сценарии.
Но хотелось бы что-то актуальнее. Вот после вопроса про флаги глянул в документацию CoWaitForMultipleHandles, там про ASTA / STA пишут, вопрос возник что такое ASTA. А ещё NTA есть.
Если сильно интересуют оконные точности вроде "какое имя класса COM-окна?", "какие сообщения оно принимает?", "это SendMessage или PostMessage сообщения?", это можно исследовать через оконные хуки, самому их установив, или воспользовавшись готовой утилитой вроде Spy++ или Winspector.
Но т.к. это детали реализации, которые могут меняться, я предпочитаю минимум предположений: в STA существует какое-то невидимое окно, возможно, не одно, что именно ему/им нужно — знает только CoWaitForMultipleObjects, если же крутить цикл самому — диспетчеризировать все подряд сообщения.
В принципе, многие не-оконные тонкости тоже можно экспериментально исследовать, время только придётся потратить больше.
A>Я пробовал, но мне приходится извлекать IStream из IDataObject (на основе пары форматов CF_FILEDESCRIPTOR/CF_FILECONTENTS), и в доппотоке я при работе с IStream я натыкался на ошибку (пишу по памяти) "объект был marshaled в другом потоке". Можно конечно IStream маршалить, но какой смысл менять шило на мыло.
Я бы проверил, действительно ли это дело перформит заметно хуже архивации без COM. Если да, то можно следующее попробовать:
* Если 7z.dll читает мелкими порциями, читать у себя большими, ему уже отдавать мелкими из своего буфера.
* Если есть случаи, где вместо замаршаленого IStream можно взять настоящий файл, и их можно обнаружить — воспользоваться прямым чтением из файла.
A>Imho, как раз в Проводнике это и является частым случаем. Например, при drag&drop или copy&paste из источника данных, который может быть вообще в другом процессе. И именно из-за Проводника я и стал городить всю эту лабуду с маршалингом. А в wscript все однопоточно и просто, все бы работало и без маршалинга, wscript просто стал жертвой унификации моего кода, который должен уметь обрабатывать наиболее сложные сценарии.
Это может сойти за объяснение, почему проблема вылезла только с wscript. Если пришедший IDataObject уже замаршален, то он повторно не маршалится, новый прокси в 7z.dll потоке будет обращаться напрямую к оригинальному IDataObject, а не через IDropTarget::Drop поток.
Вообще, если у тебя STA поток, то нельзя просто взять объекты оттуда и использовать их в другом потоке, даже если ошибки не возникает.
Да, при отсутствии маршалинга вызовы будут прямыми вызовами, поэтому COM-слоя, который мог бы обнаружить ошибку, нет.
Но сама ошибка есть.
Потому что если COM-объект регистрируется как STA-объект, то он ожидает, что все вызовы к нему будут из того же потока. Так, библиотеке ATL есть некоторые стандартные реализации COM-лабуды, они защищены критической секцией, которая является настоящей критической секцией в MTA и "пустышкой" в STA. И даже синхронизированные обращения из разных потоков тоже недопустимы: STA-объект может хранить данные в thread-local storage, либо сам обращаться к STA-прокси.
То есть относиться к такой ситуации лучше не "из-за Проводника я и стал городить всю эту лабуду с маршалингом/wscript просто стал жертвой унификации моего кода", а "из-за правил COM пришлось городить всю эту лабуду с маршалингом/wscript просто стал жертвой унификации COM".