Ще причини, чому не треба робити нічого страшного в DllMain: випадкова блокування
Ваша функція DllMain працює всередині блокування завантажувача - це один з небагатьох моментів, коли ОС дозволяє вашому коду виконуватися в момент утримування внутрішньої блокування. Це означає, що ваш код повинен бути дуже уважний, щоб не порушити ієрархію блокувань у своїй DllMain; інакше вам загрожує мертва блокування.
(У вас же є така ієрархія. Да?).
Блокування завантажувача ОС потрібно для будь-якої функції, якій потрібен доступ до списку завантажених в процес DLL. Це включає в себе такі функції як GetModuleHandle і GetModuleFileName. Якщо ваша DllMain входить в критичну секцію або очікує на об'єкті синхронізації, а ці критична секція або об'єкт синхронізації належать (owned) якомусь іншому потоку, який, в свою чергу, чекає звільнення блокування завантажувача, то ви тільки що створили мертву блокування (deadlock ): Тепер уявіть, що якийсь потік щасливо виконує перший блок коду і входить в csGlobal, потім управління передайтся ще комусь. В цей час інший потік завершує свою роботу. При цьому береться блокування завантажувача і розсилається повідомлення DLL_THREAD_DETACH (блокування завантажувача в цей час тримається).
Ви отримуєте DLL_THREAD_DETACH і намагаєтеся увійти в csGlobal. Це блокується першим потоком, який зараз володіє критичною секцією. Потім цей потік продовжує виконання і викликає GetModuleFileName. Ця функція вимагає блокування завантажувача (оскільки їй потрібен доступ до списку DLL, завантажених в процес), тому потік блокується, тому що блокуванням завантажувача володіє хтось ще.
Тепер у вас deadlock:- csGlobal-му володіє перший потік, який чекає блокування завантажувача.
- Блокуванням завантажувача володіє другою потік, який чекає csGlobal.
Мораль історії: не забувайте про блокування завантажувача. Включайте її в свої ієрархії блокувань, якщо ви хочете використовувати будь-які блокування в своїй DllMain.
Мабуть, я погано володію теорією.
Олександр, я правильно зрозумів, що безіменний блок begin / end, який зазвичай буває в коді бібліотеки - це і є, образно кажучи, процедура DllMain? У чому, взагалі, різниця між DllMain і DllProc, і в якій послідовності вони виконуються?
Блок begin / end в .dpr файлі DLL - це досить хитра магія компілятора. Це частина "DllMain", але тільки для DLL_PROCESS_ATTACH.
DllEntryPoint - це поняття API Windows. По суті, це точка входу в модуль (IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint). Для exe це покажчик на першу інструкцію, яка буде виконуватися. DllEntryPoint не експортується і не має символьного імені. "DllEntryPoint" - це просто набір букв для позначення концепції. Для DLL це покажчик на функцію, яка буде служити "як DllMain". У мовах програмування високого рівня в якості точки входу (включаючи і випадок з DllMain) завжди використовується код бібліотеки підтримки мови (RTL). Зокрема, в Delphi для програм це буде _InitExe, а для DLL - _InitLib. У C ++ це буде _DllMainCRTStartup (для DLL).
DllMain - це поняття RTL C ++. Це функція, яка викликається з DllEntryPoint (_DllMainCRTStartup), але ще не написана - її зобов'язаний написати програміст. Функція повинна мати це ж ім'я, інакше її не знайдете компоновщик.
DllProc - це поняття RTL Delphi. Це звичайний покажчик на функцію (callback, подія). Він викликається з DllEntryPoint (_InitLib). Сам покажчик називається DllProc, але функція, на яку він вказує, може називатися як завгодно - DllProc, DllMain, DllEntryPoint, MySuperDuperHandler. За замовчуванням (і в 99% випадків) DllProc не заповнюється і = nil.
Що тут заплутує, DllMain не існує в Delphi, це поняття RTL C ++. DllMain описується в MSDN, тому що MSDN каже про C ++. По суті, всі "за звичкою" копіюють цю назву, хоча в контексті Delphi правильніше говорити тільки про DllEntryPoint і DllProc.
У будь-якому випадку, ланцюжок викликів при завантаженні / вивантаженні DLL Delphi йде так:
- LoadLibrary -> Kernel32 / NTDLL (десь всередині там йде захоплення критичної секції системного завантажувача) -> _InitLib (aka DllEntryPoint) -> InitializeModule (тільки для DLL_PROCESS_ATTACH) -> _StartLib -> DllProc (якщо є) -> InitUnits -> секції initialization модулів -> повернення до begin / end .dpr -> вихід з _InitLib.
- FreeLibrary -> Kerenel32 / NTDLL (десь всередині там йде захоплення критичної секції системного завантажувача) -> _InitLib (aka DllEntryPoint) -> _StartLib -> DllProc (якщо є) -> _Halt0 -> FinalizeUnits -> відпрацювання секцій finalization модулів -> вихід з _InitLib.