Вызываемая оболочка COM

Когда клиент COM вызывает объект .NET, среда CLR создает для этого объекта управляемый объект и вызываемую оболочку COM. Не имея возможности обращаться к объекту .NET напрямую, клиенты COM используют вызываемую оболочку COM в качестве посредника для управляемого объекта.

Среда выполнения создает одну вызываемую оболочку COM для управляемого объекта независимо от числа клиентов COM, которым требуются его службы. Как показано на рисунке ниже, несколько клиентов COM могут содержать ссылку на вызываемую оболочку COM, предоставляющую интерфейс INew. Вызываемая оболочка COM, в свою очередь, содержит единственную ссылку на управляемый объект, который реализует интерфейс и обрабатывается сборщиком мусора. Клиенты COM и .NET могут одновременно выполнять запросы к одному и тому же управляемому объекту.

Multiple COM clients holding a reference to the CCW that exposes INew.

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

Идентификация объектов

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

Время жизни объекта

В отличие от клиента .NET, учет ссылок для вызываемой оболочки COM, в которую инкапсулирован клиент, ведется обычным для модели COM образом. Когда счетчик ссылок на вызываемую оболочку COM достигает нуля, оболочка освобождает свою ссылку на управляемый объект. Управляемый объект, на который не осталось ссылок, обрабатывается сборщиком мусора в течение следующего цикла.

Имитация COM-интерфейсов

Вызываемая оболочка COM предоставляет клиентам COM доступ ко всем открытым и видимым COM-интерфейсам, типам данных и возвращаемым значениям способом, совместимым с реализацией средствами COM взаимодействия на основе интерфейсов. Для клиента COM вызов методов в объекте .NET идентичен вызову методов в COM-объекте.

Для обеспечения такой унификации вызываемая оболочка COM создает традиционные COM-интерфейсы, такие как IUnknown и IDispatch. Как показано на рисунке ниже, вызываемая оболочка COM содержит единственную ссылку на инкапсулируемый в нее объект .NET. Клиент COM и объект .NET взаимодействуют друг с другом с помощью посредника и создания заглушки вызываемой оболочки COM.

Diagram that shows how CCW manufactures COM interfaces.

Среда выполнения .NET не только обеспечивает доступ к интерфейсам, которые явным образом реализуются классом в управляемой среде, но и от имени объекта предоставляет реализации COM-интерфейсов, перечисленных в таблице ниже. Класс .NET может переопределять заданное по умолчанию поведение, предоставляя собственную реализацию этих интерфейсов. Однако среда выполнения всегда предоставляет реализацию интерфейсов IUnknown и IDispatch.

Интерфейс Description
IDispatch Предоставляет механизм позднего связывания с типом.
IErrorInfo Предоставляет текстовое описание ошибки, ее источник, файл справки, контекст справки и идентификатор GUID интерфейса, определившего ошибку (для классов .NET всегда GUID_NULL).
IProvideClassInfo Позволяет клиентам COM получать доступ к интерфейсу ITypeInfo, реализованному управляемым классом. Возвращает COR_E_NOTSUPPORTED в .NET Core для типов, не импортированных из COM.
ISupportErrorInfo Позволяет клиенту COM определить, поддерживает ли управляемый объект интерфейс IErrorInfo. Если это так, то клиент может получить указатель на последний по времени объект исключения. Все управляемые типы поддерживают интерфейс IErrorInfo.
ITypeInfo (только в .NET Framework) Предоставляет для класса сведения о типе, которые совпадают со сведениями о типе, предоставленными Tlbexp.exe.
IUnknown Предоставляет стандартную реализацию интерфейса IUnknown, с помощью которой клиент COM управляет временем жизни вызываемой оболочки COM и обеспечивает приведение типов.

Управляемый класс также может предоставлять COM-интерфейсы, описанные в таблице ниже.

Интерфейс Description
Интерфейс класса (_classname) Интерфейс, предоставляемый средой выполнения и не определенный явным образом, который обеспечивает доступ ко всем открытым интерфейсам, методам, свойствам и полям, явно предоставляемым в управляемом объекте.
IConnectionPoint и IConnectionPointContainer Интерфейс для объектов, которые являются источниками событий на основе делегатов (интерфейс для регистрации подписчиков событий).
IDispatchEx (только в .NET Framework) Интерфейс, предоставляемый средой выполнения, если класс реализует интерфейс IExpando. Интерфейс IDispatchEx является расширением интерфейса IDispatch, который, в отличие от интерфейса IDispatch, позволяет перечислять, добавлять, удалять и вызывать члены с учетом регистра.
IEnumVARIANT Интерфейс для классов типа коллекции, в которых перечислены ее объекты, если класс реализует интерфейс IEnumerable.

