Запрет выгрузки dll до момента освобождения последнего объекта из dll
От: Aniskin  
Дата: 25.10.18 18:12
Оценка: 3 (1)
Что бы изучить получше оконную систему Windows решил я написать более-менее сложный контрол на чистом WinApi. Для одного из проектов мне нужно было дерево, отображающее себя несколько иначе по сравнению со стандартным SysTreeView32, его и решил реализовать. Контрол я успешно написал, оформил по взрослому, но осталась у меня одна не решенная проблема.

Приличный контрол должен уметь UI Automation. Его я тоже реализовал. Но возникло одно но. Сам контрол реализован в dll (для использования в проекте на любом языке). Когда какое-нибудь приложение типа Narrator желает поработать с моим контролом через UI Automation, то в контрол начинают посылаться сообщения WM_GETOBJECT (OBJID_CLIENT), в ответ на которое я возвращаю экземпляр объекта, реализующего IAccessible. И как бы все работает, Narrator видит отдельные ноды дерева, произносит человеческим голосом текст ноды, и т.д. Проблема возникает при завершении хост приложения. Хост приложение ничего не знает о том, что были запросы WM_GETOBJECT и есть живые объекты из dll, и может выгрузить dll, а лишь затем вызвать OleUninitialize. Соответственно при OleUninitialize, когда вызывается последний Release объекта, происходит AV.

Вопрос – как то можно это исправить в самой dll?

(Я нашел временный workaround – в ответ на WM_GETOBJECT (OBJID_QUERYCLASSNAMEIDX) я возвращаю 65536+25, это позволяет системе создавать собственный внутренний прокси, реализующий IAccessible, который общается с моим деревом на чистом WinApi, но мне нужна имена моя реализация.)
Re: Запрет выгрузки dll до момента освобождения последнего объекта из dll
От: bnk СССР http://unmanagedvisio.com/
Дата: 25.10.18 21:16
Оценка:
Здравствуйте, Aniskin, Вы писали:

A>Что бы изучить получше оконную систему Windows решил я написать более-менее сложный контрол на чистом WinApi. Для одного из проектов мне нужно было дерево, отображающее себя несколько иначе по сравнению со стандартным SysTreeView32, его и решил реализовать. Контрол я успешно написал, оформил по взрослому, но осталась у меня одна не решенная проблема.


A>Приличный контрол должен уметь UI Automation. Его я тоже реализовал. Но возникло одно но. Сам контрол реализован в dll (для использования в проекте на любом языке). Когда какое-нибудь приложение типа Narrator желает поработать с моим контролом через UI Automation, то в контрол начинают посылаться сообщения WM_GETOBJECT (OBJID_CLIENT), в ответ на которое я возвращаю экземпляр объекта, реализующего IAccessible. И как бы все работает, Narrator видит отдельные ноды дерева, произносит человеческим голосом текст ноды, и т.д. Проблема возникает при завершении хост приложения. Хост приложение ничего не знает о том, что были запросы WM_GETOBJECT и есть живые объекты из dll, и может выгрузить dll, а лишь затем вызвать OleUninitialize. Соответственно при OleUninitialize, когда вызывается последний Release объекта, происходит AV.


A>Вопрос – как то можно это исправить в самой dll?


Так не должно быть. Похоже на какой-то косяк при подсчете ссылок (как именно ты реализовал WM_GETOBJECT/IAccessible)
Когда система хочет выгрузить DLL, она ее предварительно спрашивает, "можно ли"? (DllCanUnloadNow)?
Твоя DLL похоже отвечает "да, можно", хотя у нее есть живые объекты.

К сожалению, я в Delphi не в зуб ногой, как починить, посоветовать не смогу.
Re[2]: Запрет выгрузки dll до момента освобождения последнег
От: Aniskin  
Дата: 25.10.18 21:26
Оценка:
Здравствуйте, bnk, Вы писали:

bnk>Похоже на какой-то косяк при подсчете ссылок


Если бы был косяк с подсчетом ссылок на моей стороне, то объект просто бы терялся и была бы утечка памяти. Проблема возникает при OleUninitialize, т.е. (предполагаю что) COM освобождает ссылку на мой объект в то время как dll уже выгружена.

