From 68f6fb8db8742c57c2bdbeddc1351b83942261fb Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 31 Aug 2021 13:43:36 -0600 Subject: [PATCH 1/9] Enable creating C# projections based on multiple input winmd's and docs --- .../AnalyzerReleases.Unshipped.md | 3 +- src/Microsoft.Windows.CsWin32/Docs.cs | 43 +++- src/Microsoft.Windows.CsWin32/Generator.cs | 32 +-- .../SourceGenerator.cs | 233 ++++++++++++------ .../build/Microsoft.Windows.CsWin32.props | 14 ++ src/Win32MetaGeneration/Program.cs | 2 +- .../GeneratorTests.cs | 90 +++---- .../Microsoft.Windows.CsWin32.Tests.csproj | 17 +- 8 files changed, 272 insertions(+), 162 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md b/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md index 25203f87..bcacfa87 100644 --- a/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md +++ b/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md @@ -9,4 +9,5 @@ PInvoke001 | Functionality | Warning | SourceGenerator PInvoke002 | Functionality | Warning | SourceGenerator PInvoke003 | Functionality | Warning | SourceGenerator PInvoke004 | Functionality | Warning | SourceGenerator -PInvoke005 | Functionality | Warning | SourceGenerator \ No newline at end of file +PInvoke005 | Functionality | Warning | SourceGenerator +PInvoke006 | Configuration | Warning | SourceGenerator \ No newline at end of file diff --git a/src/Microsoft.Windows.CsWin32/Docs.cs b/src/Microsoft.Windows.CsWin32/Docs.cs index 3d2fdcb6..e07f51eb 100644 --- a/src/Microsoft.Windows.CsWin32/Docs.cs +++ b/src/Microsoft.Windows.CsWin32/Docs.cs @@ -7,11 +7,14 @@ namespace Microsoft.Windows.CsWin32 using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; - using System.Reflection; + using System.Linq; using MessagePack; using Microsoft.Windows.SDK.Win32Docs; - internal class Docs + /// + /// An in-memory representation of API documentation. + /// + public class Docs { private static readonly Dictionary DocsByPath = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -22,7 +25,12 @@ private Docs(Dictionary apisAndDocs) this.apisAndDocs = apisAndDocs; } - internal static Docs Get(string docsPath) + /// + /// Loads docs from a file. + /// + /// The messagepack docs file to read from. + /// An instance of that accesses the documentation in the file specified by . + public static Docs Get(string docsPath) { lock (DocsByPath) { @@ -48,6 +56,35 @@ internal static Docs Get(string docsPath) } } + /// + /// Returns a instance that contains all the merged documentation from a list of docs. + /// + /// The docs to be merged. When API documentation is provided by multiple docs in this list, the first one appearing in this list is taken. + /// An instance that contains all the docs provided. When contains exactly one element, that element is returned. + public static Docs Merge(IReadOnlyList docs) + { + if (docs.Count == 1) + { + // Nothing to merge. + return docs[0]; + } + + Dictionary mergedDocs = new(docs.Sum(d => d.apisAndDocs.Count), StringComparer.OrdinalIgnoreCase); + foreach (Docs doc in docs) + { + foreach (KeyValuePair api in doc.apisAndDocs) + { + // We want a first one wins policy. + if (!mergedDocs.ContainsKey(api.Key)) + { + mergedDocs.Add(api.Key, api.Value); + } + } + } + + return new Docs(mergedDocs); + } + internal bool TryGetApiDocs(string apiName, [NotNullWhen(true)] out ApiDetails? docs) => this.apisAndDocs.TryGetValue(apiName, out docs); } } diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index cb13793c..3e3c15d9 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -286,16 +286,16 @@ public class Generator : IDisposable /// Initializes a new instance of the class. /// /// The path to the winmd metadata to generate APIs from. - /// The path to the API docs file. + /// The API docs to include in the generated code. /// Options that influence the result of generation. /// The compilation that the generated code will be added to. /// The parse options that will be used for the generated code. - public Generator(string metadataLibraryPath, string? apiDocsPath, GeneratorOptions? options = null, CSharpCompilation? compilation = null, CSharpParseOptions? parseOptions = null) + public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions options, CSharpCompilation? compilation = null, CSharpParseOptions? parseOptions = null) { this.MetadataIndex = MetadataIndex.Get(metadataLibraryPath, compilation?.Options.Platform); - this.ApiDocs = apiDocsPath is object ? Docs.Get(apiDocsPath) : null; + this.ApiDocs = docs; - this.options = options ??= new GeneratorOptions(); + this.options = options; this.options.Validate(); this.compilation = compilation; this.parseOptions = parseOptions; @@ -312,11 +312,6 @@ public Generator(string metadataLibraryPath, string? apiDocsPath, GeneratorOptio this.generateSupportedOSPlatformAttributesOnInterfaces = (targets & AttributeTargets.Interface) == AttributeTargets.Interface; } - if (options.AllowMarshaling) - { - this.BannedAPIs.Add("VARIANT", "Use `object` instead of VARIANT when in COM interface mode. VARIANT can only be emitted when emitting COM interfaces as structs."); - } - bool useComInterfaces = options.AllowMarshaling; this.generalTypeSettings = new TypeSyntaxSettings( this, @@ -343,13 +338,16 @@ private enum FriendlyOverloadOf InterfaceMethod, } - internal Dictionary BannedAPIs { get; } = new Dictionary - { - { "GetLastError", "Do not generate GetLastError. Call Marshal.GetLastWin32Error() instead. Learn more from https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.marshal.getlastwin32error" }, - { "OLD_LARGE_INTEGER", "Use the C# long keyword instead." }, - { "LARGE_INTEGER", "Use the C# long keyword instead." }, - { "ULARGE_INTEGER", "Use the C# ulong keyword instead." }, - }; + internal static ImmutableDictionary BannedAPIsWithoutMarshaling { get; } = ImmutableDictionary.Empty + .Add("GetLastError", "Do not generate GetLastError. Call Marshal.GetLastWin32Error() instead. Learn more from https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.marshal.getlastwin32error") + .Add("OLD_LARGE_INTEGER", "Use the C# long keyword instead.") + .Add("LARGE_INTEGER", "Use the C# long keyword instead.") + .Add("ULARGE_INTEGER", "Use the C# ulong keyword instead."); + + internal static ImmutableDictionary BannedAPIsWithMarshaling { get; } = BannedAPIsWithoutMarshaling + .Add("VARIANT", "Use `object` instead of VARIANT when in COM interface mode. VARIANT can only be emitted when emitting COM interfaces as structs."); + + internal ImmutableDictionary BannedAPIs => GetBannedAPIs(this.options); internal MetadataIndex MetadataIndex { get; } @@ -970,6 +968,8 @@ nsContents.Key is object return normalizedResults; } + internal static ImmutableDictionary GetBannedAPIs(GeneratorOptions options) => options.AllowMarshaling ? BannedAPIsWithMarshaling : BannedAPIsWithoutMarshaling; + [return: NotNullIfNotNull("marshalAs")] internal static AttributeSyntax? MarshalAs(MarshalAsAttribute? marshalAs) { diff --git a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs index 1d2d0407..9f03ed94 100644 --- a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs @@ -90,6 +90,14 @@ public class SourceGenerator : ISourceGenerator DiagnosticSeverity.Warning, isEnabledByDefault: true); + private static readonly DiagnosticDescriptor DocParsingError = new DiagnosticDescriptor( + "PInvoke006", + "DocsParseError", + "An error occurred while reading docs file: \"{0}\": {1}", + "Configuration", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + /// public void Initialize(GeneratorInitializationContext context) { @@ -98,20 +106,12 @@ public void Initialize(GeneratorInitializationContext context) /// public void Execute(GeneratorExecutionContext context) { - if (!(context.Compilation is CSharpCompilation)) + if (context.Compilation is not CSharpCompilation compilation) { return; } - if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.MicrosoftWindowsSdkWin32MetadataBasePath", out string? metadataBasePath) || - string.IsNullOrWhiteSpace(metadataBasePath)) - { - return; - } - - context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.MicrosoftWindowsSdkApiDocsPath", out string? apiDocsPath); - - GeneratorOptions? options = null; + GeneratorOptions options; AdditionalText? nativeMethodsJsonFile = context.AdditionalFiles .FirstOrDefault(af => string.Equals(Path.GetFileName(af.Path), NativeMethodsJsonAdditionalFileName, StringComparison.OrdinalIgnoreCase)); if (nativeMethodsJsonFile is object) @@ -124,6 +124,10 @@ public void Execute(GeneratorExecutionContext context) PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); } + else + { + options = new GeneratorOptions(); + } AdditionalText? nativeMethodsTxtFile = context.AdditionalFiles .FirstOrDefault(af => string.Equals(Path.GetFileName(af.Path), NativeMethodsTxtAdditionalFileName, StringComparison.OrdinalIgnoreCase)); @@ -132,8 +136,6 @@ public void Execute(GeneratorExecutionContext context) return; } - string metadataPath = Path.Combine(metadataBasePath, "Windows.Win32.winmd"); - var compilation = (CSharpCompilation)context.Compilation; var parseOptions = (CSharpParseOptions)context.ParseOptions; if (!compilation.Options.AllowUnsafe) @@ -141,100 +143,142 @@ public void Execute(GeneratorExecutionContext context) context.ReportDiagnostic(Diagnostic.Create(UnsafeCodeRequired, location: null)); } - using var generator = new Generator(metadataPath, apiDocsPath, options, compilation, parseOptions); - - SourceText? nativeMethodsTxt = nativeMethodsTxtFile.GetText(context.CancellationToken); - if (nativeMethodsTxt is null) + Docs? docs = ParseDocs(context); + IReadOnlyList generators = CollectMetadataPaths(context).Select(path => new Generator(path, docs, options, compilation, parseOptions)).ToList(); + try { - return; - } - - foreach (TextLine line in nativeMethodsTxt.Lines) - { - context.CancellationToken.ThrowIfCancellationRequested(); - string name = line.ToString(); - if (string.IsNullOrWhiteSpace(name) || name.StartsWith("//", StringComparison.InvariantCulture)) + SourceText? nativeMethodsTxt = nativeMethodsTxtFile.GetText(context.CancellationToken); + if (nativeMethodsTxt is null) { - continue; + return; } - name = name.Trim(); - var location = Location.Create(nativeMethodsTxtFile.Path, line.Span, nativeMethodsTxt.Lines.GetLinePositionSpan(line.Span)); - try + foreach (TextLine line in nativeMethodsTxt.Lines) { - if (generator.BannedAPIs.TryGetValue(name, out string? reason)) + context.CancellationToken.ThrowIfCancellationRequested(); + string name = line.ToString(); + if (string.IsNullOrWhiteSpace(name) || name.StartsWith("//", StringComparison.InvariantCulture)) { - context.ReportDiagnostic(Diagnostic.Create(BannedApi, location, reason)); + continue; } - else if (name.EndsWith(".*", StringComparison.Ordinal)) + + name = name.Trim(); + var location = Location.Create(nativeMethodsTxtFile.Path, line.Span, nativeMethodsTxt.Lines.GetLinePositionSpan(line.Span)); + try { - var moduleName = name.Substring(0, name.Length - 2); - if (!generator.TryGenerateAllExternMethods(moduleName, context.CancellationToken)) + if (Generator.GetBannedAPIs(options).TryGetValue(name, out string? reason)) { - context.ReportDiagnostic(Diagnostic.Create(NoMethodsForModule, location, moduleName)); + context.ReportDiagnostic(Diagnostic.Create(BannedApi, location, reason)); + continue; } - } - else if (!generator.TryGenerate(name, context.CancellationToken)) - { - if (generator.TryGetEnumName(name, out string? declaringEnum)) + + if (name.EndsWith(".*", StringComparison.Ordinal)) { - context.ReportDiagnostic(Diagnostic.Create(UseEnumValueDeclaringType, location, declaringEnum)); - if (!generator.TryGenerate(declaringEnum, context.CancellationToken)) + var moduleName = name.Substring(0, name.Length - 2); + bool matchModule = false; + foreach (Generator generator in generators) { - ReportNoMatch(location, declaringEnum); + matchModule |= generator.TryGenerateAllExternMethods(moduleName, context.CancellationToken); } + + if (!matchModule) + { + context.ReportDiagnostic(Diagnostic.Create(NoMethodsForModule, location, moduleName)); + } + + continue; } - else + + bool matchApi = false; + foreach (Generator generator in generators) + { + if (generator.TryGenerate(name, context.CancellationToken)) + { + matchApi = true; + continue; + } + + if (generator.TryGetEnumName(name, out string? declaringEnum)) + { + context.ReportDiagnostic(Diagnostic.Create(UseEnumValueDeclaringType, location, declaringEnum)); + if (generator.TryGenerate(declaringEnum, context.CancellationToken)) + { + matchApi = true; + continue; + } + else + { + ReportNoMatch(location, declaringEnum); + } + } + } + + if (!matchApi) { ReportNoMatch(location, name); } } - } - catch (GenerationFailedException ex) - { - if (Generator.IsPlatformCompatibleException(ex)) + catch (GenerationFailedException ex) { - context.ReportDiagnostic(Diagnostic.Create(CpuArchitectureIncompatibility, location)); + if (Generator.IsPlatformCompatibleException(ex)) + { + context.ReportDiagnostic(Diagnostic.Create(CpuArchitectureIncompatibility, location)); + } + else + { + // Build up a complete error message. + context.ReportDiagnostic(Diagnostic.Create(InternalError, location, AssembleFullExceptionMessage(ex))); + } } - else + } + + foreach (Generator generator in generators) + { + var compilationUnits = generator.GetCompilationUnits(context.CancellationToken) + .OrderBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase) + .ThenBy(pair => pair.Key, StringComparer.Ordinal); + foreach (var unit in compilationUnits) { - // Build up a complete error message. - context.ReportDiagnostic(Diagnostic.Create(InternalError, location, AssembleFullExceptionMessage(ex))); + context.AddSource(unit.Key, unit.Value.ToFullString()); } } - } - var compilationUnits = generator.GetCompilationUnits(context.CancellationToken) - .OrderBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase) - .ThenBy(pair => pair.Key, StringComparer.Ordinal); - foreach (var unit in compilationUnits) - { - context.AddSource(unit.Key, unit.Value.ToFullString()); - } - - void ReportNoMatch(Location? location, string failedAttempt) - { - var suggestions = generator.GetSuggestions(failedAttempt).Take(4).ToList(); - if (suggestions.Count > 0) + void ReportNoMatch(Location? location, string failedAttempt) { - var suggestionBuilder = new StringBuilder(); - for (int i = 0; i < suggestions.Count; i++) + List suggestions = new(); + foreach (Generator generator in generators) + { + suggestions.AddRange(generator.GetSuggestions(failedAttempt).Take(4)); + } + + if (suggestions.Count > 0) { - if (i > 0) + var suggestionBuilder = new StringBuilder(); + for (int i = 0; i < suggestions.Count; i++) { - suggestionBuilder.Append(i < suggestions.Count - 1 ? ", " : " or "); + if (i > 0) + { + suggestionBuilder.Append(i < suggestions.Count - 1 ? ", " : " or "); + } + + suggestionBuilder.Append('"'); + suggestionBuilder.Append(suggestions[i]); + suggestionBuilder.Append('"'); } - suggestionBuilder.Append('"'); - suggestionBuilder.Append(suggestions[i]); - suggestionBuilder.Append('"'); + context.ReportDiagnostic(Diagnostic.Create(NoMatchingMethodOrTypeWithSuggestions, location, failedAttempt, suggestionBuilder)); + } + else + { + context.ReportDiagnostic(Diagnostic.Create(NoMatchingMethodOrType, location, failedAttempt)); } - - context.ReportDiagnostic(Diagnostic.Create(NoMatchingMethodOrTypeWithSuggestions, location, failedAttempt, suggestionBuilder)); } - else + } + finally + { + foreach (Generator generator in generators) { - context.ReportDiagnostic(Diagnostic.Create(NoMatchingMethodOrType, location, failedAttempt)); + generator.Dispose(); } } } @@ -258,5 +302,46 @@ private static string AssembleFullExceptionMessage(Exception ex) return sb.ToString(); } + + private static IReadOnlyList CollectMetadataPaths(GeneratorExecutionContext context) + { + if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.CsWin32InputMetadataPaths", out string? delimitedMetadataBasePaths) || + string.IsNullOrWhiteSpace(delimitedMetadataBasePaths)) + { + return Array.Empty(); + } + + string[] metadataBasePaths = delimitedMetadataBasePaths.Split(';'); + return metadataBasePaths; + } + + private static Docs? ParseDocs(GeneratorExecutionContext context) + { + Docs? docs = null; + if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.CsWin32InputDocPaths", out string? delimitedApiDocsPaths) && + !string.IsNullOrWhiteSpace(delimitedApiDocsPaths)) + { + string[] apiDocsPaths = delimitedApiDocsPaths!.Split(';'); + if (apiDocsPaths.Length > 0) + { + List docsList = new(apiDocsPaths.Length); + foreach (string path in apiDocsPaths) + { + try + { + docsList.Add(Docs.Get(path)); + } + catch (Exception e) + { + context.ReportDiagnostic(Diagnostic.Create(DocParsingError, null, path, e.Message)); + } + } + + docs = Docs.Merge(docsList); + } + } + + return docs; + } } } diff --git a/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.props b/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.props index 1a86b066..132fc745 100644 --- a/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.props +++ b/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.props @@ -7,4 +7,18 @@ + + + + + + + + + + + @(ProjectionMetadataWinmd) + @(ProjectionDocs) + + diff --git a/src/Win32MetaGeneration/Program.cs b/src/Win32MetaGeneration/Program.cs index b4918d28..90ea2b88 100644 --- a/src/Win32MetaGeneration/Program.cs +++ b/src/Win32MetaGeneration/Program.cs @@ -47,7 +47,7 @@ private static void Main(string[] args) string apiDocsPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "apidocs.msgpack"); using var generator = new Generator( metadataPath, - apiDocsPath, + Docs.Get(apiDocsPath), new GeneratorOptions { WideCharOnly = true, diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index 50983408..1d5d9601 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -837,14 +838,6 @@ public void ProjectReferenceBetweenTwoGeneratingProjects(bool internalsVisibleTo [Fact] public async Task TestSimpleStructure() { - var basePath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkWin32MetadataBasePath")?.Value; - var docsPath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkApiDocsPath")?.Value; - - var globalconfig = $@"is_global = true - -build_property.MicrosoftWindowsSdkApiDocsPath = {docsPath} -build_property.MicrosoftWindowsSdkWin32MetadataBasePath = {basePath} -"; await new VerifyTest { TestState = @@ -856,7 +849,7 @@ public async Task TestSimpleStructure() }, AnalyzerConfigFiles = { - ("/.globalconfig", globalconfig), + ("/.globalconfig", ConstructGlobalConfigString()), }, GeneratedSources = { @@ -906,14 +899,6 @@ public static implicit operator bool(BOOL value) [Fact] public async Task TestSimpleEnum() { - var basePath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkWin32MetadataBasePath")?.Value; - var docsPath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkApiDocsPath")?.Value; - - var globalconfig = $@"is_global = true - -build_property.MicrosoftWindowsSdkApiDocsPath = {docsPath} -build_property.MicrosoftWindowsSdkWin32MetadataBasePath = {basePath} -"; await new VerifyTest { TestState = @@ -925,7 +910,7 @@ public async Task TestSimpleEnum() }, AnalyzerConfigFiles = { - ("/.globalconfig", globalconfig), + ("/.globalconfig", ConstructGlobalConfigString()), }, GeneratedSources = { @@ -979,12 +964,6 @@ internal enum DISPLAYCONFIG_SCANLINE_ORDERING [Fact] public async Task TestSimpleEnumWithoutDocs() { - var basePath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkWin32MetadataBasePath")?.Value; - - var globalconfig = $@"is_global = true - -build_property.MicrosoftWindowsSdkWin32MetadataBasePath = {basePath} -"; await new VerifyTest { TestState = @@ -996,7 +975,7 @@ public async Task TestSimpleEnumWithoutDocs() }, AnalyzerConfigFiles = { - ("/.globalconfig", globalconfig), + ("/.globalconfig", ConstructGlobalConfigString(omitDocs: true)), }, GeneratedSources = { @@ -1040,14 +1019,6 @@ internal enum DISPLAYCONFIG_SCANLINE_ORDERING [Fact] public async Task TestFlagsEnum() { - var basePath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkWin32MetadataBasePath")?.Value; - var docsPath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkApiDocsPath")?.Value; - - var globalconfig = $@"is_global = true - -build_property.MicrosoftWindowsSdkApiDocsPath = {docsPath} -build_property.MicrosoftWindowsSdkWin32MetadataBasePath = {basePath} -"; await new VerifyTest { TestState = @@ -1059,7 +1030,7 @@ public async Task TestFlagsEnum() }, AnalyzerConfigFiles = { - ("/.globalconfig", globalconfig), + ("/.globalconfig", ConstructGlobalConfigString()), }, GeneratedSources = { @@ -1124,14 +1095,6 @@ internal enum FILE_ACCESS_FLAGS : uint [Fact] public async Task TestSimpleDelegate() { - var basePath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkWin32MetadataBasePath")?.Value; - var docsPath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkApiDocsPath")?.Value; - - var globalconfig = $@"is_global = true - -build_property.MicrosoftWindowsSdkApiDocsPath = {docsPath} -build_property.MicrosoftWindowsSdkWin32MetadataBasePath = {basePath} -"; await new VerifyTest { TestState = @@ -1143,7 +1106,7 @@ public async Task TestSimpleDelegate() }, AnalyzerConfigFiles = { - ("/.globalconfig", globalconfig), + ("/.globalconfig", ConstructGlobalConfigString()), }, GeneratedSources = { @@ -1298,14 +1261,6 @@ internal readonly partial struct LPARAM [Fact] public async Task TestSimpleMethod() { - var basePath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkWin32MetadataBasePath")?.Value; - var docsPath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkApiDocsPath")?.Value; - - var globalconfig = $@"is_global = true - -build_property.MicrosoftWindowsSdkApiDocsPath = {docsPath} -build_property.MicrosoftWindowsSdkWin32MetadataBasePath = {basePath} -"; await new VerifyTest { TestState = @@ -1317,7 +1272,7 @@ public async Task TestSimpleMethod() }, AnalyzerConfigFiles = { - ("/.globalconfig", globalconfig), + ("/.globalconfig", ConstructGlobalConfigString()), }, GeneratedSources = { @@ -1450,14 +1405,6 @@ internal static partial class PInvoke [Fact] public async Task TestMethodWithOverloads() { - var basePath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkWin32MetadataBasePath")?.Value; - var docsPath = this.GetType().Assembly.GetCustomAttributes().SingleOrDefault(metadata => metadata.Key == "MicrosoftWindowsSdkApiDocsPath")?.Value; - - var globalconfig = $@"is_global = true - -build_property.MicrosoftWindowsSdkApiDocsPath = {docsPath} -build_property.MicrosoftWindowsSdkWin32MetadataBasePath = {basePath} -"; await new VerifyTest { TestState = @@ -1469,7 +1416,7 @@ public async Task TestMethodWithOverloads() }, AnalyzerConfigFiles = { - ("/.globalconfig", globalconfig), + ("/.globalconfig", ConstructGlobalConfigString()), }, GeneratedSources = { @@ -2022,6 +1969,25 @@ internal partial struct SECURITY_ATTRIBUTES }.RunAsync(); } + private static string ConstructGlobalConfigString(bool omitDocs = false) + { + StringBuilder globalConfigBuilder = new(); + globalConfigBuilder.AppendLine("is_global = true"); + globalConfigBuilder.AppendLine(); + globalConfigBuilder.AppendLine($"build_property.CsWin32InputMetadataPaths = {JoinAssemblyMetadata("ProjectionMetadataWinmd")}"); + if (!omitDocs) + { + globalConfigBuilder.AppendLine($"build_property.CsWin32InputDocPaths = {JoinAssemblyMetadata("ProjectionDocs")}"); + } + + return globalConfigBuilder.ToString(); + + static string JoinAssemblyMetadata(string name) + { + return string.Join(";", typeof(GeneratorTests).Assembly.GetCustomAttributes().Where(metadata => metadata.Key == name).Select(metadata => metadata.Value)); + } + } + private static ImmutableArray FilterDiagnostics(ImmutableArray diagnostics) => diagnostics.Where(d => d.Severity > DiagnosticSeverity.Hidden).ToImmutableArray(); private static bool IsAttributePresent(AttributeListSyntax al, string attributeName) => al.Attributes.Any(a => a.Name.ToString() == attributeName); @@ -2174,7 +2140,7 @@ private async Task CreateCompilationAsync(ReferenceAssemblies return compilation; } - private Generator CreateGenerator(GeneratorOptions? options = null, CSharpCompilation? compilation = null) => new Generator(MetadataPath, ApiDocsPath, options ?? DefaultTestGeneratorOptions, compilation ?? this.compilation, this.parseOptions); + private Generator CreateGenerator(GeneratorOptions? options = null, CSharpCompilation? compilation = null) => new Generator(MetadataPath, Docs.Get(ApiDocsPath), options ?? DefaultTestGeneratorOptions, compilation ?? this.compilation, this.parseOptions); private static class MyReferenceAssemblies { diff --git a/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj b/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj index 079ea0f5..fb3008d7 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj +++ b/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj @@ -4,17 +4,24 @@ net5.0;netcoreapp3.1;net472 - + - - + <_AssemblyMetadata1 Include="@(ProjectionMetadataWinmd)"> + %(Identity) + + + + <_AssemblyMetadata2 Include="@(ProjectionDocs)"> + %(Identity) + + - + PreserveNewest - + PreserveNewest From 9e99e17362cddcf7431814cca23217fbe8212409 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 1 Sep 2021 18:08:37 -0600 Subject: [PATCH 2/9] Report an error on ambiguous matches across metadata --- .../AnalyzerReleases.Unshipped.md | 3 +- src/Microsoft.Windows.CsWin32/Generator.cs | 67 ++++++++++---- .../SourceGenerator.cs | 92 ++++++++++++------- .../GeneratorTests.cs | 2 +- 4 files changed, 112 insertions(+), 52 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md b/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md index bcacfa87..9612a53b 100644 --- a/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md +++ b/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md @@ -10,4 +10,5 @@ PInvoke002 | Functionality | Warning | SourceGenerator PInvoke003 | Functionality | Warning | SourceGenerator PInvoke004 | Functionality | Warning | SourceGenerator PInvoke005 | Functionality | Warning | SourceGenerator -PInvoke006 | Configuration | Warning | SourceGenerator \ No newline at end of file +PInvoke006 | Configuration | Warning | SourceGenerator +PInvoke007 | Functionality | Error | SourceGenerator \ No newline at end of file diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index 3e3c15d9..98d7398a 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -436,13 +436,17 @@ public void GenerateAll(CancellationToken cancellationToken) this.GenerateAllConstants(cancellationToken); } + /// + public bool TryGenerate(string apiNameOrModuleWildcard, CancellationToken cancellationToken) => this.TryGenerate(apiNameOrModuleWildcard, out _, cancellationToken); + /// /// Generates code for a given API. /// /// The name of the method, struct or constant. Or the name of a module with a ".*" suffix in order to generate all methods and supporting types for the specified module. + /// Receives the canonical API names that matched on. /// A cancellation token. /// if any matching APIs were found and generated; otherwise. - public bool TryGenerate(string apiNameOrModuleWildcard, CancellationToken cancellationToken) + public bool TryGenerate(string apiNameOrModuleWildcard, out IReadOnlyList preciseApi, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(apiNameOrModuleWildcard)) { @@ -451,15 +455,24 @@ public bool TryGenerate(string apiNameOrModuleWildcard, CancellationToken cancel if (apiNameOrModuleWildcard.EndsWith(".*", StringComparison.Ordinal)) { - return this.TryGenerateAllExternMethods(apiNameOrModuleWildcard.Substring(0, apiNameOrModuleWildcard.Length - 2), cancellationToken); + if (this.TryGenerateAllExternMethods(apiNameOrModuleWildcard.Substring(0, apiNameOrModuleWildcard.Length - 2), cancellationToken)) + { + preciseApi = ImmutableList.Create(apiNameOrModuleWildcard); + return true; + } + else + { + preciseApi = ImmutableList.Empty; + return false; + } } else { return - this.TryGenerateNamespace(apiNameOrModuleWildcard) || - this.TryGenerateExternMethod(apiNameOrModuleWildcard) || - this.TryGenerateType(apiNameOrModuleWildcard) || - this.TryGenerateConstant(apiNameOrModuleWildcard); + this.TryGenerateNamespace(apiNameOrModuleWildcard, out preciseApi) || + this.TryGenerateExternMethod(apiNameOrModuleWildcard, out preciseApi) || + this.TryGenerateType(apiNameOrModuleWildcard, out preciseApi) || + this.TryGenerateConstant(apiNameOrModuleWildcard, out preciseApi); } } @@ -467,8 +480,9 @@ public bool TryGenerate(string apiNameOrModuleWildcard, CancellationToken cancel /// Generates all APIs within a given namespace, and their dependencies. /// /// The namespace to generate APIs for. + /// Receives the canonical API names that matched on. /// if a matching namespace was found; otherwise . - public bool TryGenerateNamespace(string @namespace) + public bool TryGenerateNamespace(string @namespace, out IReadOnlyList preciseApi) { if (@namespace is null) { @@ -485,6 +499,7 @@ public bool TryGenerateNamespace(string @namespace) { if (string.Equals(item.Key, @namespace, StringComparison.OrdinalIgnoreCase)) { + @namespace = item.Key; metadata = item.Value; break; } @@ -512,9 +527,11 @@ public bool TryGenerateNamespace(string @namespace) } }); + preciseApi = ImmutableList.Create(@namespace); return true; } + preciseApi = ImmutableList.Empty; return false; } @@ -701,8 +718,9 @@ public bool TryGenerateAllExternMethods(string moduleName, CancellationToken can /// Generate code for the named extern method, if it is recognized. /// /// The name of the extern method, optionally qualified with a namespace. + /// Receives the canonical API names that matched on. /// if a match was found and the extern method generated; otherwise . - public bool TryGenerateExternMethod(string possiblyQualifiedName) + public bool TryGenerateExternMethod(string possiblyQualifiedName, out IReadOnlyList preciseApi) { if (possiblyQualifiedName is null) { @@ -723,18 +741,25 @@ public bool TryGenerateExternMethod(string possiblyQualifiedName) this.RequestExternMethod(methodDefHandle); }); + string methodNamespace = this.Reader.GetString(this.Reader.GetTypeDefinition(methodDef.GetDeclaringType()).Namespace); + preciseApi = ImmutableList.Create($"{methodNamespace}.{methodName}"); return true; } + preciseApi = ImmutableList.Empty; return false; } + /// + public bool TryGenerateType(string possiblyQualifiedName) => this.TryGenerateType(possiblyQualifiedName, out _); + /// /// Generate code for the named type, if it is recognized. /// /// The name of the interop type, optionally qualified with a namespace. + /// Receives the canonical API names that matched on. /// if a match was found and the type generated; otherwise . - public bool TryGenerateType(string possiblyQualifiedName) + public bool TryGenerateType(string possiblyQualifiedName, out IReadOnlyList preciseApi) { if (possiblyQualifiedName is null) { @@ -765,18 +790,19 @@ public bool TryGenerateType(string possiblyQualifiedName) this.RequestInteropType(matchingTypeHandles[0]); }); + TypeDefinition td = this.Reader.GetTypeDefinition(matchingTypeHandles[0]); + preciseApi = ImmutableList.Create($"{this.Reader.GetString(td.Namespace)}.{this.Reader.GetString(td.Name)}"); return true; } else if (matchingTypeHandles.Count > 1) { - string matches = string.Join( - ", ", + preciseApi = ImmutableList.CreateRange( matchingTypeHandles.Select(h => { TypeDefinition td = this.Reader.GetTypeDefinition(h); return $"{this.Reader.GetString(td.Namespace)}.{this.Reader.GetString(td.Name)}"; })); - throw new ArgumentException("The type name is ambiguous. Use the fully-qualified name instead. Possible matches: " + matches); + return false; } if (foundApiWithMismatchedPlatform) @@ -784,6 +810,7 @@ public bool TryGenerateType(string possiblyQualifiedName) throw new PlatformIncompatibleException($"The requested API ({possiblyQualifiedName}) was found but is not available given the target platform ({this.compilation?.Options.Platform})."); } + preciseApi = ImmutableList.Empty; return false; } @@ -791,8 +818,9 @@ public bool TryGenerateType(string possiblyQualifiedName) /// Generate code for the named constant, if it is recognized. /// /// The name of the constant, optionally qualified with a namespace. + /// Receives the canonical API names that matched on. /// if a match was found and the constant generated; otherwise . - public bool TryGenerateConstant(string possiblyQualifiedName) + public bool TryGenerateConstant(string possiblyQualifiedName, out IReadOnlyList preciseApi) { if (possiblyQualifiedName is null) { @@ -818,21 +846,24 @@ public bool TryGenerateConstant(string possiblyQualifiedName) this.RequestConstant(matchingFieldHandles[0]); }); + FieldDefinition fd = this.Reader.GetFieldDefinition(matchingFieldHandles[0]); + TypeDefinition td = this.Reader.GetTypeDefinition(fd.GetDeclaringType()); + preciseApi = ImmutableList.Create($"{this.Reader.GetString(td.Namespace)}.{this.Reader.GetString(fd.Name)}"); return true; } else if (matchingFieldHandles.Count > 1) { - string matches = string.Join( - ", ", + preciseApi = ImmutableList.CreateRange( matchingFieldHandles.Select(h => { FieldDefinition fd = this.Reader.GetFieldDefinition(h); TypeDefinition td = this.Reader.GetTypeDefinition(fd.GetDeclaringType()); return $"{this.Reader.GetString(td.Namespace)}.{this.Reader.GetString(fd.Name)}"; })); - throw new ArgumentException("The type name is ambiguous. Use the fully-qualified name instead. Possible matches: " + matches); + return false; } + preciseApi = ImmutableList.Empty; return false; } @@ -2535,7 +2566,7 @@ private void TryGenerateTypeOrThrow(string possiblyQualifiedName) private void TryGenerateConstantOrThrow(string possiblyQualifiedName) { - if (!this.TryGenerateConstant(possiblyQualifiedName)) + if (!this.TryGenerateConstant(possiblyQualifiedName, out _)) { throw new GenerationFailedException("Unable to find expected constant: " + possiblyQualifiedName); } @@ -3124,7 +3155,7 @@ private StructDeclarationSyntax DeclareTypeDefStruct(TypeDefinition typeDef) if (args.FixedArguments[0].Value is string freeMethodName) { ////this.GenerateSafeHandle(freeMethodName); - this.TryGenerateExternMethod(freeMethodName); + this.TryGenerateExternMethod(freeMethodName, out _); isHandle = true; } diff --git a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs index 9f03ed94..ff7013db 100644 --- a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs @@ -98,6 +98,22 @@ public class SourceGenerator : ISourceGenerator DiagnosticSeverity.Warning, isEnabledByDefault: true); + private static readonly DiagnosticDescriptor AmbiguousMatchError = new DiagnosticDescriptor( + "PInvoke007", + "AmbiguousMatch", + "The API \"{0}\" is ambiguous.", + "Functionality", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor AmbiguousMatchErrorWithSuggestions = new DiagnosticDescriptor( + "PInvoke007", + "AmbiguousMatch", + "The API \"{0}\" is ambiguous. Please specify one of: {1}", + "Functionality", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + /// public void Initialize(GeneratorInitializationContext context) { @@ -175,47 +191,54 @@ public void Execute(GeneratorExecutionContext context) if (name.EndsWith(".*", StringComparison.Ordinal)) { var moduleName = name.Substring(0, name.Length - 2); - bool matchModule = false; + int matches = 0; foreach (Generator generator in generators) { - matchModule |= generator.TryGenerateAllExternMethods(moduleName, context.CancellationToken); + if (generator.TryGenerateAllExternMethods(moduleName, context.CancellationToken)) + { + matches++; + } } - if (!matchModule) + switch (matches) { - context.ReportDiagnostic(Diagnostic.Create(NoMethodsForModule, location, moduleName)); + case 0: + context.ReportDiagnostic(Diagnostic.Create(NoMethodsForModule, location, moduleName)); + break; + case > 1: + context.ReportDiagnostic(Diagnostic.Create(AmbiguousMatchError, location, moduleName)); + break; } continue; } - bool matchApi = false; + List matchingApis = new(); foreach (Generator generator in generators) { - if (generator.TryGenerate(name, context.CancellationToken)) + if (generator.TryGenerate(name, out IReadOnlyList preciseApi, context.CancellationToken)) { - matchApi = true; + matchingApis.AddRange(preciseApi); continue; } + matchingApis.AddRange(preciseApi); if (generator.TryGetEnumName(name, out string? declaringEnum)) { context.ReportDiagnostic(Diagnostic.Create(UseEnumValueDeclaringType, location, declaringEnum)); - if (generator.TryGenerate(declaringEnum, context.CancellationToken)) - { - matchApi = true; - continue; - } - else - { - ReportNoMatch(location, declaringEnum); - } + generator.TryGenerate(declaringEnum, out preciseApi, context.CancellationToken); + matchingApis.AddRange(preciseApi); } } - if (!matchApi) + switch (matchingApis.Count) { - ReportNoMatch(location, name); + case 0: + ReportNoMatch(location, name); + break; + case > 1: + context.ReportDiagnostic(Diagnostic.Create(AmbiguousMatchErrorWithSuggestions, location, name, ConcatSuggestions(matchingApis))); + break; } } catch (GenerationFailedException ex) @@ -243,6 +266,24 @@ public void Execute(GeneratorExecutionContext context) } } + string ConcatSuggestions(IReadOnlyList suggestions) + { + var suggestionBuilder = new StringBuilder(); + for (int i = 0; i < suggestions.Count; i++) + { + if (i > 0) + { + suggestionBuilder.Append(i < suggestions.Count - 1 ? ", " : " or "); + } + + suggestionBuilder.Append('"'); + suggestionBuilder.Append(suggestions[i]); + suggestionBuilder.Append('"'); + } + + return suggestionBuilder.ToString(); + } + void ReportNoMatch(Location? location, string failedAttempt) { List suggestions = new(); @@ -253,20 +294,7 @@ void ReportNoMatch(Location? location, string failedAttempt) if (suggestions.Count > 0) { - var suggestionBuilder = new StringBuilder(); - for (int i = 0; i < suggestions.Count; i++) - { - if (i > 0) - { - suggestionBuilder.Append(i < suggestions.Count - 1 ? ", " : " or "); - } - - suggestionBuilder.Append('"'); - suggestionBuilder.Append(suggestions[i]); - suggestionBuilder.Append('"'); - } - - context.ReportDiagnostic(Diagnostic.Create(NoMatchingMethodOrTypeWithSuggestions, location, failedAttempt, suggestionBuilder)); + context.ReportDiagnostic(Diagnostic.Create(NoMatchingMethodOrTypeWithSuggestions, location, failedAttempt, ConcatSuggestions(suggestions))); } else { diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index 1d5d9601..2e5118b8 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -92,7 +92,7 @@ public void SimplestMethod(string tfm) this.compilation = this.starterCompilations[tfm]; this.generator = this.CreateGenerator(); const string methodName = "GetTickCount"; - Assert.True(this.generator.TryGenerateExternMethod(methodName)); + Assert.True(this.generator.TryGenerateExternMethod(methodName, out _)); this.CollectGeneratedCode(this.generator); this.AssertNoDiagnostics(); From 03bffb74b64a8e6f802f17e969f41fdb680ef799 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 3 Sep 2021 09:42:25 -0600 Subject: [PATCH 3/9] Generate code that references across metadata file boundaries --- src/Microsoft.Windows.CsWin32/Generator.cs | 105 ++++++++++++++--- .../SourceGenerator.cs | 9 +- .../SuperGenerator.cs | 106 ++++++++++++++++++ .../GeneratorTests.cs | 30 ++++- .../Microsoft.Windows.CsWin32.Tests.csproj | 1 + 5 files changed, 229 insertions(+), 22 deletions(-) create mode 100644 src/Microsoft.Windows.CsWin32/SuperGenerator.cs diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index 98d7398a..fb1f3081 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -292,6 +292,7 @@ public class Generator : IDisposable /// The parse options that will be used for the generated code. public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions options, CSharpCompilation? compilation = null, CSharpParseOptions? parseOptions = null) { + this.InputAssemblyName = Path.GetFileNameWithoutExtension(metadataLibraryPath); this.MetadataIndex = MetadataIndex.Get(metadataLibraryPath, compilation?.Options.Platform); this.ApiDocs = docs; @@ -349,6 +350,10 @@ private enum FriendlyOverloadOf internal ImmutableDictionary BannedAPIs => GetBannedAPIs(this.options); + internal SuperGenerator? SuperGenerator { get; set; } + + internal string InputAssemblyName { get; } + internal MetadataIndex MetadataIndex { get; } internal Docs? ApiDocs { get; } @@ -468,11 +473,31 @@ public bool TryGenerate(string apiNameOrModuleWildcard, out IReadOnlyList 1) + { + return result; + } + + result = this.TryGenerateExternMethod(apiNameOrModuleWildcard, out preciseApi); + if (result || preciseApi.Count > 1) + { + return result; + } + + result = this.TryGenerateType(apiNameOrModuleWildcard, out preciseApi); + if (result || preciseApi.Count > 1) + { + return result; + } + + result = this.TryGenerateConstant(apiNameOrModuleWildcard, out preciseApi); + if (result || preciseApi.Count > 1) + { + return result; + } + + return false; } } @@ -1176,6 +1201,26 @@ internal bool IsInterface(TypeReferenceHandle typeRefHandle) return false; } + internal void RequestInteropType(string @namespace, string name) + { + // PERF: Skip this search if this namespace/name has already been generated (committed, or still in volatileCode). + foreach (TypeDefinitionHandle tdh in this.Reader.TypeDefinitions) + { + TypeDefinition td = this.Reader.GetTypeDefinition(tdh); + if (this.Reader.StringComparer.Equals(td.Name, name) && this.Reader.StringComparer.Equals(td.Namespace, @namespace)) + { + this.volatileCode.GenerationTransaction(delegate + { + this.RequestInteropType(tdh); + }); + + return; + } + } + + throw new GenerationFailedException($"Referenced type \"{@namespace}.{name}\" not found in \"{this.InputAssemblyName}\"."); + } + internal void RequestInteropType(TypeDefinitionHandle typeDefHandle) { TypeDefinition typeDef = this.Reader.GetTypeDefinition(typeDefHandle); @@ -1220,18 +1265,19 @@ internal void RequestInteropType(TypeDefinitionHandle typeDefHandle) }); } - internal TypeDefinitionHandle? RequestInteropType(TypeReferenceHandle typeRefHandle) + internal void RequestInteropType(TypeReferenceHandle typeRefHandle) { if (this.TryGetTypeDefHandle(typeRefHandle, out TypeDefinitionHandle typeDefHandle)) { this.RequestInteropType(typeDefHandle); - return typeDefHandle; } else { - // System.Guid reaches here, but doesn't need to be generated. - ////throw new NotSupportedException($"Could not find a type def for: {this.mr.GetString(typeRef.Namespace)}.{name}"); - return null; + TypeReference typeRef = this.Reader.GetTypeReference(typeRefHandle); + if (typeRef.ResolutionScope.Kind == HandleKind.AssemblyReference) + { + this.SuperGenerator?.RequestInteropType(this, typeRef); + } } } @@ -1590,6 +1636,23 @@ internal bool TryGetTypeDefHandle(TypeReferenceHandle typeRefHandle, out TypeDef return !typeDefHandle.IsNil; } + internal bool TryGetTypeDefHandle(string @namespace, string name, out TypeDefinitionHandle typeDefinitionHandle) + { + // PERF: Use an index + foreach (TypeDefinitionHandle tdh in this.Reader.TypeDefinitions) + { + TypeDefinition td = this.Reader.GetTypeDefinition(tdh); + if (this.Reader.StringComparer.Equals(td.Name, name) && this.Reader.StringComparer.Equals(td.Namespace, @namespace)) + { + typeDefinitionHandle = tdh; + return true; + } + } + + typeDefinitionHandle = default; + return false; + } + internal bool IsNonCOMInterface(TypeDefinition interfaceTypeDef) { if (this.Reader.StringComparer.Equals(interfaceTypeDef.Name, "IUnknown")) @@ -2578,7 +2641,6 @@ private FieldDeclarationSyntax DeclareConstant(FieldDefinitionHandle fieldDefHan string name = this.Reader.GetString(fieldDef.Name); try { - bool isConst = (fieldDef.Attributes & FieldAttributes.HasDefault) == FieldAttributes.HasDefault; TypeHandleInfo fieldTypeInfo = fieldDef.DecodeSignature(SignatureHandleProvider.Instance, null) with { IsConstantField = true }; var customAttributes = fieldDef.GetCustomAttributes(); var fieldType = fieldTypeInfo.ToTypeSyntax(this.fieldTypeSettings, customAttributes); @@ -4336,10 +4398,25 @@ private bool IsTypeDefStruct(TypeHandleInfo? typeHandleInfo) TypeDefinition typeDef = this.Reader.GetTypeDefinition((TypeDefinitionHandle)handleInfo.Handle); return this.IsTypeDefStruct(typeDef); } - else if (handleInfo.Handle.Kind == HandleKind.TypeReference && this.TryGetTypeDefHandle((TypeReferenceHandle)handleInfo.Handle, out TypeDefinitionHandle tdh)) + else if (handleInfo.Handle.Kind == HandleKind.TypeReference) { - TypeDefinition typeDef = this.Reader.GetTypeDefinition(tdh); - return this.IsTypeDefStruct(typeDef); + if (this.TryGetTypeDefHandle((TypeReferenceHandle)handleInfo.Handle, out TypeDefinitionHandle tdh)) + { + TypeDefinition typeDef = this.Reader.GetTypeDefinition(tdh); + return this.IsTypeDefStruct(typeDef); + } + else if (this.SuperGenerator is object) + { + TypeReference typeReference = this.Reader.GetTypeReference((TypeReferenceHandle)handleInfo.Handle); + if (this.SuperGenerator.TryGetTargetGenerator(this, typeReference, out Generator? targetGenerator)) + { + if (targetGenerator.TryGetTypeDefHandle(this.Reader.GetString(typeReference.Namespace), this.Reader.GetString(typeReference.Name), out TypeDefinitionHandle foreignTypeDefHandle)) + { + TypeDefinition foreignTypeDef = targetGenerator.Reader.GetTypeDefinition(foreignTypeDefHandle); + return targetGenerator.IsTypeDefStruct(foreignTypeDef); + } + } + } } } else if (SpecialTypeDefNames.Contains(null!/*TODO*/)) diff --git a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs index ff7013db..4076b07b 100644 --- a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs @@ -34,16 +34,16 @@ public class SourceGenerator : ISourceGenerator private static readonly DiagnosticDescriptor NoMatchingMethodOrType = new DiagnosticDescriptor( "PInvoke001", - "No matching method or type found", - "Method or type \"{0}\" not found.", + "No matching method, type or constant found", + "Method, type or constant \"{0}\" not found.", "Functionality", DiagnosticSeverity.Warning, isEnabledByDefault: true); private static readonly DiagnosticDescriptor NoMatchingMethodOrTypeWithSuggestions = new DiagnosticDescriptor( "PInvoke001", - "No matching method or type found", - "Method or type \"{0}\" not found. Did you mean {1}?", + "No matching method, type or constant found", + "Method, type or constant \"{0}\" not found. Did you mean {1}?", "Functionality", DiagnosticSeverity.Warning, isEnabledByDefault: true); @@ -163,6 +163,7 @@ public void Execute(GeneratorExecutionContext context) IReadOnlyList generators = CollectMetadataPaths(context).Select(path => new Generator(path, docs, options, compilation, parseOptions)).ToList(); try { + SuperGenerator.Combine(generators); SourceText? nativeMethodsTxt = nativeMethodsTxtFile.GetText(context.CancellationToken); if (nativeMethodsTxt is null) { diff --git a/src/Microsoft.Windows.CsWin32/SuperGenerator.cs b/src/Microsoft.Windows.CsWin32/SuperGenerator.cs new file mode 100644 index 00000000..220280cd --- /dev/null +++ b/src/Microsoft.Windows.CsWin32/SuperGenerator.cs @@ -0,0 +1,106 @@ +// 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 +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Reflection.Metadata; + using System.Text; + + /// + /// A coordinator of many objects, allowing code to be generated that requires types from across many input winmd's. + /// + public class SuperGenerator + { + private SuperGenerator(ImmutableDictionary generators) + { + this.Generators = generators; + } + + /// + /// Gets the collection of generators managed by this , indexed by their input winmd's. + /// + public ImmutableDictionary Generators { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The objects to enable collaborative generation across. These should not have been added to a previously. + /// The new instance of . + public static SuperGenerator Combine(params Generator[] generators) => Combine((IEnumerable)generators); + + /// + public static SuperGenerator Combine(IEnumerable generators) + { + SuperGenerator super = new(generators.ToImmutableDictionary(g => g.InputAssemblyName)); + foreach (Generator generator in super.Generators.Values) + { + if (generator.SuperGenerator is object) + { + throw new InvalidOperationException($"This generator has already been added to a {nameof(SuperGenerator)}."); + } + + generator.SuperGenerator = super; + } + + return super; + } + + /// + /// Looks up the that owns a referenced type. + /// + /// The generator asking the question. + /// The type reference from the requesting generator. + /// Receives the generator that owns the referenced type. + /// if a matching generator was found; otherwise . + public bool TryGetTargetGenerator(Generator requestingGenerator, TypeReference typeRef, [NotNullWhen(true)] out Generator? targetGenerator) + { + if (typeRef.ResolutionScope.Kind != HandleKind.AssemblyReference) + { + targetGenerator = null; + return false; + } + + AssemblyReference assemblyRef = requestingGenerator.Reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); + string scope = requestingGenerator.Reader.GetString(assemblyRef.Name); + + // Workaround the fact that these winmd references may oddly have .winmd included as a suffix. + if (scope.EndsWith(".winmd", StringComparison.OrdinalIgnoreCase)) + { + scope = scope.Substring(0, scope.Length - ".winmd".Length); + } + + return this.Generators.TryGetValue(scope, out targetGenerator); + } + + /// + /// Requests generation of a type referenced across metadata files. + /// + /// The generator making the request. + /// The referenced type. + public void RequestInteropType(Generator requestingGenerator, TypeReference typeRef) + { + if (typeRef.ResolutionScope.Kind != HandleKind.AssemblyReference) + { + throw new ArgumentException("Only type references across assemblies should be requested.", nameof(typeRef)); + } + + if (this.TryGetTargetGenerator(requestingGenerator, typeRef, out Generator? generator)) + { + string ns = requestingGenerator.Reader.GetString(typeRef.Namespace); + string name = requestingGenerator.Reader.GetString(typeRef.Name); + generator.RequestInteropType(ns, name); + } + else + { + AssemblyReference assemblyRef = requestingGenerator.Reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); + string scope = requestingGenerator.Reader.GetString(assemblyRef.Name); + throw new GenerationFailedException($"Input metadata file \"{scope}\" has not been provided."); + } + } + } +} diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index 2e5118b8..2c60cdb5 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -28,6 +28,7 @@ public class GeneratorTests : IDisposable, IAsyncLifetime private static readonly GeneratorOptions DefaultTestGeneratorOptions = new GeneratorOptions { EmitSingleFile = true }; private static readonly string FileSeparator = new string('=', 140); private static readonly string MetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Windows.Win32.winmd"); + private static readonly string DiaMetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Microsoft.Dia.winmd"); private static readonly string ApiDocsPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "apidocs.msgpack"); private readonly ITestOutputHelper logger; private readonly Dictionary starterCompilations = new(); @@ -282,8 +283,10 @@ public void ComOutPtrTypedAsOutObject() public void AmbiguousApiName() { this.generator = this.CreateGenerator(); - var ex = Assert.Throws(() => this.generator.TryGenerate("IDENTITY_TYPE", CancellationToken.None)); - this.logger.WriteLine(ex.Message); + Assert.False(this.generator.TryGenerate("IDENTITY_TYPE", out IReadOnlyList preciseApi, CancellationToken.None)); + Assert.Equal(2, preciseApi.Count); + Assert.Contains("Windows.Win32.NetworkManagement.NetworkPolicyServer.IDENTITY_TYPE", preciseApi); + Assert.Contains("Windows.Win32.Security.Authentication.Identity.Provider.IDENTITY_TYPE", preciseApi); } [Fact] @@ -529,6 +532,21 @@ public void Const_PWSTR_Becomes_PCWSTR_and_String() Assert.Equal(2, overloads.Count()); } + [Fact] + public void CrossWinmdTypeReference() + { + this.generator = this.CreateGenerator(); + using Generator diaGenerator = this.CreateGenerator(DiaMetadataPath); + var super = SuperGenerator.Combine(this.generator, diaGenerator); + Assert.True(diaGenerator.TryGenerate("E_PDB_NOT_FOUND", CancellationToken.None)); + this.CollectGeneratedCode(this.generator); + this.CollectGeneratedCode(diaGenerator); + this.AssertNoDiagnostics(); + + Assert.Single(this.FindGeneratedType("HRESULT")); + Assert.Single(this.FindGeneratedConstant("E_PDB_NOT_FOUND")); + } + [Theory, CombinatorialData] public void ArchitectureSpecificAPIsTreatment( [CombinatorialValues("MEMORY_BASIC_INFORMATION", "SP_PROPCHANGE_PARAMS", "JsCreateContext", "IShellBrowser")] string apiName, @@ -2010,7 +2028,9 @@ private CSharpCompilation AddGeneratedCode(CSharpCompilation compilation, Genera private IEnumerable FindGeneratedMethod(string name) => this.compilation.SyntaxTrees.SelectMany(st => st.GetRoot().DescendantNodes().OfType()).Where(md => md.Identifier.ValueText == name); - private IEnumerable FindGeneratedType(string name) => this.compilation.SyntaxTrees.SelectMany(st => st.GetRoot().DescendantNodes().OfType()).Where(md => md.Identifier.ValueText == name); + private IEnumerable FindGeneratedType(string name) => this.compilation.SyntaxTrees.SelectMany(st => st.GetRoot().DescendantNodes().OfType()).Where(btd => btd.Identifier.ValueText == name); + + private IEnumerable FindGeneratedConstant(string name) => this.compilation.SyntaxTrees.SelectMany(st => st.GetRoot().DescendantNodes().OfType()).Where(fd => (fd.Modifiers.Any(SyntaxKind.StaticKeyword) || fd.Modifiers.Any(SyntaxKind.ConstKeyword)) && fd.Declaration.Variables.Any(vd => vd.Identifier.ValueText == name)); private bool IsMethodGenerated(string name) => this.FindGeneratedMethod(name).Any(); @@ -2140,7 +2160,9 @@ private async Task CreateCompilationAsync(ReferenceAssemblies return compilation; } - private Generator CreateGenerator(GeneratorOptions? options = null, CSharpCompilation? compilation = null) => new Generator(MetadataPath, Docs.Get(ApiDocsPath), options ?? DefaultTestGeneratorOptions, compilation ?? this.compilation, this.parseOptions); + private Generator CreateGenerator(GeneratorOptions? options = null, CSharpCompilation? compilation = null) => this.CreateGenerator(MetadataPath, options, compilation); + + private Generator CreateGenerator(string path, GeneratorOptions? options = null, CSharpCompilation? compilation = null) => new Generator(path, Docs.Get(ApiDocsPath), options ?? DefaultTestGeneratorOptions, compilation ?? this.compilation, this.parseOptions); private static class MyReferenceAssemblies { diff --git a/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj b/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj index fb3008d7..2ce5e035 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj +++ b/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj @@ -35,6 +35,7 @@ + From 6d469cf086b5d1c397294109f575ebd8f071e6e0 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 3 Sep 2021 10:00:40 -0600 Subject: [PATCH 4/9] Use | instead of ; as a delimiter --- src/Microsoft.Windows.CsWin32/SourceGenerator.cs | 4 ++-- .../build/Microsoft.Windows.CsWin32.props | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs index 4076b07b..68dfa33d 100644 --- a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs @@ -340,7 +340,7 @@ private static IReadOnlyList CollectMetadataPaths(GeneratorExecutionCont return Array.Empty(); } - string[] metadataBasePaths = delimitedMetadataBasePaths.Split(';'); + string[] metadataBasePaths = delimitedMetadataBasePaths.Split('|'); return metadataBasePaths; } @@ -350,7 +350,7 @@ private static IReadOnlyList CollectMetadataPaths(GeneratorExecutionCont if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.CsWin32InputDocPaths", out string? delimitedApiDocsPaths) && !string.IsNullOrWhiteSpace(delimitedApiDocsPaths)) { - string[] apiDocsPaths = delimitedApiDocsPaths!.Split(';'); + string[] apiDocsPaths = delimitedApiDocsPaths!.Split('|'); if (apiDocsPaths.Length > 0) { List docsList = new(apiDocsPaths.Length); diff --git a/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.props b/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.props index 132fc745..5a7b5200 100644 --- a/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.props +++ b/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.props @@ -17,8 +17,8 @@ - @(ProjectionMetadataWinmd) - @(ProjectionDocs) + @(ProjectionMetadataWinmd,'|') + @(ProjectionDocs,'|') From bf59af56610c4b26f3f2e4c48e60d98707188aa6 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 3 Sep 2021 10:34:52 -0600 Subject: [PATCH 5/9] Fixes namespace and prefix filenames to avoid collisions --- Directory.Build.props | 5 ++- src/Microsoft.Windows.CsWin32/Generator.cs | 4 +- .../GeneratorOptions.cs | 10 ----- .../SourceGenerator.cs | 2 +- test/GenerationSandbox.Tests/BasicTests.cs | 6 +++ .../GenerationSandbox.Tests.csproj | 1 + .../GenerationSandbox.Tests/NativeMethods.txt | 1 + .../GeneratorOptionsTests.cs | 6 --- .../GeneratorTests.cs | 42 +++++++++---------- 9 files changed, 35 insertions(+), 42 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index febdafd9..34f58a15 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,8 +28,9 @@ true snupkg - 10.2.118-preview - 0.1.4-alpha + 10.2.179-preview-g98239ddf1d + 0.2.185-preview-g7e1e6a442c + 0.1.5-alpha-g83f1525e8f diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index fb1f3081..c7c9bb9e 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -368,7 +368,7 @@ private enum FriendlyOverloadOf private bool GroupByModule => string.IsNullOrEmpty(this.options.ClassName); - private string Namespace => this.options.Namespace; + private string Namespace => this.InputAssemblyName; private string SingleClassName => this.options.ClassName ?? throw new InvalidOperationException("Not in one-class mode."); @@ -1000,7 +1000,7 @@ nsContents.Key is object usingDirectives.Add(UsingDirective(ParseName(GlobalNamespacePrefix + "System.Runtime.Versioning"))); } - usingDirectives.Add(UsingDirective(NameEquals(GlobalWin32NamespaceAlias), ParseName(GlobalNamespacePrefix + this.Namespace))); + usingDirectives.Add(UsingDirective(NameEquals(GlobalWin32NamespaceAlias), ParseName(GlobalNamespacePrefix + CommonNamespace))); var normalizedResults = new Dictionary(StringComparer.OrdinalIgnoreCase); results.AsParallel().WithCancellation(cancellationToken).ForAll(kv => diff --git a/src/Microsoft.Windows.CsWin32/GeneratorOptions.cs b/src/Microsoft.Windows.CsWin32/GeneratorOptions.cs index 933dab8d..578b7476 100644 --- a/src/Microsoft.Windows.CsWin32/GeneratorOptions.cs +++ b/src/Microsoft.Windows.CsWin32/GeneratorOptions.cs @@ -23,12 +23,6 @@ public record GeneratorOptions /// The default value is "PInvoke". public string? ClassName { get; init; } = "PInvoke"; - /// - /// Gets the namespace for generated code. - /// - /// The default value is "Windows.Win32". Must be non-empty. - public string Namespace { get; init; } = "Windows.Win32"; - /// /// Gets a value indicating whether to emit a single source file as opposed to types spread across many files. /// @@ -57,10 +51,6 @@ public record GeneratorOptions /// Thrown when some setting is invalid. public void Validate() { - if (string.IsNullOrWhiteSpace(this.Namespace)) - { - throw new InvalidOperationException("The namespace must be set."); - } } /// diff --git a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs index 68dfa33d..82eec0f8 100644 --- a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs @@ -263,7 +263,7 @@ public void Execute(GeneratorExecutionContext context) .ThenBy(pair => pair.Key, StringComparer.Ordinal); foreach (var unit in compilationUnits) { - context.AddSource(unit.Key, unit.Value.ToFullString()); + context.AddSource($"{generator.InputAssemblyName}.{unit.Key}", unit.Value.ToFullString()); } } diff --git a/test/GenerationSandbox.Tests/BasicTests.cs b/test/GenerationSandbox.Tests/BasicTests.cs index 5c453103..07b63ed7 100644 --- a/test/GenerationSandbox.Tests/BasicTests.cs +++ b/test/GenerationSandbox.Tests/BasicTests.cs @@ -41,6 +41,12 @@ public void DISPLAYCONFIG_VIDEO_SIGNAL_INFO_Test() // TODO: write code that sets/gets memory on the inner struct (e.g. videoStandard). } + [Fact] + public void E_PDB_LIMIT() + { + Assert.Equal(-2140340211, global::Microsoft.Dia.Constants.E_PDB_LIMIT.Value); + } + [Fact] public void Bool() { diff --git a/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj b/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj index 666938a7..c219909f 100644 --- a/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj +++ b/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/test/GenerationSandbox.Tests/NativeMethods.txt b/test/GenerationSandbox.Tests/NativeMethods.txt index f4580594..622917b8 100644 --- a/test/GenerationSandbox.Tests/NativeMethods.txt +++ b/test/GenerationSandbox.Tests/NativeMethods.txt @@ -10,3 +10,4 @@ EnumWindows GetWindowTextLength GetWindowText WPARAM +E_PDB_LIMIT diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorOptionsTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorOptionsTests.cs index 30d87e96..7cecd1a6 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorOptionsTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorOptionsTests.cs @@ -12,10 +12,4 @@ public void Validate_Default() { new GeneratorOptions().Validate(); } - - [Fact] - public void Validate_EmptyNamespace() - { - Assert.Throws(() => new GeneratorOptions { Namespace = string.Empty }.Validate()); - } } diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index 2c60cdb5..01633093 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -871,7 +871,7 @@ public async Task TestSimpleStructure() }, GeneratedSources = { - (typeof(SourceGenerator), "BOOL.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.BOOL.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -932,7 +932,7 @@ public async Task TestSimpleEnum() }, GeneratedSources = { - (typeof(SourceGenerator), "DISPLAYCONFIG_SCANLINE_ORDERING.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.DISPLAYCONFIG_SCANLINE_ORDERING.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -997,7 +997,7 @@ public async Task TestSimpleEnumWithoutDocs() }, GeneratedSources = { - (typeof(SourceGenerator), "DISPLAYCONFIG_SCANLINE_ORDERING.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.DISPLAYCONFIG_SCANLINE_ORDERING.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1052,7 +1052,7 @@ public async Task TestFlagsEnum() }, GeneratedSources = { - (typeof(SourceGenerator), "FILE_ACCESS_FLAGS.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.FILE_ACCESS_FLAGS.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1128,7 +1128,7 @@ public async Task TestSimpleDelegate() }, GeneratedSources = { - (typeof(SourceGenerator), "BOOL.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.BOOL.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1166,7 +1166,7 @@ public static implicit operator bool(BOOL value) } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "Delegates.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.Delegates.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1191,7 +1191,7 @@ namespace UI.WindowsAndMessaging } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "HWND.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.HWND.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1231,7 +1231,7 @@ internal readonly partial struct HWND } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "LPARAM.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.LPARAM.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1294,7 +1294,7 @@ public async Task TestSimpleMethod() }, GeneratedSources = { - (typeof(SourceGenerator), "HDC.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.HDC.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1336,7 +1336,7 @@ internal readonly partial struct HDC } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "HWND.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.HWND.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1376,7 +1376,7 @@ internal readonly partial struct HWND } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "PInvoke.User32.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.PInvoke.User32.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1438,7 +1438,7 @@ public async Task TestMethodWithOverloads() }, GeneratedSources = { - (typeof(SourceGenerator), "BOOL.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.BOOL.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1476,7 +1476,7 @@ public static implicit operator bool(BOOL value) } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "FILE_ACCESS_FLAGS.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.FILE_ACCESS_FLAGS.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1529,7 +1529,7 @@ internal enum FILE_ACCESS_FLAGS : uint } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "FILE_CREATION_DISPOSITION.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.FILE_CREATION_DISPOSITION.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1560,7 +1560,7 @@ internal enum FILE_CREATION_DISPOSITION : uint } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "FILE_FLAGS_AND_ATTRIBUTES.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.FILE_FLAGS_AND_ATTRIBUTES.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1629,7 +1629,7 @@ internal enum FILE_FLAGS_AND_ATTRIBUTES : uint } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "FILE_SHARE_MODE.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.FILE_SHARE_MODE.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1660,7 +1660,7 @@ internal enum FILE_SHARE_MODE : uint } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "HANDLE.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.HANDLE.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1702,7 +1702,7 @@ internal readonly partial struct HANDLE } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "PCWSTR.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.PCWSTR.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1780,7 +1780,7 @@ internal int Length } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "PInvoke.Kernel32.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.PInvoke.Kernel32.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1886,7 +1886,7 @@ internal static unsafe Microsoft.Win32.SafeHandles.SafeFileHandle CreateFile(str } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "PWSTR.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.PWSTR.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1943,7 +1943,7 @@ internal int Length } } ".Replace("\r\n", "\n")), - (typeof(SourceGenerator), "SECURITY_ATTRIBUTES.g.cs", @"// ------------------------------------------------------------------------------ + (typeof(SourceGenerator), "Windows.Win32.SECURITY_ATTRIBUTES.g.cs", @"// ------------------------------------------------------------------------------ // // This code was generated by a tool. // From 1439ce5f8d943a48ea3069e4308282384fa49c4d Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 3 Sep 2021 10:38:18 -0600 Subject: [PATCH 6/9] Beef up test a bit --- test/GenerationSandbox.Tests/BasicTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/GenerationSandbox.Tests/BasicTests.cs b/test/GenerationSandbox.Tests/BasicTests.cs index 07b63ed7..651cb313 100644 --- a/test/GenerationSandbox.Tests/BasicTests.cs +++ b/test/GenerationSandbox.Tests/BasicTests.cs @@ -44,7 +44,9 @@ public void DISPLAYCONFIG_VIDEO_SIGNAL_INFO_Test() [Fact] public void E_PDB_LIMIT() { - Assert.Equal(-2140340211, global::Microsoft.Dia.Constants.E_PDB_LIMIT.Value); + // We are very particular about the namespace the generated type comes from to ensure it is as expected. + HRESULT hr = global::Microsoft.Dia.Constants.E_PDB_LIMIT; + Assert.Equal(-2140340211, hr.Value); } [Fact] From ef80b3c6730d684d7107ebb9b98a0acd0bcc1543 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 7 Sep 2021 15:20:38 -0600 Subject: [PATCH 7/9] Update metadata versions --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 34f58a15..6a635bc2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,9 +28,9 @@ true snupkg - 10.2.179-preview-g98239ddf1d 0.2.185-preview-g7e1e6a442c - 0.1.5-alpha-g83f1525e8f + 10.2.185-preview + 0.1.6-alpha From a260e00c51a81c1abeb52532912f7f0536c0114f Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 7 Sep 2021 15:22:46 -0600 Subject: [PATCH 8/9] Remove Dia test dependencies (for now) --- Directory.Build.props | 2 +- test/GenerationSandbox.Tests/BasicTests.cs | 14 ++++----- .../GenerationSandbox.Tests.csproj | 2 +- .../GenerationSandbox.Tests/NativeMethods.txt | 1 - .../GeneratorTests.cs | 30 +++++++++---------- .../Microsoft.Windows.CsWin32.Tests.csproj | 4 +-- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6a635bc2..a894eb5d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,8 +28,8 @@ true snupkg - 0.2.185-preview-g7e1e6a442c 10.2.185-preview + 0.1.6-alpha diff --git a/test/GenerationSandbox.Tests/BasicTests.cs b/test/GenerationSandbox.Tests/BasicTests.cs index 651cb313..46959f71 100644 --- a/test/GenerationSandbox.Tests/BasicTests.cs +++ b/test/GenerationSandbox.Tests/BasicTests.cs @@ -41,13 +41,13 @@ public void DISPLAYCONFIG_VIDEO_SIGNAL_INFO_Test() // TODO: write code that sets/gets memory on the inner struct (e.g. videoStandard). } - [Fact] - public void E_PDB_LIMIT() - { - // We are very particular about the namespace the generated type comes from to ensure it is as expected. - HRESULT hr = global::Microsoft.Dia.Constants.E_PDB_LIMIT; - Assert.Equal(-2140340211, hr.Value); - } + ////[Fact] + ////public void E_PDB_LIMIT() + ////{ + //// // We are very particular about the namespace the generated type comes from to ensure it is as expected. + //// HRESULT hr = global::Microsoft.Dia.Constants.E_PDB_LIMIT; + //// Assert.Equal(-2140340211, hr.Value); + ////} [Fact] public void Bool() diff --git a/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj b/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj index c219909f..a31771ea 100644 --- a/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj +++ b/test/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/test/GenerationSandbox.Tests/NativeMethods.txt b/test/GenerationSandbox.Tests/NativeMethods.txt index 622917b8..f4580594 100644 --- a/test/GenerationSandbox.Tests/NativeMethods.txt +++ b/test/GenerationSandbox.Tests/NativeMethods.txt @@ -10,4 +10,3 @@ EnumWindows GetWindowTextLength GetWindowText WPARAM -E_PDB_LIMIT diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index 01633093..20fdd3be 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -28,7 +28,7 @@ public class GeneratorTests : IDisposable, IAsyncLifetime private static readonly GeneratorOptions DefaultTestGeneratorOptions = new GeneratorOptions { EmitSingleFile = true }; private static readonly string FileSeparator = new string('=', 140); private static readonly string MetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Windows.Win32.winmd"); - private static readonly string DiaMetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Microsoft.Dia.winmd"); + ////private static readonly string DiaMetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Microsoft.Dia.winmd"); private static readonly string ApiDocsPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "apidocs.msgpack"); private readonly ITestOutputHelper logger; private readonly Dictionary starterCompilations = new(); @@ -532,20 +532,20 @@ public void Const_PWSTR_Becomes_PCWSTR_and_String() Assert.Equal(2, overloads.Count()); } - [Fact] - public void CrossWinmdTypeReference() - { - this.generator = this.CreateGenerator(); - using Generator diaGenerator = this.CreateGenerator(DiaMetadataPath); - var super = SuperGenerator.Combine(this.generator, diaGenerator); - Assert.True(diaGenerator.TryGenerate("E_PDB_NOT_FOUND", CancellationToken.None)); - this.CollectGeneratedCode(this.generator); - this.CollectGeneratedCode(diaGenerator); - this.AssertNoDiagnostics(); - - Assert.Single(this.FindGeneratedType("HRESULT")); - Assert.Single(this.FindGeneratedConstant("E_PDB_NOT_FOUND")); - } + ////[Fact] + ////public void CrossWinmdTypeReference() + ////{ + //// this.generator = this.CreateGenerator(); + //// using Generator diaGenerator = this.CreateGenerator(DiaMetadataPath); + //// var super = SuperGenerator.Combine(this.generator, diaGenerator); + //// Assert.True(diaGenerator.TryGenerate("E_PDB_NOT_FOUND", CancellationToken.None)); + //// this.CollectGeneratedCode(this.generator); + //// this.CollectGeneratedCode(diaGenerator); + //// this.AssertNoDiagnostics(); + + //// Assert.Single(this.FindGeneratedType("HRESULT")); + //// Assert.Single(this.FindGeneratedConstant("E_PDB_NOT_FOUND")); + ////} [Theory, CombinatorialData] public void ArchitectureSpecificAPIsTreatment( diff --git a/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj b/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj index 2ce5e035..ddee3609 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj +++ b/test/Microsoft.Windows.CsWin32.Tests/Microsoft.Windows.CsWin32.Tests.csproj @@ -4,7 +4,7 @@ net5.0;netcoreapp3.1;net472 - + <_AssemblyMetadata1 Include="@(ProjectionMetadataWinmd)"> %(Identity) @@ -35,7 +35,7 @@ - + From 73eef681a711bac9b0c93519cdf1923f7ff13f4c Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 7 Sep 2021 16:40:51 -0600 Subject: [PATCH 9/9] Fix build of WinRT projects --- src/Microsoft.Windows.CsWin32/Generator.cs | 13 ++++++++++++- src/Microsoft.Windows.CsWin32/SuperGenerator.cs | 14 ++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index 29750f10..09901706 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -1305,7 +1305,18 @@ internal void RequestInteropType(TypeReferenceHandle typeRefHandle) TypeReference typeRef = this.Reader.GetTypeReference(typeRefHandle); if (typeRef.ResolutionScope.Kind == HandleKind.AssemblyReference) { - this.SuperGenerator?.RequestInteropType(this, typeRef); + if (this.SuperGenerator?.TryRequestInteropType(this, typeRef) is not true) + { + // We can't find the interop among our metadata inputs. + // Before we give up and report an error, search for the required type among the compilation's referenced assemblies. + string metadataName = $"{this.Reader.GetString(typeRef.Namespace)}.{this.Reader.GetString(typeRef.Name)}"; + if (this.compilation?.GetTypeByMetadataName(metadataName) is null) + { + AssemblyReference assemblyRef = this.Reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); + string scope = this.Reader.GetString(assemblyRef.Name); + throw new GenerationFailedException($"Input metadata file \"{scope}\" has not been provided."); + } + } } } } diff --git a/src/Microsoft.Windows.CsWin32/SuperGenerator.cs b/src/Microsoft.Windows.CsWin32/SuperGenerator.cs index 220280cd..a4244cbf 100644 --- a/src/Microsoft.Windows.CsWin32/SuperGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SuperGenerator.cs @@ -57,7 +57,7 @@ public static SuperGenerator Combine(IEnumerable generators) /// The type reference from the requesting generator. /// Receives the generator that owns the referenced type. /// if a matching generator was found; otherwise . - public bool TryGetTargetGenerator(Generator requestingGenerator, TypeReference typeRef, [NotNullWhen(true)] out Generator? targetGenerator) + internal bool TryGetTargetGenerator(Generator requestingGenerator, TypeReference typeRef, [NotNullWhen(true)] out Generator? targetGenerator) { if (typeRef.ResolutionScope.Kind != HandleKind.AssemblyReference) { @@ -82,7 +82,8 @@ public bool TryGetTargetGenerator(Generator requestingGenerator, TypeReference t /// /// The generator making the request. /// The referenced type. - public void RequestInteropType(Generator requestingGenerator, TypeReference typeRef) + /// if a matching generator was found; otherwise. + internal bool TryRequestInteropType(Generator requestingGenerator, TypeReference typeRef) { if (typeRef.ResolutionScope.Kind != HandleKind.AssemblyReference) { @@ -94,13 +95,10 @@ public void RequestInteropType(Generator requestingGenerator, TypeReference type string ns = requestingGenerator.Reader.GetString(typeRef.Namespace); string name = requestingGenerator.Reader.GetString(typeRef.Name); generator.RequestInteropType(ns, name); + return true; } - else - { - AssemblyReference assemblyRef = requestingGenerator.Reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); - string scope = requestingGenerator.Reader.GetString(assemblyRef.Name); - throw new GenerationFailedException($"Input metadata file \"{scope}\" has not been provided."); - } + + return false; } } }