Введение в интерфейс класса

Интерфейс класса, не определенный явным образом в управляемом коде, — это интерфейс, который предоставляет доступ ко всем открытым методам, свойствам, полям и событиям, к которым предоставлен явный доступ в объекте .NET. Этот интерфейс может быть сдвоенным или интерфейсом диспетчеризации. Интерфейс класса получает имя самого класса .NET с символом подчеркивания впереди. Например, для класса Mammal интерфейс класса _Mammal.

В случае с производными классами интерфейс класса также предоставляет доступ ко всем открытым методам, свойствам и полям базового класса. Производный класс также предоставляет доступ к интерфейсу класса для каждого базового класса. Например, если класс Mammal расширяет класс MammalSuperclass, который сам расширяет System.Object, объект .NET предоставляет клиентам COM три интерфейса класса с именем _Mammal, _MammalSuperclass и _Object.

Например, рассмотрим приведенный ниже класс .NET.

' Applies the ClassInterfaceAttribute to set the interface to dual.
<ClassInterface(ClassInterfaceType.AutoDual)> _
' Implicitly extends System.Object.
Public Class Mammal
    Sub Eat()
    Sub Breathe()
    Sub Sleep()
End Class
// Applies the ClassInterfaceAttribute to set the interface to dual.
[ClassInterface(ClassInterfaceType.AutoDual)]
// Implicitly extends System.Object.
public class Mammal
{
    public void Eat() {}
    public void Breathe() {}
    public void Sleep() {}
}

Клиент COM может получить указатель на интерфейс класса с именем _Mammal. В .NET Framework можно использовать средство экспорта библиотек типов (Tlbexp.exe) для создания библиотеки типов, содержащей определение интерфейса _Mammal. Средство экспорта библиотек типов не поддерживается в .NET Core. Если класс Mammal реализовал один или несколько интерфейсов, они будут отображены в коклассе.

[odl, uuid(…), hidden, dual, nonextensible, oleautomation]
interface _Mammal : IDispatch
{
    [id(0x00000000), propget] HRESULT ToString([out, retval] BSTR*
        pRetVal);
    [id(0x60020001)] HRESULT Equals([in] VARIANT obj, [out, retval]
        VARIANT_BOOL* pRetVal);
    [id(0x60020002)] HRESULT GetHashCode([out, retval] short* pRetVal);
    [id(0x60020003)] HRESULT GetType([out, retval] _Type** pRetVal);
    [id(0x6002000d)] HRESULT Eat();
    [id(0x6002000e)] HRESULT Breathe();
    [id(0x6002000f)] HRESULT Sleep();
}
[uuid(…)]
coclass Mammal
{
    [default] interface _Mammal;
}

Создавать интерфейс класса необязательно. По умолчанию COM-взаимодействие создает для каждого класса, экспортируемого в библиотеку типов, интерфейс диспетчеризации. Автоматическое создание этого интерфейса можно предотвратить или изменить, применив к классу атрибут ClassInterfaceAttribute. Хотя интерфейс класса и может упростить задачу обеспечения доступа из COM к управляемым классам, возможности его использования ограничены.

Внимание

Использование интерфейса класса вместо явного определения собственного интерфейса может усложнить управление версиями управляемого класса в будущем. Перед использованием интерфейса класса следует ознакомиться с приведенными ниже рекомендациями.

Вместо создания интерфейса класса лучше определить явный интерфейс, который могли бы использовать клиенты COM.

Так как COM-взаимодействие создает интерфейс класса автоматически, изменения, вносимые в класс в следующих версиях, могут повлиять на компоновку интерфейса класса, предоставляемого средой CLR. Так как клиенты COM обычно не готовы к изменениям в компоновке интерфейса, изменение компоновки членов класса вызовет сбой в их работе.

Эта рекомендация подкрепляет представление о том, что интерфейсы, предоставляемые клиентам COM, лучше оставлять неизменными. Чтобы снизить риск сбоя в работе клиентов COM в результате непреднамеренного изменения компоновки интерфейса, нужно изолировать все изменения, вносимые в класс, от интерфейса путем его явного определения.

