Skip to content

Source-generated COM interop should honor MarshalAsAttribute.IidParameterIndex for out object parameters #128212

@AaronRobinsonMSFT

Description

@AaronRobinsonMSFT

Background

A large class of COM activation-style APIs in C/C++ uses the "request an interface by IID, receive it as void**" pattern:

HRESULT IClassFactory::CreateInstance(IUnknown* outer, REFIID iid, void** ppv);
HRESULT DllGetClassObject(REFCLSID clsid, REFIID iid, void** ppv);
HRESULT GetActivationFactory(REFIID iid, void** factory);

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:

[GeneratedComInterface]
[Guid("...")]
partial interface IMyActivator
{
    void GetActivationFactory(
        in Guid iid,
        [MarshalAs(UnmanagedType.Interface, IidParameterIndex = 0)] out object factory);
}

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 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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")]
partial interface IClassFactory
{
    void CreateInstance(
        nint pUnkOuter,
        in Guid riid,
        [MarshalAs(UnmanagedType.Interface, IidParameterIndex = 1)] out object ppvObject);

    void LockServer(bool fLock);
}

[GeneratedComClass]
partial class MyFactory : IClassFactory
{
    public void CreateInstance(nint pUnkOuter, in Guid riid, out object ppvObject)
    {
        // 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 = new MyActivatedObject();
    }

    public void LockServer(bool fLock) { /* ... */ }
}

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 / in object 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.

Metadata

Metadata

Labels

Type

No type
No fields configured for issues without a type.

Projects

Status

No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions