Работа с COM-объектами

54

Объектная модель программных компонентов (Component Object Model, COM) проектировалась с целью дать возможность создавать компоненты на любом языке/платформе, обладающем поддержкой этой модели, и использовать их в любом языке/платформе (другом), так же обладающем поддержкой COM. Платформа .NET не исключение и позволяет легко использовать сторонние COM-объекты и экспортировать типы .NET в виде COM-объектов.

Суть организации взаимодействий с COM-объектами та же, что и при использовании механизма P/Invoke: вы объявляете управляемое представление COM-объекта, а среда выполнения CLR создает объект-обертку, реализующий маршалинг. Существует две разновидности оберток: обертка, вызываемая средой выполнения (Runtime Callable Wrapper, RCW), которая позволяет управляемому коду использовать COM-объекты:

Управляемый клиент вызывает неуправляемый COM-объект

и обертка, вызываемая COM-объектами (COM Callable Wrapper, CCW), дающая возможность COM-объектам вызывать управляемый код:

Неуправляемый клиент вызывает управляемый COM-объект

Сторонние COM-объекты часто поставляются вместе с основной сборкой взаимодействий (Primary Interop Assembly, PIA), содержащей определения, одобренные производителем, подписанной и устанавливаемой в глобальный кеш сборок (Global Assembly Cache, GAC). В противном случае можно воспользоваться инструментом tlbimp.exe, являющийся частью Windows SDK, который автоматически генерирует сборку взаимодействий, опираясь на информацию, содержащуюся в библиотеке типов.

При взаимодействиях с COM-объектами повторно используется инфраструктура маршалинга параметров механизма P/Invoke, но с иными умолчаниями (например, по умолчанию строки преобразуются в тип BSTR), поэтому все советы, что были даны в предыдущей статье относительно механизма P/Invoke, также применимы и здесь.

Модель COM имеет собственные проблемы производительности, обусловленные характерными особенностями COM, такими как многопоточная модель подразделений и несогласованность между природой COM, основанной на подсчете ссылок, и моделью сборки мусора в .NET.

Управление жизненным циклом

Получая ссылку на COM-объект в .NET, вы фактически получаете ссылку на объект-обертку RCW. Обертка RCW всегда хранит единственную ссылку на COM-объект и для каждого COM-объекта создается только один экземпляр объекта-обертки RCW. Обертка RCW поддерживает собственный счетчик ссылок, не связанный со счетчиком ссылок COM-объекта. Значение этого счетчика ссылок обычно равно 1, но может быть больше, при участии в маршалинге большего числа интерфейсов или когда к одному и тому же интерфейсу обращается несколько потоков выполнения.

Как правило, когда удаляется последняя управляемая ссылка на RCW, в следующем же цикле сборки мусора в поколении, где находится обертка RCW, вызывается финализатор RCW, который уменьшает счетчик ссылок в COM-объекте (который имеет значение 1) вызовом метода Release() интерфейса IUnknown этого COM-объекта. COM-объект тут же уничтожает себя и освобождает занимаемую им память.

Поскольку сборщик мусора в .NET запускается в непредсказуемые моменты времени и не знает о блоках неуправляемой памяти, выделенных при создании оберток RCW для COM-объектов, он не может ускорить сборку мусора, вследствие чего объем занимаемой памяти может оказаться очень большим.

При необходимости можно вызвать метод Marshal.ReleaseComObject(), чтобы явно освободить объект. Каждый вызов уменьшает счетчик ссылок в RCW и когда он достигнет нуля, автоматически уменьшается счетчик ссылок в соответствующем COM-объекте (точно так же, как при вызове фииализатора RCW). После вызова метода Marshal.ReleaseComObject() нельзя использовать обертку RCW.

Если после вызова счетчик ссылок RCW может оказаться больше нуля, метод Marshal.ReleaseComObject() следует вызывать в цикле, пока он не вернет нулевое значение. Лучше всего вызывать Marshal.ReleaseComObject() внутри блока finally, чтобы гарантировать освобождение COM-объекта, даже если где-то между созданием его экземпляра и освобождением возникнет исключение.