С помощью атрибута ClassInterfaceAttribute отключите автоматическое создание интерфейса класса и реализуйте для класса явный интерфейс, как показано во фрагменте программы ниже:

<ClassInterface(ClassInterfaceType.None)>Public Class LoanApp
    Implements IExplicit
    Sub M() Implements IExplicit.M
…
End Class
[ClassInterface(ClassInterfaceType.None)]
public class LoanApp : IExplicit
{
    int IExplicit.M() { return 0; }
}

Значение ClassInterfaceType.None предотвращает создание интерфейса класса при экспорте метаданных класса в библиотеку типов. В предыдущем примере клиенты COM могут получить доступ к классу LoanApp только через интерфейс IExplicit.

Избегайте кэширования идентификаторов диспетчеризации (DispId)

Использование интерфейса класса является допустимым вариантом для клиентов со сценариями, клиентов Microsoft Visual Basic 6.0, а также клиентов с поздним связыванием, которые не кэшируют идентификаторы DispId членов интерфейса. Идентификаторы DispId определяют члены интерфейса, разрешающие позднее связывание.

Для интерфейса класса идентификаторы DispId создаются на основе позиции члена в интерфейсе. Если вы изменяете порядок членов и экспортируете класс в библиотеку типов, меняются и идентификаторы DispId, созданные в интерфейсе класса.

Чтобы избежать нарушений в работе клиентов COM с поздним связыванием при использовании интерфейса класса, примените атрибут ClassInterfaceAttribute со значением ClassInterfaceType.AutoDispatch. Это значение реализует интерфейс диспетчеризации класса, но пропускает описание интерфейса в библиотеке типов. Без описания интерфейса клиенты не могут кэшировать идентификаторы DispId во время компиляции. Хотя это тип интерфейса по умолчанию для интерфейса класса, значение атрибута можно задать явным образом.

<ClassInterface(ClassInterfaceType.AutoDispatch)> Public Class LoanApp
    Implements IAnother
    Sub M() Implements IAnother.M
…
End Class
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class LoanApp
{
    public int M() { return 0; }
}

Чтобы получить идентификатор DispId члена интерфейса во время выполнения, клиент COM может вызвать метод IDispatch.GetIdsOfNames. Чтобы вызвать метод для интерфейса, передайте возвращенный идентификатор DispId в качестве аргумента для IDispatch.Invoke.

Ограничьте использование сдвоенного интерфейса для интерфейса класса.

Сдвоенные интерфейсы позволяют клиентам COM выполнять раннее и позднее связывание с членами интерфейсов. Во время разработки и тестирования может оказаться полезным сделать интерфейс класса сдвоенным. Этот вариант также допустим и для управляемого класса (и его базовых классов), который никогда не будет изменяться. Во всех остальных случаях следует воздержаться от использования сдвоенного интерфейса класса.

Изредка автоматически созданный сдвоенный интерфейс может оказаться полезным, но чаще он создает трудности при работе с версиями. Например, внесение изменений в базовый класс легко может нарушить работу клиентов COM, использующих интерфейс производного класса. Если базовый класс предоставляется сторонним поставщиком, вы не можете контролировать компоновку интерфейса класса. Кроме того, в отличие от интерфейса диспетчеризации сдвоенный интерфейс (ClassInterfaceType.AutoDual) предоставляет описание интерфейса класса в экспортированной библиотеке типов. Это описание стимулирует клиентов с поздним связыванием кэшировать идентификаторы DispId во время компиляции.

Все уведомления о событиях COM должны быть с поздним связыванием.

По умолчанию сведения о типах COM внедряются непосредственно в управляемые сборки, устраняя необходимость в использовании основных сборок взаимодействия. Тем не менее одним из ограничений внедренных сведений о типах является то, что доставка уведомлений о событиях COM с использованием вызовов vtable с ранним связыванием не поддерживается (в отличие от вызовов IDispatch::Invoke с поздним связыванием).

Если для приложения требуются вызовы методов интерфейса событий COM с ранним связыванием, можно задать для свойства Embed Interop Types в Visual Studio значение true или включить следующий элемент в файле проекта:

<EmbedInteropTypes>True</EmbedInteropTypes>

См. также