Одного разу, коли я писав DLL, переді мною постала елементарна, на перший погляд, завдання: створити і використовувати COM об'єкт. Але виявилося, що це не так-то просто. Основні проблеми полягали в тому, що клієнт моєї DLL нічого не знав про COM, тому не збирався його форматувати, зате мав купу потоків, кожен з яких періодично викликав функції DLL. Як мені здається, після деяких роздумів, експериментів (і консультацій в форумі RSDN COM / DCOM / COM +) було знайдено спільне і досить гарне рішення, яке і описано в цій статті.
Необхідно написати DLL, що не пред'являє до клієнта ніяких вимог, пов'язаних з COM. Подібна необхідність виникає, якщо вже існує кілька різних клієнтів, які використовують DLL з подібним інтерфейсом, а вам потрібно підмінити її. Або, якщо ви пишете plug-in до чого-небудь.
З якихось причин при реалізації дуже хочеться використовувати COM. У моєму випадку причиною була можливість прозорого (для клієнта моєї DLL) використання сервера в іншому процесі (ну дуже не хотілося використовувати традиційні методи взаємодії між процесами. Довелося вигадувати). У вашому випадку це може бути, наприклад, наявність COM-компонентів, що реалізують потрібну функціональність.
- Деякі потоки клієнта нічого не знають про COM. А DLL використовувати хочуть. І CoInitialize [Ex] перед зверненням до DLL викликати не здогадаються.
- Деякі потоки клієнта щось знають про COM. Вони всі належать різним апартаментів, але, незважаючи на це, будуть по черзі (або одночасно) звертатися до DLL.
уточнення завдання
Будемо вирішувати спрощену версію моєї завдання. Це простий, але показовий випадок. Сподіваюся, ваші завдання можна буде звести до чогось подібного, або хоча б використовувати той же підхід.
Потрібно створити DLL, що експортує наступні функції:
Якщо в інтерфейсі вашої DLL немає функцій, подібних Init і Cleanup, ви матимете змогу дзвонити неявно. Тобто, якщо SomeFunc виявляє, що Init ще не була викликана, вона викликає її сама. А Cleanup можна викликати з DllMain. При використанні описаної нижче реалізації Init виклик Init з DllMain призведе до взаємної блокування потоків.
Крім цього є COM-компонент CoolServer, який реалізує інтерфейс ICoolServer, який містить SomeFunc. Якби COM був всюди инициализирован, і не було б проблем з апартаментами, потрібно було б реалізувати DLL приблизно так:
Тобто потрібно використовувати тільки один покажчик на інтерфейс, отриманий при ініціалізації бібліотеки і звільняється при очищенні. Якщо можна створювати об'єкт в SomeFunc і там же його знищувати, то функції Init і Cleanup можна прибрати, проблеми з апартаментами теж зникнуть. Випадок, коли екземпляр COM-об'єкта потрібно зберігати між викликами, більш цікавий.
Отже, потрібно щоб:
Деініціалізіровать COM важливо! Можливий сценарій: клієнт викликав функцію з DLL, вона инициализирует COM, не чистить за собою, після цього клієнт намагається самостійно форматувати COM і обламується, так як хоче зробити це трохи не так (за краще STA, а не MTA, або навпаки; ці клієнти такі непередбачувані.).
Для виконання перших двох вимог я пропоную створити додатковий потік, форматувати в ньому COM і створити об'єкт. Після цього потік буде чекати очищення, під час якої звільнить покажчик на інтерфейс, деініціалізірует COM і завершиться.
Додатковий потік потрібен для гарантії існування створених в ньому об'єктів між викликами функцій. Тому, якщо між викликами функцій DLL вам не потрібно зберігати покажчики на інтерфейси, не варто возитися з додатковим потоком, Init-му і Cleanup-му, простіше створювати і видаляти об'єкти в тих функціях, які їх використовують.
У псевдокоді це виглядає так:
Кілька моментів, які потрібно врахувати, щоб це працювало:
При очікуванні завершення ініціалізації в функції Init необхідно запустити цикл вибірки повідомлень. Інакше клієнт, який мав необережність форматувати COM і увійти в STA до виклику Init повисне в InitCleanupThread при спробі створити об'єкт.
При створенні об'єкта в InitCleanupThread дуже бажано, щоб потоковая модель об'єкта дозволяла створити його не в головному STA. Інакше, якщо головний STA вже існує, об'єкт створиться в ньому, і, відповідно, при знищенні головного STA (виклик CoUninitialize відповідним потоком) об'єкт теж зникне. Звернення по отмаршаленному вказівником на інтерфейс об'єкта буде повертати RPC_E_SERVER_DIED_DNE.
Потік InitCleanupThread повинен входити в MTA. Інакше він може випадково опинитися головним STA, в ньому будуть створюватися об'єкти клієнта, і коли всі об'єкти помруть слідом за потоком (а їх методи почнуть повертати RPC_E_SERVER_DIED_DNE), клієнт буде дуже здивований. Навіть, я б сказав, ошелешений. Адже він і про існування потоку не здогадується.
Якщо з якихось причин потік InitCleanupThread все ж входить в STA, то чекати очищення він повинен в циклі вибірки повідомлень.
Для виконання третього вимоги можна вчинити так:
1. Спробувати форматувати COM.
якщо CoInitialize [Ex] повернула помилку RPC_E_CHANGED_MODE, то COM вже був инициализирован, просто трохи не так.
якщо CoInitialize [Ex] повернула S_OK, то COM успішно ініціалізувати.
якщо CoInitialize [Ex] повернула S_FALSE, то COM вже був инициализирован, причому з тими ж параметрами.
якщо CoInitialize [Ex] повернула щось інше, сталося щось жахливе.
2. Отримати допустимий для використання в даному апартаменті покажчик на потрібний інтерфейс. Метод отримання покажчика може залежати від результату виклику CoInitialize [Ex].
3. Викликати метод SomeFunc.
4. Якщо CoInitialize [Ex] повернула S_OK або S_FALSE, деініціалізіровать COM.
1. Спробувати отримати допустимий для використання в даному апартмент покажчик на потрібний інтерфейс.
- якщо в результаті отримана помилка CO_E_NOTINITIALIZED, перейти до пункту 2;
- якщо все в порядку до пункту 4;
- якщо щось інше, сталося щось жахливе.
2. Ініціалізувати COM.
3. Отримати допустимий для використання в даному апартаменті покажчик на потрібний інтерфейс.
4. Викликати метод SomeFunc.
5. Якщо виконувався пункт 2, деініціалізіровать COM.
Якщо потік InitCleanupTheard входить в MTA, то, незалежно від того, чи був инициализирован COM в поточному потоці, функції CoUnmarshalInterface і IGlobalInterfaceTable :: GetInterfaceFromGlobal повертають мені S_OK. При цьому виклик методу SomeFunc працює. Але гарантій, що він буде працювати завжди, я не дам. Для чогось же, напевно, все-таки потрібно форматувати COM?
Якщо потік InitCleanupTheard входить в STA (так, звичайно, робити не можна, але мені було цікаво.), То IGlobalInterfaceTable :: GetInterfaceFromGlobal повертає E_UNEXPECTED, до тих пір, поки хоч раз не виконається послідовність CoInitialize [Ex] + IGlobalInterfaceTable :: GetInterfaceFromGlobal. Після цього вона, як годиться, буде повертати CO_E_NOTINITIALIZED.
Для практичного застосування не рекомендується.
Отримати «допустимий для використання в даному апартаменті покажчик на потрібний інтерфейс» можна трьома шляхами:
CoMarshalInterface краще викликати з прапором MSHLFLAGS_TABLESTRONG. Тому що з MSHLFLAGS_TABLEWEAK іноді не працює. Чесно кажучи, поки не розібрався чому.
Перед викликом CoUnmarshalInterface необхідно відмотати IStream на початок. Тобто щось таке:
pStream-> Seek (li, STREAM_SEEK_SET, NULL);
Інакше працювати не буде. І, до речі, не сподівайтеся, що Seek коли-небудь поверне CO_E_NOTINITIALIZED, всередині - чисто механічна операція. Хоча перевірити, звичайно, не шкідливо.
Останній метод можна застосовувати тільки якщо потік, який створив об'єкт (InitCleanupThread), і потік, який бажає цей об'єкт використовувати, належать одному апартаменту, тобто обидва входять в MTA; перші два методи застосовні в будь-якому випадку.
Як приклад я написав два сервера (один - exe, інший - DLL), дві DLL (на кожен сервер по одній) і клієнта з набором тестів.
Обидва сервера реалізують наступний інтерфейс:
SomeFunction просто повертає S_OK, WorkFunction виводить передану їй рядок (сервер в DLL - через printf, сервер в exe - через MessageBox). Перша використовується для порівняння швидкодії різних методів виклику, друга для перевірки їх принципової працездатності.
Як завжди, сервери потрібно зареєструвати. При цьому вони обидва намагаються зареєструвати свої бібліотеки типів. Обидва сервера припускають, що бібліотека називається «iface.tlb» і лежить в тому ж каталозі, що і виконуваний файл сервера.
Клієнт, в залежності від значення в командному рядку (цифра від 0 до 6, відсутність аргументів еквівалентно «0»), виконує один з тестів. Тест «0» перевіряє функціональність, інші дозволяють оцінити продуктивність. Їх результати наведені в розділі «Статистика».
Відрізняються один від одного тільки іменами експортованих функцій і GUID-ами створюваних об'єктів. Теоретичні побудови з розділу "Рішення" реалізовані наступним чином:
- Потік InitCleanupThread входить в MTA.
- Потік InitCleanupThread повідомляє Init про завершення ініціалізації за допомогою події, яке приймає як параметр.
- Cleanup повідомляє потоку InitCleanupThread про початок очищення за допомогою глобального події.
- Cleanup не чекає завершення очищення. Лінь.
- InitCleanupThread реєструє інтерфейс в GIT.
- InitCleanupThread виробляє маршалинга інтерфейсу в глобальний IStream. CoMarshalInterface викликається з прапором MSHLFLAGS_TABLESTRONG.
- При спробі ініціалізувати COM все потоки намагаються увійти в MTA. Тому, якщо у них це виходить, маршалинга не потрібно.
- Dll експортує наступні функції ( «xx» - або «In», або «Out»): xx_Init, xx_Cleanup, xx_One, xx_Two, xx_Three, xx_Four, xx_One_Work, xx_Two_Work, xx_Three_Work, xx_Four_Work. «One», «Two», «Three», «Four» - це чотири методи отримання покажчика на об'єкт. Work-функції викликають WorkFunction, звичайні функції - SomeFunction.
Параметр init встановлюється в true, якщо була викликана і успішно відпрацювала CoInitializeEx і, отже, перед поверненням з DLL необхідно викликати CoUninitialize.
Статистика
Швидше за все, після того, як ви прочитали, що за кожен виклик доведеться платити инициализацией / деініціалізацію COM або чимось подібним, ви задумалися над питанням: скільки це коштує? Мене це теж цікавило, я поставив кілька експериментів.
Все експерименти проводилися наступним чином:
COM НЕ инициализирован
Ну, трохи я допоможу :) Дані в колонках «COINIT_APARTMENTTHREADED» і «COINIT_MULTITHREADED» були отримані приблизно так:
Тобто COM Ініціалізувати до виклику xx_Init. Це пояснює дивні результати в колонці «COINIT_APARTMENTTHREADED» першої таблиці. У цьому випадку основний потік ініціалізувати COM першим, внаслідок чого увійшов в головний STA, і об'єкт CoolServer створився в ньому. Тому наступні виклики SomeFunction проходили безпосередньо.
P.S. Навіть Microsoft робить це ...
Ви ніколи не замислювалися над питанням, як реалізована функція ShellExecute [Ex]? З одного боку, вона не вимагає попередньої ініціалізації COM, а з іншого, явно його використовує ... Я от не замислювався. А виявляється, в ній застосовується схожий підхід.
А функція SHCoInitialize виглядає так:
Тобто, якщо їй не вдалося ініціалізувати COM з параметром COINIT_APARTMENTTHREADED, вона намагається ініціювати його з COINIT_MULTITHREADED. Якщо не відбудеться нічого несподіваного, то після виконання цієї функції можна:
- бути впевненим, що COM инициализирован
- сміливо викликати CoUninitialize, так як клієнтові це не зашкодить
ShellExecuteExW так і надходить.
Подяки
Під час написання коду, що послужив основою і для цього тексту і для прівёдённого прикладу, мені дуже допоміг Vi2 (Віктор кулях). Велике йому спасибі. Друге спасибі - Павлу Блудову, який звернув мою увагу на функцію ShellExecute [Ex].