자습서: ComWrappers
API 사용
이 자습서에서는 최적화되고 AOT 친화적인 COM interop 솔루션을 제공하기 위해 ComWrappers
형식을 적절하게 하위 클래스화하는 방법을 알아봅니다. 이 자습서를 시작하기 전에 COM, 해당 아키텍처 및 기존 COM interop 솔루션에 대해 잘 알고 있어야 합니다.
이 자습서에서는 다음 인터페이스 정의를 구현합니다. 이러한 인터페이스와 해당 구현은 다음을 보여 줍니다.
- COM/.NET 경계를 넘어 형식을 마샬링 및 역마샬링합니다.
- .NET에서 네이티브 COM 개체를 사용하는 두 가지 고유한 방식입니다.
- .NET 5 이상에서 사용자 지정 COM interop을 사용하도록 설정하기 위한 권장 패턴입니다.
이 자습서에 사용된 모든 소스 코드는 dotnet/samples 리포지토리에서 사용할 수 있습니다.
참고 항목
.NET 8 SDK 이상 버전에서는 ComWrappers
API 구현을 자동으로 생성하기 위해 원본 생성기가 제공됩니다. 자세한 내용은 ComWrappers
원본 생성을 참조하세요.
C# 정의
interface IDemoGetType
{
string? GetString();
}
interface IDemoStoreType
{
void StoreString(int len, string? str);
}
Win32 C++ 정의
MIDL_INTERFACE("92BAA992-DB5A-4ADD-977B-B22838EE91FD")
IDemoGetType : public IUnknown
{
HRESULT STDMETHODCALLTYPE GetString(_Outptr_ wchar_t** str) = 0;
};
MIDL_INTERFACE("30619FEA-E995-41EA-8C8B-9A610D32ADCB")
IDemoStoreType : public IUnknown
{
HRESULT STDMETHODCALLTYPE StoreString(int len, _In_z_ const wchar_t* str) = 0;
};
ComWrappers
디자인 개요
ComWrappers
API는 .NET 5+ 런타임과 COM interop을 달성하는 데 필요한 최소한의 상호 작용을 제공하도록 설계되었습니다. 이는 기본 제공 COM interop 시스템에 존재하는 많은 세부 사항이 존재하지 않으며 기본 구성 요소로 빌드되어야 함을 의미합니다. API의 두 가지 주요 책임은 다음과 같습니다.
- 효율적인 개체 식별(예:
IUnknown*
인스턴스와 관리 개체 간의 매핑) - GC(가비지 수집기) 상호 작용.
이러한 효율성은 ComWrappers
API를 통해 래퍼 만들기 및 획득을 요구함으로써 달성됩니다.
ComWrappers
API에는 책임이 거의 없기 때문에 대부분의 interop 작업을 소비자가 처리해야 하는 것은 당연한 일입니다. 이는 사실입니다. 그러나 추가 작업은 대체로 기계적이며 원본 생성 솔루션을 통해 수행할 수 있습니다. 예를 들어, C#/WinRT 도구 체인은 WinRT interop 지원을 제공하기 위해 ComWrappers
를 기반으로 빌드된 원본 생성 솔루션입니다.
ComWrappers
하위 클래스 구현
ComWrappers
하위 클래스를 제공한다는 것은 .NET 런타임에 충분한 정보를 제공하여 COM에 프로젝션되는 관리 개체 및 .NET에 프로젝션되는 COM 개체에 대한 래퍼를 만들고 기록하는 것을 의미합니다. 하위 클래스의 개요를 살펴보기 전에 몇 가지 용어를 정의해야 합니다.
관리 개체 래퍼 – 관리 .NET 개체를 비.NET 환경에서 사용하려면 래퍼가 필요합니다. 이러한 래퍼는 역사적으로 COM 호출 가능 래퍼(CCW)라고 합니다.
네이티브 개체 래퍼 – .NET 이외의 언어로 구현된 COM 개체를 .NET에서 사용할 수 있으려면 래퍼가 필요합니다. 이러한 래퍼는 역사적으로 런타임 호출 가능 래퍼(RCW)라고 합니다.
1단계 - 의도를 구현하고 이해하기 위한 방법 정의
ComWrappers
형식을 확장하려면 다음 세 가지 메서드를 구현해야 합니다. 이러한 각 메서드는 래퍼 형식의 만들기 또는 삭제에 대한 사용자의 참여를 나타냅니다. ComputeVtables()
및 CreateObject()
메서드는 각각 관리 개체 래퍼와 네이티브 개체 래퍼를 만듭니다. ReleaseObjects()
메서드는 제공된 래퍼 컬렉션이 기본 네이티브 개체에서 "해제"되도록 요청하기 위해 런타임에서 사용됩니다. 대부분의 경우 ReleaseObjects()
메서드의 본문은 참조 추적기 프레임워크와 관련된 고급 시나리오에서만 호출되기 때문에 간단히 NotImplementedException을 throw할 수 있습니다.
// See referenced sample for implementation.
class DemoComWrappers : ComWrappers
{
protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count) =>
throw new NotImplementedException();
protected override object? CreateObject(IntPtr externalComObject, CreateObjectFlags flags) =>
throw new NotImplementedException();
protected override void ReleaseObjects(IEnumerable objects) =>
throw new NotImplementedException();
}
ComputeVtables()
메서드를 구현하려면 지원할 관리 형식을 결정합니다. 이 자습서에서는 이전에 정의된 두 개의 인터페이스(IDemoGetType
및 IDemoStoreType
)와 두 개의 인터페이스(DemoImpl
)를 구현하는 관리 형식을 지원합니다.
class DemoImpl : IDemoGetType, IDemoStoreType
{
string? _string;
public string? GetString() => _string;
public void StoreString(int _, string? str) => _string = str;
}
CreateObject()
메서드의 경우 지원하려는 항목도 결정해야 합니다. 하지만 이 경우에는 COM 클래스가 아니라 관심 있는 COM 인터페이스만 알 수 있습니다. COM 쪽에서 사용되는 인터페이스는 .NET 쪽에서 프로젝션하는 인터페이스(즉, IDemoGetType
및 IDemoStoreType
)와 동일합니다.
이 자습서에서는 ReleaseObjects()
를 구현하지 않습니다.
2단계 – ComputeVtables()
구현
관리 개체 래퍼부터 시작할 예정입니다. 이 래퍼는 더 쉽습니다. 각 인터페이스를 COM 환경에 프로젝션하기 위해 각 인터페이스에 대해 가상 메서드 테이블 또는 vtable을 빌드합니다. 이 자습서에서는 vtable을 일련의 포인터로 정의합니다. 여기서 각 포인터는 인터페이스의 함수 구현을 나타냅니다. 여기서 순서는 매우 중요합니다. COM에서 모든 인터페이스는 IUnknown
에서 상속됩니다. IUnknown
형식에는 QueryInterface()
, AddRef()
, Release()
순서로 정의된 세 가지 메서드가 있습니다. IUnknown
메서드 뒤에는 특정 인터페이스 메서드가 옵니다. 예를 들어, IDemoGetType
및 IDemoStoreType
을 고려해보세요. 개념적으로 형식에 대한 vtable은 다음과 같습니다.
IDemoGetType | IDemoStoreType
==================================
QueryInterface | QueryInterface
AddRef | AddRef
Release | Release
GetString | StoreString
DemoImpl
을 보면 이미 GetString()
및 StoreString()
에 대한 구현이 있지만 IUnknown
함수는 어떻게 되나요? IUnknown
인스턴스를 구현하는 방법은 이 자습서의 범위를 벗어나지만 ComWrappers
에서 수동으로 수행할 수 있습니다. 그러나 이 자습서에서는 런타임이 해당 부분을 처리하도록 합니다. ComWrappers.GetIUnknownImpl()
메서드를 사용하여 IUnknown
구현을 가져올 수 있습니다.
모든 메서드를 구현한 것처럼 보일 수 있지만 불행하게도 COM vtable에서는 IUnknown
함수만 사용할 수 있습니다. COM은 런타임 외부에 있으므로 DemoImpl
구현에 대한 네이티브 함수 포인터를 만들어야 합니다. 이는 C# 함수 포인터와 UnmanagedCallersOnlyAttribute
를 사용하여 수행할 수 있습니다. COM 함수 시그니처를 모방하는 static
함수를 만들어 vtable에 삽입할 함수를 만들 수 있습니다. 다음은 IDemoGetType.GetString()
에 대한 COM 서명의 예입니다. COM ABI에서 첫 번째 인수가 인스턴스 자체임을 기억해야 합니다.
[UnmanagedCallersOnly]
public static int GetString(IntPtr _this, IntPtr* str);
IDemoGetType.GetString()
의 래퍼 구현은 마샬링 논리와 래핑되는 관리 개체에 대한 디스패치로 구성되어야 합니다. 모든 디스패치 상태는 제공된 _this
인수 내에 포함됩니다. _this
인수는 실제로 ComInterfaceDispatch*
형식입니다. 이 형식은 나중에 설명할 단일 필드 Vtable
이 있는 하위 수준 구조를 나타냅니다. 이 형식 및 해당 레이아웃에 대한 추가 세부 정보는 런타임의 구현 세부 정보이므로 이에 의존해서는 안 됩니다. ComInterfaceDispatch*
인스턴스에서 관리되는 인스턴스를 검색하려면 다음 코드를 사용합니다.
IDemoGetType inst = ComInterfaceDispatch.GetInstance<IDemoGetType>((ComInterfaceDispatch*)_this);
이제 vtable에 삽입할 수 있는 C# 메서드가 있으므로 vtable을 구성할 수 있습니다. 언로드할 수 있는 어셈블리에서 작동하는 방식으로 메모리를 할당하기 위해 RuntimeHelpers.AllocateTypeAssociatedMemory()
를 사용하는 것에 유의해야 합니다.
GetIUnknownImpl(
out IntPtr fpQueryInterface,
out IntPtr fpAddRef,
out IntPtr fpRelease);
// Local variables with increment act as a guard against incorrect construction of
// the native vtable. It also enables a quick validation of final size.
int tableCount = 4;
int idx = 0;
var vtable = (IntPtr*)RuntimeHelpers.AllocateTypeAssociatedMemory(
typeof(DemoComWrappers),
IntPtr.Size * tableCount);
vtable[idx++] = fpQueryInterface;
vtable[idx++] = fpAddRef;
vtable[idx++] = fpRelease;
vtable[idx++] = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr*, int>)&ABI.IDemoGetTypeManagedWrapper.GetString;
Debug.Assert(tableCount == idx);
s_IDemoGetTypeVTable = (IntPtr)vtable;
vtable 할당은 ComputeVtables()
구현의 첫 번째 부분입니다. 또한 지원하려는 형식에 대한 포괄적인 COM 정의를 구성해야 합니다. DemoImpl
과 그중 COM에서 사용할 수 있는 부분을 생각해 보세요. 이제 구성된 vtable을 사용하여 COM에서 관리 개체의 전체 보기를 나타내는 일련의 ComInterfaceEntry
인스턴스를 만들 수 있습니다.
s_DemoImplDefinitionLen = 2;
int idx = 0;
var entries = (ComInterfaceEntry*)RuntimeHelpers.AllocateTypeAssociatedMemory(
typeof(DemoComWrappers),
sizeof(ComInterfaceEntry) * s_DemoImplDefinitionLen);
entries[idx].IID = IDemoGetType.IID_IDemoGetType;
entries[idx++].Vtable = s_IDemoGetTypeVTable;
entries[idx].IID = IDemoStoreType.IID_IDemoStoreType;
entries[idx++].Vtable = s_IDemoStoreVTable;
Debug.Assert(s_DemoImplDefinitionLen == idx);
s_DemoImplDefinition = entries;
관리 개체 래퍼에 대한 vtable 및 항목 할당은 데이터가 해당 형식의 모든 인스턴스에 사용될 수 있으므로 미리 수행될 수 있고 수행되어야 합니다. 여기서 작업은 static
생성자 또는 모듈 이니셜라이저에서 수행할 수 있지만 ComputeVtables()
메서드가 최대한 간단하고 빠르도록 미리 수행해야 합니다.
protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags,
out int count)
{
if (obj is DemoImpl)
{
count = s_DemoImplDefinitionLen;
return s_DemoImplDefinition;
}
// Unknown type
count = 0;
return null;
}
ComputeVtables()
메서드를 구현하면 ComWrappers
하위 클래스가 DemoImpl
인스턴스에 대한 관리 개체 래퍼를 생성할 수 있습니다. GetOrCreateComInterfaceForObject()
호출에서 반환된 관리 개체 래퍼는 IUnknown*
형식입니다. 래퍼에 전달되는 네이티브 API에 다른 인터페이스가 필요한 경우 해당 인터페이스에 대한 Marshal.QueryInterface()
를 수행해야 합니다.
var cw = new DemoComWrappers();
var demo = new DemoImpl();
IntPtr ccw = cw.GetOrCreateComInterfaceForObject(demo, CreateComInterfaceFlags.None);
3단계 – CreateObject()
구현
네이티브 개체 래퍼를 구성하면 관리 개체 래퍼를 구성하는 것보다 더 많은 구현 옵션과 미묘한 차이가 있습니다. 해결해야 할 첫 번째 질문은 ComWrappers
하위 클래스의 COM 형식 지원을 얼마나 허용하는지입니다. 가능한 모든 COM 형식을 지원하려면 상당한 양의 코드를 작성하거나 Reflection.Emit
를 현명하게 사용해야 합니다. 이 자습서에서는 IDemoGetType
과 IDemoStoreType
을 모두 구현하는 COM 인스턴스만 지원합니다. 유한한 집합이 있다는 것을 알고 제공된 COM 인스턴스가 두 인터페이스를 모두 구현해야 한다고 제한했기 때문에 정적으로 정의된 단일 래퍼를 제공할 수 있습니다. 그러나 동적 사례는 COM에서 충분히 일반적이므로 두 가지 옵션을 모두 살펴보겠습니다.
정적 네이티브 개체 래퍼
먼저 정적 구현을 살펴보겠습니다. 정적 네이티브 개체 래퍼에는 .NET 인터페이스를 구현하고 관리되는 형식에 대한 호출을 COM 인스턴스로 전달할 수 있는 관리되는 형식을 정의하는 작업이 포함됩니다. 정적 래퍼의 대략적인 개요는 다음과 같습니다.
// See referenced sample for implementation.
class DemoNativeStaticWrapper
: IDemoGetType
, IDemoStoreType
{
public string? GetString() =>
throw new NotImplementedException();
public void StoreString(int len, string? str) =>
throw new NotImplementedException();
}
이 클래스의 인스턴스를 생성하고 이를 래퍼로 제공하려면 몇 가지 정책을 정의해야 합니다. 이 형식이 래퍼로 사용되는 경우 두 인터페이스를 모두 구현하므로 기본 COM 인스턴스도 두 인터페이스를 모두 구현해야 하는 것처럼 보입니다. 이 정책을 채택하는 경우 COM 인스턴스에서 Marshal.QueryInterface()
호출을 통해 이를 확인해야 합니다.
int hr = Marshal.QueryInterface(ptr, ref IDemoGetType.IID_IDemoGetType, out IntPtr IDemoGetTypeInst);
if (hr != 0)
{
return null;
}
hr = Marshal.QueryInterface(ptr, ref IDemoStoreType.IID_IDemoStoreType, out IntPtr IDemoStoreTypeInst);
if (hr != 0)
{
Marshal.Release(IDemoGetTypeInst);
return null;
}
return new DemoNativeStaticWrapper()
{
IDemoGetTypeInst = IDemoGetTypeInst,
IDemoStoreTypeInst = IDemoStoreTypeInst
};
동적 네이티브 개체 래퍼
동적 래퍼는 형식을 정적으로 쿼리하는 대신 런타임에 쿼리할 수 있는 방법을 제공하므로 더 유연합니다. 이러한 지원을 제공하려면 IDynamicInterfaceCastable
을 활용해야 합니다. 자세한 내용은 여기에서 확인할 수 있습니다. DemoNativeDynamicWrapper
는 이 인터페이스만 구현한다는 점에 유의해야 합니다. 인터페이스가 제공하는 기능은 런타임에 어떤 형식이 지원되는지 확인할 수 있는 기회입니다. 이 자습서의 원본은 만드는 동안 정적 검사를 수행하지만 DemoNativeDynamicWrapper.IsInterfaceImplemented()
를 호출할 때까지 검사가 지연될 수 있으므로 이는 단순히 코드 공유를 위한 것입니다.
// See referenced sample for implementation.
internal class DemoNativeDynamicWrapper
: IDynamicInterfaceCastable
{
public RuntimeTypeHandle GetInterfaceImplementation(RuntimeTypeHandle interfaceType) =>
throw new NotImplementedException();
public bool IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented) =>
throw new NotImplementedException();
}
DemoNativeDynamicWrapper
가 동적으로 지원하는 인터페이스 중 하나를 살펴보겠습니다. 다음 코드는 기본 인터페이스 메서드 기능을 사용하여 IDemoStoreType
구현을 제공합니다.
[DynamicInterfaceCastableImplementation]
unsafe interface IDemoStoreTypeNativeWrapper : IDemoStoreType
{
public static void StoreString(IntPtr inst, int len, string? str);
void IDemoStoreType.StoreString(int len, string? str)
{
var inst = ((DemoNativeDynamicWrapper)this).IDemoStoreTypeInst;
StoreString(inst, len, str);
}
}
이 예에서는 주목해야 할 두 가지 중요한 사항이 있습니다.
DynamicInterfaceCastableImplementationAttribute
특성입니다. 이 특성은IDynamicInterfaceCastable
메서드에서 반환되는 모든 형식에 필요합니다. IL 트리밍을 더 쉽게 만드는 추가 이점이 있습니다. 이는 AOT 시나리오의 신뢰성이 더 높다는 것을 의미합니다.DemoNativeDynamicWrapper
에 대한 캐스트입니다. 이는IDynamicInterfaceCastable
의 동적 특성의 일부입니다.IDynamicInterfaceCastable.GetInterfaceImplementation()
에서 반환된 형식은IDynamicInterfaceCastable
을 구현하는 형식을 "포괄"하는 데 사용됩니다. 여기서 요점은DemoNativeDynamicWrapper
에서IDemoStoreTypeNativeWrapper
까지의 사례를 허용하기 때문에this
포인터가 가장하는 것과 다르다는 것입니다.
COM 인스턴스로 호출 전달
어떤 네이티브 개체 래퍼를 사용하든 COM 인스턴스에서 함수를 호출하는 기능이 필요합니다. IDemoStoreTypeNativeWrapper.StoreString()
의 구현은 unmanaged
C# 함수 포인터를 사용하는 예가 될 수 있습니다.
public static void StoreString(IntPtr inst, int len, string? str)
{
IntPtr strLocal = Marshal.StringToCoTaskMemUni(str);
int hr = ((delegate* unmanaged<IntPtr, int, IntPtr, int>)(*(*(void***)inst + 3 /* IDemoStoreType.StoreString slot */)))(inst, len, strLocal);
if (hr != 0)
{
Marshal.FreeCoTaskMem(strLocal);
Marshal.ThrowExceptionForHR(hr);
}
}
vtable 구현에 액세스하기 위해 COM 인스턴스의 역참조를 살펴보겠습니다. COM ABI는 개체의 첫 번째 포인터가 해당 형식의 vtable에 대한 것이며 해당 위치에서 원하는 슬롯에 액세스할 수 있음을 정의합니다. COM 개체의 주소가 0x10000
이라고 가정해 보겠습니다. 첫 번째 포인터 크기 값은 vtable의 주소여야 합니다(이 예에서는 0x20000
). vtable에 있으면 네 번째 슬롯(0부터 시작하는 인덱싱의 인덱스 3)을 찾아 StoreString()
구현에 액세스합니다.
COM instance
0x10000 0x20000
VTable for IDemoStoreType
0x20000 <Address of QueryInterface>
0x20008 <Address of AddRef>
0x20010 <Address of Release>
0x20018 <Address of StoreString>
함수 포인터가 있으면 개체 인스턴스를 첫 번째 매개 변수로 전달하여 해당 개체의 해당 멤버 함수에 디스패치할 수 있습니다. 이 패턴은 관리 개체 래퍼 구현의 함수 정의에 따라 친숙해 보일 것입니다.
CreateObject()
메서드가 구현되면 ComWrappers
하위 클래스는 IDemoGetType
및 IDemoStoreType
을 모두 구현하는 COM 인스턴스용 네이티브 개체 래퍼를 생성할 수 있습니다.
IntPtr iunk = ...; // Get a COM instance from native code.
object rcw = cw.GetOrCreateObjectForComInstance(iunk, CreateObjectFlags.UniqueInstance);
4단계 - 네이티브 개체 래퍼 수명 세부 정보 처리
ComputeVtables()
및 CreateObject()
구현에서는 일부 래퍼 수명 세부 정보를 다루었지만 추가 고려 사항이 있습니다. 이는 짧은 단계일 수 있지만 ComWrappers
디자인의 복잡성을 크게 증가시킬 수도 있습니다.
AddRef()
및 Release()
메서드 호출로 제어되는 관리 개체 래퍼와 달리 네이티브 개체 래퍼의 수명은 GC에 의해 비결정적으로 처리됩니다. 여기서 질문은 다음과 같습니다. COM 인스턴스를 나타내는 IntPtr
에서 네이티브 개체 래퍼가 언제 Release()
를 호출하나요? 두 가지 일반 버킷이 있습니다.
네이티브 개체 래퍼의 Finalizer는 COM 인스턴스의
Release()
메서드 호출을 담당합니다. 이때가 이 메서드를 호출하는 것이 안전한 유일한 시간입니다. 이 시점에서는 .NET 런타임에 네이티브 개체 래퍼에 대한 다른 참조가 없다는 것이 GC에 의해 올바르게 확인되었습니다. COM 아파트를 적절하게 지원하는 경우 여기에는 복잡성이 있을 수 있습니다. 자세한 내용은 추가 고려 사항 섹션을 참조하세요.네이티브 개체 래퍼는
IDisposable
을 구현하고Dispose()
에서Release()
를 호출합니다.
참고 항목
IDisposable
패턴은 CreateObject()
호출 중에 CreateObjectFlags.UniqueInstance
플래그가 전달된 경우에만 지원되어야 합니다. 이 요구 사항을 따르지 않으면 폐기된 네이티브 개체 래퍼를 폐기한 후 재사용할 수 있습니다.
ComWrappers
하위 클래스 사용
이제 테스트할 수 있는 ComWrappers
하위 클래스가 있습니다. IDemoGetType
및 IDemoStoreType
을 구현하는 COM 인스턴스를 반환하는 네이티브 라이브러리를 만들지 않으려면 관리 개체 래퍼를 사용하고 이를 COM 인스턴스로 처리합니다. COM을 전달하려면 이 방법이 가능해야 합니다.
먼저 관리 개체 래퍼를 만들어 보겠습니다. DemoImpl
인스턴스를 인스턴스화하고 현재 문자열 상태를 표시합니다.
var demo = new DemoImpl();
string? value = demo.GetString();
Console.WriteLine($"Initial string: {value ?? "<null>"}");
이제 DemoComWrappers
의 인스턴스와 관리 개체 래퍼를 만들어 COM 환경에 전달할 수 있습니다.
var cw = new DemoComWrappers();
IntPtr ccw = cw.GetOrCreateComInterfaceForObject(demo, CreateComInterfaceFlags.None);
관리 개체 래퍼를 COM 환경에 전달하는 대신 방금 이 COM 인스턴스를 받았다고 가정하고 대신 이를 위한 네이티브 개체 래퍼를 만듭니다.
var rcw = cw.GetOrCreateObjectForComInstance(ccw, CreateObjectFlags.UniqueInstance);
네이티브 개체 래퍼를 사용하면 이를 원하는 인터페이스 중 하나로 캐스팅하고 일반 관리 개체로 사용할 수 있습니다. DemoImpl
인스턴스를 검사하고 관리되는 인스턴스를 래핑하는 관리 개체 래퍼를 래핑하는 네이티브 개체 래퍼에 대한 작업의 영향을 관찰할 수 있습니다.
var getter = (IDemoGetType)rcw;
var store = (IDemoStoreType)rcw;
string msg = "hello world!";
store.StoreString(msg.Length, msg);
Console.WriteLine($"Setting string through wrapper: {msg}");
value = demo.GetString();
Console.WriteLine($"Get string through managed object: {value}");
msg = msg.ToUpper();
demo.StoreString(msg.Length, msg.ToUpper());
Console.WriteLine($"Setting string through managed object: {msg}");
value = getter.GetString();
Console.WriteLine($"Get string through wrapper: {value}");
ComWrapper
하위 클래스는 CreateObjectFlags.UniqueInstance
를 지원하도록 설계되었으므로 GC가 발생할 때까지 기다리는 대신 네이티브 개체 래퍼를 즉시 정리할 수 있습니다.
(rcw as IDisposable)?.Dispose();
ComWrappers
로 COM 활성화
COM 개체 만들기는 일반적으로 COM 활성화(이 문서의 범위를 벗어나는 복잡한 시나리오)를 통해 수행됩니다. 따라야 할 개념적 패턴을 제공하기 위해 COM 활성화에 사용되는 CoCreateInstance()
API를 소개하고 이를 ComWrappers
와 함께 사용할 수 있는 방법을 보여 줍니다.
애플리케이션에 다음 C# 코드가 있다고 가정합니다. 아래 예에서는 CoCreateInstance()
를 사용하여 COM 클래스와 기본 제공 COM interop 시스템을 활성화하여 COM 인스턴스를 적절한 인터페이스로 마샬링합니다. typeof(I).GUID
의 사용은 어설션으로 제한되며 코드가 AOT 친화적인지 여부에 영향을 미칠 수 있는 리플렉션을 사용하는 경우입니다.
public static I ActivateClass<I>(Guid clsid, Guid iid)
{
Debug.Assert(iid == typeof(I).GUID);
int hr = CoCreateInstance(ref clsid, IntPtr.Zero, /*CLSCTX_INPROC_SERVER*/ 1, ref iid, out object obj);
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
return (I)obj;
}
[DllImport("Ole32")]
private static extern int CoCreateInstance(
ref Guid rclsid,
IntPtr pUnkOuter,
int dwClsContext,
ref Guid riid,
[MarshalAs(UnmanagedType.Interface)] out object ppObj);
ComWrappers
를 사용하도록 위 항목을 변환하려면 CoCreateInstance()
P/Invoke에서 MarshalAs(UnmanagedType.Interface)
를 제거하고 수동으로 마샬링을 수행해야 합니다.
static ComWrappers s_ComWrappers = ...;
public static I ActivateClass<I>(Guid clsid, Guid iid)
{
Debug.Assert(iid == typeof(I).GUID);
int hr = CoCreateInstance(ref clsid, IntPtr.Zero, /*CLSCTX_INPROC_SERVER*/ 1, ref iid, out IntPtr obj);
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
return (I)s_ComWrappers.GetOrCreateObjectForComInstance(obj, CreateObjectFlags.None);
}
[DllImport("Ole32")]
private static extern int CoCreateInstance(
ref Guid rclsid,
IntPtr pUnkOuter,
int dwClsContext,
ref Guid riid,
out IntPtr ppObj);
네이티브 개체 래퍼의 클래스 생성자에 활성화 논리를 포함하여 ActivateClass<I>
와 같은 팩터리 스타일 함수를 추상화하는 것도 가능합니다. 생성자는 ComWrappers.GetOrRegisterObjectForComInstance()
API를 사용하여 새로 생성된 관리 개체를 활성화된 COM 인스턴스와 연결할 수 있습니다.
추가 고려 사항
네이티브 AOT – AOT(Ahead-Of-Time) 컴파일은 JIT 컴파일을 방지하므로 시작 비용이 개선됩니다. 일부 플랫폼에서는 JIT 컴파일의 필요성을 제거해야 하는 경우도 많습니다. AOT 지원은 ComWrappers
API의 목표였지만 모든 래퍼 구현에서는 리플렉션 사용과 같이 실수로 AOT가 중단되지 않도록 주의해야 합니다. Type.GUID
속성은 리플렉션이 사용되는 예이지만 명확하지 않습니다. Type.GUID
속성은 리플렉션을 사용하여 형식의 특성을 검사한 다음 해당 값을 생성하기 위해 잠재적으로 형식의 이름과 포함 어셈블리를 검사합니다.
원본 생성 – COM interop 및 ComWrappers
구현에 필요한 대부분의 코드는 일부 도구를 통해 자동 생성될 수 있습니다. 적절한 COM 정의(예: TLB(형식 라이브러리), IDL 또는 PIA(기본 Interop 어셈블리))가 제공되면 두 가지 형식의 래퍼에 대한 원본을 생성할 수 있습니다.
전역 등록 – ComWrappers
API는 COM interop의 새로운 단계로 설계되었으므로 기존 시스템과 부분적으로 통합할 수 있는 방법이 필요했습니다. 다양한 지원을 위해 전역 인스턴스 등록을 허용하는 ComWrappers
API에는 전역적으로 영향을 미치는 정적 메서드가 있습니다. 이러한 메서드는 기본 제공 COM interop 시스템과 유사하게 모든 경우에 포괄적인 COM interop 지원을 제공할 것으로 예상되는 ComWrappers
인스턴스용으로 설계되었습니다.
참조 추적기 지원 – 이 지원은 주로 WinRT 시나리오에 사용되며 고급 시나리오를 나타냅니다. 대부분의 ComWrapper
구현에서는 CreateComInterfaceFlags.TrackerSupport
또는 CreateObjectFlags.TrackerObject
플래그가 NotSupportedException을 throw해야 합니다. Windows 또는 Windows가 아닌 플랫폼에서 이 지원을 사용하도록 설정하려면 C#/WinRT 도구 체인을 참조하는 것이 좋습니다.
이전에 토론된 수명, 형식 시스템 및 기능적 기능 외에도 ComWrappers
의 COM 호환 구현에는 추가 고려 사항이 필요합니다. Windows 플랫폼에서 사용되는 구현의 경우 다음 사항을 고려해야 합니다.
아파트 – COM의 스레딩 조직 구조를 "아파트"라고 하며 안정적인 운영을 위해 따라야 하는 엄격한 규칙이 있습니다. 이 자습서에서는 아파트 인식 네이티브 개체 래퍼를 구현하지 않지만 프로덕션에 즉시 사용 가능한 구현은 아파트를 인식해야 합니다. 이를 달성하려면 Windows 8에 도입된
RoGetAgileReference
API를 사용하는 것이 좋습니다. Windows 8 이전 버전의 경우 글로벌 인터페이스 테이블을 고려합니다.보안 – COM은 클래스 활성화 및 프록시 권한을 위한 풍부한 보안 모델을 제공합니다.
.NET