From 52643a77a2b622125dbd30896ee7ff5090078ef3 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Wed, 10 Jun 2026 15:48:46 -0700 Subject: [PATCH] Generate generic overloads for IID_PPV_ARGS pattern Auto-detect COM methods with the IID_PPV_ARGS pattern (a Guid* parameter immediately followed by a void** [ComOutPtr] parameter) and generate generic overloads where the GUID is derived from typeof(T).GUID and the output pointer is typed as T. For marshaling mode: out T ppv where T : class For non-marshaling mode: out T* ppv where T : unmanaged Added friendlyOverloads.comOutPtrGenericOverloads option (default true) to nativemethods.json for opt-out. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator.Com.cs | 7 +- .../Generator.FriendlyOverloads.cs | 147 ++++++++++++++++-- .../GeneratorOptions.cs | 8 + .../settings.schema.json | 5 + .../templates/marshaling/CoCreateInstance.cs | 8 - .../no_marshaling/CoCreateInstance.cs | 8 - .../CsWin32GeneratorTests.cs | 2 +- .../COMTests.cs | 6 +- .../ComRuntimeTests.cs | 70 +++++++-- .../COMTests.cs | 3 +- .../COMTests.cs | 126 +++++++++++++++ 11 files changed, 342 insertions(+), 48 deletions(-) delete mode 100644 src/Microsoft.Windows.CsWin32/templates/marshaling/CoCreateInstance.cs delete mode 100644 src/Microsoft.Windows.CsWin32/templates/no_marshaling/CoCreateInstance.cs diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 9517c73b..951ec2f7 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -714,11 +714,8 @@ static ExpressionSyntax ThisPointer(PointerTypeSyntax? typedPointer = null) return typedPointer is not null ? CastExpression(typedPointer, invocation) : invocation; } - // Add helper methods when appropriate. - if (hasIUnknownMembers && this.Options.FriendlyOverloads.Enabled) - { - members.AddRange(this.ExtractMembersFromTemplate("IUnknownHelperMethods")); - } + // The IID_PPV_ARGS pattern in DeclareFriendlyOverload now handles QueryInterface generically, + // so the IUnknownHelperMethods template is no longer needed. // We expose the vtbl struct to support CCWs. IdentifierNameSyntax vtblStructName = IdentifierName("Vtbl"); diff --git a/src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs b/src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs index ffa027ef..dc7100c7 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.Windows.CsWin32; @@ -75,17 +75,14 @@ private IEnumerable DeclareFriendlyOverloads(MethodDefi yield return (MethodDeclarationSyntax)templateFriendlyOverload; } - if (externMethodDeclaration.Identifier.ValueText != "CoCreateInstance" || !this.options.ComInterop.UseIntPtrForComOutPointers) + if (this.options.AllowMarshaling && this.TryFetchTemplate("marshaling/" + externMethodDeclaration.Identifier.ValueText, out templateFriendlyOverload)) { - if (this.options.AllowMarshaling && this.TryFetchTemplate("marshaling/" + externMethodDeclaration.Identifier.ValueText, out templateFriendlyOverload)) - { - yield return (MethodDeclarationSyntax)templateFriendlyOverload; - } + yield return (MethodDeclarationSyntax)templateFriendlyOverload; + } - if (!this.options.AllowMarshaling && this.TryFetchTemplate("no_marshaling/" + externMethodDeclaration.Identifier.ValueText, out templateFriendlyOverload)) - { - yield return (MethodDeclarationSyntax)templateFriendlyOverload; - } + if (!this.options.AllowMarshaling && this.TryFetchTemplate("no_marshaling/" + externMethodDeclaration.Identifier.ValueText, out templateFriendlyOverload)) + { + yield return (MethodDeclarationSyntax)templateFriendlyOverload; } bool improvePointersToSpansAndRefs = this.canUseSpan; @@ -153,6 +150,51 @@ private IEnumerable DeclareFriendlyOverload( SyntaxToken friendlyMethodName = externMethodDeclaration.Identifier; bool emulateMemberFunctionCallConv = friendlyMethodName.ValueText.EndsWith(EmulateMemberFunctionCallConvSuffix); + // Pre-scan for IID_PPV_ARGS pattern: a Guid* [In] parameter immediately followed by a void** [ComOutPtr] parameter. + int iidPpvRiidOrigIndex = -1; + int iidPpvPpvOrigIndex = -1; + bool iidPpvMarshalingMode = false; + + if (this.options.FriendlyOverloads.ComOutPtrGenericOverloads) + { + var metadataParamsForScan = new List<(Parameter Param, int OrigIndex)>(); + foreach (ParameterHandle ph in methodDefinition.GetParameters()) + { + Parameter p = this.Reader.GetParameter(ph); + if (p.SequenceNumber > 0 && p.SequenceNumber - 1 < originalSignature.ParameterTypes.Length) + { + metadataParamsForScan.Add((p, p.SequenceNumber - 1)); + } + } + + // Only match when the Guid* + void** [ComOutPtr] pair are the final two parameters (the canonical IID_PPV_ARGS position). + if (metadataParamsForScan.Count >= 2) + { + int i = metadataParamsForScan.Count - 2; + int riidOrig = metadataParamsForScan[i].OrigIndex; + int ppvOrig = metadataParamsForScan[i + 1].OrigIndex; + + if (ppvOrig == riidOrig + 1 + && originalSignature.ParameterTypes[riidOrig] is PointerTypeHandleInfo { ElementType: HandleTypeHandleInfo guidInfo } + && guidInfo.IsType("Guid") + && this.FindInteropDecorativeAttribute(metadataParamsForScan[i + 1].Param.GetCustomAttributes(), "ComOutPtrAttribute") is not null + && originalSignature.ParameterTypes[ppvOrig] is PointerTypeHandleInfo { ElementType: PointerTypeHandleInfo { ElementType: PrimitiveTypeHandleInfo { PrimitiveTypeCode: PrimitiveTypeCode.Void } } } + && riidOrig < parameters.Count && ppvOrig < parameters.Count) + { + ParameterSyntax ppvExtern = externMethodDeclaration.ParameterList.Parameters[ppvOrig]; + + // Skip if ppv is typed as IntPtr (UseIntPtrForComOutPointers mode). + if (ppvExtern.Type is not IdentifierNameSyntax { Identifier.ValueText: nameof(IntPtr) }) + { + iidPpvRiidOrigIndex = riidOrig; + iidPpvPpvOrigIndex = ppvOrig; + iidPpvMarshalingMode = ppvExtern.Modifiers.Any(SyntaxKind.OutKeyword) + && ppvExtern.Type is PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.ObjectKeyword }; + } + } + } + } + foreach (ParameterHandle paramHandle in methodDefinition.GetParameters()) { Parameter param = this.Reader.GetParameter(paramHandle); @@ -175,6 +217,80 @@ private IEnumerable DeclareFriendlyOverload( paramIndex++; } + // Handle IID_PPV_ARGS pattern: riid parameter is removed, ppv parameter is genericized. + if (origParamIndex == iidPpvRiidOrigIndex) + { + signatureChanged = true; + ParameterSyntax riidExternParam = externMethodDeclaration.ParameterList.Parameters[origParamIndex]; + ExpressionSyntax typeofTGuid = MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + TypeOfExpression(IdentifierName("T")), + IdentifierName("GUID")); + + if (riidExternParam.Type is PointerTypeSyntax) + { + leadingStatements.Add(LocalDeclarationStatement( + VariableDeclaration( + ParseTypeName("global::System.Guid"), + [VariableDeclarator(Identifier("__riid"), EqualsValueClause(typeofTGuid))]))); + arguments[paramIndex] = Argument(PrefixUnaryExpression(SyntaxKind.AddressOfExpression, IdentifierName("__riid"))); + } + else + { + arguments[paramIndex] = Argument(typeofTGuid); + } + + parametersToRemove.Add(paramIndex); + continue; + } + + if (origParamIndex == iidPpvPpvOrigIndex) + { + signatureChanged = true; + IdentifierNameSyntax tName = IdentifierName("T"); + + if (iidPpvMarshalingMode) + { + parameters[paramIndex] = StripAttributes(externMethodDeclaration.ParameterList.Parameters[paramIndex]) + .WithType(tName.WithTrailingTrivia(TriviaList(Space))) + .WithModifiers([TokenWithSpace(SyntaxKind.OutKeyword)]); + + arguments[paramIndex] = Argument(DeclarationExpression( + PredefinedType(TokenWithSpace(SyntaxKind.ObjectKeyword)), + SingleVariableDesignation(Identifier("__ppv")))) + .WithRefKindKeyword(TokenWithSpace(SyntaxKind.OutKeyword)); + + IdentifierNameSyntax ppvName = IdentifierName(externMethodDeclaration.ParameterList.Parameters[paramIndex].Identifier.ValueText); + trailingStatements.Add(ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + ppvName, + CastExpression(tName, IdentifierName("__ppv"))))); + } + else + { + parameters[paramIndex] = StripAttributes(externMethodDeclaration.ParameterList.Parameters[paramIndex]) + .WithType(PointerType(tName).WithTrailingTrivia(TriviaList(Space))) + .WithModifiers([TokenWithSpace(SyntaxKind.OutKeyword)]); + + leadingStatements.Add(LocalDeclarationStatement( + VariableDeclaration( + PointerType(PredefinedType(Token(SyntaxKind.VoidKeyword))), + [VariableDeclarator(Identifier("__ppv"))]))); + + arguments[paramIndex] = Argument(PrefixUnaryExpression(SyntaxKind.AddressOfExpression, IdentifierName("__ppv"))); + + IdentifierNameSyntax ppvName = IdentifierName(externMethodDeclaration.ParameterList.Parameters[paramIndex].Identifier.ValueText); + trailingStatements.Add(ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + ppvName, + CastExpression(PointerType(tName), IdentifierName("__ppv"))))); + } + + continue; + } + bool isOptional = (param.Attributes & ParameterAttributes.Optional) == ParameterAttributes.Optional; CustomAttributeHandleCollection paramAttributes = param.GetCustomAttributes(); bool isReserved = this.FindInteropDecorativeAttribute(paramAttributes, "ReservedAttribute") is not null; @@ -1344,6 +1460,17 @@ bool TryHandleCountParam(TypeSyntax elementType, bool nullableSource) .WithBody(body) .WithSemicolonToken(default); + // If the IID_PPV_ARGS pattern was detected, make this method generic. + if (iidPpvRiidOrigIndex >= 0) + { + TypeParameterConstraintClauseSyntax constraintClause = iidPpvMarshalingMode + ? TypeParameterConstraintClause(IdentifierName("T"), [ClassOrStructConstraint(SyntaxKind.ClassConstraint)]) + : TypeParameterConstraintClause(IdentifierName("T"), [TypeConstraint(IdentifierName("unmanaged"))]); + friendlyDeclaration = friendlyDeclaration + .AddTypeParameterListParameters(TypeParameter(Identifier("T"))) + .AddConstraintClauses(constraintClause); + } + if (returnSafeHandleType is object) { friendlyDeclaration = friendlyDeclaration.WithReturnType(returnSafeHandleType.WithTrailingTrivia(TriviaList(Space))); diff --git a/src/Microsoft.Windows.CsWin32/GeneratorOptions.cs b/src/Microsoft.Windows.CsWin32/GeneratorOptions.cs index 7dc7fab9..4f01b8e8 100644 --- a/src/Microsoft.Windows.CsWin32/GeneratorOptions.cs +++ b/src/Microsoft.Windows.CsWin32/GeneratorOptions.cs @@ -134,5 +134,13 @@ public record FriendlyOverloadOptions /// which normally appear as spans. /// public bool IncludePointerOverloads { get; set; } = false; + + /// + /// Gets or sets a value indicating whether to generate generic <T> overloads for methods + /// with the IID_PPV_ARGS pattern (a Guid* parameter immediately preceding a void** [ComOutPtr] parameter), + /// where the GUID is derived from typeof(T).GUID and the output pointer is typed as T. + /// + /// The default value is . + public bool ComOutPtrGenericOverloads { get; set; } = true; } } diff --git a/src/Microsoft.Windows.CsWin32/settings.schema.json b/src/Microsoft.Windows.CsWin32/settings.schema.json index e7dde71e..55fe0b08 100644 --- a/src/Microsoft.Windows.CsWin32/settings.schema.json +++ b/src/Microsoft.Windows.CsWin32/settings.schema.json @@ -55,6 +55,11 @@ "description": "A value indicating whether to also generate overloads that use pointer types for parameters that are [MemorySize] annotated buffers which normally appear as spans.", "type": "boolean", "default": false + }, + "comOutPtrGenericOverloads": { + "description": "A value indicating whether to generate generic overloads for methods with the IID_PPV_ARGS pattern (a Guid* parameter immediately preceding a void** [ComOutPtr] parameter), where the GUID is derived from typeof(T).GUID and the output pointer is typed as T.", + "type": "boolean", + "default": true } } }, diff --git a/src/Microsoft.Windows.CsWin32/templates/marshaling/CoCreateInstance.cs b/src/Microsoft.Windows.CsWin32/templates/marshaling/CoCreateInstance.cs deleted file mode 100644 index 99e6a739..00000000 --- a/src/Microsoft.Windows.CsWin32/templates/marshaling/CoCreateInstance.cs +++ /dev/null @@ -1,8 +0,0 @@ -/// -internal static unsafe global::Windows.Win32.Foundation.HRESULT CoCreateInstance(in Guid rclsid, object pUnkOuter, global::Windows.Win32.System.Com.CLSCTX dwClsContext, out T ppv) - where T : class -{ - global::Windows.Win32.Foundation.HRESULT hr = CoCreateInstance(rclsid, pUnkOuter, dwClsContext, typeof(T).GUID, out object o); - ppv = (T)o; - return hr; -} diff --git a/src/Microsoft.Windows.CsWin32/templates/no_marshaling/CoCreateInstance.cs b/src/Microsoft.Windows.CsWin32/templates/no_marshaling/CoCreateInstance.cs deleted file mode 100644 index 33594768..00000000 --- a/src/Microsoft.Windows.CsWin32/templates/no_marshaling/CoCreateInstance.cs +++ /dev/null @@ -1,8 +0,0 @@ -/// -internal static unsafe global::Windows.Win32.Foundation.HRESULT CoCreateInstance(in Guid rclsid, global::Windows.Win32.System.Com.IUnknown* pUnkOuter, global::Windows.Win32.System.Com.CLSCTX dwClsContext, out T* ppv) - where T : unmanaged -{ - global::Windows.Win32.Foundation.HRESULT hr = CoCreateInstance(rclsid, pUnkOuter, dwClsContext, typeof(T).GUID, out void* o); - ppv = (T*)o; - return hr; -} diff --git a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs index 0b6faf3f..1a663e74 100644 --- a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs +++ b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs @@ -205,7 +205,7 @@ public async Task DelegatesGetStructsGenerated() // Optional and MemorySize-d struct params, optional params included ["SetupDiGetClassInstallParams", "SetupDiGetClassInstallParams", "SafeHandle DeviceInfoSet, [Optional] winmdroot.Devices.DeviceAndDriverInstallation.SP_DEVINFO_DATA? DeviceInfoData, [Optional] Span ClassInstallParams, out uint RequiredSize"], ["IEnumString", "Next", "this winmdroot.System.Com.IEnumString @this, Span rgelt, out uint pceltFetched"], - ["PSCreateMemoryPropertyStore", "PSCreateMemoryPropertyStore", "in global::System.Guid riid, out object ppv"], + ["PSCreateMemoryPropertyStore", "PSCreateMemoryPropertyStore", "out T ppv"], ["DeviceIoControl", "DeviceIoControl", "SafeHandle hDevice, uint dwIoControlCode, [Optional] ReadOnlySpan lpInBuffer, [Optional] Span lpOutBuffer, out uint lpBytesReturned, [Optional] global::System.Threading.NativeOverlapped* lpOverlapped"], ["DeviceIoControl", "DeviceIoControl", "SafeHandle hDevice, uint dwIoControlCode, [Optional] ReadOnlySpan lpInBuffer, [Optional] Span lpOutBuffer, out uint lpBytesReturned, [Optional] global::System.Threading.NativeOverlapped* lpOverlapped", true, "NativeMethods.IncludePointerOverloads.json"], ["NtQueryObject", "NtQueryObject", "[Optional] global::Windows.Win32.Foundation.HANDLE Handle, winmdroot.Foundation.OBJECT_INFORMATION_CLASS ObjectInformationClass, [Optional] Span ObjectInformation, out uint ReturnLength"], diff --git a/test/GenerationSandbox.BuildTask.Tests/COMTests.cs b/test/GenerationSandbox.BuildTask.Tests/COMTests.cs index d7d3b16c..e6732350 100644 --- a/test/GenerationSandbox.BuildTask.Tests/COMTests.cs +++ b/test/GenerationSandbox.BuildTask.Tests/COMTests.cs @@ -312,10 +312,8 @@ public void IShellItem_BindToHandler_IStream_ReadWorks() unsafe { - PInvoke.SHCreateItemFromParsingName(filePath, null, typeof(IShellItem).GUID, out object shellItemObj).ThrowOnFailure(); - IShellItem shellItem = (IShellItem)shellItemObj; - shellItem.BindToHandler(null, bhidStream, typeof(IStream).GUID, out object streamObj); - IStream stream = (IStream)streamObj; + PInvoke.SHCreateItemFromParsingName(filePath, null, out IShellItem shellItem).ThrowOnFailure(); + shellItem.BindToHandler(null, bhidStream, out IStream stream); // Friendly Span overload — the original repro for #1716. In source-generator mode this used to // throw InvalidCastException because the extension method's `this` parameter was typed as diff --git a/test/GenerationSandbox.Tests/ComRuntimeTests.cs b/test/GenerationSandbox.Tests/ComRuntimeTests.cs index 600de4cf..f2e25a4e 100644 --- a/test/GenerationSandbox.Tests/ComRuntimeTests.cs +++ b/test/GenerationSandbox.Tests/ComRuntimeTests.cs @@ -175,16 +175,18 @@ public void CanCallIDispatchOnlyMethods() phwnd: out _, ShellWindowFindWindowOptions.SWFO_NEEDDISPATCH); - serviceProvider.QueryService(PInvoke.SID_STopLevelBrowser, typeof(IShellBrowser).GUID, out var shellBrowserAsObject); - var shellBrowser = (IShellBrowser)shellBrowserAsObject; + serviceProvider.QueryService(PInvoke.SID_STopLevelBrowser, out IShellBrowser shellBrowser); shellBrowser.QueryActiveShellView(out var shellView); - var iid_IDispatch = new Guid("00020400-0000-0000-C000-000000000046"); - shellView.GetItemObject((uint)_SVGIO.SVGIO_BACKGROUND, iid_IDispatch, out var folderViewAsObject); - var folderView = (IShellFolderViewDual)folderViewAsObject; + unsafe + { + var iid_IDispatch = new Guid("00020400-0000-0000-C000-000000000046"); + shellView.GetItemObject((uint)_SVGIO.SVGIO_BACKGROUND, &iid_IDispatch, out var folderViewAsObject); + var folderView = (IShellFolderViewDual)folderViewAsObject; - _ = folderView.Application; // Throws InvalidOleVariantTypeException "Specified OLE variant is invalid" + _ = folderView.Application; // Throws InvalidOleVariantTypeException "Specified OLE variant is invalid" + } } [Fact] @@ -237,10 +239,8 @@ public void IShellItem_BindToHandler_IStream_ReadWorks() unsafe { - PInvoke.SHCreateItemFromParsingName(filePath, null, typeof(IShellItem).GUID, out object shellItemObj).ThrowOnFailure(); - IShellItem shellItem = (IShellItem)shellItemObj; - shellItem.BindToHandler(null, bhidStream, typeof(IStream).GUID, out object streamObj); - IStream stream = (IStream)streamObj; + PInvoke.SHCreateItemFromParsingName(filePath, null, out IShellItem shellItem).ThrowOnFailure(); + shellItem.BindToHandler(null, bhidStream, out IStream stream); // Friendly Span overload — the original repro for #1716. In source-generator mode this used to // throw InvalidCastException because the extension method's `this` parameter was typed as @@ -264,4 +264,54 @@ public void IShellItem_BindToHandler_IStream_ReadWorks() Assert.Equal(bytesRead, bytesReadAfterSeek); } } + + /// + /// Verifies the generic BindToHandler<T> overload (IID_PPV_ARGS pattern) works at runtime. + /// The generic overload removes the explicit IID parameter and types the output as T. + /// + [Fact] + [Trait("TestCategory", "RequiresHardware")] + public void IShellItem_BindToHandler_GenericOverload() + { + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test calls Windows-specific APIs"); + + Guid bhidStream = new Guid(0x1cebb3ab, 0x7c10, 0x499a, 0xa4, 0x17, 0x92, 0xca, 0x16, 0xc4, 0xcb, 0x83); + + string filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini"); + Assert.True(File.Exists(filePath), $"Expected '{filePath}' to exist on Windows."); + + unsafe + { + PInvoke.SHCreateItemFromParsingName(filePath, null, out IShellItem shellItem).ThrowOnFailure(); + + // Use the generic overload instead of passing typeof(IStream).GUID + out object manually. + shellItem.BindToHandler(null, bhidStream, out IStream stream); + + byte[] buffer = new byte[16]; + stream.Read(buffer, out uint bytesRead); + Assert.True(bytesRead > 0, "Expected to read at least one byte from win.ini via generic overload."); + } + } + + /// + /// Verifies the generic SHCreateItemFromParsingName<T> overload (IID_PPV_ARGS pattern) works at runtime. + /// + [Fact] + [Trait("TestCategory", "RequiresHardware")] + public void SHCreateItemFromParsingName_GenericOverload() + { + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test calls Windows-specific APIs"); + + string filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini"); + Assert.True(File.Exists(filePath), $"Expected '{filePath}' to exist on Windows."); + + unsafe + { + // Use the generic overload — no explicit typeof(IShellItem).GUID needed. + PInvoke.SHCreateItemFromParsingName(filePath, null, out IShellItem shellItem).ThrowOnFailure(); + + // Verify the returned object is usable. + Assert.NotNull(shellItem); + } + } } diff --git a/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs b/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs index 4bcbbd35..fc6b0cd1 100644 --- a/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs +++ b/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs @@ -35,8 +35,7 @@ public unsafe void CocreatableClassesWithImplicitInterfaces() Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test calls Windows-specific APIs"); ShellLink.CreateInstance(out IShellLinkW* shellLinkWPtr).ThrowOnFailure(); - shellLinkWPtr->QueryInterface(typeof(IPersistFile).GUID, out void* ppv).ThrowOnFailure(); - IPersistFile* persistFilePtr = (IPersistFile*)ppv; + shellLinkWPtr->QueryInterface(out IPersistFile* persistFilePtr).ThrowOnFailure(); Assert.NotNull(persistFilePtr); persistFilePtr->Release(); shellLinkWPtr->Release(); diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index 571feade..a9a09650 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -351,6 +351,132 @@ public void ComOutPtrTypedAsIntPtr() Assert.Contains(this.FindGeneratedMethod(methodName), m => m.ParameterList.Parameters.Last() is { } last && last.Modifiers.Any(SyntaxKind.OutKeyword) && last.Type is IdentifierNameSyntax { Identifier: { ValueText: "IntPtr" } }); } + [Fact] + public void ComOutPtrGenericOverload_Marshaling_IShellItem() + { + const string methodName = "BindToHandler"; + this.generator = this.CreateGenerator(new GeneratorOptions { AllowMarshaling = true }); + this.GenerateApi("IShellItem"); + + // Verify a generic overload was generated with `out T` and `where T : class`. + // Scope to the IShellItem extension class by checking the first parameter has `this` and refers to IShellItem. + MethodDeclarationSyntax genericOverload = Assert.Single( + this.FindGeneratedMethod(methodName), + m => m.TypeParameterList?.Parameters.Count == 1 + && m.ParameterList.Parameters.FirstOrDefault() is { } first + && first.Modifiers.Any(SyntaxKind.ThisKeyword) + && first.Type?.ToString().Contains("IShellItem") == true); + Assert.Contains(genericOverload.ConstraintClauses, cc => cc.Constraints.Any(c => c is ClassOrStructConstraintSyntax { ClassOrStructKeyword.RawKind: (int)SyntaxKind.ClassKeyword })); + + // The last parameter should be `out T`. + ParameterSyntax ppvParam = genericOverload.ParameterList.Parameters.Last(); + Assert.True(ppvParam.Modifiers.Any(SyntaxKind.OutKeyword)); + Assert.IsType(ppvParam.Type); + Assert.Equal("T", ((IdentifierNameSyntax)ppvParam.Type).Identifier.ValueText); + } + + [Fact] + public void ComOutPtrGenericOverload_NoMarshaling_IShellItem() + { + const string methodName = "BindToHandler"; + this.generator = this.CreateGenerator(new GeneratorOptions { AllowMarshaling = false }); + this.GenerateApi("IShellItem"); + + // Verify a generic overload was generated with `out T*` and `where T : unmanaged`. + MethodDeclarationSyntax genericOverload = Assert.Single( + this.FindGeneratedMethod(methodName), + m => m.TypeParameterList?.Parameters.Count == 1 && m.Parent is StructDeclarationSyntax { Identifier.ValueText: "IShellItem" }); + Assert.Contains(genericOverload.ConstraintClauses, cc => cc.Constraints.Any(c => c is TypeConstraintSyntax { Type: IdentifierNameSyntax { Identifier.ValueText: "unmanaged" } })); + + // The last parameter should be `out T*`. + ParameterSyntax ppvParam = genericOverload.ParameterList.Parameters.Last(); + Assert.True(ppvParam.Modifiers.Any(SyntaxKind.OutKeyword)); + Assert.IsType(ppvParam.Type); + Assert.Equal("T", ((IdentifierNameSyntax)((PointerTypeSyntax)ppvParam.Type).ElementType).Identifier.ValueText); + } + + [Fact] + public void ComOutPtrGenericOverload_OptOut() + { + const string methodName = "BindToHandler"; + this.generator = this.CreateGenerator(new GeneratorOptions + { + AllowMarshaling = true, + FriendlyOverloads = new GeneratorOptions.FriendlyOverloadOptions { ComOutPtrGenericOverloads = false }, + }); + this.GenerateApi("IShellItem"); + + // With the opt-out, no generic overloads should be generated for COM interface methods. + Assert.DoesNotContain( + this.FindGeneratedMethod(methodName), + m => m.TypeParameterList?.Parameters.Count == 1); + } + + [Fact] + public void ComOutPtrGenericOverload_CoCreateInstance() + { + // CoCreateInstance follows the IID_PPV_ARGS pattern — the friendly overload should be generic. + const string methodName = "CoCreateInstance"; + this.GenerateApi(methodName); + + MethodDeclarationSyntax genericOverload = Assert.Single( + this.FindGeneratedMethod(methodName), + m => m.TypeParameterList?.Parameters.Count == 1); + + // 4 parameters: rclsid, pUnkOuter, dwClsContext, ppv (riid removed). + Assert.Equal(4, genericOverload.ParameterList.Parameters.Count); + } + + [Fact] + public void ComOutPtrGenericOverload_IntPtrMode_Skipped() + { + // When UseIntPtrForComOutPointers is true, no generic overload should be generated. + const string methodName = "BindToHandler"; + this.generator = this.CreateGenerator(new GeneratorOptions + { + AllowMarshaling = true, + ComInterop = new GeneratorOptions.ComInteropOptions { UseIntPtrForComOutPointers = true }, + }); + this.GenerateApi("IShellItem"); + + Assert.DoesNotContain( + this.FindGeneratedMethod(methodName), + m => m.TypeParameterList?.Parameters.Count == 1); + } + + /// + /// Regression test for #1604. + /// D3D12 device creation methods like CreateCommandQueue should get generic overloads. + /// + [Theory, PairwiseData] + public void ComOutPtrGenericOverload_D3D12_CreateCommandQueue(bool allowMarshaling) + { + const string methodName = "CreateCommandQueue"; + this.generator = this.CreateGenerator(new GeneratorOptions { AllowMarshaling = allowMarshaling }); + this.GenerateApi("ID3D12Device"); + + // Verify a generic overload was generated. + Assert.Contains( + this.FindGeneratedMethod(methodName), + m => m.TypeParameterList?.Parameters.Count == 1); + } + + /// + /// Regression test for #374. + /// IMoniker.BindToObject should get a generic overload. + /// + [Theory, PairwiseData] + public void ComOutPtrGenericOverload_IMoniker_BindToObject(bool allowMarshaling) + { + const string methodName = "BindToObject"; + this.generator = this.CreateGenerator(new GeneratorOptions { AllowMarshaling = allowMarshaling }); + this.GenerateApi("IMoniker"); + + Assert.Contains( + this.FindGeneratedMethod(methodName), + m => m.TypeParameterList?.Parameters.Count == 1); + } + [Theory, PairwiseData] public void NonCOMInterfaceReferences(bool allowMarshaling) {