bnk>как именно ты реализовал WM_GETOBJECT/IAccessible


Стандартно:

Accessible := TTreeViewAccessible.Create(Self);
Result := LresultFromObject(IID_IAccessible, WParam, Accessible);

bnk>Когда система хочет выгрузить DLL, она ее предварительно спрашивает, "можно ли"? (DllCanUnloadNow)?


У меня же не COM сервер, и dll загружена не COM системой. COM вообще ни чего не знает о моей dll.

bnk>К сожалению, я в Delphi не в зуб ногой, как починить, посоветовать не смогу.


Дело в общем то не в Delphi. То, на чем написана dll, к моему вопросу имеет весьма косвенное отношение.
Отредактировано 25.10.2018 21:40 Aniskin . Предыдущая версия .
Re[3]: Запрет выгрузки dll до момента освобождения последнег
От: bnk СССР http://unmanagedvisio.com/
Дата: 25.10.18 21:58
Оценка:
Здравствуйте, Aniskin, Вы писали:

bnk>>Когда система хочет выгрузить DLL, она ее предварительно спрашивает, "можно ли"? (DllCanUnloadNow)?


A>У меня же не COM сервер, и dll загружена не COM системой. COM вообще ни чего не знает о моей dll.


Это windows спрашивает. DLL, которые нельзя выгружать, обычно отвечают, что их выгружать нельзя
Для случая COM, реализация обычно считает все созданные COM объекты (в твоем случае, все IAccessible), и если кто-то еще жив, возвращает FALSE.
Если не хочешь считать, можешь попробовать всегда возвращать FALSE из этой функции,
тогда система выгрузит DLL только когда будет завершать программу.
Re[4]: Запрет выгрузки dll до момента освобождения последнег
От: Aniskin  
Дата: 25.10.18 22:53
Оценка:
Здравствуйте, bnk, Вы писали:

bnk>>>Когда система хочет выгрузить DLL, она ее предварительно спрашивает, "можно ли"? (DllCanUnloadNow)?


A>>У меня же не COM сервер, и dll загружена не COM системой. COM вообще ни чего не знает о моей dll.


bnk>Это windows спрашивает.


Не могу подтвердить это утверждение. Мои познания говорят, что DllCanUnloadNow вызывается COM подсистемой для dll, которые были загружены вследствие вызова CoCreateInstanceEx (или других связанных функций). Для обычных dll она не вызывается, для них есть DllMain. Эксперимент подтвердил, DllCanUnloadNow не вызывается при простом FreeLibrary.
Re: Запрет выгрузки dll до момента освобождения последнего объекта из dll
От: Мёртвый Даун Россия  
Дата: 26.10.18 02:36
Оценка:
Здравствуйте, Aniskin, Вы писали:

A>Что бы изучить получше оконную систему Windows решил я написать более-менее сложный контрол на чистом WinApi. Для одного из проектов мне нужно было дерево, отображающее себя несколько иначе по сравнению со стандартным SysTreeView32, его и решил реализовать. Контрол я успешно написал, оформил по взрослому, но осталась у меня одна не решенная проблема.


А не на Delphi есть?
Только Путин, и никого кроме Путина! О Великий и Могучий Путин — царь на веки веков, навсегда!
Смотрю только Соловьева и Михеева, для меня это самые авторитетные эксперты.
КРЫМ НАШ! СКОРО И ВСЯ УКРАИНА БУДЕТ НАШЕЙ!
Re[5]: Запрет выгрузки dll до момента освобождения последнег
От: Mystic Artifact  
Дата: 26.10.18 05:21
Оценка:
Здравствуйте, Aniskin, Вы писали:

