자습서: 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
디자인 개요
API는 ComWrappers
.NET 5+ 런타임과의 COM 상호 작용을 수행하는 데 필요한 최소한의 상호 작용을 제공하도록 설계되었습니다. 즉, 기본 제공 COM interop 시스템에 존재하는 많은 멋진 항목이 존재하지 않으며 기본 구성 요소에서 빌드해야 합니다. API의 두 가지 주요 책임은 다음과 같습니다.
- 효율적인 개체 식별(예: 인스턴스와 관리되는
IUnknown*
개체 간의 매핑). - GC(가비지 수집기) 상호 작용.
이러한 효율성은 래퍼 생성 및 획득을 ComWrappers
API를 통해 요구하여 달성됩니다.
ComWrappers
API에는 책임이 거의 없으므로 대부분의 interop 작업을 소비자가 처리해야 한다고 추론할 수 있습니다. 이는 사실입니다. 그러나 추가 작업은 대체로 기계적이며 소스 생성 솔루션에서 수행할 수 있습니다. 예를 들어 C#/WinRT 도구 체인은 ComWrappers
위에 구축되어 WinRT 상호 운용성 지원을 제공하는 소스 생성 솔루션입니다.
ComWrappers
하위 클래스 구현
서브클래스를 제공한다는 ComWrappers
것은 .NET 런타임에 충분한 정보를 제공하여 관리되는 객체가 COM으로, 그리고 COM 객체가 .NET으로 투영되는 상황에서 그 객체들에 대한 래퍼를 생성하고 기록하는 것을 의미합니다. 하위 클래스의 개요를 살펴보기 전에 몇 가지 용어를 정의해야 합니다.
관리되는 개체 래퍼 – 관리되는 .NET 개체는 non-.NET 환경에서 사용할 수 있도록 래퍼가 필요합니다. 이러한 래퍼는 역사적으로 COM CCW(호출 가능 래퍼)라고 합니다.
네이티브 개체 래퍼 – non-.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
을 고려하십시오. 개념적으로, 타입의 vtables은 다음과 같습니다.
IDemoGetType | IDemoStoreType
==================================
QueryInterface | QueryInterface
AddRef | AddRef
Release | Release
GetString | StoreString
DemoImpl
를 보면, 우리는 이미 GetString()
와 StoreString()
에 대한 구현을 가지고 있습니다. 하지만 IUnknown
함수들은 어떻습니까?
인스턴스를 IUnknown
구현하는 방법은 이 자습서의 범위를 벗어나지만, ComWrappers
에서 수동으로 수행할 수 있습니다. 그러나 이 자습서에서는 런타임에서 해당 부분을 처리하도록 합니다.
IUnknown
메서드를 사용하여 ComWrappers.GetIUnknownImpl()
구현을 가져올 수 있습니다.
모든 메서드를 구현한 것처럼 보일 수 있지만, 안타깝게도 COM vtable에서는 함수만 IUnknown
사용할 수 있습니다. COM이 런타임 외부에 있으므로 DemoImpl
구현에 대한 네이티브 함수 포인터를 만들어야 합니다. 이 작업은 C# 함수 포인터 및 .를 UnmanagedCallersOnlyAttribute
사용하여 수행할 수 있습니다. COM 함수 시그니처를 모방하는 함수를 만들어 static
vtable에 삽입할 함수를 만들 수 있습니다. 다음은 COM ABI에서 첫 번째 인수가 인스턴스 자체임을 나타내는 COM 서명 IDemoGetType.GetString()
의 예입니다.
[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()
로부터 반환된 Managed Object Wrapper는 IUnknown*
유형입니다. 래퍼에 전달되는 네이티브 API에 다른 인터페이스 Marshal.QueryInterface()
가 필요한 경우 해당 인터페이스에 대한 작업을 수행해야 합니다.
var cw = new DemoComWrappers();
var demo = new DemoImpl();
IntPtr ccw = cw.GetOrCreateComInterfaceForObject(demo, CreateComInterfaceFlags.None);
3단계 - 구현 CreateObject()
네이티브 개체 래퍼를 생성하면 관리되는 개체 래퍼를 생성하는 것보다 더 많은 구현 옵션과 미묘한 차이가 있습니다. 해결해야 할 첫 번째 질문은 서브클래스가 COM 형식을 지원하는 데 얼마나 허용되는 ComWrappers
지입니다. 가능한 모든 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
를 구현하는 형식을 포괄하는 데 사용됩니다. 여기서 요점은this
포인터가DemoNativeDynamicWrapper
에서IDemoStoreTypeNativeWrapper
로의 경우를 허용하기 때문에 실제로는 자신이 척하는 것과 다른 것입니다.
COM 인스턴스에 대한 호출 전달
사용되는 네이티브 개체 래퍼에 관계없이 COM 인스턴스에서 함수를 호출하는 기능이 필요합니다. 구현 IDemoStoreTypeNativeWrapper.StoreString()
은 C# 함수 포인터를 사용하는 unmanaged
예제로 사용될 수 있습니다.
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);
}
}
COM 인스턴스의 역참조를 검토하여 해당 vtable 구현에 액세스해 보겠습니다. COM ABI는 개체의 첫 번째 포인터가 형식의 vtable에 대한 것이며, 여기에서 원하는 슬롯에 액세스할 수 있음을 정의합니다. COM 개체 0x10000
의 주소가 .이라고 가정해 보겠습니다. 첫 번째 포인터 크기 값은 이 예제 0x20000
에서 vtable의 주소여야 합니다. 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 인스턴스를 나타내는 Release()
에 IntPtr
를 호출할 때는 언제입니까? 다음과 같은 두 가지 일반 버킷이 있습니다.
네이티브 개체 래퍼의 종료자는 COM 인스턴스의
Release()
메서드를 호출합니다. 이 메서드를 호출하는 것이 안전한 유일한 시간입니다. 이 시점에서 GC는 .NET 런타임에서 네이티브 객체 래퍼에 대한 다른 참조가 없다는 것을 올바르게 결정했습니다. COM 아파트먼트를 제대로 지원하는 경우 복잡성이 있을 수 있습니다. 자세한 내용은 추가 고려 사항 섹션을 참조하세요 .네이티브 개체 래퍼는
IDisposable
를 구현하고Release()
를Dispose()
에서 호출합니다.
비고
이 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>"}");
이제 COM 환경으로 전달할 수 있는 관리되는 개체 래퍼 및 DemoComWrappers
인스턴스를 만들 수 있습니다.
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();
COM ComWrappers
와의 활성화
COM 개체의 생성은 일반적으로 COM 활성화를 통해 수행됩니다. 이 문서의 범위를 벗어나는 복잡한 시나리오입니다. 따라야 할 개념적 패턴을 제공하기 위해, COM 활성화에 사용되는 CoCreateInstance()
API를 도입하고 ComWrappers
을 사용하여 이를 어떻게 활용할 수 있는지 설명합니다.
애플리케이션에 다음 C# 코드가 있다고 가정합니다. 아래 예제에서는 COM 클래스 및 기본 제공 COM interop 시스템을 활성화하여 COM 인스턴스를 적절한 인터페이스로 마샬링하는 데 사용합니다 CoCreateInstance()
.
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
로 변환하려면 MarshalAs(UnmanagedType.Interface)
P/Invoke에서 CoCreateInstance()
을 제거하고 마샬링을 수동으로 수행해야 합니다.
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>
와 같은 공장 스타일 함수들을 추상화할 수도 있습니다. 생성자는 API를 ComWrappers.GetOrRegisterObjectForComInstance()
사용하여 새로 생성된 관리되는 개체를 활성화된 COM 인스턴스와 연결할 수 있습니다.
추가 고려 사항
네이티브 AOT – JIT 컴파일이 방지되므로 AOT(Ahead-of-Time) 컴파일은 향상된 시작 비용을 제공합니다. 일부 플랫폼에서도 JIT 컴파일의 필요성을 제거해야 하는 경우가 많습니다. AOT 지원은 API의 ComWrappers
목표였지만 모든 래퍼 구현은 리플렉션 사용과 같이 AOT가 중단되는 경우를 실수로 도입하지 않도록 주의해야 합니다. 이 Type.GUID
속성은 리플렉션이 사용되는 위치의 예이지만 명확하지 않은 방식으로 사용됩니다. 이 속성은 Type.GUID
리플렉션을 사용하여 형식의 특성을 검사한 다음 해당 값을 생성하기 위해 잠재적으로 형식의 이름과 포함 어셈블리를 검사합니다.
원본 생성 – COM interop 및 ComWrappers
구현에 필요한 대부분의 코드는 일부 도구에서 자동으로 생성될 수 있습니다. TLB(형식 라이브러리), IDL 또는 PIA(기본 Interop 어셈블리)와 같은 적절한 COM 정의를 사용하여 두 가지 유형의 래퍼에 대한 원본을 생성할 수 있습니다.
전역 등록 – ComWrappers
API는 COM interop의 새로운 단계로 설계되었으므로 기존 시스템과 부분적으로 통합할 수 있는 방법이 필요했습니다. 다양한 지원을 위해 전역 인스턴스의 등록을 허용하는 API에 전역적으로 영향을 주는 정적 메서드 ComWrappers
가 있습니다. 이러한 메서드는 기본 제공 COM interop 시스템과 유사하게 모든 경우에 포괄적인 COM interop 지원을 제공할 것으로 예상되는 인스턴스를 위해 ComWrappers
설계되었습니다.
참조 추적기 지원 – 이 지원은 WinRT 시나리오에 주로 사용되며 고급 시나리오를 나타냅니다. 대부분의 ComWrapper
구현에서 CreateComInterfaceFlags.TrackerSupport
또는 CreateObjectFlags.TrackerObject
플래그는 NotSupportedException을(를) 던져야 합니다. Windows 또는 Windows가 아닌 플랫폼에서도 이 지원을 사용하도록 설정하려면 C#/WinRT 도구 체인을 참조하는 것이 좋습니다.
이전에 설명한 수명, 형식 시스템 및 기능 기능 외에도 COM 규격 구현 ComWrappers
에는 추가 고려 사항이 필요합니다. Windows 플랫폼에서 사용되는 구현의 경우 다음과 같은 고려 사항이 있습니다.
아파트 - COM의 스레딩 조직 구조를 "아파트"라고 하며 안정적인 운영을 위해 따라야 하는 엄격한 규칙이 있습니다. 이 자습서에서는 아파트 인식 네이티브 개체 래퍼를 구현하지 않지만 프로덕션 준비 구현은 아파트를 인식해야 합니다. 이를 위해 Windows 8에
RoGetAgileReference
도입된 API를 사용하는 것이 좋습니다. Windows 8 이전 버전의 경우 전역 인터페이스 테이블을 고려합니다.보안 – COM은 클래스 활성화 및 프록시 권한에 대한 풍부한 보안 모델을 제공합니다.
.NET