다음을 통해 공유


COM 상호 운용을 위한 .NET 형식 검증

.NET 형식을 COM에 노출

어셈블리에서 형식을 COM 애플리케이션으로 노출하려는 경우 디자인 타임에 COM interop의 요구 사항을 고려하세요. 다음 지침을 준수하면 관리되는 형식(클래스, 인터페이스, 구조체 및 열거형)이 COM 형식과 원활하게 통합됩니다.

  • 클래스에서 인터페이스를 명시적으로 구현해야 합니다.

    COM interop에서 클래스의 모든 멤버와 기본 클래스의 멤버를 포함하는 인터페이스를 자동으로 생성하는 메커니즘을 제공하지만 명시적 인터페이스를 제공하는 것이 훨씬 좋습니다. 자동으로 생성된 인터페이스는 클래스 인터페이스라고 합니다. 지침은 클래스 인터페이스 소개를 참조하세요.

    IDL(Interface Definition Language) 또는 이와 동등한 사항을 사용할 필요 없이 Visual Basic, C# 및 C++를 사용하여 코드에 인터페이스 정의를 통합할 수 있습니다. 구문에 대한 세부 정보는 언어 문서를 참조하세요.

  • 관리되는 형식은 public이어야 합니다.

    어셈블리의 public 형식만 등록하고 형식 라이브러리로 내보냅니다. 결과적으로 public 형식만 COM에 표시됩니다.

    관리되는 형식은 COM에 노출되지 않을 수 있는 기타 관리 코드에 기능을 공개합니다. 예를 들어 매개 변수화된 생성자, 정적 메서드 및 상수 필드는 COM 클라이언트에 노출되지 않습니다. 또한 런타임 시 데이터를 형식에 대해 마샬링할 때 데이터가 복사되거나 변환될 수 있습니다.

  • 메서드, 속성, 필드 및 이벤트는 public이어야 합니다.

    public 형식의 멤버를 COM에 표시하려는 경우 해당 멤버도 public이어야 합니다. ComVisibleAttribute를 적용하여 어셈블리의 가시성, public 형식 또는 public 형식의 공용 멤버를 제한할 수 있습니다. 기본적으로 모든 public 형식 및 멤버만 표시됩니다.

  • 형식에는 COM에서 활성화될 public 매개 변수가 없는 생성자가 있어야 합니다.

    관리되는 public 형식만 COM에 표시됩니다. 그러나 public 매개 변수가 없는 생성자(인수 없는 생성자)가 없으면 COM 클라이언트에서 형식을 만들 수 없습니다. 다른 방법으로 활성화된 경우에도 COM 클라이언트에서 여전히 형식을 사용할 수 있습니다.

  • 형식은 추상일 수 없습니다.

    COM 클라이언트와 .NET 클라이언트 모두 추상 형식을 만들 수 없습니다.

COM으로 내보낼 때 관리되는 형식의 상속 계층 구조가 평면화됩니다. 관리되는 환경과 관리되지 않는 환경에서는 버전 관리도 다릅니다. COM에 노출된 형식에는 관리되는 다른 형식과 동일한 버전 관리 특성이 없습니다.

.NET에서 COM 형식 사용

.NET에서 COM 형식을 사용하고 Tlbimp.exe(형식 라이브러리 가져오기)와 같은 도구를 사용하지 않으려는 경우 다음 지침을 따라야 합니다.

  • 인터페이스에는 ComImportAttribute가 적용되어야 합니다.
  • 인터페이스에는 COM 인터페이스의 인터페이스 ID와 함께 적용된 GuidAttribute가 있어야 합니다.
  • 인터페이스에는 이 인터페이스의 기본 인터페이스 형식(IUnknown, IDispatch 또는 IInspectable)을 지정하려면 InterfaceTypeAttribute가 적용되어야 합니다.
    • 기본 옵션은 기본 형식이 IDispatch이고 선언된 메서드를 인터페이스의 예상 가상 함수 테이블에 추가하는 것입니다.
    • .NET Framework만 IInspectable의 기본 형식 지정을 지원합니다.

이러한 지침은 일반적인 시나리오에 대한 최소 요구 사항을 제공합니다. 더 많은 사용자 지정 옵션이 존재하며 자세한 내용은 Interop 특성 적용에 설명되어 있습니다.

.NET에서 COM 인터페이스 정의

.NET 코드가 ComImportAttribute 특성이 있는 인터페이스를 통해 COM 개체에 대한 메서드를 호출하려고 하면 가상 함수 테이블(vtable 또는 vftable이라고도 함)을 빌드하여 호출할 네이티브 코드를 결정하도록 인터페이스의 .NET 정의를 구성해야 합니다. 이 과정은 복잡합니다. 다음 예에서는 몇 가지 간단한 사례를 보여 줍니다.

몇 가지 방법이 있는 COM 인터페이스를 고려해보세요.

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

이 인터페이스의 경우 다음 표에서는 가상 함수 테이블 레이아웃을 설명합니다.

IComInterface 가상 함수 테이블 슬롯 메서드 이름
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2

각 메서드는 선언된 순서대로 가상 함수 테이블에 추가됩니다. 특정 순서는 C++ 컴파일러에 의해 정의되지만 오버로드가 없는 간단한 경우에는 선언 순서가 테이블의 순서를 정의합니다.

다음과 같이 이 인터페이스에 해당하는 .NET 인터페이스를 선언합니다.

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid(/* The IID for IComInterface */)]
interface IComInterface
{
    void Method();
    void Method2();
}

InterfaceTypeAttribute는 기본 인터페이스를 지정합니다. 몇 가지 옵션을 제공합니다.

