From a67e44850e6d581cf48c4308161a728556105f9e Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 17 Mar 2021 08:40:18 -0600 Subject: [PATCH] Add `SupportedOSPlatformAttribute` to generated code Extern methods and COM interfaces-as-structs get it on net5.0+. Genuine COM interfaces don't get it before net6.0 because the attribute forbids application on interfaces. Closes #40 --- src/Microsoft.Windows.CsWin32/Generator.cs | 56 +++++++++++++++-- .../GenerationSandbox.Tests.csproj | 2 +- .../GeneratorTests.cs | 62 ++++++++++++++++--- test/SpellChecker/SpellChecker.csproj | 2 +- 4 files changed, 105 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index 25ff5e49..31a2544d 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -100,6 +100,7 @@ public class Generator : IDisposable private static readonly TypeSyntax SafeHandleTypeSyntax = IdentifierName("SafeHandle"); private static readonly IdentifierNameSyntax IntPtrTypeSyntax = IdentifierName(nameof(IntPtr)); private static readonly AttributeSyntax PreserveSigAttribute = Attribute(IdentifierName("PreserveSig")); + private static readonly AttributeSyntax SupportedOSPlatformAttribute = Attribute(IdentifierName("SupportedOSPlatform")); private static readonly AttributeListSyntax DefaultDllImportSearchPathsAttributeList = AttributeList().AddAttributes( Attribute(IdentifierName("DefaultDllImportSearchPaths")).AddArgumentListArguments(AttributeArgument(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IdentifierName(nameof(DllImportSearchPath)), IdentifierName(nameof(DllImportSearchPath.System32)))))); @@ -320,6 +321,8 @@ public class Generator : IDisposable private readonly CSharpCompilation? compilation; private readonly CSharpParseOptions? parseOptions; private readonly bool canCallCreateSpan; + private readonly bool generateSupportedOSPlatformAttributes; + private readonly bool generateSupportedOSPlatformAttributesOnInterfaces; // only supported on net6.0 (https://github.com/dotnet/runtime/pull/48838) /// /// Initializes a new instance of the class. @@ -336,6 +339,13 @@ public Generator(Stream metadataLibraryStream, GeneratorOptions? options = null, this.parseOptions = parseOptions; this.canCallCreateSpan = this.compilation?.GetTypeByMetadataName(typeof(MemoryMarshal).FullName)?.GetMembers("CreateSpan").Any() is true; + if (this.compilation?.GetTypeByMetadataName("System.Runtime.Versioning.SupportedOSPlatformAttribute") is { } attribute) + { + this.generateSupportedOSPlatformAttributes = true; + AttributeData usageAttribute = attribute.GetAttributes().Single(att => att.AttributeClass?.Name == nameof(AttributeUsageAttribute)); + var targets = (AttributeTargets)usageAttribute.ConstructorArguments[0].Value!; + this.generateSupportedOSPlatformAttributesOnInterfaces = (targets & AttributeTargets.Interface) == AttributeTargets.Interface; + } if (options.AllowMarshaling) { @@ -843,16 +853,25 @@ public IEnumerable GetSuggestions(string name) } } + var usingDirectives = new List + { + UsingDirective(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName(nameof(System)))), + UsingDirective(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName(nameof(System) + "." + nameof(System.Diagnostics)))), + UsingDirective(ParseName(GlobalNamespacePrefix + SystemRuntimeCompilerServices)), + UsingDirective(ParseName(GlobalNamespacePrefix + SystemRuntimeInteropServices)), + }; + + if (this.generateSupportedOSPlatformAttributes) + { + usingDirectives.Add(UsingDirective(ParseName(GlobalNamespacePrefix + "System.Runtime.Versioning"))); + } + var normalizedResults = new Dictionary(StringComparer.OrdinalIgnoreCase); results.AsParallel().WithCancellation(cancellationToken).ForAll(kv => { var compilationUnit = CompilationUnit() .AddMembers( - kv.Value.AddUsings( - UsingDirective(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName(nameof(System)))), - UsingDirective(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName(nameof(System) + "." + nameof(System.Diagnostics)))), - UsingDirective(ParseName(GlobalNamespacePrefix + SystemRuntimeCompilerServices)), - UsingDirective(ParseName(GlobalNamespacePrefix + SystemRuntimeInteropServices)))) + kv.Value.AddUsings(usingDirectives.ToArray())) .WithLeadingTrivia(ParseLeadingTrivia(AutoGeneratedHeader).Add( Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true).AddErrorCodes(WarningsToSuppressInGeneratedCode.Select(code => IdentifierName(code)).ToArray())))) .NormalizeWhitespace(); @@ -2197,6 +2216,11 @@ private void DeclareExternMethod(MethodDefinitionHandle methodDefinitionHandle) Token(SyntaxKind.SemicolonToken)); methodDeclaration = returnType.AddReturnMarshalAs(methodDeclaration); + if (this.GetSupportedOSPlatformAttribute(methodDefinition.GetCustomAttributes()) is AttributeSyntax supportedOSPlatformAttribute) + { + methodDeclaration = methodDeclaration.AddAttributeLists(AttributeList().AddAttributes(supportedOSPlatformAttribute)); + } + // Add documentation if we can find it. methodDeclaration = AddApiDocumentation(entrypoint ?? methodName, methodDeclaration); @@ -2216,6 +2240,18 @@ private void DeclareExternMethod(MethodDefinitionHandle methodDefinitionHandle) } } + private AttributeSyntax? GetSupportedOSPlatformAttribute(CustomAttributeHandleCollection attributes) + { + AttributeSyntax? supportedOSPlatformAttribute = null; + if (this.generateSupportedOSPlatformAttributes && this.FindInteropDecorativeAttribute(attributes, "SupportedOSPlatformAttribute") is CustomAttribute templateOSPlatformAttribute) + { + CustomAttributeValue args = templateOSPlatformAttribute.DecodeValue(CustomAttributeTypeProvider.Instance); + supportedOSPlatformAttribute = SupportedOSPlatformAttribute.AddArgumentListArguments(AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal((string)args.FixedArguments[0].Value!)))); + } + + return supportedOSPlatformAttribute; + } + private FieldDeclarationSyntax DeclareField(FieldDefinitionHandle fieldDefHandle) { FieldDefinition fieldDef = this.mr.GetFieldDefinition(fieldDefHandle); @@ -2443,6 +2479,11 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinition typeDef, I iface = iface.AddAttributeLists(AttributeList().AddAttributes(GUID(guid))); } + if (this.GetSupportedOSPlatformAttribute(typeDef.GetCustomAttributes()) is AttributeSyntax supportedOSPlatformAttribute) + { + iface = iface.AddAttributeLists(AttributeList().AddAttributes(supportedOSPlatformAttribute)); + } + return iface; } @@ -2594,6 +2635,11 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinition typeDef, I .AddBaseListTypes(baseTypeSyntaxList.ToArray()); } + if (this.generateSupportedOSPlatformAttributesOnInterfaces && this.GetSupportedOSPlatformAttribute(typeDef.GetCustomAttributes()) is AttributeSyntax supportedOSPlatformAttribute) + { + ifaceDeclaration = ifaceDeclaration.AddAttributeLists(AttributeList().AddAttributes(supportedOSPlatformAttribute)); + } + return ifaceDeclaration; } diff --git a/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj b/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj index 392ee595..f0f7255b 100644 --- a/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj +++ b/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj @@ -2,7 +2,7 @@ - net5.0;netcoreapp3.1;net472 + net5.0-windows7.0;netcoreapp3.1;net472 diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index ca716268..50d6a68f 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -4,12 +4,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -28,7 +26,7 @@ public class GeneratorTests : IDisposable, IAsyncLifetime private readonly ITestOutputHelper logger; private readonly FileStream metadataStream; private CSharpCompilation compilation; - private CSharpCompilation fastSpanCompilation; + private CSharpCompilation net50Compilation; private CSharpParseOptions parseOptions; private Generator? generator; @@ -43,7 +41,7 @@ public GeneratorTests(ITestOutputHelper logger) // set in InitializeAsync this.compilation = null!; - this.fastSpanCompilation = null!; + this.net50Compilation = null!; } public async Task InitializeAsync() @@ -52,8 +50,8 @@ public async Task InitializeAsync() ReferenceAssemblies.NetStandard.NetStandard20 .AddPackages(ImmutableArray.Create(new PackageIdentity("System.Memory", "4.5.4")))); - this.fastSpanCompilation = await this.CreateCompilationAsync( - ReferenceAssemblies.NetStandard.NetStandard21); + this.net50Compilation = await this.CreateCompilationAsync( + ReferenceAssemblies.Net.Net50); } public Task DisposeAsync() => Task.CompletedTask; @@ -76,13 +74,55 @@ public void TryGetEnumName(string candidate, string? declaringEnum) Assert.Equal(declaringEnum, actualDeclaringEnum); } - [Fact] - public void SimplestMethod() + [Theory, PairwiseData] + public void SimplestMethod(bool net50) { + if (net50) + { + this.compilation = this.net50Compilation; + } + this.generator = new Generator(this.metadataStream, DefaultTestGeneratorOptions, this.compilation, this.parseOptions); - Assert.True(this.generator.TryGenerateExternMethod("GetTickCount")); + const string methodName = "GetTickCount"; + Assert.True(this.generator.TryGenerateExternMethod(methodName)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + + var generatedMethod = this.FindGeneratedMethod(methodName).Single(); + if (net50) + { + Assert.Contains(generatedMethod.AttributeLists, this.IsAttributePresent); + } + else + { + Assert.DoesNotContain(generatedMethod.AttributeLists, this.IsAttributePresent); + } + } + + [Theory, PairwiseData] + public void COMInterfaceWithSupportedOSPlatform(bool net50, bool allowMarshaling) + { + if (net50) + { + this.compilation = this.net50Compilation; + } + + const string typeName = "IInkCursors"; + this.generator = new Generator(this.metadataStream, DefaultTestGeneratorOptions with { AllowMarshaling = allowMarshaling }, this.compilation, this.parseOptions); + Assert.True(this.generator.TryGenerateType(typeName)); this.CollectGeneratedCode(this.generator); this.AssertNoDiagnostics(); + + var iface = this.FindGeneratedType(typeName).Single(); + + if (net50 && !allowMarshaling) + { + Assert.Contains(iface.AttributeLists, this.IsAttributePresent); + } + else + { + Assert.DoesNotContain(iface.AttributeLists, this.IsAttributePresent); + } } [Theory] @@ -597,7 +637,7 @@ internal static partial class InlineArrayIndexerExtensions } "; - this.compilation = this.fastSpanCompilation; + this.compilation = this.net50Compilation; this.AssertGeneratedType("MainAVIHeader", expected, expectedIndexer); } @@ -754,6 +794,8 @@ private void AssertGeneratedType(string apiName, string expectedSyntax, string? } } + private bool IsAttributePresent(AttributeListSyntax al) => al.Attributes.Any(a => a.Name.ToString() == "SupportedOSPlatform"); + private async Task CreateCompilationAsync(ReferenceAssemblies references) { ImmutableArray metadataReferences = await references diff --git a/test/SpellChecker/SpellChecker.csproj b/test/SpellChecker/SpellChecker.csproj index 8b4466ef..4dd4cfbb 100644 --- a/test/SpellChecker/SpellChecker.csproj +++ b/test/SpellChecker/SpellChecker.csproj @@ -3,7 +3,7 @@ Exe - net5.0;net472 + net5.0-windows7.0;net472