Маршалинг через границы подразделений

Модель COM реализует собственные механизмы синхронизации потоков выполнения для поддержки вызовов между разными потоками, которые могут использоваться даже при работе с объектами, изначально не предназначенными для использования в многопоточной среде. Эти механизмы могут снижать производительность при неправильном их применении. Хотя эта проблема не имеет прямого отношения к взаимодействиям с COM-объектами из .NET, тем не менее, ее стоит обсудить, потому что с ней часто сталкиваются на практике, вероятно потому, что разработчики, привыкшие к типичным приемам синхронизации в .NET могут не знать, что конкретно происходит под покровом COM-объектов.

Модель COM связывает объекты и потоки выполнения с подразделениями (apartments), служащими границами, через которые модель COM выполняет вызовы. Всего имеется несколько типов подразделений:

Однопоточные подразделения (Single-Threaded Apartment, STA)

В каждом подразделении имеется единственный поток выполнения, но может иметься любое количество объектов. В процессе может быть несколько подразделений STA.

Многопоточные подразделения (Multi-Threaded Apartment, МТА)

В каждом подразделении может иметься любое количество потоков выполнения и объектов, но в процессе может быть только одно подразделение МТА. Этот тип используется в .NET по умолчанию.

Потоконезависимые подразделения (Neutral-Threaded Apartment, NTA)

Содержат объекты, но не потоки. В процессе может быть только одно подразделение NTA.

Связывание потока выполнения с подразделением происходит при вызове CoInitialize или CoInitializeEx для инициализации COM-объекта в этом потоке. Функция CoInitialize связывает поток с новым подразделением STA, а функция CoInitializeEx позволяет указать тип подразделения, STA или МТА.

В .NET вам не придется вызывать эти функции непосредственно, вместо этого достаточно добавить атрибут STAThread или MTAThread к точке входа в поток (методу Main). При желании можно также вызвать метод Thread.SetApartmentState() или установить значение в свойстве Thread.ApartmentState перед запуском потока выполнения. Если не указано иное, .NET инициализирует потоки (включая главный поток приложения) как принадлежащие подразделению МТА.

Связывание COM-объектов с подразделениями выполняется, исходя из параметра ThreadingModel в реестре, который может иметь следующие значения:

На рисунке ниже изображена схема взаимоотношений между подразделениями, потоками и объектами:

Деление процесса на подразделения COM

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

В процессе маршалинга вызов функции (включая параметры) преобразуется в сообщение, которое будет послано в очередь принимающего подразделения STA. Для объектов STA очередь реализуется как скрытое окно, оконная процедура которого принимает сообщения и передает COM-объекту с помощью заглушки (stub). При таком подходе COM-объекты в подразделении STA COM всегда вызываются в одном и том же потоке выполнения, благодаря чему обеспечивается безопасность при работе в многопоточном окружении.

Если вызывающее подразделение не совместимо с подразделением COM-объекта, происходит переключение потока и выполняется маршалинг параметра между потоками.

Чтобы избежать падения производительности из-за маршалинга между потоками, старайтесь обеспечить соответствие между подразделением COM-объекта и подразделением создающего его потока. Создавайте и используйте COM-объекты STA в потоках из подразделения STA, а COM объекты из подразделения МТА - в потоках МТА. COM-объекты, помеченные, как поддерживающие оба типа подразделений, могут свободно использоваться из любых потоков выполнения без лишних накладных расходов.

Вызов объектов STA из ASP.NET

По умолчанию среда ASP.NET выполняет страницы в потоках МТА. Если из этих страниц вызываются объекты, находящиеся в подразделениях STA, в дело вступает механизм маршалинга. Если основная масса используемых объектов принадлежит подразделениям STA, это приведет к деградации производительности. Эту проблему можно ликвидировать, пометив страницы атрибутом AspCompat, как показано ниже:

