Honor MarshalAs.IidParameterIndex for out object in source-generated COM stubs#128214
Merged
Conversation
Co-authored-by: AaronRobinsonMSFT <30635565+AaronRobinsonMSFT@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Fix COM interop to honor MarshalAsAttribute.IidParameterIndex
Honor May 14, 2026
MarshalAs.IidParameterIndex for out object in source-generated COM stubs
Co-authored-by: AaronRobinsonMSFT <30635565+AaronRobinsonMSFT@users.noreply.github.com>
Contributor
|
Tagging subscribers to this area: @dotnet/interop-contrib |
Co-authored-by: AaronRobinsonMSFT <30635565+AaronRobinsonMSFT@users.noreply.github.com>
Contributor
Author
Fixed in 3d9dc1c.
|
jtschuster
reviewed
May 15, 2026
Member
|
I had CCA emit the compiler generated code and copied it here to review. I don't see any issues. Details// <auto-generated />
#pragma warning disable CS0612, CS0618, CS0649, CS1591
[global::System.Runtime.InteropServices.StructLayoutAttribute(global::System.Runtime.InteropServices.LayoutKind.Sequential)]
file unsafe struct InterfaceImplementationVtable
{
public delegate* unmanaged[MemberFunction]<void*, global::System.Guid*, void**, int> QueryInterface_0;
public delegate* unmanaged[MemberFunction]<void*, uint> AddRef_1;
public delegate* unmanaged[MemberFunction]<void*, uint> Release_2;
public delegate* unmanaged[MemberFunction]<global::System.Runtime.InteropServices.ComWrappers.ComInterfaceDispatch*, global::System.Guid*, void**, int> GetActivationFactory_3;
}
file unsafe sealed class InterfaceInformation : global::System.Runtime.InteropServices.Marshalling.IIUnknownInterfaceType
{
public static global::System.Guid Iid { get; } = new([194, 86, 30, 120, 48, 165, 143, 74, 144, 254, 1, 36, 68, 38, 224, 204]);
public static void** ManagedVirtualMethodTable => (void**)global::System.Runtime.CompilerServices.Unsafe.AsPointer(in InterfaceImplementation.Vtable);
}
[global::System.Runtime.InteropServices.DynamicInterfaceCastableImplementationAttribute]
file unsafe interface InterfaceImplementation : global::ComInterfaceGenerator.Tests.IActivationFactory
{
[global::System.Runtime.CompilerServices.FixedAddressValueTypeAttribute]
public static readonly InterfaceImplementationVtable Vtable;
static InterfaceImplementation()
{
global::System.Runtime.InteropServices.ComWrappers.GetIUnknownImpl(
out *(nint*)&((InterfaceImplementationVtable*)global::System.Runtime.CompilerServices.Unsafe.AsPointer(ref Vtable))->QueryInterface_0,
out *(nint*)&((InterfaceImplementationVtable*)global::System.Runtime.CompilerServices.Unsafe.AsPointer(ref Vtable))->AddRef_1,
out *(nint*)&((InterfaceImplementationVtable*)global::System.Runtime.CompilerServices.Unsafe.AsPointer(ref Vtable))->Release_2);
Vtable.GetActivationFactory_3 = &ABI_GetActivationFactory;
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.ComInterfaceGenerator", "42.42.42.42")]
[global::System.Runtime.CompilerServices.SkipLocalsInitAttribute]
void global::ComInterfaceGenerator.Tests.IActivationFactory.GetActivationFactory(in global::System.Guid iid, out object factory)
{
var(__this, __vtable) = ((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::ComInterfaceGenerator.Tests.IActivationFactory));
var __target = ((delegate* unmanaged[MemberFunction]<void*, global::System.Guid*, void**, int> )__vtable[3]);
{
bool __invokeSucceeded = default;
factory = default;
void* __factory_native = default;
int __invokeRetVal = default;
try
{
// Pin - Pin data in preparation for calling the P/Invoke.
fixed (global::System.Guid* __iid_native = &iid)
{
__invokeRetVal = __target(__this, __iid_native, &__factory_native);
}
// NotifyForSuccessfulInvoke - Keep alive any managed objects that need to stay alive across the call.
global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal, new([194, 86, 30, 120, 48, 165, 143, 74, 144, 254, 1, 36, 68, 38, 224, 204]), (global::System.IntPtr)__this);
global::System.GC.KeepAlive(this);
__invokeSucceeded = true;
// Unmarshal - Convert native data to managed data.
factory = global::System.Runtime.InteropServices.Marshalling.ComInterfaceMarshaller<object>.ConvertToManaged(__factory_native);
}
finally
{
if (__invokeSucceeded)
{
// CleanupCalleeAllocated - Perform cleanup of callee allocated resources.
global::System.Runtime.InteropServices.Marshalling.ComInterfaceMarshaller<object>.Free(__factory_native);
}
}
}
}
[global::System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute(CallConvs = new[] { typeof(global::System.Runtime.CompilerServices.CallConvMemberFunction) })]
internal static int ABI_GetActivationFactory(global::System.Runtime.InteropServices.ComWrappers.ComInterfaceDispatch* __this_native, global::System.Guid* __iid_native__param, void** __factory_native__param)
{
global::ComInterfaceGenerator.Tests.IActivationFactory @this = default;
ref global::System.Guid __iid_native = ref *__iid_native__param;
global::System.Guid iid = default;
ref void* __factory_native = ref *__factory_native__param;
object factory = default;
int __retVal = default;
try
{
// Unmarshal - Convert native data to managed data.
iid = __iid_native;
@this = global::System.Runtime.InteropServices.ComWrappers.ComInterfaceDispatch.GetInstance<global::ComInterfaceGenerator.Tests.IActivationFactory>(__this_native);
@this.GetActivationFactory(in iid, out factory);
// NotifyForSuccessfulInvoke - Keep alive any managed objects that need to stay alive across the call.
__retVal = 0; // S_OK
// Marshal - Convert managed data to native data.
void* __factory_native__unknown = (void*)global::System.Runtime.InteropServices.Marshalling.ComInterfaceMarshaller<object>.ConvertToUnmanaged(factory);
if (__factory_native__unknown != null)
{
global::System.IntPtr __factory_native__queriedInterface = 0;
int __factory_native__queryInterfaceHResult = global::System.Runtime.InteropServices.Marshal.QueryInterface((global::System.IntPtr)__factory_native__unknown, in iid, out __factory_native__queriedInterface);
global::System.Runtime.InteropServices.Marshal.Release((global::System.IntPtr)__factory_native__unknown);
if (__factory_native__queryInterfaceHResult != 0)
{
if (__factory_native__queriedInterface != 0)
global::System.Runtime.InteropServices.Marshal.Release(__factory_native__queriedInterface);
__factory_native = null;
return __factory_native__queryInterfaceHResult;
}
__factory_native = (void*)__factory_native__queriedInterface;
}
else
{
__factory_native = null;
}
}
catch (global::System.Exception __exception)
{
__retVal = global::System.Runtime.InteropServices.Marshalling.ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);
}
return __retVal;
}
}
namespace ComInterfaceGenerator.Tests
{
[global::System.Runtime.InteropServices.Marshalling.IUnknownDerivedAttribute<InterfaceInformation, InterfaceImplementation>]
unsafe partial interface IActivationFactory
{
}
} |
Fixes the four remaining items raised on the PR:
1. Route QI failure through the standard exception-to-HRESULT path.
Replaces the inlined 'return queryInterfaceHResult;' inside the per-
parameter Marshal stage with Marshal.ThrowExceptionForHR(hr). This
eliminates the bake-in 'int' return-type assumption and ensures the
stub's normal cleanup stages run on failure, by reusing the existing
ManagedHResultExceptionGeneratorResolver-driven catch path.
2. Localize the diagnostic hint text. Adds the
IidParameterIndexUnsupportedConfigurationName resx entry (with
translator comment and {0} placeholder for the API name) and switches
MarshalAsParser to build the hint via SR.Format. The English text is
unchanged so existing test expectations continue to hold.
3. Add compile-fail coverage for additional invalid IidParameterIndex
shapes: out-of-bounds index, IID pointing at a non-Guid parameter,
IID pointing at the marshalled out object itself, and IID pointing
at an 'out Guid'. (A 'ref Guid' IID parameter is now accepted, so
the corresponding compile-fail test is replaced with a compile-pass
test in item 4.) Tests that were already asserting only the primary
IidParameterIndex diagnostic for shapes that also produce a natural
secondary diagnostic (Struct on out object, LPArray on out object[],
IID-points-at-self) are updated to assert the full diagnostic set.
4. Add compile-pass coverage for multi-parameter shapes: two out
object parameters sharing one IID, two out object parameters with
distinct IIDs, an IID-driven out object interleaved with a non-IID
out object and other parameters, by-value Guid IID, and ref Guid IID.
The IID-parameter validation also gains a RefKind constraint
(RefKind.None | In | Ref) so out Guid is rejected at parse time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Member
|
@jtschuster Please take another look. |
Two follow-up fixes in IidParameterIndexMarshallerResolver: - Release the intermediate IUnknown* via ComInterfaceMarshaller<object>.Free instead of a direct Marshal.Release call. Functionally identical today (Free just delegates to Marshal.Release after a null check) but keeps the cleanup consistent with the marshaller contract and forward- compatible with any future change to Free's logic. - Change the QI-failure check from 'hr != 0' to 'hr < 0' to match the semantics of Marshal.ThrowExceptionForHR. With the previous condition, a positive non-zero HRESULT (e.g., S_FALSE) would enter the cleanup branch, release the QI'd interface, set the native out parameter to null, then return without throwing because ThrowExceptionForHR only throws on hr < 0 - leaving the subsequent fall-through assignment to rewrite the native pointer from a freshly-released variable. The scenario is theoretical for IUnknown::QueryInterface in practice (which is documented to return only S_OK or E_NOINTERFACE) but the fix removes the latent inconsistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AaronRobinsonMSFT
approved these changes
May 16, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Source-generated COM accepted
[MarshalAs(UnmanagedType.Interface)] out objectbut ignoredIidParameterIndex, always returningIUnknown*to native callers. This change enables activation-style behavior by using the runtime IID for marshal-out and preserving unsupported-shape diagnostics.IidParameterIndexuse-site propagation through marshalling info.[MarshalAs(UnmanagedType.Interface)] out object.IUnknown*viaComInterfaceMarshaller<object>,Marshal.QueryInterfacewith the runtime IID parameter,void**,IUnknown*,COMException(hr)on QI failure (HRESULT propagation path).ref/inusage path).E_NOINTERFACE,