Если вам нужен способ предотвратить выгрузку... то можете вызвать LoadLibrary своей dll внутри своей dll.
Re: Запрет выгрузки dll до момента освобождения последнего о
От: ononim  
Дата: 26.10.18 06:49
Оценка:
1) Ну так тот кто загрузил длл — пускай спрашивает ее перед выгрузкой, можно ли ее выгрущить. С помощью той же DllCanUnloadNow.
2) Если 1 невозможен (например длл загружается изза SetWindowsHookEx и нежелательная выгрузка происходит изза того что процесс который поставил хук случайно сдыхает), то dll может вызвать LoadLibrary сама на себя чтобы самозалочиться, а когда решит саморазлочиться — запустит рабочий поток, который вызовет FreeLibraryAndExitThread на результат той LoadLibrary, при этом потребуется хитрый трюк, чтобы исключить race condition когда поток-разлочиватель будет выгружать длл, а тот кто его запустил — все еще исполняет ее код.
Как много веселых ребят, и все делают велосипед...
Отредактировано 26.10.2018 7:01 ononim . Предыдущая версия .
Re[2]: Запрет выгрузки dll до момента освобождения последнего объекта из dll
От: Aniskin  
Дата: 26.10.18 12:07
Оценка:
Здравствуйте, Мёртвый Даун, Вы писали:

МД>А не на Delphi есть?


В смысле есть ли этот же код, но с перламутровыми пуговицами написанный на C++? Нет. Для C++ есть dll, которая используется аналогично ComCtl32.dll.
Re[6]: Запрет выгрузки dll до момента освобождения последнег
От: Aniskin  
Дата: 26.10.18 12:11
Оценка:
Здравствуйте, Mystic Artifact, Вы писали:

MA>можете вызвать LoadLibrary своей dll внутри своей dll.


Ок, я вызываю LoadLibrary. Это приводит к тому, что dll будут висеть до конца жизни процесса. Соответственно я должен когда-нибудь из dll вызвать FreeLibrary. Но FreeLibrary вызовет выгрузку dll, и попытка возврата из вызова FreeLibrary в выгруженную dll может иметь трагические последствия.
Re[2]: Запрет выгрузки dll до момента освобождения последнего о
От: Aniskin  
Дата: 26.10.18 12:27
Оценка:
Здравствуйте, ononim, Вы писали:

O>1) Ну так тот кто загрузил длл — пускай спрашивает ее перед выгрузкой, можно ли ее выгрущить. С помощью той же DllCanUnloadNow.


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

O>2) dll может вызвать LoadLibrary сама на себя чтобы самозалочиться, а когда решит саморазлочиться — запустит рабочий поток, который вызовет FreeLibraryAndExitThread на результат той LoadLibrary, при этом потребуется хитрый трюк, чтобы исключить race condition когда поток-разлочиватель будет выгружать длл, а тот кто его запустил — все еще исполняет ее код.


Вот о чем то таком я и мечтал. Но не могу до конца понять, в какой момент мне можно вызвать FreeLibraryAndExitThread из доп потока. Это должен быть такой момент, когда последний вызов IAccessible.Release отработал и произошел выход из процедуры. Вопрос переформулируется на "Как исполнить этот хитрый трюк".
Re[7]: Запрет выгрузки dll до момента освобождения последнег
От: ononim  
Дата: 26.10.18 13:27
Оценка:
A>Ок, я вызываю LoadLibrary. Это приводит к тому, что dll будут висеть до конца жизни процесса. Соответственно я должен когда-нибудь из dll вызвать FreeLibrary. Но FreeLibrary вызовет выгрузку dll, и попытка возврата из вызова FreeLibrary в выгруженную dll может иметь трагические последствия.
Вариант 1 — tail call (псевдоасм):
x86-32:
void __fastcall CallbackAndOptionallyFreeLibrary(void *arg, void *(__fastcall *pCallback)(void *arg))
{
    call edx ; вызовем pCallback, arg уже в ecx
    cmp eax, 0 ; если pCallback вернул не 0 - значит это hmod, которому надо сделать FreeLibrary
    jne need_free_library
    ret

need_free_library:
    pop rcx ; адрес возврата
    push eax ; dwExitCode - не заморачиваемся
    push eax ; hLibModule
    push rcx ; адрес возврата положим туда где ему место
    jmp FreeLibrary
}

x86-64:
void CallbackAndOptionallyFreeLibrary(void *arg, void *(*pCallback)(void *arg))
{
    call rdx
    cmp rax, 0
    jne need_free_library
    ret

need_free_library:
    mov rcx, rax
    jmp FreeLibrary
}

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

Вариант 2:
Тот кто снаружи (управляющий процесс) должен в правильный момент вызвать CreateRemoteThread(RemoteProcess, ..., &FreeLibrary, RemoteLibraryHandle,...); или CreateRemoteThread, который вызовет функцию, которая сделает деинициализацию и выйдет через FreeLibraryAndExitThread.
Как много веселых ребят, и все делают велосипед...
Отредактировано 26.10.2018 13:29 ononim . Предыдущая версия .
Re[8]: Запрет выгрузки dll до момента освобождения последнег
От: Aniskin  
Дата: 26.10.18 14:59
Оценка:
Здравствуйте, ononim, Вы писали:

O>Вариант 1 — tail call


Правильно ли я понял идею, что ее реализация должна выглядеть как то так:

0) В IAccessible.AddRef увеличиваю счетчик.
1) Если счетчик == 1 то вызываю LoadLibrary(Self)
2) В IAccessible.Release уменьшаю счетчик.
3.1) Если счетчик == 0 то вызываю CallbackAndFreeLibrary, которая вызывает деструктор моего объекта, производит манипуляции со стеком и передает управление в реальную FreeLibrary.
3.2) Иначе просто выхожу из IAccessible.Release

При этом стек вызовов внутри CallbackAndFreeLibrary будет примерно таким:
CallbackAndFreeLibrary
IAccessible.Release
SomeOleInternalFunction

И идея состоит в том, что бы выход из реальной FreeLibrary произошел сразу в SomeOleInternalFunction.

O>Вариант 2:

O>управляющий процесс

Вот ну очень не хочется плодить сущности без надобности.
Re[9]: Запрет выгрузки dll до момента освобождения последнег
От: ononim  
Дата: 26.10.18 16:29
Оценка:
A>При этом стек вызовов внутри CallbackAndFreeLibrary будет примерно таким:
A> CallbackAndFreeLibrary
A> IAccessible.Release
A> SomeOleInternalFunction

A>И идея состоит в том, что бы выход из реальной FreeLibrary произошел сразу в SomeOleInternalFunction.

Неа. CallbackAndFreeLibrary должен быть самым первым на стеке, кто находится в выгружаемой длл. То есть это он должен быть прописан в VTBL и вызывать реальную IAccessible.Release.

O>>Вариант 2:

O>>управляющий процесс
A>Вот ну очень не хочется плодить сущности без надобности.
Но ведь ктото же загрузил ту длл в процесс. Вот та сущность которая длл загрузила, и должна заниматься ее безопасной выгрузкой.
Как много веселых ребят, и все делают велосипед...
Re[10]: Запрет выгрузки dll до момента освобождения последнег
От: Aniskin  
Дата: 26.10.18 17:00
Оценка:
Здравствуйте, ononim, Вы писали:

A>>При этом стек вызовов внутри CallbackAndFreeLibrary будет примерно таким:

A>> CallbackAndFreeLibrary
A>> IAccessible.Release
A>> SomeOleInternalFunction

A>>И идея состоит в том, что бы выход из реальной FreeLibrary произошел сразу в SomeOleInternalFunction.

O>Неа. CallbackAndFreeLibrary должен быть самым первым на стеке, кто находится в выгружаемой длл. То есть это он должен быть прописан в VTBL и вызывать реальную IAccessible.Release.

(Возможно я путаю понятия первый/последний в стеке.) Сейчас имеем ситуацию, в которой SomeOleInternalFunction вызывает мою IAccessible.Release. Если я правильно понял, то SomeOleInternalFunction должна вызвать CallbackAndFreeLibrary, которая в свою должна вызывать IAccessible.Release. Т.е. CallbackAndFreeLibrary должна иметь прототип аналогичный IAccessible.Release. Но в этом случае мне не совсем понятно, зачем нужна отдельная функция CallbackAndFreeLibrary, ее тело может быть прямо в IAccessible.Release.

O>Но ведь ктото же загрузил ту длл в процесс. Вот та сущность которая длл загрузила, и должна заниматься ее безопасной выгрузкой.

Вот этого я и пытаюсь всячески избежать. Процессу незачем вникать в проблемы dll, он просто должен сделать LoadLib, поработать с ней, FreeLib.
Re[11]: Запрет выгрузки dll до момента освобождения последнег
От: ononim  
Дата: 26.10.18 17:13
Оценка:
A>(Возможно я путаю понятия первый/последний в стеке.) Сейчас имеем ситуацию, в которой SomeOleInternalFunction вызывает мою IAccessible.Release. Если я правильно понял, то SomeOleInternalFunction должна вызвать CallbackAndFreeLibrary, которая в свою должна вызывать IAccessible.Release. Т.е. CallbackAndFreeLibrary должна иметь прототип аналогичный IAccessible.Release.
Ага.

