From 11bc618d7d3220bdc00f290a0467ead243b2e6e2 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Tue, 21 Oct 2025 15:07:28 -0700 Subject: [PATCH 1/8] Handle CoCreateable classes --- .../FastSyntaxFactory.cs | 2 + .../Generator.Com.cs | 72 +++++++++++++++++-- src/Microsoft.Windows.CsWin32/Generator.cs | 2 +- .../CsWin32GeneratorTests.cs | 17 +++++ .../COMTests.cs | 41 +++++++++++ .../NativeMethods.txt | 5 +- 6 files changed, 131 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/FastSyntaxFactory.cs b/src/Microsoft.Windows.CsWin32/FastSyntaxFactory.cs index af899efb..a4c620a4 100644 --- a/src/Microsoft.Windows.CsWin32/FastSyntaxFactory.cs +++ b/src/Microsoft.Windows.CsWin32/FastSyntaxFactory.cs @@ -321,6 +321,8 @@ internal static SyntaxList List(IEnumerable nodes) internal static TypeConstraintSyntax TypeConstraint(TypeSyntax type) => SyntaxFactory.TypeConstraint(type); + internal static ClassOrStructConstraintSyntax ClassOrStructConstraint(SyntaxKind kind) => SyntaxFactory.ClassOrStructConstraint(kind); + internal static TypeParameterConstraintClauseSyntax TypeParameterConstraintClause(IdentifierNameSyntax name, SeparatedSyntaxList constraints) => SyntaxFactory.TypeParameterConstraintClause(TokenWithSpace(SyntaxKind.WhereKeyword), name, TokenWithSpaces(SyntaxKind.ColonToken), constraints); internal static FieldDeclarationSyntax FieldDeclaration(VariableDeclarationSyntax declaration) => SyntaxFactory.FieldDeclaration(default, default, declaration, Semicolon); diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 2c210e67..367b6a11 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -68,6 +68,10 @@ private static bool GenerateCcwFor(MetadataReader reader, StringHandle typeName, return true; } + private static StatementSyntax ThrowOnHRFailure(ExpressionSyntax hrExpression) => ExpressionStatement(InvocationExpression( + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, hrExpression, HRThrowOnFailureMethodName), + ArgumentList())); + /// /// Generates a type to represent a COM interface. /// @@ -327,10 +331,6 @@ FunctionPointerParameterSyntax ToFunctionPointerParameter(ParameterSyntax p) if (methodDefinition.Generator.TryGetPropertyAccessorInfo(methodDefinition, originalIfaceName, context, out IdentifierNameSyntax? propertyName, out SyntaxKind? accessorKind, out TypeSyntax? propertyType) && declaredProperties.Contains(propertyName.Identifier.ValueText)) { - StatementSyntax ThrowOnHRFailure(ExpressionSyntax hrExpression) => ExpressionStatement(InvocationExpression( - MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, hrExpression, HRThrowOnFailureMethodName), - ArgumentList())); - BlockSyntax? body; switch (accessorKind) { @@ -1307,7 +1307,7 @@ private bool TryDeclareCOMGuidInterfaceIfNecessary() /// Creates an empty class that when instantiated, creates a cocreatable Windows object /// that may implement a number of interfaces at runtime, discoverable only by documentation. /// - private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef) + private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, Context context) { IdentifierNameSyntax name = IdentifierName(this.Reader.GetString(typeDef.Name)); Guid guid = this.FindGuidFromAttribute(typeDef) ?? throw new ArgumentException("Type does not have a GuidAttribute."); @@ -1315,7 +1315,67 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef) classModifiers = classModifiers.Add(TokenWithSpace(SyntaxKind.PartialKeyword)); ClassDeclarationSyntax result = ClassDeclaration(name.Identifier) .WithModifiers(classModifiers) - .AddAttributeLists(AttributeList().AddAttributes(GUID(guid), ComImportAttributeSyntax)); + .AddAttributeLists(AttributeList().AddAttributes(GUID(guid)).AddAttributes(this.useSourceGenerators ? [] : [ComImportAttributeSyntax])); + + if (context.AllowMarshaling && this.useSourceGenerators) + { + // If using source generators, generate a constructor with obsolete attribute like this: + // [Obsolete("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")] + // public Foo() { throw new NotSupportedException("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead."); } + AttributeSyntax obsoleteAttribute = + Attribute(IdentifierName(nameof(ObsoleteAttribute))) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")))); + ConstructorDeclarationSyntax constructor = ConstructorDeclaration(name.Identifier) + .AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword)) + .AddAttributeLists(AttributeList().AddAttributes(obsoleteAttribute)) + .WithBody( + Block( + ThrowStatement( + ObjectCreationExpression(IdentifierName(nameof(NotSupportedException))) + .WithArgumentList( + ArgumentList().AddArguments( + Argument( + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")))))))); + result = result.AddMembers(constructor); + + // Then add the CreateInstance method: + // public static T CreateInstance() where T : class + // { + // PInvoke.CoCreateInstance(typeof(Foo).GUID, null, CLSCTX.CLSCTX_SERVER, out T ret).ThrowOnFailure(); + // return ret; + // } + this.MainGenerator.TryGenerateExternMethod("CoCreateInstance", out IReadOnlyCollection preciseApi); + this.MainGenerator.TryGenerateConstant("CLSCTX", out preciseApi); + + TypeParameterSyntax typeParameter = TypeParameter(Identifier("T")); + GenericNameSyntax genericName = GenericName("CreateInstance").AddTypeArgumentListArguments(IdentifierName("T")); + MethodDeclarationSyntax createInstanceMethod = MethodDeclaration(IdentifierName("T"), genericName.Identifier) + .AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword)) + .AddTypeParameterListParameters(typeParameter) + .AddConstraintClauses( + TypeParameterConstraintClause(IdentifierName("T"), SingletonSeparatedList(ClassOrStructConstraint(SyntaxKind.ClassConstraint)))) + .WithBody( + Block( + ThrowOnHRFailure( + InvocationExpression(QualifiedName(ParseName($"{this.Win32NamespacePrefix}.{this.options.ClassName}"), GenericName("CoCreateInstance").AddTypeArgumentListArguments(IdentifierName("T")))) + .WithArgumentList( + ArgumentList().AddArguments( + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + TypeOfExpression(name), + IdentifierName("GUID"))), + Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)), + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + QualifiedName(ParseName($"{this.Win32NamespacePrefix}.System.Com"), IdentifierName("CLSCTX")), + IdentifierName("CLSCTX_SERVER"))), + Argument(DeclarationExpression(IdentifierName("T").WithTrailingTrivia(Space), SingleVariableDesignation(Identifier("ret")))).WithRefKindKeyword(Token(SyntaxKind.OutKeyword))))), + ReturnStatement(IdentifierName("ret")))); + result = result.AddMembers(createInstanceMethod); + } result = this.AddApiDocumentation(name.Identifier.ValueText, result); return result; diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index 1e3f6c13..aeb743ca 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -1440,7 +1440,7 @@ private IReadOnlyList FindTypeSymbolsIfAlreadyAvailable(string fullyQua } else if (this.IsEmptyStructWithGuid(typeDef)) { - typeDeclaration = this.DeclareCocreatableClass(typeDef); + typeDeclaration = this.DeclareCocreatableClass(typeDef, context); } else { diff --git a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs index cf367f1d..cc988378 100644 --- a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs +++ b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Xunit; namespace CsWin32Generator.Tests; @@ -87,6 +88,22 @@ public async Task TestGenerateSomethingInWin32System() await this.InvokeGeneratorAndCompile(); } + [Fact] + public async Task TestGenerateCoCreateableClass() + { + // If we need CharSet _and_ we generate something in Windows.Win32.System, the partially qualified reference breaks. + this.nativeMethods.Add("ShellLink"); + await this.InvokeGeneratorAndCompile(); + + var shellLinkType = Assert.Single(this.FindGeneratedType("ShellLink")); + + // Check that it does not have the ComImport attribute. + Assert.DoesNotContain(shellLinkType.AttributeLists, al => al.Attributes.Any(attr => attr.Name.ToString().Contains("ComImport"))); + + // Check that it contains a CreateInstance method + Assert.Contains(shellLinkType.DescendantNodes().OfType(), method => method.Identifier.Text == "CreateInstance"); + } + [Theory] [InlineData("IMFMediaKeySession", "get_KeySystem", "winmdroot.Foundation.BSTR* keySystem")] [InlineData("AddPrinterW", "AddPrinter", "winmdroot.Foundation.PWSTR pName, uint Level, Span pPrinter")] diff --git a/test/GenerationSandbox.BuildTask.Tests/COMTests.cs b/test/GenerationSandbox.BuildTask.Tests/COMTests.cs index 908cb7b7..680669cc 100644 --- a/test/GenerationSandbox.BuildTask.Tests/COMTests.cs +++ b/test/GenerationSandbox.BuildTask.Tests/COMTests.cs @@ -4,6 +4,7 @@ #pragma warning disable IDE0005 #pragma warning disable SA1201, SA1512, SA1005, SA1507, SA1515, SA1403, SA1402, SA1411, SA1300, SA1313, SA1134, SA1307, SA1308 +using System.ComponentModel; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; @@ -17,6 +18,7 @@ using Windows.Win32.Graphics.Direct3D11; using Windows.Win32.System.Com; using Windows.Win32.System.WinRT.Composition; +using Windows.Win32.UI.Shell; [Trait("WindowsOnly", "true")] public partial class COMTests @@ -70,4 +72,43 @@ public async Task CanInteropWithICompositorInterop() Assert.Skip("Skipping due to UnauthorizedAccessException."); } } + + [Fact] + public void CocreatableClassesWithImplicitInterfaces() + { + var shellLinkW = ShellLink.CreateInstance(); + var persistFile = (IPersistFile)shellLinkW; + Assert.NotNull(persistFile); + } } + +[Guid("00021401-0000-0000-C000-000000000046")] +[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.217+533aa1bddf.RR")] +internal partial class ShellLink +{ + [Obsolete("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")] + public ShellLink() { throw new NotSupportedException("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead."); } + + public static T CreateInstance() where T : class + { + PInvoke.CoCreateInstance(typeof(ShellLink).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, out T ret).ThrowOnFailure(); + return ret; + } +} + +//[Guid("00021401-0000-0000-C000-000000000046")] +//[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.217+533aa1bddf.RR")] +//internal partial struct ShellLink2 +//{ +// public object Instance; + +// public ShellLink2() +// { +// PInvoke.CoCreateInstance(typeof(ShellLink).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, out Instance).ThrowOnFailure(); +// } + +// public static implicit operator T(in ShellLink2 instance) +// { +// return (T)instance.Instance; +// } +//} diff --git a/test/GenerationSandbox.BuildTask.Tests/NativeMethods.txt b/test/GenerationSandbox.BuildTask.Tests/NativeMethods.txt index a4fddae7..f977d933 100644 --- a/test/GenerationSandbox.BuildTask.Tests/NativeMethods.txt +++ b/test/GenerationSandbox.BuildTask.Tests/NativeMethods.txt @@ -34,4 +34,7 @@ WINTRUST_DATA WINTRUST_FILE_INFO WinVerifyTrust WM_HOTKEY -WNDCLASSW \ No newline at end of file +WNDCLASSW +ShellLink +CoCreateInstance +IShellLinkW \ No newline at end of file From ea0d77e91fdecdbd98982f4a5300cb746eb1b8e8 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Tue, 21 Oct 2025 15:51:48 -0700 Subject: [PATCH 2/8] Merge and fix test break --- test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs index 57b3f225..b7202ef6 100644 --- a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs +++ b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs @@ -97,7 +97,7 @@ public async Task TestPlatformCaseSensitivity(string platform) { this.platform = platform; this.nativeMethods.Add("SetWindowLongPtr"); - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompile($"{nameof(this.TestPlatformCaseSensitivity)}_{platform}"); } [Fact] @@ -105,7 +105,7 @@ public async Task TestGenerateCoCreateableClass() { // If we need CharSet _and_ we generate something in Windows.Win32.System, the partially qualified reference breaks. this.nativeMethods.Add("ShellLink"); - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompileFromFact(); var shellLinkType = Assert.Single(this.FindGeneratedType("ShellLink")); From 093244039f1807b1286894e4e101cfe41c00fac5 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Tue, 21 Oct 2025 22:41:50 -0700 Subject: [PATCH 3/8] Support allowMarshaling=false too. --- .../Generator.Com.cs | 124 ++++++++++++------ .../CsWin32GeneratorTests.cs | 1 - .../COMTests.cs | 31 ----- .../COMTests.cs | 14 +- .../NativeMethods.txt | 2 + .../COMTests.cs | 16 +++ 6 files changed, 117 insertions(+), 71 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 367b6a11..aaa46230 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -1309,23 +1309,29 @@ private bool TryDeclareCOMGuidInterfaceIfNecessary() /// private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, Context context) { + bool canUseComImport = context.AllowMarshaling && !this.useSourceGenerators; + IdentifierNameSyntax name = IdentifierName(this.Reader.GetString(typeDef.Name)); Guid guid = this.FindGuidFromAttribute(typeDef) ?? throw new ArgumentException("Type does not have a GuidAttribute."); SyntaxTokenList classModifiers = TokenList(TokenWithSpace(this.Visibility)); classModifiers = classModifiers.Add(TokenWithSpace(SyntaxKind.PartialKeyword)); ClassDeclarationSyntax result = ClassDeclaration(name.Identifier) .WithModifiers(classModifiers) - .AddAttributeLists(AttributeList().AddAttributes(GUID(guid)).AddAttributes(this.useSourceGenerators ? [] : [ComImportAttributeSyntax])); + .AddAttributeLists(AttributeList().AddAttributes(GUID(guid)).AddAttributes(canUseComImport ? [ComImportAttributeSyntax] : [])); - if (context.AllowMarshaling && this.useSourceGenerators) + if (!canUseComImport) { - // If using source generators, generate a constructor with obsolete attribute like this: + string obsoleteMessage = context.AllowMarshaling + ? $"COM source generators do not support direct instantiation of co-creatable classes. Use {name.Identifier}.CreateInstance instead." + : $"Marshaling is disabled, so direct instantiation of co-creatable classes is not supported. Use {name.Identifier}.CreateInstance instead."; + + // If using source generators or marshalling is disabled, generate a constructor with obsolete attribute like this: // [Obsolete("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")] // public Foo() { throw new NotSupportedException("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead."); } AttributeSyntax obsoleteAttribute = Attribute(IdentifierName(nameof(ObsoleteAttribute))) .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")))); + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(obsoleteMessage)))); ConstructorDeclarationSyntax constructor = ConstructorDeclaration(name.Identifier) .AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword)) .AddAttributeLists(AttributeList().AddAttributes(obsoleteAttribute)) @@ -1336,45 +1342,87 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C .WithArgumentList( ArgumentList().AddArguments( Argument( - LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")))))))); + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(obsoleteMessage)))))))); result = result.AddMembers(constructor); - // Then add the CreateInstance method: - // public static T CreateInstance() where T : class - // { - // PInvoke.CoCreateInstance(typeof(Foo).GUID, null, CLSCTX.CLSCTX_SERVER, out T ret).ThrowOnFailure(); - // return ret; - // } this.MainGenerator.TryGenerateExternMethod("CoCreateInstance", out IReadOnlyCollection preciseApi); this.MainGenerator.TryGenerateConstant("CLSCTX", out preciseApi); - TypeParameterSyntax typeParameter = TypeParameter(Identifier("T")); - GenericNameSyntax genericName = GenericName("CreateInstance").AddTypeArgumentListArguments(IdentifierName("T")); - MethodDeclarationSyntax createInstanceMethod = MethodDeclaration(IdentifierName("T"), genericName.Identifier) - .AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword)) - .AddTypeParameterListParameters(typeParameter) - .AddConstraintClauses( - TypeParameterConstraintClause(IdentifierName("T"), SingletonSeparatedList(ClassOrStructConstraint(SyntaxKind.ClassConstraint)))) - .WithBody( - Block( - ThrowOnHRFailure( - InvocationExpression(QualifiedName(ParseName($"{this.Win32NamespacePrefix}.{this.options.ClassName}"), GenericName("CoCreateInstance").AddTypeArgumentListArguments(IdentifierName("T")))) - .WithArgumentList( - ArgumentList().AddArguments( - Argument( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - TypeOfExpression(name), - IdentifierName("GUID"))), - Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)), - Argument( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - QualifiedName(ParseName($"{this.Win32NamespacePrefix}.System.Com"), IdentifierName("CLSCTX")), - IdentifierName("CLSCTX_SERVER"))), - Argument(DeclarationExpression(IdentifierName("T").WithTrailingTrivia(Space), SingleVariableDesignation(Identifier("ret")))).WithRefKindKeyword(Token(SyntaxKind.OutKeyword))))), - ReturnStatement(IdentifierName("ret")))); - result = result.AddMembers(createInstanceMethod); + if (context.AllowMarshaling) + { + // Then add the CreateInstance method: + // public static T CreateInstance() where T : class + // { + // PInvoke.CoCreateInstance(typeof(Foo).GUID, null, CLSCTX.CLSCTX_SERVER, out T ret).ThrowOnFailure(); + // return ret; + // } + TypeParameterSyntax typeParameter = TypeParameter(Identifier("T")); + GenericNameSyntax genericName = GenericName("CreateInstance").AddTypeArgumentListArguments(IdentifierName("T")); + MethodDeclarationSyntax createInstanceMethod = MethodDeclaration(IdentifierName("T"), genericName.Identifier) + .AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword)) + .AddTypeParameterListParameters(typeParameter) + .AddConstraintClauses( + TypeParameterConstraintClause(IdentifierName("T"), SingletonSeparatedList(ClassOrStructConstraint(SyntaxKind.ClassConstraint)))) + .WithBody( + Block( + ThrowOnHRFailure( + InvocationExpression(QualifiedName(ParseName($"{this.Win32NamespacePrefix}.{this.options.ClassName}"), GenericName("CoCreateInstance").AddTypeArgumentListArguments(IdentifierName("T")))) + .WithArgumentList( + ArgumentList().AddArguments( + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + TypeOfExpression(name), + IdentifierName("GUID"))), + Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)), + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + QualifiedName(ParseName($"{this.Win32NamespacePrefix}.System.Com"), IdentifierName("CLSCTX")), + IdentifierName("CLSCTX_SERVER"))), + Argument(DeclarationExpression(IdentifierName("T").WithTrailingTrivia(Space), SingleVariableDesignation(Identifier("ret")))).WithRefKindKeyword(Token(SyntaxKind.OutKeyword))))), + ReturnStatement(IdentifierName("ret")))); + result = result.AddMembers(createInstanceMethod); + } + else + { + // Then add a CreateInstance method that looks like this: + // public static HRESULT CreateInstance(out T* instance) where T : unmanaged + // { + // return PInvoke.CoCreateInstance(typeof(Foo).GUID, null, CLSCTX.CLSCTX_SERVER, out T* ret); + // } + TypeParameterSyntax typeParameter = TypeParameter(Identifier("T")); + GenericNameSyntax genericName = GenericName("CreateInstance").AddTypeArgumentListArguments(IdentifierName("T")); + MethodDeclarationSyntax createInstanceMethod = MethodDeclaration(IdentifierName($"{this.Win32NamespacePrefix}.Foundation.HRESULT"), genericName.Identifier) + .AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.UnsafeKeyword)) + .AddTypeParameterListParameters(typeParameter) + .AddConstraintClauses( + TypeParameterConstraintClause(IdentifierName("T"), SingletonSeparatedList(TypeConstraint(IdentifierName("unmanaged"))))) + .WithParameterList( + ParameterList().AddParameters( + Parameter(Identifier("instance")) + .WithType(PointerType(IdentifierName("T"))) + .WithModifiers(TokenList(Token(SyntaxKind.OutKeyword))))) + .WithBody( + Block( + ReturnStatement( + InvocationExpression(QualifiedName(ParseName($"{this.Win32NamespacePrefix}.{this.options.ClassName}"), GenericName("CoCreateInstance").AddTypeArgumentListArguments(IdentifierName("T")))) + .WithArgumentList( + ArgumentList().AddArguments( + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + TypeOfExpression(name), + IdentifierName("GUID"))), + Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)), + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + QualifiedName(ParseName($"{this.Win32NamespacePrefix}.System.Com"), IdentifierName("CLSCTX")), + IdentifierName("CLSCTX_SERVER"))), + Argument(IdentifierName("instance")).WithRefKindKeyword(Token(SyntaxKind.OutKeyword))))))); + result = result.AddMembers(createInstanceMethod); + } } result = this.AddApiDocumentation(name.Identifier.ValueText, result); diff --git a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs index b7202ef6..30aea6f8 100644 --- a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs +++ b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs @@ -103,7 +103,6 @@ public async Task TestPlatformCaseSensitivity(string platform) [Fact] public async Task TestGenerateCoCreateableClass() { - // If we need CharSet _and_ we generate something in Windows.Win32.System, the partially qualified reference breaks. this.nativeMethods.Add("ShellLink"); await this.InvokeGeneratorAndCompileFromFact(); diff --git a/test/GenerationSandbox.BuildTask.Tests/COMTests.cs b/test/GenerationSandbox.BuildTask.Tests/COMTests.cs index 680669cc..13318860 100644 --- a/test/GenerationSandbox.BuildTask.Tests/COMTests.cs +++ b/test/GenerationSandbox.BuildTask.Tests/COMTests.cs @@ -81,34 +81,3 @@ public void CocreatableClassesWithImplicitInterfaces() Assert.NotNull(persistFile); } } - -[Guid("00021401-0000-0000-C000-000000000046")] -[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.217+533aa1bddf.RR")] -internal partial class ShellLink -{ - [Obsolete("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")] - public ShellLink() { throw new NotSupportedException("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead."); } - - public static T CreateInstance() where T : class - { - PInvoke.CoCreateInstance(typeof(ShellLink).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, out T ret).ThrowOnFailure(); - return ret; - } -} - -//[Guid("00021401-0000-0000-C000-000000000046")] -//[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.217+533aa1bddf.RR")] -//internal partial struct ShellLink2 -//{ -// public object Instance; - -// public ShellLink2() -// { -// PInvoke.CoCreateInstance(typeof(ShellLink).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, out Instance).ThrowOnFailure(); -// } - -// public static implicit operator T(in ShellLink2 instance) -// { -// return (T)instance.Instance; -// } -//} diff --git a/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs b/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs index 388e88c6..81f6fecc 100644 --- a/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs +++ b/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#pragma warning disable IDE0005 +#pragma warning disable IDE0005,SA1202 using Windows.Win32; using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; public class COMTests { @@ -19,5 +20,16 @@ public void COMStaticGuid() private static Guid GetGuid() where T : IComIID => T.Guid; + + [Fact] + public unsafe void CocreatableClassesWithImplicitInterfaces() + { + ShellLink.CreateInstance(out IShellLinkW* shellLinkWPtr).ThrowOnFailure(); + shellLinkWPtr->QueryInterface(typeof(IPersistFile).GUID, out void* ppv).ThrowOnFailure(); + IPersistFile* persistFilePtr = (IPersistFile*)ppv; + Assert.NotNull(persistFilePtr); + persistFilePtr->Release(); + shellLinkWPtr->Release(); + } #endif } diff --git a/test/GenerationSandbox.Unmarshalled.Tests/NativeMethods.txt b/test/GenerationSandbox.Unmarshalled.Tests/NativeMethods.txt index c59c92ce..9a5ec71c 100644 --- a/test/GenerationSandbox.Unmarshalled.Tests/NativeMethods.txt +++ b/test/GenerationSandbox.Unmarshalled.Tests/NativeMethods.txt @@ -1,3 +1,5 @@ IEventSubscription IPersistFile IStream +ShellLink +IShellLinkW \ No newline at end of file diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index ca8602e2..30bca437 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -487,4 +487,20 @@ public void IUnknown_Derived_QueryInterfaceGenericHelper() this.FindGeneratedMethod("QueryInterface"), m => m.Parent is StructDeclarationSyntax { Identifier.Text: "ITypeLib" } && m.TypeParameterList?.Parameters.Count == 1); } + + [Fact] + public void TestGenerateCoCreateableClass() + { + this.generator = this.CreateGenerator(new GeneratorOptions { AllowMarshaling = false }); + + this.GenerateApi("ShellLink"); + + var shellLinkType = Assert.Single(this.FindGeneratedType("ShellLink")); + + // Check that it does not have the ComImport attribute. + Assert.DoesNotContain(shellLinkType.AttributeLists, al => al.Attributes.Any(attr => attr.Name.ToString().Contains("ComImport"))); + + // Check that it contains a CreateInstance method + Assert.Contains(shellLinkType.DescendantNodes().OfType(), method => method.Identifier.Text == "CreateInstance"); + } } From c5c3b11e76d97d484ce2910165ae94687d1409a4 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Wed, 22 Oct 2025 23:27:21 -0700 Subject: [PATCH 4/8] Fix linux test --- test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs b/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs index 81f6fecc..b686a2dd 100644 --- a/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs +++ b/test/GenerationSandbox.Unmarshalled.Tests/COMTests.cs @@ -3,6 +3,7 @@ #pragma warning disable IDE0005,SA1202 +using System.Runtime.InteropServices; using Windows.Win32; using Windows.Win32.System.Com; using Windows.Win32.UI.Shell; @@ -21,9 +22,12 @@ private static Guid GetGuid() where T : IComIID => T.Guid; + [Trait("WindowsOnly", "true")] [Fact] 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; From bf59218e6e5d02480cd5cd6b76f9b9117b4018fe Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Thu, 23 Oct 2025 09:15:39 -0700 Subject: [PATCH 5/8] PR feedback --- .../FastSyntaxFactory.cs | 4 +++ .../Generator.Com.cs | 21 +++++++------- .../SimpleSyntaxFactory.cs | 28 +++++++++++++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/FastSyntaxFactory.cs b/src/Microsoft.Windows.CsWin32/FastSyntaxFactory.cs index a4c620a4..54cae8bd 100644 --- a/src/Microsoft.Windows.CsWin32/FastSyntaxFactory.cs +++ b/src/Microsoft.Windows.CsWin32/FastSyntaxFactory.cs @@ -76,6 +76,10 @@ internal static SyntaxToken Token(SyntaxKind kind) internal static ImplicitArrayCreationExpressionSyntax ImplicitArrayCreationExpression(InitializerExpressionSyntax initializerExpression) => SyntaxFactory.ImplicitArrayCreationExpression(Token(SyntaxKind.NewKeyword), Token(SyntaxKind.OpenBracketToken), default, Token(SyntaxKind.CloseBracketToken), initializerExpression); + internal static CollectionExpressionSyntax CollectionExpression(SeparatedSyntaxList elements = default) => SyntaxFactory.CollectionExpression(elements); + + internal static ExpressionElementSyntax ExpressionElement(ExpressionSyntax expression) => SyntaxFactory.ExpressionElement(expression); + internal static ForStatementSyntax ForStatement(VariableDeclarationSyntax? declaration, ExpressionSyntax condition, SeparatedSyntaxList incrementors, StatementSyntax statement) { SyntaxToken semicolonToken = SyntaxFactory.Token(TriviaList(), SyntaxKind.SemicolonToken, TriviaList(Space)); diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index aaa46230..43e3da31 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -1325,6 +1325,15 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C ? $"COM source generators do not support direct instantiation of co-creatable classes. Use {name.Identifier}.CreateInstance instead." : $"Marshaling is disabled, so direct instantiation of co-creatable classes is not supported. Use {name.Identifier}.CreateInstance instead."; + // Generate a private property for the Guid + // private static Guid CLSID_Foo => new Guid(...); + SyntaxToken clsidPropertyName = Identifier($"CLSID_{name.Identifier}"); + PropertyDeclarationSyntax clsidProperty = PropertyDeclaration(GuidTypeSyntax.WithTrailingTrivia(Space), clsidPropertyName) + .AddModifiers(TokenWithSpace(SyntaxKind.PrivateKeyword), TokenWithSpace(SyntaxKind.StaticKeyword)) + .WithExpressionBody(ArrowExpressionClause(GuidValue(guid))) + .WithSemicolonToken(SemicolonWithLineFeed); + result = result.AddMembers(clsidProperty); + // If using source generators or marshalling is disabled, generate a constructor with obsolete attribute like this: // [Obsolete("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")] // public Foo() { throw new NotSupportedException("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead."); } @@ -1369,11 +1378,7 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C InvocationExpression(QualifiedName(ParseName($"{this.Win32NamespacePrefix}.{this.options.ClassName}"), GenericName("CoCreateInstance").AddTypeArgumentListArguments(IdentifierName("T")))) .WithArgumentList( ArgumentList().AddArguments( - Argument( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - TypeOfExpression(name), - IdentifierName("GUID"))), + Argument(IdentifierName(clsidPropertyName)), Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)), Argument( MemberAccessExpression( @@ -1409,11 +1414,7 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C InvocationExpression(QualifiedName(ParseName($"{this.Win32NamespacePrefix}.{this.options.ClassName}"), GenericName("CoCreateInstance").AddTypeArgumentListArguments(IdentifierName("T")))) .WithArgumentList( ArgumentList().AddArguments( - Argument( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - TypeOfExpression(name), - IdentifierName("GUID"))), + Argument(IdentifierName(clsidPropertyName)), Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)), Argument( MemberAccessExpression( diff --git a/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs b/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs index 59553bc0..1d085800 100644 --- a/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs +++ b/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs @@ -425,6 +425,34 @@ internal static ObjectCreationExpressionSyntax GuidValue(CustomAttribute guidAtt Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(k), k)))); } + internal static ObjectCreationExpressionSyntax GuidValue(Guid guid) + { + byte[] bytes = guid.ToByteArray(); + uint a = BitConverter.ToUInt32(bytes, 0); + ushort b = BitConverter.ToUInt16(bytes, 4); + ushort c = BitConverter.ToUInt16(bytes, 6); + byte d = bytes[8]; + byte e = bytes[9]; + byte f = bytes[10]; + byte g = bytes[11]; + byte h = bytes[12]; + byte i = bytes[13]; + byte j = bytes[14]; + byte k = bytes[15]; + return ObjectCreationExpression(GuidTypeSyntax).AddArgumentListArguments( + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(a), a))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(b), b))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(c), c))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(d), d))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(e), e))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(f), f))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(g), g))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(h), h))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(i), i))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(j), j))), + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(ToHex(k), k)))); + } + internal static ExpressionSyntax IntPtrExpr(IntPtr value) => ObjectCreationExpression(IntPtrTypeSyntax).AddArgumentListArguments( Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(value.ToInt64())))); From bfc3fa159127f89f50287181a905ae17b65a1086 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Thu, 23 Oct 2025 10:39:13 -0700 Subject: [PATCH 6/8] Update to static field --- .../Generator.Com.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 43e3da31..2c0f7898 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -1325,14 +1325,14 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C ? $"COM source generators do not support direct instantiation of co-creatable classes. Use {name.Identifier}.CreateInstance instead." : $"Marshaling is disabled, so direct instantiation of co-creatable classes is not supported. Use {name.Identifier}.CreateInstance instead."; - // Generate a private property for the Guid - // private static Guid CLSID_Foo => new Guid(...); - SyntaxToken clsidPropertyName = Identifier($"CLSID_{name.Identifier}"); - PropertyDeclarationSyntax clsidProperty = PropertyDeclaration(GuidTypeSyntax.WithTrailingTrivia(Space), clsidPropertyName) - .AddModifiers(TokenWithSpace(SyntaxKind.PrivateKeyword), TokenWithSpace(SyntaxKind.StaticKeyword)) - .WithExpressionBody(ArrowExpressionClause(GuidValue(guid))) - .WithSemicolonToken(SemicolonWithLineFeed); - result = result.AddMembers(clsidProperty); + // Generate a private readonly field for the Guid + // private static readonly Guid CLSID_Foo = new Guid(...); + SyntaxToken clsidFieldName = Identifier($"CLSID_{name.Identifier}"); + FieldDeclarationSyntax clsidField = FieldDeclaration( + VariableDeclaration(IdentifierName(nameof(Guid))) + .AddVariables(VariableDeclarator(clsidFieldName).WithInitializer(EqualsValueClause(GuidValue(guid))))) + .AddModifiers(TokenWithSpace(SyntaxKind.PrivateKeyword), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.ReadOnlyKeyword)); + result = result.AddMembers(clsidField); // If using source generators or marshalling is disabled, generate a constructor with obsolete attribute like this: // [Obsolete("COM source generators do not support direct instantiation of co-creatable classes. Use CreateInstance method instead.")] @@ -1362,7 +1362,7 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C // Then add the CreateInstance method: // public static T CreateInstance() where T : class // { - // PInvoke.CoCreateInstance(typeof(Foo).GUID, null, CLSCTX.CLSCTX_SERVER, out T ret).ThrowOnFailure(); + // PInvoke.CoCreateInstance(CLSID_Foo, null, CLSCTX.CLSCTX_SERVER, out T ret).ThrowOnFailure(); // return ret; // } TypeParameterSyntax typeParameter = TypeParameter(Identifier("T")); @@ -1378,7 +1378,7 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C InvocationExpression(QualifiedName(ParseName($"{this.Win32NamespacePrefix}.{this.options.ClassName}"), GenericName("CoCreateInstance").AddTypeArgumentListArguments(IdentifierName("T")))) .WithArgumentList( ArgumentList().AddArguments( - Argument(IdentifierName(clsidPropertyName)), + Argument(IdentifierName(clsidFieldName)), Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)), Argument( MemberAccessExpression( @@ -1394,7 +1394,7 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C // Then add a CreateInstance method that looks like this: // public static HRESULT CreateInstance(out T* instance) where T : unmanaged // { - // return PInvoke.CoCreateInstance(typeof(Foo).GUID, null, CLSCTX.CLSCTX_SERVER, out T* ret); + // return PInvoke.CoCreateInstance(CLSID_Foo, null, CLSCTX.CLSCTX_SERVER, out instance); // } TypeParameterSyntax typeParameter = TypeParameter(Identifier("T")); GenericNameSyntax genericName = GenericName("CreateInstance").AddTypeArgumentListArguments(IdentifierName("T")); @@ -1414,7 +1414,7 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C InvocationExpression(QualifiedName(ParseName($"{this.Win32NamespacePrefix}.{this.options.ClassName}"), GenericName("CoCreateInstance").AddTypeArgumentListArguments(IdentifierName("T")))) .WithArgumentList( ArgumentList().AddArguments( - Argument(IdentifierName(clsidPropertyName)), + Argument(IdentifierName(clsidFieldName)), Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)), Argument( MemberAccessExpression( From eaa2a01df6672cfd9b2a8c8a9ce4706c3345da6c Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Thu, 23 Oct 2025 15:07:22 -0700 Subject: [PATCH 7/8] Add regression test --- test/Microsoft.Windows.CsWin32.Tests/COMTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index 30bca437..3b9365fc 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -488,10 +488,10 @@ public void IUnknown_Derived_QueryInterfaceGenericHelper() m => m.Parent is StructDeclarationSyntax { Identifier.Text: "ITypeLib" } && m.TypeParameterList?.Parameters.Count == 1); } - [Fact] - public void TestGenerateCoCreateableClass() + [Theory, PairwiseData] + public void TestGenerateCoCreateableClass(bool useIntPtrForComOutPtr) { - this.generator = this.CreateGenerator(new GeneratorOptions { AllowMarshaling = false }); + this.generator = this.CreateGenerator(new GeneratorOptions { AllowMarshaling = false, ComInterop = new GeneratorOptions.ComInteropOptions { UseIntPtrForComOutPointers = useIntPtrForComOutPtr } }); this.GenerateApi("ShellLink"); From a22dabbb25c6098a7ff385ba85c58aa03b5ef410 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Thu, 23 Oct 2025 15:14:58 -0700 Subject: [PATCH 8/8] Handle UseIntPtrForComOutrPtr mode. --- src/Microsoft.Windows.CsWin32/Generator.Com.cs | 2 +- test/Microsoft.Windows.CsWin32.Tests/COMTests.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 2c0f7898..f87a7f05 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -1319,7 +1319,7 @@ private ClassDeclarationSyntax DeclareCocreatableClass(TypeDefinition typeDef, C .WithModifiers(classModifiers) .AddAttributeLists(AttributeList().AddAttributes(GUID(guid)).AddAttributes(canUseComImport ? [ComImportAttributeSyntax] : [])); - if (!canUseComImport) + if (!canUseComImport && !this.Options.ComInterop.UseIntPtrForComOutPointers) { string obsoleteMessage = context.AllowMarshaling ? $"COM source generators do not support direct instantiation of co-creatable classes. Use {name.Identifier}.CreateInstance instead." diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index 3b9365fc..3218191b 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -500,7 +500,10 @@ public void TestGenerateCoCreateableClass(bool useIntPtrForComOutPtr) // Check that it does not have the ComImport attribute. Assert.DoesNotContain(shellLinkType.AttributeLists, al => al.Attributes.Any(attr => attr.Name.ToString().Contains("ComImport"))); - // Check that it contains a CreateInstance method - Assert.Contains(shellLinkType.DescendantNodes().OfType(), method => method.Identifier.Text == "CreateInstance"); + if (!useIntPtrForComOutPtr) + { + // Check that it contains a CreateInstance method + Assert.Contains(shellLinkType.DescendantNodes().OfType(), method => method.Identifier.Text == "CreateInstance"); + } } }