From 4c2a7ad8377e496899a0fb03b46eca3f6b3935a8 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 9 Dec 2022 13:14:22 -0700 Subject: [PATCH 1/9] Fill in the rest of the C# reserved keywords --- .../SimpleSyntaxFactory.cs | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs b/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs index 0366ffff..418e5c8f 100644 --- a/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs +++ b/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs @@ -5,27 +5,91 @@ namespace Microsoft.Windows.CsWin32; internal static class SimpleSyntaxFactory { + /// + /// C# keywords that must be escaped or changed when they appear as identifiers from metadata. + /// + /// + /// This list comes from this documentation. + /// internal static readonly HashSet CSharpKeywords = new HashSet(StringComparer.Ordinal) { + "abstract", "as", "base", + "bool", + "break", + "byte", + "case", + "catch", + "char", "checked", + "class", + "const", + "continue", "decimal", + "default", + "delegate", + "do", + "double", + "else", + "enum", "event", + "explicit", + "extern", + "false", + "finally", + "fixed", + "float", + "for", + "foreach", + "goto", + "if", + "implicit", "in", - "is", + "int", + "interface", "internal", + "is", "lock", + "long", + "namespace", + "new", + "null", "object", + "operator", "out", "override", "params", "private", "protected", "public", + "readonly", "ref", + "return", + "sbyte", + "sealed", + "short", + "sizeof", + "stackalloc", + "static", "string", + "struct", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "uint", + "ulong", + "unchecked", + "unsafe", + "ushort", + "using", "virtual", + "void", + "volatile", + "while", }; internal static readonly XmlTextSyntax DocCommentStart = XmlText(" ").WithLeadingTrivia(DocumentationCommentExterior("///")); From 2acd20aa1cc9a4961a04c5c84ce8870d5135e1d9 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 9 Dec 2022 13:41:29 -0700 Subject: [PATCH 2/9] Avoid building CCW for non-COM compliant interfaces --- src/Microsoft.Windows.CsWin32/Generator.Com.cs | 2 +- test/Microsoft.Windows.CsWin32.Tests/COMTests.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 9584f577..5852b562 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -99,7 +99,7 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinitionHandle type var vtblMembers = new List(); TypeSyntaxSettings typeSettings = this.comSignatureTypeSettings; IdentifierNameSyntax pThisLocal = IdentifierName("pThis"); - ParameterSyntax? ccwThisParameter = this.canUseUnmanagedCallersOnlyAttribute && !this.options.AllowMarshaling && originalIfaceName != "IUnknown" && originalIfaceName != "IDispatch" ? Parameter(pThisLocal.Identifier).WithType(PointerType(ifaceName).WithTrailingTrivia(Space)) : null; + ParameterSyntax? ccwThisParameter = this.canUseUnmanagedCallersOnlyAttribute && !this.options.AllowMarshaling && originalIfaceName != "IUnknown" && originalIfaceName != "IDispatch" && !this.IsNonCOMInterface(typeDef) ? Parameter(pThisLocal.Identifier).WithType(PointerType(ifaceName).WithTrailingTrivia(Space)) : null; List ccwMethodsToSkip = new(); IdentifierNameSyntax vtblParamName = IdentifierName("vtable"); BlockSyntax populateVTableBody = Block(); diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index dcd21371..cb6f7456 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -240,6 +240,19 @@ public void MethodWithHRParameter() this.AssertNoDiagnostics(); } + /// + /// A non-COM compliant interface (since it doesn't derive from IUnknown). + /// + [Fact] + public void IVssCreateWriterMetadata() + { + this.compilation = this.starterCompilations["net6.0"]; + this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false }); + Assert.True(this.generator.TryGenerate("IVssCreateWriterMetadata", CancellationToken.None)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + } + [Fact] public void ComOutPtrTypedAsOutObject() { From b9146bcf2c953d3a8bedac39be9e4ce25cce9383 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 9 Dec 2022 14:01:49 -0700 Subject: [PATCH 3/9] Fix tests when emitted code requires System.Drawing.Point --- .../GeneratorTestBase.cs | 4 ++-- test/Microsoft.Windows.CsWin32.Tests/StructTests.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs index 23f3cbcb..5a2ff10d 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs @@ -363,8 +363,8 @@ protected static class MyReferenceAssemblies internal static class NetFramework { - internal static readonly ReferenceAssemblies Net35 = ReferenceAssemblies.NetFramework.Net35.Default.AddPackages(AdditionalLegacyPackages); - internal static readonly ReferenceAssemblies Net472 = ReferenceAssemblies.NetFramework.Net472.Default.AddPackages(AdditionalModernPackages); + internal static readonly ReferenceAssemblies Net35 = ReferenceAssemblies.NetFramework.Net35.WindowsForms.AddPackages(AdditionalLegacyPackages); + internal static readonly ReferenceAssemblies Net472 = ReferenceAssemblies.NetFramework.Net472.WindowsForms.AddPackages(AdditionalModernPackages); } internal static class Net diff --git a/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs b/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs index e49bcef6..20c8869d 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs @@ -34,6 +34,17 @@ public void StructsArePartial(string structName) Assert.True(structDecl.Modifiers.Any(SyntaxKind.PartialKeyword)); } + [Theory] + [MemberData(nameof(TFMData))] + public void PointReferencingStruct(string tfm) + { + this.compilation = this.starterCompilations[tfm]; + this.generator = this.CreateGenerator(); + Assert.True(this.generator.TryGenerate("INPUTCONTEXT", CancellationToken.None)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + } + [Fact] public void CollidingStructNotGenerated() { From 31800e92c96d0020c5dbef36f05988c5973ab2d9 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 9 Dec 2022 14:07:04 -0700 Subject: [PATCH 4/9] Fix IInspectable-derived CCW generation --- src/Microsoft.Windows.CsWin32/Generator.Com.cs | 2 +- test/Microsoft.Windows.CsWin32.Tests/COMTests.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 5852b562..d55141b5 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -119,7 +119,7 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinitionHandle type // We do *not* emit CCW methods for IUnknown, because those are provided by ComWrappers. if (ccwThisParameter is not null && - (qualifiedBaseType.Reader.StringComparer.Equals(baseType.Name, "IUnknown") || qualifiedBaseType.Reader.StringComparer.Equals(baseType.Name, "IDispatch"))) + (qualifiedBaseType.Reader.StringComparer.Equals(baseType.Name, "IUnknown") || qualifiedBaseType.Reader.StringComparer.Equals(baseType.Name, "IDispatch") || qualifiedBaseType.Reader.StringComparer.Equals(baseType.Name, "IInspectable"))) { ccwMethodsToSkip.AddRange(methodsThisType); } diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index cb6f7456..b6c03d72 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -253,6 +253,19 @@ public void IVssCreateWriterMetadata() this.AssertNoDiagnostics(); } + /// + /// An IInspectable-derived interface. + /// + [Fact] + public void IProtectionPolicyManagerInterop3() + { + this.compilation = this.starterCompilations["net6.0"]; + this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false }); + Assert.True(this.generator.TryGenerate("IProtectionPolicyManagerInterop3", CancellationToken.None)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + } + [Fact] public void ComOutPtrTypedAsOutObject() { From e6a44c22473c3fb557ef5aaf15ba3968c6fed3b6 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 9 Dec 2022 14:12:55 -0700 Subject: [PATCH 5/9] Log total diagnostics count before listing them --- test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs index 5a2ff10d..b20695b2 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs @@ -191,6 +191,7 @@ protected CSharpCompilation AddGeneratedCode(CSharpCompilation compilation, Gene protected void AssertNoDiagnostics(CSharpCompilation compilation, bool logAllGeneratedCode = true) { var diagnostics = FilterDiagnostics(compilation.GetDiagnostics()); + this.logger.WriteLine($"{diagnostics.Length} diagnostics reported."); this.LogDiagnostics(diagnostics); var emitDiagnostics = ImmutableArray.Empty; From 6ff54c71faf55879ebf2bb37de1d9f6a1ff322b6 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 9 Dec 2022 14:52:33 -0700 Subject: [PATCH 6/9] Avoid CCW generation for interfaces that require marshaling --- src/Microsoft.Windows.CsWin32/Generator.Com.cs | 17 +++++++++++++++++ .../Microsoft.Windows.CsWin32.Tests/COMTests.cs | 13 +++++++++++++ .../GeneratorTestBase.cs | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index d55141b5..1324c5f7 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -101,6 +101,7 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinitionHandle type IdentifierNameSyntax pThisLocal = IdentifierName("pThis"); ParameterSyntax? ccwThisParameter = this.canUseUnmanagedCallersOnlyAttribute && !this.options.AllowMarshaling && originalIfaceName != "IUnknown" && originalIfaceName != "IDispatch" && !this.IsNonCOMInterface(typeDef) ? Parameter(pThisLocal.Identifier).WithType(PointerType(ifaceName).WithTrailingTrivia(Space)) : null; List ccwMethodsToSkip = new(); + List ccwEntrypointMethods = new(); IdentifierNameSyntax vtblParamName = IdentifierName("vtable"); BlockSyntax populateVTableBody = Block(); IdentifierNameSyntax objectLocal = IdentifierName("__object"); @@ -147,6 +148,7 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinitionHandle type ParameterListSyntax parameterList = methodDefinition.Generator.CreateParameterList(methodDefinition.Method, signature, typeSettings); ParameterListSyntax parameterListPreserveSig = parameterList; // preserve a copy that has no mutations. + bool requiresMarshaling = parameterList.Parameters.Any(p => p.AttributeLists.Count > 0 || p.Modifiers.Any(SyntaxKind.RefKeyword) || p.Modifiers.Any(SyntaxKind.OutKeyword) || p.Modifiers.Any(SyntaxKind.InKeyword)); FunctionPointerParameterListSyntax funcPtrParameters = FunctionPointerParameterList() .AddParameters(FunctionPointerParameter(PointerType(ifaceName))) .AddParameters(parameterList.Parameters.Select(p => FunctionPointerParameter(p.Type!).WithModifiers(p.Modifiers)).ToArray()) @@ -385,6 +387,20 @@ void AddCcwThunk(params StatementSyntax[] thunkInvokeAndReturn) return; } + if (requiresMarshaling) + { + // Oops. This method requires marshaling, which isn't supported in a native-callable function. + // Abandon all efforts to add CCW support to this interface. + ccwThisParameter = null; + foreach (MethodDeclarationSyntax ccwEntrypointMethod in ccwEntrypointMethods) + { + members.Remove(ccwEntrypointMethod); + } + + ccwEntrypointMethods.Clear(); + return; + } + this.RequestComHelpers(context); bool hrReturnType = returnTypePreserveSig is QualifiedNameSyntax { Right.Identifier.ValueText: "HRESULT" }; @@ -434,6 +450,7 @@ void AddCcwThunk(params StatementSyntax[] thunkInvokeAndReturn) ccwBody, semicolonToken: default); members.Add(ccwMethod); + ccwEntrypointMethods.Add(ccwMethod); populateVTableBody = populateVTableBody.AddStatements( ExpressionStatement(AssignmentExpression( diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index b6c03d72..43e97a90 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -266,6 +266,19 @@ public void IProtectionPolicyManagerInterop3() this.AssertNoDiagnostics(); } + /// + /// An interface with managed types. + /// + [Fact] + public void ICompositionCapabilitiesInteropFactory() + { + this.compilation = this.starterCompilations["net6.0"]; + this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false }); + Assert.True(this.generator.TryGenerate("ICompositionCapabilitiesInteropFactory", CancellationToken.None)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + } + [Fact] public void ComOutPtrTypedAsOutObject() { diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs index b20695b2..ca62c931 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs @@ -314,7 +314,7 @@ protected async Task CreateCompilationAsync(ReferenceAssembli protected Generator CreateGenerator(string path, GeneratorOptions? options = null, CSharpCompilation? compilation = null, bool includeDocs = false) => new Generator(path, includeDocs ? Docs.Get(ApiDocsPath) : null, options ?? DefaultTestGeneratorOptions, compilation ?? this.compilation, this.parseOptions); - private static ImmutableArray FilterDiagnostics(ImmutableArray diagnostics) => diagnostics.Where(d => d.Severity > DiagnosticSeverity.Hidden).ToImmutableArray(); + private static ImmutableArray FilterDiagnostics(ImmutableArray diagnostics) => diagnostics.Where(d => d.Severity > DiagnosticSeverity.Hidden && d.Descriptor.Id != "CS1701").ToImmutableArray(); private static void AssertConsistentLineEndings(Compilation compilation) { From f467fd54380391e60bcb7e3a3e15533cf056c775 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 9 Dec 2022 17:07:34 -0700 Subject: [PATCH 7/9] Fix IPicture CCW generation --- .../Generator.Com.cs | 71 +++++++++++-------- .../COMTests.cs | 39 ++-------- 2 files changed, 48 insertions(+), 62 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 1324c5f7..31af36d8 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -133,6 +133,8 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinitionHandle type allMethods.Select(qh => qh.Reader.GetMethodDefinition(qh.MethodHandle)), originalIfaceName, allowNonConsecutiveAccessors: true); + ISet? ifaceDeclaredProperties = ccwThisParameter is not null ? this.GetDeclarableProperties(allMethods.Select(qh => qh.Reader.GetMethodDefinition(qh.MethodHandle)), originalIfaceName, allowNonConsecutiveAccessors: false) : null; + foreach (QualifiedMethodDefinitionHandle methodDefHandle in allMethods) { methodCounter++; @@ -176,7 +178,7 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinitionHandle type .AddArguments(Argument(pThisLocal)) .AddArguments(parameterList.Parameters.Select(p => Argument(IdentifierName(p.Identifier.ValueText)).WithRefKindKeyword(p.Modifiers.Count > 0 ? p.Modifiers[0] : default)).ToArray()))); - MemberDeclarationSyntax propertyOrMethod; + MemberDeclarationSyntax? propertyOrMethod; MethodDeclarationSyntax? methodDeclaration = null; // We can declare this method as a property accessor if it represents a property. @@ -214,18 +216,6 @@ StatementSyntax ThrowOnHRFailure(ExpressionSyntax hrExpression) => ExpressionSta resultLocalDeclaration, vtblInvocationStatement, returnStatement)).WithFixedKeyword(TokenWithSpace(SyntaxKind.FixedKeyword))); - - if (ccwThisParameter is not null && !ccwMethodsToSkip.Contains(methodDefHandle)) - { - //// *inputArg = @object.Property; - StatementSyntax propertyGet = ExpressionStatement(AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - PrefixUnaryExpression(SyntaxKind.PointerIndirectionExpression, IdentifierName(parameterListPreserveSig.Parameters.Last().Identifier.ValueText)), - MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, objectLocal, propertyName))); - this.TryGenerateConstantOrThrow("S_OK"); - AddCcwThunk(propertyGet, returnSOK); - } - break; case SyntaxKind.SetAccessorDeclaration: // vtblInvoke(pThis, value).ThrowOnFailure(); @@ -235,18 +225,6 @@ StatementSyntax ThrowOnHRFailure(ExpressionSyntax hrExpression) => ExpressionSta VariableDeclaration(PointerType(ifaceName)).AddVariables( VariableDeclarator(pThisLocal.Identifier).WithInitializer(EqualsValueClause(PrefixUnaryExpression(SyntaxKind.AddressOfExpression, ThisExpression())))), vtblInvocationStatement).WithFixedKeyword(TokenWithSpace(SyntaxKind.FixedKeyword))); - - if (ccwThisParameter is not null && !ccwMethodsToSkip.Contains(methodDefHandle)) - { - //// @object.Property = inputArg; - StatementSyntax propertySet = ExpressionStatement(AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, objectLocal, propertyName), - IdentifierName(parameterListPreserveSig.Parameters.Last().Identifier.ValueText))); - this.TryGenerateConstantOrThrow("S_OK"); - AddCcwThunk(propertySet, returnSOK); - } - break; default: throw new NotSupportedException("Unsupported accessor kind: " + accessorKind); @@ -260,7 +238,7 @@ StatementSyntax ThrowOnHRFailure(ExpressionSyntax hrExpression) => ExpressionSta // Add the accessor to the existing property declaration. PropertyDeclarationSyntax priorDeclaration = (PropertyDeclarationSyntax)members[priorPropertyDeclarationIndex]; members[priorPropertyDeclarationIndex] = priorDeclaration.WithAccessorList(priorDeclaration.AccessorList!.AddAccessors(accessor)); - continue; + propertyOrMethod = null; } else { @@ -361,8 +339,38 @@ StatementSyntax InvokeVtblAndThrow() => ExpressionStatement(InvocationExpression propertyOrMethod = methodDeclaration; members.AddRange(methodDefinition.Generator.DeclareFriendlyOverloads(methodDefinition.Method, methodDeclaration, IdentifierName(ifaceName.Identifier.ValueText), FriendlyOverloadOf.StructMethod, helperMethodsInStruct)); + } - if (ccwThisParameter is not null && !ccwMethodsToSkip.Contains(methodDefHandle)) + if (ccwThisParameter is not null && !ccwMethodsToSkip.Contains(methodDefHandle)) + { + if (this.TryGetPropertyAccessorInfo(methodDefinition.Method, originalIfaceName, out propertyName, out accessorKind, out propertyType) && + ifaceDeclaredProperties!.Contains(propertyName.Identifier.ValueText)) + { + switch (accessorKind) + { + case SyntaxKind.GetAccessorDeclaration: + //// *inputArg = @object.Property; + StatementSyntax propertyGet = ExpressionStatement(AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + PrefixUnaryExpression(SyntaxKind.PointerIndirectionExpression, IdentifierName(parameterListPreserveSig.Parameters.Last().Identifier.ValueText)), + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, objectLocal, propertyName))); + this.TryGenerateConstantOrThrow("S_OK"); + AddCcwThunk(propertyGet, returnSOK); + break; + case SyntaxKind.SetAccessorDeclaration: + //// @object.Property = inputArg; + StatementSyntax propertySet = ExpressionStatement(AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, objectLocal, propertyName), + IdentifierName(parameterListPreserveSig.Parameters.Last().Identifier.ValueText))); + this.TryGenerateConstantOrThrow("S_OK"); + AddCcwThunk(propertySet, returnSOK); + break; + default: + throw new NotSupportedException("Unsupported accessor kind: " + accessorKind); + } + } + else { // Prepare the args for the thunk call. The Interface we thunk into *always* uses PreserveSig, which is super convenient for us. ArgumentListSyntax args = ArgumentList().AddArguments(parameterListPreserveSig.Parameters.Select(p => Argument(IdentifierName(p.Identifier.ValueText))).ToArray()); @@ -459,9 +467,12 @@ void AddCcwThunk(params StatementSyntax[] thunkInvokeAndReturn) PrefixUnaryExpression(SyntaxKind.AddressOfExpression, SafeIdentifierName(methodName))))); } - // Add documentation if we can find it. - propertyOrMethod = this.AddApiDocumentation($"{ifaceName}.{methodName}", propertyOrMethod); - members.Add(propertyOrMethod); + if (propertyOrMethod is not null) + { + // Add documentation if we can find it. + propertyOrMethod = this.AddApiDocumentation($"{ifaceName}.{methodName}", propertyOrMethod); + members.Add(propertyOrMethod); + } } // We expose the vtbl struct to support CCWs. diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index 43e97a90..e767b4fc 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -240,41 +240,16 @@ public void MethodWithHRParameter() this.AssertNoDiagnostics(); } - /// - /// A non-COM compliant interface (since it doesn't derive from IUnknown). - /// - [Fact] - public void IVssCreateWriterMetadata() - { - this.compilation = this.starterCompilations["net6.0"]; - this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false }); - Assert.True(this.generator.TryGenerate("IVssCreateWriterMetadata", CancellationToken.None)); - this.CollectGeneratedCode(this.generator); - this.AssertNoDiagnostics(); - } - - /// - /// An IInspectable-derived interface. - /// - [Fact] - public void IProtectionPolicyManagerInterop3() - { - this.compilation = this.starterCompilations["net6.0"]; - this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false }); - Assert.True(this.generator.TryGenerate("IProtectionPolicyManagerInterop3", CancellationToken.None)); - this.CollectGeneratedCode(this.generator); - this.AssertNoDiagnostics(); - } - - /// - /// An interface with managed types. - /// - [Fact] - public void ICompositionCapabilitiesInteropFactory() + [Theory] + [InlineData("IVssCreateWriterMetadata")] // A non-COM compliant interface (since it doesn't derive from IUnknown). + [InlineData("IProtectionPolicyManagerInterop3")] // An IInspectable-derived interface. + [InlineData("ICompositionCapabilitiesInteropFactory")] // An interface with managed types. + [InlineData("IPicture")] // An interface with properties that cannot be represented as properties. + public void InterestingComInterfaces(string api) { this.compilation = this.starterCompilations["net6.0"]; this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false }); - Assert.True(this.generator.TryGenerate("ICompositionCapabilitiesInteropFactory", CancellationToken.None)); + Assert.True(this.generator.TryGenerate(api, CancellationToken.None)); this.CollectGeneratedCode(this.generator); this.AssertNoDiagnostics(); } From 6094cdfde8d4f8eb2bcf1131cf55dbbae9885b9a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 9 Dec 2022 13:04:52 -0700 Subject: [PATCH 8/9] Fill test hole by generating for net6.0 and net472 as well as netstandard2.0 --- Directory.Packages.props | 2 +- .../FullGenerationTests.cs | 30 +++++++++++++------ .../GeneratorTestBase.cs | 15 ++++++---- .../GeneratorTests.cs | 2 +- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d3c669aa..ecb877a3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,7 +28,7 @@ - + diff --git a/test/Microsoft.Windows.CsWin32.Tests/FullGenerationTests.cs b/test/Microsoft.Windows.CsWin32.Tests/FullGenerationTests.cs index 7c7efa77..0bddf65f 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/FullGenerationTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/FullGenerationTests.cs @@ -20,36 +20,47 @@ public FullGenerationTests(ITestOutputHelper logger) [Trait("TestCategory", "FailsInCloudTest")] // these take ~4GB of memory to run. [Theory, PairwiseData] - public void Everything(MarshalingOptions marshaling, bool useIntPtrForComOutPtr, [CombinatorialMemberData(nameof(AnyCpuArchitectures))] Platform platform) + public void Everything( + MarshalingOptions marshaling, + bool useIntPtrForComOutPtr, + [CombinatorialMemberData(nameof(AnyCpuArchitectures))] Platform platform, + [CombinatorialMemberData(nameof(TFMDataNoNetFx35))] string tfm) { - this.TestHelper(marshaling, useIntPtrForComOutPtr, platform, generator => generator.GenerateAll(CancellationToken.None)); + this.TestHelper(marshaling, useIntPtrForComOutPtr, platform, tfm, generator => generator.GenerateAll(CancellationToken.None)); } [Theory, PairwiseData] - public void InteropTypes(MarshalingOptions marshaling, bool useIntPtrForComOutPtr) + public void InteropTypes( + MarshalingOptions marshaling, + bool useIntPtrForComOutPtr, + [CombinatorialMemberData(nameof(TFMDataNoNetFx35))] string tfm) { - this.TestHelper(marshaling, useIntPtrForComOutPtr, Platform.X64, generator => generator.GenerateAllInteropTypes(CancellationToken.None)); + this.TestHelper(marshaling, useIntPtrForComOutPtr, Platform.X64, tfm, generator => generator.GenerateAllInteropTypes(CancellationToken.None)); } [Fact] public void Constants() { - this.TestHelper(marshaling: MarshalingOptions.FullMarshaling, useIntPtrForComOutPtr: false, Platform.X64, generator => generator.GenerateAllConstants(CancellationToken.None)); + this.TestHelper(marshaling: MarshalingOptions.FullMarshaling, useIntPtrForComOutPtr: false, Platform.X64, DefaultTFM, generator => generator.GenerateAllConstants(CancellationToken.None)); } [Theory, PairwiseData] - public void ExternMethods(MarshalingOptions marshaling, bool useIntPtrForComOutPtr, [CombinatorialMemberData(nameof(SpecificCpuArchitectures))] Platform platform) + public void ExternMethods( + MarshalingOptions marshaling, + bool useIntPtrForComOutPtr, + [CombinatorialMemberData(nameof(SpecificCpuArchitectures))] Platform platform, + [CombinatorialMemberData(nameof(TFMDataNoNetFx35))] string tfm) { - this.TestHelper(marshaling, useIntPtrForComOutPtr, platform, generator => generator.GenerateAllExternMethods(CancellationToken.None)); + this.TestHelper(marshaling, useIntPtrForComOutPtr, platform, tfm, generator => generator.GenerateAllExternMethods(CancellationToken.None)); } [Fact] public void Macros() { - this.TestHelper(marshaling: MarshalingOptions.FullMarshaling, useIntPtrForComOutPtr: false, Platform.X64, generator => generator.GenerateAllMacros(CancellationToken.None)); + this.TestHelper(marshaling: MarshalingOptions.FullMarshaling, useIntPtrForComOutPtr: false, Platform.X64, DefaultTFM, generator => generator.GenerateAllMacros(CancellationToken.None)); } - private void TestHelper(MarshalingOptions marshaling, bool useIntPtrForComOutPtr, Platform platform, Action generationCommands) + private void TestHelper(MarshalingOptions marshaling, bool useIntPtrForComOutPtr, Platform platform, string targetFramework, Action generationCommands) { var generatorOptions = new GeneratorOptions { @@ -57,6 +68,7 @@ private void TestHelper(MarshalingOptions marshaling, bool useIntPtrForComOutPtr UseSafeHandles = marshaling == MarshalingOptions.FullMarshaling, ComInterop = new() { UseIntPtrForComOutPointers = useIntPtrForComOutPtr }, }; + this.compilation = this.starterCompilations[targetFramework]; this.compilation = this.compilation.WithOptions(this.compilation.Options.WithPlatform(platform)); long? lastHeapSize = null; diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs index ca62c931..9218c3e3 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs @@ -3,6 +3,7 @@ public abstract class GeneratorTestBase : IDisposable, IAsyncLifetime { + protected const string DefaultTFM = "netstandard2.0"; protected static readonly GeneratorOptions DefaultTestGeneratorOptions = new GeneratorOptions { EmitSingleFile = true }; protected static readonly string FileSeparator = new string('=', 140); protected static readonly string MetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Windows.Win32.winmd"); @@ -44,12 +45,14 @@ public enum MarshalingOptions new object[] { "net6.0" }, }; - public static IEnumerable TFMDataNoNetFx35 => - new object[][] + public static IEnumerable TFMDataNoNetFx35MemberData => TFMDataNoNetFx35.Select(tfm => new object[] { tfm }).ToArray(); + + public static string[] TFMDataNoNetFx35 => + new string[] { - new object[] { "net472" }, - new object[] { "netstandard2.0" }, - new object[] { "net6.0" }, + "net472", + "netstandard2.0", + "net6.0", }; public static Platform[] SpecificCpuArchitectures => @@ -105,7 +108,7 @@ void AddSymbols(params string[] symbols) } } - this.compilation = this.starterCompilations["netstandard2.0"]; + this.compilation = this.starterCompilations[DefaultTFM]; } public Task DisposeAsync() => Task.CompletedTask; diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index 7d1d5e8a..5db0e1f8 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -975,7 +975,7 @@ public void OpensMetadataForSharedReading() } [Theory] - [MemberData(nameof(TFMDataNoNetFx35))] + [MemberData(nameof(TFMDataNoNetFx35MemberData))] public void MiniDumpWriteDump_AllOptionalPointerParametersAreOptional(string tfm) { // We split on TFMs because the generated code is slightly different depending on TFM. From b0d8b7f03e2c5fc6a65c43bf62bfc7d465fdb14c Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 9 Dec 2022 17:28:44 -0700 Subject: [PATCH 9/9] Fix CCW generation for interfaces with `[Optional]` parameters --- src/Microsoft.Windows.CsWin32/Generator.Com.cs | 2 +- .../GenerationSandbox.Unmarshalled.Tests/GeneratedForm.cs | 8 ++++++++ .../NativeMethods.txt | 5 +++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index 31af36d8..122b4ef4 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -150,7 +150,7 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinitionHandle type ParameterListSyntax parameterList = methodDefinition.Generator.CreateParameterList(methodDefinition.Method, signature, typeSettings); ParameterListSyntax parameterListPreserveSig = parameterList; // preserve a copy that has no mutations. - bool requiresMarshaling = parameterList.Parameters.Any(p => p.AttributeLists.Count > 0 || p.Modifiers.Any(SyntaxKind.RefKeyword) || p.Modifiers.Any(SyntaxKind.OutKeyword) || p.Modifiers.Any(SyntaxKind.InKeyword)); + bool requiresMarshaling = parameterList.Parameters.Any(p => p.AttributeLists.SelectMany(al => al.Attributes).Any(a => a.Name is IdentifierNameSyntax { Identifier.ValueText: "MarshalAs" }) || p.Modifiers.Any(SyntaxKind.RefKeyword) || p.Modifiers.Any(SyntaxKind.OutKeyword) || p.Modifiers.Any(SyntaxKind.InKeyword)); FunctionPointerParameterListSyntax funcPtrParameters = FunctionPointerParameterList() .AddParameters(FunctionPointerParameter(PointerType(ifaceName))) .AddParameters(parameterList.Parameters.Select(p => FunctionPointerParameter(p.Type!).WithModifiers(p.Modifiers)).ToArray()) diff --git a/test/GenerationSandbox.Unmarshalled.Tests/GeneratedForm.cs b/test/GenerationSandbox.Unmarshalled.Tests/GeneratedForm.cs index caeba087..5934a311 100644 --- a/test/GenerationSandbox.Unmarshalled.Tests/GeneratedForm.cs +++ b/test/GenerationSandbox.Unmarshalled.Tests/GeneratedForm.cs @@ -28,6 +28,14 @@ private static unsafe void COMStructsPreserveSig() o.MachineName = bstr; } +#if NET5_0_OR_GREATER + private static unsafe void IStream_GetsCCW() + { + IStream.Vtbl vtbl; + IStream.PopulateVTable(&vtbl); + } +#endif + private static unsafe void IUnknownGetsVtbl() { // WinForms needs the v-table to be declared for these base interfaces. diff --git a/test/GenerationSandbox.Unmarshalled.Tests/NativeMethods.txt b/test/GenerationSandbox.Unmarshalled.Tests/NativeMethods.txt index 0e0b9e49..c59c92ce 100644 --- a/test/GenerationSandbox.Unmarshalled.Tests/NativeMethods.txt +++ b/test/GenerationSandbox.Unmarshalled.Tests/NativeMethods.txt @@ -1,2 +1,3 @@ -IPersistFile -IEventSubscription +IEventSubscription +IPersistFile +IStream