You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The contract: the implementation should perform QueryInterface(iid) on the object it produces and return the resulting interface pointer through ppv. The caller then uses the returned pointer immediately as the requested interface, without an additional QI round-trip.
The natural C# spelling of this contract on the managed-implementation side is:
[MarshalAs(UnmanagedType.Interface)] out object is accepted today and is routed through ComInterfaceMarshaller<object>. Because typeof(object) has no associated IUnknownDerivedDetails, TargetInterfaceIID is null and CastIUnknownToInterfaceType returns the raw IUnknown* from ComWrappers.GetOrCreateComInterfaceForObject without performing a QI (ComInterfaceMarshaller.cs lines 72–86).
MarshalAsAttribute.IidParameterIndex, however, is explicitly rejected by the source-generator infrastructure: Microsoft.Interop.SourceGeneration/MarshalAsParser.cs (~line 175) emits a SYSLIB1051 "configuration not supported" diagnostic when it's set. An existing unit test asserts this (LibraryImportGenerator.UnitTests/Diagnostics.cs ~line 227).
The practical result: an unmanaged caller invoking a managed-implemented activation API through the source-generated stub always receives an IUnknown* regardless of which IID they asked for, forcing them to perform a follow-up QueryInterface themselves. This breaks the de-facto activation-API contract and diverges from the built-in interop behavior.
Proposal
Extend the COM source generator (scope: [GeneratedComInterface]-attributed interfaces, managed → unmanaged direction — i.e., the unmanaged-to-managed stub that exposes a managed implementation to a native caller) to:
Honor MarshalAsAttribute.IidParameterIndex when, and only when, all of the following hold:
The parameter has an explicit [MarshalAs(UnmanagedType.Interface)].
The parameter type is object.
The parameter passing mode is out.
The value of IidParameterIndex identifies the sibling parameter that supplies the runtime IID.
Perform QueryInterface on marshal-out. In the generated stub, after obtaining the IUnknown* for the managed object via ComWrappers, call QueryInterface with the runtime IID supplied at the indexed parameter and write the QI'd interface pointer to the void** out-param. Release the intermediate IUnknown* reference.
Propagate QI failure. If QueryInterface returns a failing HRESULT, the stub should propagate that HRESULT (typically E_NOINTERFACE) as the method's return value. This matches built-in interop semantics and the activation-API contract.
Preserve and improve the existing diagnostic for non-qualifying shapes. For any use of IidParameterIndex that does not satisfy the constraints in (1) — e.g., on a non-out parameter, without [MarshalAs(UnmanagedType.Interface)], or on a non-object parameter — continue to emit the "configuration not supported" diagnostic. The diagnostic message should be updated to document the supported shape so authors are directed to the correct usage.
No new public attribute or API surface is required — this is a behavior addition for an existing, well-understood field on an already-supported attribute.
Example
[GeneratedComInterface][Guid("00000001-0000-0000-C000-000000000046")]partialinterfaceIClassFactory{voidCreateInstance(nintpUnkOuter,inGuidriid,[MarshalAs(UnmanagedType.Interface,IidParameterIndex=1)]outobjectppvObject);voidLockServer(boolfLock);}[GeneratedComClass]partialclassMyFactory:IClassFactory{publicvoidCreateInstance(nintpUnkOuter,inGuidriid,outobjectppvObject){// The managed implementation simply returns the activated object.// The generated marshalling stub is responsible for QI'ing to `riid`// and writing the QI'd interface pointer to the unmanaged void** ppvObject.ppvObject=newMyActivatedObject();}publicvoidLockServer(boolfLock){/* ... */}}
Implementation notes
The source-generator parser (MarshalAsParser / MarshalAsWithCustomMarshallersParser) needs to start propagating IidParameterIndex for the qualifying shape, and update its diagnostic text for the non-qualifying cases.
The marshalling-step generation needs to wire the IID parameter's runtime value into the ConvertToUnmanaged step. Because ComInterfaceMarshaller<T> is a stateless static class, a dedicated marshaller (e.g., a sibling that takes a Guid as an additional input) or an inline emitted snippet is likely needed to accept the IID as an additional input on the unmanaged-side step.
Reference counting on the intermediate IUnknown* must remain correct (single AddRef from GetOrCreateComInterfaceForObject → Release after the QI, regardless of QI success).
Out of scope
ref / inobject with IidParameterIndex. Limited to out only for now; other passing modes will continue to emit the diagnostic.
Unmanaged → managed direction (calling an activation API from C# and receiving a strongly-typed RCW based on a runtime IID). Related but distinct; can be tracked separately if there is demand.
LibraryImport-generated stubs. The activation pattern shows up almost exclusively on COM vtables; P/Invoke support for IidParameterIndex can be a separate issue if needed.
Note
This issue was drafted with the assistance of GitHub Copilot.
Background
A large class of COM activation-style APIs in C/C++ uses the "request an interface by IID, receive it as
void**" pattern:The contract: the implementation should perform
QueryInterface(iid)on the object it produces and return the resulting interface pointer throughppv. The caller then uses the returned pointer immediately as the requested interface, without an additional QI round-trip.The natural C# spelling of this contract on the managed-implementation side is:
This mirrors what the built-in COM marshaller has long supported via
MarshalAsAttribute.IidParameterIndex.Current behavior in source-generated COM interop
[MarshalAs(UnmanagedType.Interface)] out objectis accepted today and is routed throughComInterfaceMarshaller<object>. Becausetypeof(object)has no associatedIUnknownDerivedDetails,TargetInterfaceIIDisnullandCastIUnknownToInterfaceTypereturns the rawIUnknown*fromComWrappers.GetOrCreateComInterfaceForObjectwithout performing a QI (ComInterfaceMarshaller.cslines 72–86).MarshalAsAttribute.IidParameterIndex, however, is explicitly rejected by the source-generator infrastructure:Microsoft.Interop.SourceGeneration/MarshalAsParser.cs(~line 175) emits aSYSLIB1051"configuration not supported" diagnostic when it's set. An existing unit test asserts this (LibraryImportGenerator.UnitTests/Diagnostics.cs~line 227).The practical result: an unmanaged caller invoking a managed-implemented activation API through the source-generated stub always receives an
IUnknown*regardless of which IID they asked for, forcing them to perform a follow-upQueryInterfacethemselves. This breaks the de-facto activation-API contract and diverges from the built-in interop behavior.Proposal
Extend the COM source generator (scope:
[GeneratedComInterface]-attributed interfaces, managed → unmanaged direction — i.e., the unmanaged-to-managed stub that exposes a managed implementation to a native caller) to:Honor
MarshalAsAttribute.IidParameterIndexwhen, and only when, all of the following hold:[MarshalAs(UnmanagedType.Interface)].object.out.The value of
IidParameterIndexidentifies the sibling parameter that supplies the runtime IID.Perform
QueryInterfaceon marshal-out. In the generated stub, after obtaining theIUnknown*for the managed object viaComWrappers, callQueryInterfacewith the runtime IID supplied at the indexed parameter and write the QI'd interface pointer to thevoid**out-param. Release the intermediateIUnknown*reference.Propagate QI failure. If
QueryInterfacereturns a failing HRESULT, the stub should propagate that HRESULT (typicallyE_NOINTERFACE) as the method's return value. This matches built-in interop semantics and the activation-API contract.Preserve and improve the existing diagnostic for non-qualifying shapes. For any use of
IidParameterIndexthat does not satisfy the constraints in (1) — e.g., on a non-outparameter, without[MarshalAs(UnmanagedType.Interface)], or on a non-objectparameter — continue to emit the "configuration not supported" diagnostic. The diagnostic message should be updated to document the supported shape so authors are directed to the correct usage.No new public attribute or API surface is required — this is a behavior addition for an existing, well-understood field on an already-supported attribute.
Example
Implementation notes
MarshalAsParser/MarshalAsWithCustomMarshallersParser) needs to start propagatingIidParameterIndexfor the qualifying shape, and update its diagnostic text for the non-qualifying cases.ConvertToUnmanagedstep. BecauseComInterfaceMarshaller<T>is a statelessstatic class, a dedicated marshaller (e.g., a sibling that takes aGuidas an additional input) or an inline emitted snippet is likely needed to accept the IID as an additional input on the unmanaged-side step.IUnknown*must remain correct (single AddRef fromGetOrCreateComInterfaceForObject→ Release after the QI, regardless of QI success).Out of scope
ref/inobjectwithIidParameterIndex. Limited tooutonly for now; other passing modes will continue to emit the diagnostic.LibraryImport-generated stubs. The activation pattern shows up almost exclusively on COM vtables; P/Invoke support forIidParameterIndexcan be a separate issue if needed.Note
This issue was drafted with the assistance of GitHub Copilot.