Поделиться через


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

Когда com-клиент вызывает объект .NET, среда CLR создает управляемый объект и вызываемую оболочку COM для объекта. Поскольку на объект .NET нельзя ссылаться напрямую, COM-клиенты используют CCW как прокси для управляемого объекта.

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

Несколько клиентов COM, содержащих ссылку на CCW, которая предоставляет доступ к INew.

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

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

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

Время существования объекта

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

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

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

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

Схема, показывающая, как CCW производит com-интерфейсы.

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

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

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

Интерфейс Описание
Интерфейс класса (_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-взаимодействие автоматически создает интерфейс класса, послеверсионные изменения вашего класса могут изменить макет интерфейса класса, предоставляемого общеязыковой средой выполнения. Так как 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.

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

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

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

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

<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) предоставляет описание интерфейса класса в экспортируемой библиотеке типов. Такое описание позволяет клиентам с поздней привязкой кэшировать dispIds во время компиляции.

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

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

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

<EmbedInteropTypes>True</EmbedInteropTypes>

См. также