ComInterfaceType 기본 인터페이스 형식 특성이 지정된 인터페이스의 멤버 동작
InterfaceIsIUnknown IUnknown 가상 함수 테이블에는 먼저 IUnknown의 멤버가 있고 선언 순서대로 이 인터페이스의 멤버가 있습니다.
InterfaceIsIDispatch IDispatch 가상 함수 테이블에는 멤버가 추가되지 않습니다. IDispatch를 통해서만 액세스할 수 있습니다.
InterfaceIsDual IDispatch 가상 함수 테이블에는 먼저 IDispatch의 멤버가 있고 선언 순서대로 이 인터페이스의 멤버가 있습니다.
InterfaceIsIInspectable IInspectable 가상 함수 테이블에는 먼저 IInspectable의 멤버가 있고 선언 순서대로 이 인터페이스의 멤버가 있습니다. .NET Framework에서만 지원됩니다.

COM 인터페이스 상속 및 .NET

ComImportAttribute를 사용하는 COM interop 시스템은 인터페이스 상속과 상호 작용하지 않으므로 일부 완화 단계를 수행하지 않으면 예기치 않은 동작이 발생할 수 있습니다.

System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute 특성을 사용하는 COM 원본 생성기는 인터페이스 상속과 상호 작용하므로 예상대로 더 많이 작동합니다.

C++의 COM 인터페이스 상속

C++에서 개발자는 다음과 같이 다른 COM 인터페이스에서 파생되는 COM 인터페이스를 선언할 수 있습니다.

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

struct IComInterface2 : public IComInterface
{
    STDMETHOD(Method3)() = 0;
};

이 선언 스타일은 기존 인터페이스를 변경하지 않고 COM 개체에 메서드를 추가하는 메커니즘으로 정기적으로 사용됩니다. 이는 호환성이 손상되는 변경이 될 수 있습니다. 이 상속 메커니즘으로 인해 다음과 같은 가상 함수 테이블 레이아웃이 생성됩니다.

IComInterface 가상 함수 테이블 슬롯 메서드 이름
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 가상 함수 테이블 슬롯 메서드 이름
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

결과적으로 IComInterface2*에서 IComInterface에 정의된 메서드를 쉽게 호출할 수 있습니다. 특히 기본 인터페이스에서 메서드를 호출하는 경우 기본 인터페이스에 대한 포인터를 가져오기 위해 QueryInterface를 호출할 필요가 없습니다. 또한 C++에서는 IComInterface2*에서 IComInterface*로의 암시적 변환을 허용합니다. 이는 잘 정의되어 있으며 QueryInterface를 다시 호출하는 것을 방지할 수 있습니다. 결과적으로 C 또는 C++에서는 원하지 않는 경우 기본 형식에 도달하기 위해 QueryInterface를 호출할 필요가 없으며, 이로 인해 성능이 어느 정도 개선될 수 있습니다.

참고 항목

WinRT 인터페이스는 이 상속 모델을 따르지 않습니다. .NET의 [ComImport] 기반 COM interop 모델과 동일한 모델을 따르도록 정의되었습니다.

ComImportAttribute를 사용한 인터페이스 상속

.NET에서 인터페이스 상속처럼 보이는 C# 코드는 실제로 인터페이스 상속이 아닙니다. 다음 코드를 생각해 봅시다.

interface I
{
    void Method1();
}
interface J : I
{
    void Method2();
}

이 코드는 "JI를 구현합니다"라고 말하지 않습니다. 코드에는 실제로 "J를 구현하는 모든 형식은 I도 구현해야 합니다."라고 나와 있습니다. 이러한 차이는 ComImportAttribute 기반 Interop의 인터페이스 상속을 비인체공학적으로 만드는 근본적인 디자인 결정으로 이어집니다. 인터페이스는 항상 자체적으로 고려됩니다. 인터페이스의 기본 인터페이스 목록은 지정된 .NET 인터페이스에 대한 가상 함수 테이블을 결정하는 계산에 영향을 미치지 않습니다.

결과적으로 이전 C++ COM 인터페이스 예와 자연스럽게 동등한 가상 함수 테이블 레이아웃이 생성됩니다.

C# 코드:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

가상 함수 테이블 레이아웃:

IComInterface 가상 함수 테이블 슬롯 메서드 이름
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 가상 함수 테이블 슬롯 메서드 이름
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface2::Method3

이러한 가상 함수 테이블은 C++ 예와 다르므로 런타임 시 심각한 문제가 발생합니다. ComImportAttribute를 사용하는 .NET에서 이러한 인터페이스의 올바른 정의는 다음과 같습니다.

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    new void Method();
    new void Method2();
    void Method3();
}

메타데이터 수준에서 IComInterface2IComInterface를 구현하지 않지만 IComInterface2 구현자가 IComInterface도 구현해야 한다는 점만 지정합니다. 따라서 기본 인터페이스 형식의 각 메서드를 다시 선언해야 합니다.

GeneratedComInterfaceAttribute를 사용한 인터페이스 상속(.NET 8 이상)

GeneratedComInterfaceAttribute에 의해 트리거된 COM 원본 생성기는 C# 인터페이스 상속을 COM 인터페이스 상속으로 구현하므로 가상 함수 테이블이 예상대로 배치됩니다. 이전 예를 사용하는 경우 System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute를 사용하는 .NET에서 이러한 인터페이스의 올바른 정의는 다음과 같습니다.

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

기본 인터페이스의 메서드는 다시 선언할 필요가 없으며 다시 선언해서는 안 됩니다. 다음 표에서는 결과 가상 함수 테이블에 대해 설명합니다.

IComInterface 가상 함수 테이블 슬롯 메서드 이름
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 가상 함수 테이블 슬롯 메서드 이름
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

보시다시피 이 테이블은 C++ 예와 일치하므로 이러한 인터페이스는 올바르게 작동합니다.

참고 항목