A>Но в этом случае мне не совсем понятно, зачем нужна отдельная функция CallbackAndFreeLibrary, ее тело может быть прямо в IAccessible.Release.

Чтоб писать тяжелый код на нормальном человеческом ЯП.
O>>Но ведь ктото же загрузил ту длл в процесс. Вот та сущность которая длл загрузила, и должна заниматься ее безопасной выгрузкой.
A>Вот этого я и пытаюсь всячески избежать. Процессу незачем вникать в проблемы dll, он просто должен сделать LoadLib, поработать с ней, FreeLib.
Дык это.. Может таки воспользоваться COMом, зарегистрировать свою дллку, и вместо LoadLib/FreeLib процесс будет делать CoCreateInstance/Release? Тогда по идее заработает DllCanUnloadNow.
Как много веселых ребят, и все делают велосипед...
Re[12]: Запрет выгрузки dll до момента освобождения последнег
От: Aniskin  
Дата: 26.10.18 17:39
Оценка:
O>Может таки воспользоваться COMом
Убивает портабельность.
Re[5]: Запрет выгрузки dll до момента освобождения последнег
От: bnk СССР http://unmanagedvisio.com/
Дата: 26.10.18 18:10
Оценка:
Здравствуйте, Aniskin, Вы писали:

A>Не могу подтвердить это утверждение. Мои познания говорят, что DllCanUnloadNow вызывается COM подсистемой для dll, которые были загружены вследствие вызова CoCreateInstanceEx (или других связанных функций). Для обычных dll она не вызывается, для них есть DllMain. Эксперимент подтвердил, DllCanUnloadNow не вызывается при простом FreeLibrary.


То есть ты говоришь, что когда Narrator использует твою DLL, то DllCanUnloadNow не вызывается? Это было бы странно.
Да, COM у тебя похоже все же есть (но возможно что ты пока этого не осознал :), потому как IAccessible — это COM.
Уверен, что функция с названием "DllCanUnloadNow" в твоей DLL (1) существует и (2) экспортируется (т.е. может быть вызвана) и (3) возвращает FALSE при вызове?
Re[11]: Запрет выгрузки dll до момента освобождения последнег
От: ononim  
Дата: 26.10.18 18:15
Оценка: +1
O>>Но ведь ктото же загрузил ту длл в процесс. Вот та сущность которая длл загрузила, и должна заниматься ее безопасной выгрузкой.
A>Вот этого я и пытаюсь всячески избежать. Процессу незачем вникать в проблемы dll, он просто должен сделать LoadLib, поработать с ней, FreeLib.
У решения с LoadLib/FreeLib с внутренне-отложенной обработкой выгрузки есть неявное усложнение: FreeLib _не_ гарантирует реальной выгрузки либы сейчас. Соответственно если вызывателю FreeLib реально нужна (чтоб удалить файл?) то таким образом рождается фундаментальный гейзенбаг. А если FreeLib ему реально не нужна — почему бы не заморозить либу в АП процесса до его завершения?
Как много веселых ребят, и все делают велосипед...
Re[6]: Запрет выгрузки dll до момента освобождения последнег
От: Aniskin  
Дата: 26.10.18 18:20
Оценка:
Здравствуйте, bnk, Вы писали:

bnk>То есть ты говоришь, что когда Narrator использует твою DLL, то DllCanUnloadNow не вызывается? Это было бы странно.


Все что делает Narrator (в лице COM системы) — это посылает WM_GETOBJECT оконному элементу. Он в принципе не знает, что окно создано с использованием dll.

bnk>IAccessible — это COM.


Обратного я и не утверждал.

bnk>Уверен, что функция с названием "DllCanUnloadNow" в твоей DLL (1) существует и (2) экспортируется (т.е. может быть вызвана)


Уверен.

bnk> и (3) возвращает FALSE при вызове?


Не вызывается.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.