<%@Page Language="C#" AspCompat="true" %>

Обратите внимание, что конструкторы страниц все еще выполняются в потоке выполнения МТА, поэтому создание объектов STA следует выполнять в обработчиках событий Page_Load и Page_Init.

Импортирование библиотек типов и Code Access Security

Механизм Code Access Security выполняет те же проверки безопасности, что и P/Invoke. Вы можете добавлять ключ /unsafe при вызове утилиты tlbimp.exe, которая будет добавлять атрибут SuppressUnmanagedCodeSecurityAttribute к сгенерированным типам. Используйте эту возможность только в системах, пользующихся у вас безусловным доверием, так как она может порождать проблемы безопасности.

NoPIA

До выхода версии .NET Framework 4.0 приходилось вместе с приложением распространять сборки взаимодействий или основные сборки взаимодействий (Primary Interop Assemblies, PIA). Эти сборки обычно получались очень большими (даже в сравнении с кодом, использующим их) и как правило не входят в установочный комплект COM-компонентов; вместо этого их необходимо устанавливать отдельно, потому что сами они не требуются для работы самих COM-объектов. Другая причина, почему сборки PIA не включаются в установочные комплекты, состоит в том, что они устанавливаются в глобальный кеш сборок (GAC). Это вводит зависимость от .NET Framework в иначе полностью независимые приложения.

Начиная с версии .NET Framework 4.0, компиляторы C# и VB.NET могут проверить, какие COM-интерфейсы и методы используются в коде, и скопировать и встроить в вызывающую сборку только действительно необходимые определения, уменьшая размер кода и избавляя от необходимости распространять библиотеки PIA. В Microsoft эта особенность была названа NoPIA. Она действует как в отношении основных сборок взаимодействий, так и в отношении сборок взаимодействий в целом.

Сборки PIA обладают одной важной особенностью, которая называется эквивалентностью типов. Так как они имеют строгое именование и помещаются в глобальный кеш сборок, различные управляемые компоненты могут обмениваться обертками RCW и с точки зрения .NET они будут иметь эквивалентные типы. Напротив, сборки взаимодействий, сгенерированные с помощью tlbimp.exe, не обладают такой особенностью, так как каждый компонент в этом случае получит собственную, отдельную от других, сборку взаимодействий. С появлением поддержки особенности NoPIA отпала необходимость в строгом именовании сборок, и в Microsoft было предложено решение, позволяющее интерпретировать обертки RCW из других сборок, как принадлежащие тому же типу, если интерфейсы имеют одинаковый идентификатор GUID.

Чтобы включить поддержку NoPIA, выберите пункт Properties в контекстном меню Visual Studio после щелчка правой кнопкой мыши на сборке взаимодействий в разделе References, и установите параметр Embed Interop Types (Внедрять типы взаимодействий) в значение True:

Включение поддержки NoPIA в свойствах ссылки на сборку взаимодействий

Исключения

Большинство методов COM-интерфейсов сообщают об успехе или неудаче, возвращая значение типа HRESULT. Отрицательные значения HRESULT (с установленным старшим битом) сообщают об ошибке, а ноль (S_OK) или положительные значения - об успехе. Кроме того, COM-объект может возвращать дополнительную информацию об ошибке при вызове функции SetErrorInfo, передавая объект IErrorInfo, созданный вызовом CreateErrorInfo.

При вызове COM-метода через механизм взаимодействий с COM, заглушка маршалера преобразует значение HRESULT в управляемое исключение, согласно самому значению HRESULT и данным, содержащимся в объекте IErrorInfo. Поскольку возбуждение исключения является достаточно дорогостоящей операцией, функции COM-объекта, которые часто терпят неудачу, могут отрицательно сказываться на производительности. Вы можете подавить автоматическое преобразование исключений, пометив методы атрибутом PreserveSigAttribute. При этом вам придется изменить управляемую сигнатуру, как возвращающую значение типа int, в результате чего параметр retval станет параметром out.

Пройди тесты
Лучший чат для C# программистов