From 466d3c7ab263471eedfb63781ae5a9ab6a40c237 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 2 Oct 2020 21:07:01 -0700 Subject: [PATCH] Add project for dumping p/invoke information from assembly metadata --- DllImportGenerator/DllImportGenerator.sln | 9 + .../PInvokeDump/PInvokeDump.TypeProviders.cs | 187 ++++++ .../Tools/PInvokeDump/PInvokeDump.cs | 165 ++++++ .../Tools/PInvokeDump/PInvokeDump.csproj | 13 + .../Tools/PInvokeDump/Program.cs | 150 +++++ .../Tools/PInvokeDump/Reporting/Csv.cs | 35 ++ .../Tools/PInvokeDump/Reporting/Html.cs | 552 ++++++++++++++++++ .../Tools/PInvokeDump/Reporting/Text.cs | 34 ++ 8 files changed, 1145 insertions(+) create mode 100644 DllImportGenerator/Tools/PInvokeDump/PInvokeDump.TypeProviders.cs create mode 100644 DllImportGenerator/Tools/PInvokeDump/PInvokeDump.cs create mode 100644 DllImportGenerator/Tools/PInvokeDump/PInvokeDump.csproj create mode 100644 DllImportGenerator/Tools/PInvokeDump/Program.cs create mode 100644 DllImportGenerator/Tools/PInvokeDump/Reporting/Csv.cs create mode 100644 DllImportGenerator/Tools/PInvokeDump/Reporting/Html.cs create mode 100644 DllImportGenerator/Tools/PInvokeDump/Reporting/Text.cs diff --git a/DllImportGenerator/DllImportGenerator.sln b/DllImportGenerator/DllImportGenerator.sln index 83e498c7727e..b2d908631a36 100644 --- a/DllImportGenerator/DllImportGenerator.sln +++ b/DllImportGenerator/DllImportGenerator.sln @@ -15,6 +15,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestAssets", "TestAssets", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeExports", "TestAssets\NativeExports\NativeExports.csproj", "{32FDA079-0E9F-4A36-ADA5-6593B67A54AC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{69D56AC9-232B-4E76-B6C1-33A7B06B6855}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PInvokeDump", "Tools\PInvokeDump\PInvokeDump.csproj", "{6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,12 +45,17 @@ Global {32FDA079-0E9F-4A36-ADA5-6593B67A54AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {32FDA079-0E9F-4A36-ADA5-6593B67A54AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {32FDA079-0E9F-4A36-ADA5-6593B67A54AC}.Release|Any CPU.Build.0 = Release|Any CPU + {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {32FDA079-0E9F-4A36-ADA5-6593B67A54AC} = {2CFB9A7A-4AAF-4B6A-8CC8-540F64C3B45F} + {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB} = {69D56AC9-232B-4E76-B6C1-33A7B06B6855} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5344B739-3A02-402A-8777-0D54DEC4F3BA} diff --git a/DllImportGenerator/Tools/PInvokeDump/PInvokeDump.TypeProviders.cs b/DllImportGenerator/Tools/PInvokeDump/PInvokeDump.TypeProviders.cs new file mode 100644 index 000000000000..4fd141b00cff --- /dev/null +++ b/DllImportGenerator/Tools/PInvokeDump/PInvokeDump.TypeProviders.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Reflection.Metadata; +using System.Runtime.InteropServices; + +namespace DllImportGenerator.Tools +{ + /// + /// Information about return/argument type in a P/Invoke + /// + public class ParameterInfo + { + public enum IndirectKind + { + None, + Array, + Pointer, + ByRef, + FunctionPointer + } + + public IndirectKind Indirection { get; init; } + public string Name { get; init; } + public ParameterInfo Element { get; init; } + public string AssemblyName { get; init; } + public MarshalAsAttribute MarshalAsInfo { get; set; } + + public ParameterInfo(IndirectKind indirection, ParameterInfo element) + { + Debug.Assert(indirection != IndirectKind.None && indirection != IndirectKind.FunctionPointer); + Debug.Assert(element != null); + + Indirection = indirection; + Element = element; + Name = element.Name; + } + + public ParameterInfo(IndirectKind indirection, string name) + { + Debug.Assert(indirection == IndirectKind.None || indirection == IndirectKind.FunctionPointer); + + Indirection = indirection; + Name = name; + } + + public override string ToString() + { + string result = string.Empty; + ParameterInfo currType = this; + while (currType.Indirection != IndirectKind.None && currType.Indirection != IndirectKind.FunctionPointer) + { + Debug.Assert(currType.Element != null); + string modifier = currType.Indirection switch + { + IndirectKind.Array => "[]", + IndirectKind.Pointer => "*", + IndirectKind.ByRef => "&", + _ => "", + }; + result = $"{modifier}{result}"; + currType = currType.Element; + } + + result = $"{currType.Name}{result}"; + return MarshalAsInfo == null + ? result + : $"{result} marshal({MarshalAsInfo.Value})"; + } + } + + public sealed partial class PInvokeDump + { + public class NotSupportedTypeException : Exception + { + public string Type { get; init; } + public NotSupportedTypeException(string type) { this.Type = type; } + } + + private class UnusedGenericContext { } + + /// + /// Simple type provider for decoding a method signature + /// + private class TypeProvider : ISignatureTypeProvider + { + public ParameterInfo GetArrayType(ParameterInfo elementType, ArrayShape shape) + { + throw new NotSupportedTypeException($"Array ({elementType.Name}) - {shape}"); + } + + public ParameterInfo GetByReferenceType(ParameterInfo elementType) + { + return new ParameterInfo(ParameterInfo.IndirectKind.ByRef, elementType); + } + + public ParameterInfo GetFunctionPointerType(MethodSignature signature) + { + return new ParameterInfo( + ParameterInfo.IndirectKind.FunctionPointer, + $"method {signature.ReturnType} ({string.Join(", ", signature.ParameterTypes.Select(t => t.ToString()))})"); + } + + public ParameterInfo GetGenericInstantiation(ParameterInfo genericType, ImmutableArray typeArguments) + { + throw new NotSupportedTypeException($"Generic ({genericType.Name})"); + } + + public ParameterInfo GetGenericMethodParameter(UnusedGenericContext genericContext, int index) + { + throw new NotSupportedTypeException($"Generic - {index}"); + } + + public ParameterInfo GetGenericTypeParameter(UnusedGenericContext genericContext, int index) + { + throw new NotSupportedTypeException($"Generic - {index}"); + } + + public ParameterInfo GetModifiedType(ParameterInfo modifier, ParameterInfo unmodifiedType, bool isRequired) + { + throw new NotSupportedTypeException($"Modified ({unmodifiedType.Name})"); + } + + public ParameterInfo GetPinnedType(ParameterInfo elementType) + { + throw new NotSupportedTypeException($"Pinned ({elementType.Name})"); + } + + public ParameterInfo GetPointerType(ParameterInfo elementType) + { + return new ParameterInfo(ParameterInfo.IndirectKind.Pointer, elementType); + } + + public ParameterInfo GetPrimitiveType(PrimitiveTypeCode typeCode) + { + return new ParameterInfo(ParameterInfo.IndirectKind.None, typeCode.ToString()); + } + + public ParameterInfo GetSZArrayType(ParameterInfo elementType) + { + return new ParameterInfo(ParameterInfo.IndirectKind.Array, elementType); + } + + public ParameterInfo GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + TypeDefinition typeDef = reader.GetTypeDefinition(handle); + + string name = GetTypeDefinitionFullName(reader, typeDef); + return new ParameterInfo(ParameterInfo.IndirectKind.None, name) + { + AssemblyName = reader.GetString(reader.GetAssemblyDefinition().Name), + }; + } + + public ParameterInfo GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + TypeReference typeRef = reader.GetTypeReference(handle); + Handle scope = typeRef.ResolutionScope; + + string name = typeRef.Namespace.IsNil + ? reader.GetString(typeRef.Name) + : reader.GetString(typeRef.Namespace) + Type.Delimiter + reader.GetString(typeRef.Name); + + switch (scope.Kind) + { + case HandleKind.AssemblyReference: + AssemblyReference assemblyRef = reader.GetAssemblyReference((AssemblyReferenceHandle)scope); + string assemblyName = reader.GetString(assemblyRef.Name); + return new ParameterInfo(ParameterInfo.IndirectKind.None, name) + { + AssemblyName = assemblyName + }; + case HandleKind.TypeReference: + return GetTypeFromReference(reader, (TypeReferenceHandle)scope, rawTypeKind); + default: + throw new NotSupportedTypeException($"TypeReference ({name}) - Resolution scope: {scope.Kind}"); + } + } + + public ParameterInfo GetTypeFromSpecification(MetadataReader reader, UnusedGenericContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + { + throw new NotSupportedTypeException($"TypeSpecification - {reader.GetTypeSpecification(handle)}"); + } + } + } +} diff --git a/DllImportGenerator/Tools/PInvokeDump/PInvokeDump.cs b/DllImportGenerator/Tools/PInvokeDump/PInvokeDump.cs new file mode 100644 index 000000000000..a1896340581e --- /dev/null +++ b/DllImportGenerator/Tools/PInvokeDump/PInvokeDump.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Runtime.InteropServices; + +namespace DllImportGenerator.Tools +{ + /// + /// P/Invoke method information from assembly metadata + /// + public record PInvokeMethod + { + public string EnclosingTypeName { get; init; } + public string MethodName { get; init; } + + public bool PreserveSig { get; init; } + public bool SetLastError { get; init; } + public bool BestFitMapping { get; init; } + public bool ThrowOnUnmappableChar { get; init; } + + public ParameterInfo ReturnType { get; init; } + public List ArgumentTypes { get; init; } + } + + public class NotSupportedPInvokeException : Exception + { + public string AssemblyPath { get; init; } + public string MethodName { get; init; } + + public NotSupportedPInvokeException(string assemblyPath, string methodName, string message) + : base(message) + { + this.AssemblyPath = assemblyPath; + this.MethodName = methodName; + } + } + + /// + /// Class for processing assemblies to retrieve information about their P/Invokes + /// + public sealed partial class PInvokeDump + { + private readonly TypeProvider typeProvider = new TypeProvider(); + private readonly Dictionary> methodsByAssemblyPath = new Dictionary>(); + private readonly HashSet allTypeNames = new HashSet(); + + public IReadOnlySet AllTypeNames => allTypeNames; + public IReadOnlyDictionary> MethodsByAssemblyPath => methodsByAssemblyPath; + public int Count { get; private set; } + + /// + /// Process an assembly + /// + /// Assembly to process + /// + /// hasMetadata: True if the assembly has metadata. + /// count: Number of P/Invoke methods found in the assembly. + /// + public (bool hasMetadata, int count) Process(FileInfo assemblyFile) + { + using var peReader = new PEReader(assemblyFile.OpenRead()); + if (!peReader.HasMetadata) + return (false, 0); + + MetadataReader mdReader = peReader.GetMetadataReader(MetadataReaderOptions.None); + List pinvokeMethods = new List(); + foreach (var methodDefHandle in mdReader.MethodDefinitions) + { + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + + // Not a P/Invoke. + if (!methodDef.Attributes.HasFlag(MethodAttributes.PinvokeImpl)) + continue; + + MethodImport methodImp = methodDef.GetImport(); + string methodName = mdReader.GetString(methodDef.Name); + + // Process method signature + MethodSignature signature; + try + { + signature = methodDef.DecodeSignature(this.typeProvider, null); + } + catch (NotSupportedTypeException e) + { + throw new NotSupportedPInvokeException(assemblyFile.FullName, methodName, $"Method '{methodName}' has unsupported type '{e.Type}'"); + } + + // Process method details + MethodImportAttributes impAttr = methodImp.Attributes; + TypeDefinition typeDef = mdReader.GetTypeDefinition(methodDef.GetDeclaringType()); + var method = new PInvokeMethod() + { + EnclosingTypeName = GetTypeDefinitionFullName(mdReader, typeDef), + MethodName = methodName, + PreserveSig = (methodDef.ImplAttributes & MethodImplAttributes.PreserveSig) != 0, + SetLastError = (impAttr & MethodImportAttributes.SetLastError) != 0, + BestFitMapping = (impAttr & MethodImportAttributes.BestFitMappingMask) == MethodImportAttributes.BestFitMappingEnable, + ThrowOnUnmappableChar = (impAttr & MethodImportAttributes.ThrowOnUnmappableCharMask) == MethodImportAttributes.ThrowOnUnmappableCharEnable, + ReturnType = signature.ReturnType, + ArgumentTypes = signature.ParameterTypes.ToList(), + }; + + // Track all types - just uses the full name, ignoring assembly + allTypeNames.Add(signature.ReturnType.Name); + allTypeNames.UnionWith(signature.ParameterTypes.Select(t => t.Name)); + + // Process marshalling descriptors + foreach (var paramHandle in methodDef.GetParameters()) + { + Parameter param = mdReader.GetParameter(paramHandle); + bool isReturn = param.SequenceNumber == 0; + BlobHandle marshallingInfo = param.GetMarshallingDescriptor(); + + MarshalAsAttribute marshalAs = null; + if (!marshallingInfo.IsNil) + { + BlobReader br = mdReader.GetBlobReader(marshallingInfo); + + // Just reads the unmanaged type, ignoring any other data + var unmanagedType = (UnmanagedType)br.ReadByte(); + marshalAs = new MarshalAsAttribute(unmanagedType); + } + + if (isReturn) + { + method.ReturnType.MarshalAsInfo = marshalAs; + } + else + { + method.ArgumentTypes[param.SequenceNumber - 1].MarshalAsInfo = marshalAs; + } + } + + pinvokeMethods.Add(method); + } + + methodsByAssemblyPath.Add(assemblyFile.FullName, pinvokeMethods); + Count += pinvokeMethods.Count; + return (true, pinvokeMethods.Count); + } + + private static string GetTypeDefinitionFullName(MetadataReader reader, TypeDefinition typeDef) + { + var enclosingTypes = new List() { reader.GetString(typeDef.Name) }; + TypeDefinition parentTypeDef = typeDef; + while (parentTypeDef.IsNested) + { + parentTypeDef = reader.GetTypeDefinition(parentTypeDef.GetDeclaringType()); + enclosingTypes.Add(reader.GetString(parentTypeDef.Name)); + } + + enclosingTypes.Reverse(); + string name = string.Join(Type.Delimiter, enclosingTypes); + if (!parentTypeDef.Namespace.IsNil) + name = $"{reader.GetString(parentTypeDef.Namespace)}{Type.Delimiter}{name}"; + + return name; + } + } +} diff --git a/DllImportGenerator/Tools/PInvokeDump/PInvokeDump.csproj b/DllImportGenerator/Tools/PInvokeDump/PInvokeDump.csproj new file mode 100644 index 000000000000..5b8d77adb876 --- /dev/null +++ b/DllImportGenerator/Tools/PInvokeDump/PInvokeDump.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + DllImportGenerator.Tools + + + + + + + diff --git a/DllImportGenerator/Tools/PInvokeDump/Program.cs b/DllImportGenerator/Tools/PInvokeDump/Program.cs new file mode 100644 index 000000000000..eb0820433781 --- /dev/null +++ b/DllImportGenerator/Tools/PInvokeDump/Program.cs @@ -0,0 +1,150 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace DllImportGenerator.Tools +{ + public enum OutputFormat + { + Csv, + Html, + Text + } + + public class Options + { + public FileInfo Assembly { get; init; } + public DirectoryInfo Directory { get; init; } + public FileInfo OutputFile { get; init; } + public string[] Exclude { get; init; } + public OutputFormat OutputFormat { get; init; } + public bool Quiet { get; init; } + } + + class Program + { + private readonly Options options; + private readonly PInvokeDump dump = new PInvokeDump(); + + private Program (Options options) + { + this.options = options; + } + + public void Run() + { + if (options.Assembly != null) + { + Console.WriteLine($"Processing assembly '{options.Assembly}'..."); + ReadAssembly(options.Assembly); + } + + if (options.Directory != null) + { + Console.WriteLine($"Processing directory '{options.Directory}'..."); + foreach (var file in options.Directory.GetFiles("*.dll")) + { + ReadAssembly(file); + } + } + + Console.WriteLine($"Total: {dump.Count}"); + Print(); + } + + private void ReadAssembly(FileInfo assemblyFile) + { + if (options.Exclude != null && options.Exclude.Contains(assemblyFile.Name, StringComparer.OrdinalIgnoreCase)) + { + Console.WriteLine($" Skipping '{assemblyFile.Name}'"); + return; + } + + (bool hasMetadata, int count) = dump.Process(assemblyFile); + if (hasMetadata && !options.Quiet) + Console.WriteLine($" {assemblyFile.Name} :\t{count} P/Invoke method(s)"); + } + + private void Print() + { + string output = options.OutputFormat switch + { + OutputFormat.Csv => Reporting.Csv.Generate(dump), + OutputFormat.Html => Reporting.Html.Generate(dump, "P/Invokes", ((FileSystemInfo)options.Assembly ?? options.Directory).ToString()), + _ => Reporting.Text.Generate(dump) + }; + + if (options.OutputFile != null) + { + FileInfo outputFile = options.OutputFile; + + // Delete any pre-existing file + if (outputFile.Exists) + outputFile.Delete(); + + // Write info to the output file. + using var outputFileStream = new StreamWriter(outputFile.OpenWrite()); + outputFileStream.Write(output); + } + else + { + if (!options.Quiet) + Console.WriteLine(output); + } + } + + public static Task Main(string[] args) + { + var rootCommand = new RootCommand + { + new Option( + new string[] { "--assembly", "-a" }, + "Assembly to inspect"), + new Option( + new string[] { "--directory", "-d" }, + "Directory with assemblies to inspect"), + new Option( + new string[] { "--output-file", "-o" }, + "Output file"), + new Option( + new string[] { "--output-format", "-fmt" }, + getDefaultValue: () => OutputFormat.Text, + "Output format"), + new Option( + new string[] { "--exclude", "-x"}, + "Name of file to exclude") + { + Argument = new Argument() { Arity = ArgumentArity.ZeroOrMore } + }, + new Option( + new string[] { "--quiet", "-q" }, + getDefaultValue: () => false, + "Reduced console output") + }; + + rootCommand.Description = "Read P/Invoke information from assembly metadata"; + rootCommand.AddValidator(result => + { + if (result.Children.Contains("--assembly") && + result.Children.Contains("--directory")) + { + return "Options '--assembly' and '--directory' cannot be used together."; + } + + if (!result.Children.Contains("--assembly") && + !result.Children.Contains("--directory")) + { + return "Either '--assembly' or '--directory' must be specified."; + } + + return null; + }); + + rootCommand.Handler = CommandHandler.Create((Options o) => new Program(o).Run()); + return rootCommand.InvokeAsync(args); + } + } +} diff --git a/DllImportGenerator/Tools/PInvokeDump/Reporting/Csv.cs b/DllImportGenerator/Tools/PInvokeDump/Reporting/Csv.cs new file mode 100644 index 000000000000..5f1016600d6a --- /dev/null +++ b/DllImportGenerator/Tools/PInvokeDump/Reporting/Csv.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DllImportGenerator.Tools.Reporting +{ + internal static class Csv + { + private const char Separator = ','; + + /// + /// Generate a CSV + /// + /// P/Invoke information + /// CSV text + public static string Generate(PInvokeDump dump) + { + var result = new StringBuilder($"Assembly Path{Separator}Method Name{Separator}Return{Separator}Arguments{Environment.NewLine}"); + foreach ((string assemblyPath, IReadOnlyCollection importedMethods) in dump.MethodsByAssemblyPath) + { + if (importedMethods.Count == 0) + continue; + + foreach (var method in importedMethods) + { + var argumentTypes = string.Join(Separator, method.ArgumentTypes.Select(t => t.ToString())); + result.AppendLine($"\"{assemblyPath}\"{Separator}{method.EnclosingTypeName}{Type.Delimiter}{method.MethodName}{Separator}{method.ReturnType}{Separator}\"{argumentTypes}\""); + } + } + + return result.ToString(); + } + } +} diff --git a/DllImportGenerator/Tools/PInvokeDump/Reporting/Html.cs b/DllImportGenerator/Tools/PInvokeDump/Reporting/Html.cs new file mode 100644 index 000000000000..dfe03ddbbe7e --- /dev/null +++ b/DllImportGenerator/Tools/PInvokeDump/Reporting/Html.cs @@ -0,0 +1,552 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DllImportGenerator.Tools.Reporting +{ + internal static class Html + { + private const string Style = @" + html, body { + height: 100%; + width: 100%; + margin: 0px; + padding: 0px; + font-family: sans-serif; + } + .title { + text-align: center; + font-weight: bold; + font-size: x-large; + margin: 5px; + } + .subtitle { + text-align: center; + font-style: italic; + font-size: large; + margin: 5px; + } + .main { + margin: 5px; + display: flex; + } + .data-table { + flex: 75%; + } + .group-header { + font-weight: bold; + font-style: italic; + background-color: #f8f8f8; + } + table { + width: 100%; + height: 100%; + table-layout: fixed; + border-collapse: collapse; + } + th { + padding: 5px 10px; + font-weight: bold; + text-align: left; + } + tbody { + font-family: monospace; + } + td { + padding: 5px; + overflow-wrap: break-word; + } + tr { + background-color: #ffffff; + border: 1px solid #cccccc; + box-sizing: border-box; + } + label { + display: flex; + align-items: center; + padding: 1px; + } + .side-bar { + flex: 25%; + overflow: hidden; + } + .ellipsis-text { + overflow: hidden; + text-overflow: ellipsis; + } + .side-bar-group { + padding: 5px; + margin-left: 5px; + margin-bottom: 5px; + border: 1px solid #cccccc; + } + .filter-list { + list-style-type: none; + margin: 0px; + padding: 0px; + font-family: monospace; + } + .filter-list > li:hover, .filter-list > li.selected { + background-color: #eeeeee; + } + .filter-input { + line-height: 1.5; + width: 100%; + box-sizing: border-box; + margin: 1px 0px; + } + #option-select-all { + border-bottom: 1px solid #cccccc; + } + .summary-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + align-items: baseline; + } + .summary-column { + display: flex; + flex-direction: column; + flex-basis: 100%; + flex: 1; + } + .right-align { + text-align: right; + } + .status-indicator { + text-align: center; + width: 25px; + flex: 0 0 auto; + } + .cross-status { + display: none; + } + .table-data-row.disabled > td.cross-status { + display: table-cell; + } + .table-data-row.disabled > td.check-status { + display: none; + } +"; + private const string Script = @" + function filterOptions(filter) { + let css = filter + ? ` + li[data-option] { + display: none; + } + li[data-option*=""${filter}"" i] { + display: list-item; + }` + : ``; + document.getElementById('option-filter-style').innerHTML = css; + document.getElementById('option-select-all-checkbox').checked = false; + } + function filterTable() { + // Re-enable all rows + let allRows = document.getElementsByClassName('table-data-row'); + for (let row of allRows) { + row.classList.remove('disabled'); + } + + updateTableForFilter('category'); + updateTableForFilter('option'); + + // Update counts + let disabledCount = document.getElementsByClassName('table-data-row disabled').length; + let enabledCount = document.querySelectorAll('.table-data-row:not(.disabled)').length; + document.getElementById('enabled-total').innerHTML = enabledCount; + document.getElementById('disabled-total').innerHTML = disabledCount; + + let total = enabledCount + disabledCount; + document.getElementById('enabled-percent').innerHTML = (enabledCount / total * 100).toFixed() + '%'; + document.getElementById('disabled-percent').innerHTML = (disabledCount / total * 100).toFixed() + '%'; + } + function updateTableForFilter(filterType) { + let checkboxes = document.getElementsByClassName(`${filterType}-checkbox`); + for (let checkbox of checkboxes) { + if (checkbox.checked) + continue; + + let name = checkbox.dataset.name; + if (!name) + continue; + + // Set disabled class on row + let rows = document.querySelectorAll(`tr[data-${filterType}-filter~=""${name}""].table-data-row`); + for (let row of rows) { + row.classList.add('disabled'); + } + } + } + function showTableData(enabled, show) { + let style = enabled ? hideEnabledStyle : hideDisabledStyle; + if (show) { + document.head.removeChild(style); + } else { + document.head.appendChild(style); + } + } + + class FilterList { + constructor(element, onFilterUpdateFunc, hasSelectAllFirstItem) { + this.list = element; + this.focusedIndex = -1; + this.selectedItems = []; + this.onFilterUpdateFunc = onFilterUpdateFunc; + this.items = [...element.children]; + if (hasSelectAllFirstItem) { + this.selectAllItem = element.children[0]; + this.selectAllIndex = -1; + this.items.shift(); + this.selectAllItem.addEventListener('change', this.onSelectAllChanged.bind(this)); + } + element.addEventListener('keydown', this.onKeyDown.bind(this)); + element.addEventListener('click', this.onClick.bind(this)); + element.addEventListener('focus', this.resetFocus.bind(this)); + element.addEventListener('focusout', this.focusOut.bind(this)); + } + onKeyDown(event) { + if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { + let down = event.key === 'ArrowDown'; + this.focusNextItem(this.focusedIndex, down, event.shiftKey); + event.preventDefault(); + return false; + } else if (event.key === ' ' && this.selectedItems.length !== 0) { + for (let item of this.selectedItems) { + let inputs = item.getElementsByTagName('input'); + if (inputs.length !== 0 && inputs[0]) { + let newValue = !inputs[0].checked; + inputs[0].checked = newValue; + if (item === this.selectAllItem) { + this.checkAll(newValue); + break; + } + } + } + event.preventDefault(); + this.onFilterUpdateFunc(); + return false; + } else if (event.key === 'a' && event.ctrlKey) { + for (let [index, item] of this.items.entries()) { + if (getComputedStyle(item).display === 'none') + continue; + + this.selectItem(index, false); + } + event.preventDefault(); + return false; + } + return true; + } + onSelectAllChanged(event) { + let checked = event.target.checked; + this.checkAll(checked); + this.onFilterUpdateFunc(); + } + checkAll(checked) { + for (let item of this.items) { + if (getComputedStyle(item).display === 'none') + continue; + + let inputs = item.getElementsByTagName('input'); + if (inputs.length !== 0 && inputs[0]) { + inputs[0].checked = checked; + } + } + } + resetFocus(event) { + if (this.list.contains(event.relatedTarget)) + return; + + // Focus on first item + if (this.selectAllItem) { + this.selectItem(-1, true); + } else { + this.focusNextItem(-1, true, false); + } + if (event) + event.preventDefault(); + } + focusOut(event) { + if (!this.list.contains(event.relatedTarget)) { + this.clearSelection(); + } + } + onClick(event) { + let item = event.target.closest('li'); + let index = this.items.indexOf(item) + if (index !== -1) { + this.clearSelection(); + this.selectItem(index, true); + } + }; + focusNextItem(startIndex, increment, addToSelection) { + if (startIndex == this.selectAllIndex && this.selectAllItem && addToSelection) { + return; + } + + let delta = increment ? 1 : -1; + let index = startIndex + delta; + let focusedIndexMaybe; + if (index == this.selectAllIndex && this.selectAllItem && !addToSelection) { + focusedIndexMaybe = index; + } else { + if (index < 0 || index >= this.items.length) + return; + + // Get next visible item + for (let i = startIndex + delta; i < this.items.length; i+= delta) { + let itemMaybe = this.items[i]; + if (getComputedStyle(itemMaybe).display === 'none') + continue; + + focusedIndexMaybe = i; + break; + } + } + + if (typeof focusedIndexMaybe !== 'undefined') { + if (!addToSelection) { + this.clearSelection(); + } + + let isNewlySelected = this.selectItem(focusedIndexMaybe, true); + console.log('isNewlySelected: ' + isNewlySelected + ', newSelection: ' + focusedIndexMaybe + ', addToSelection: ' + addToSelection); + if (addToSelection && !isNewlySelected && startIndex >= 0 && startIndex < this.items.length) { + let previous = this.items[startIndex]; + previous.classList.remove('selected'); + this.selectedItems.splice(this.selectedItems.indexOf(previous), 1); + } + } + } + selectItem(index, setFocus) { + let item; + if (index == this.selectAllIndex && this.selectAllItem) { + item = this.selectAllItem; + } else { + if (index < 0 || index >= this.items.length) + return; + + item = this.items[index]; + } + + if (setFocus) { + item.focus(); + this.focusedIndex = index; + } + + if (this.selectedItems.includes(item)) + return false; + + item.classList.add('selected'); + this.selectedItems.push(item); + return true; + } + clearSelection() { + for (let item of this.selectedItems) { + item.classList.remove('selected'); + } + + this.selectedItems = []; + } + } + + (function () { + window.addEventListener('DOMContentLoaded', function () { + onLoad(); + }); + + function onLoad() + { + let categoryList = new FilterList(document.getElementById('category-list'), filterTable); + let optionList = new FilterList(document.getElementById('option-list'), filterTable, true); + } + })(); + + const hideDisabledStyle = document.createElement('style'); + hideDisabledStyle.innerHTML = ` + .table-data-row.disabled { + display: none; + }`; + const hideEnabledStyle = document.createElement('style'); + hideEnabledStyle.innerHTML = ` + .table-data-row:not(.disabled) { + display: none; + }`; +"; + + /// + /// Generate an HTML report + /// + /// P/Invoke information + /// Report title + /// Report subtitle + /// HTML text + public static string Generate(PInvokeDump dump, string title, string subtitle) + { + IReadOnlyDictionary> importsByAssemblyPath = dump.MethodsByAssemblyPath; + + string[] headers = { "Name", "Return", "Arguments" }; + var tableHeader = new StringBuilder(); + tableHeader.Append(@$""); + foreach (var header in headers) + tableHeader.Append(@$"{header}"); + + var tableBody = new StringBuilder(); + foreach ((string assemblyPath, IReadOnlyCollection importedMethods) in importsByAssemblyPath) + { + if (importedMethods.Count > 0) + { + tableBody.Append(@$"{System.IO.Path.GetFileName(assemblyPath)} (total: {importedMethods.Count})"); + foreach (var method in importedMethods) + { + var indirectionValues = method.ArgumentTypes.Select(t => t.Indirection).Append(method.ReturnType.Indirection).Distinct(); + var nameValues = method.ArgumentTypes.Select(t => $"{t.Name}").Append(method.ReturnType.Name).Distinct(); + tableBody.Append( + CreateTableRow( + categoryFilter: string.Join(' ', indirectionValues), + optionFilter: string.Join(' ', nameValues), + $"{method.EnclosingTypeName}
{method.MethodName}", + method.ReturnType.ToString(), + string.Join("
", method.ArgumentTypes.Select(t => t.ToString())))); + } + } + } + + string filters = CreateFilters(Enum.GetValues().Select(i => i.ToString()), dump.AllTypeNames.OrderBy(s => s)); + + int total = dump.Count; + return @$" + + + + {title} - {subtitle} + + + + + +
{title}
+
{subtitle}
+
+
+ + + + {tableHeader} + + + + {tableBody} + +
+
+
+
+
+
+
Total
+
{total}
+
+
+
+
✔️
+
Enabled
+
{total}
+
100 %
+
+
+
+
Disabled
+
0
+
0 %
+
+
+
+ + +
+ {filters} +
+
+ + +"; + } + + private static string CreateTableRow(string categoryFilter, string optionFilter, params string[] cellValues) + { + var tableRow = new StringBuilder(); + tableRow.AppendLine($@""); + tableRow.Append(@"❌"); + tableRow.Append(@"✔️"); + foreach (string value in cellValues) + tableRow.Append($"{value}"); + + tableRow.AppendLine(""); + return tableRow.ToString(); + } + + private static string CreateFilters(IEnumerable categories, IEnumerable options) + { + var categoryListItems = new StringBuilder(); + foreach (var category in categories) + { + categoryListItems.Append(CreateFilterListItem(category, "category")); + } + + var optionListItems = new StringBuilder(); + optionListItems.Append(@$" +
  • + +
  • "); + foreach (string option in options) + { + optionListItems.Append(CreateFilterListItem(option, "option")); + } + + return @$" +
    + Categories: +
      + {categoryListItems} +
    +
    +
    + Options: + +
      + {optionListItems} +
    +
    + "; + } + + private static string CreateFilterListItem(string text, string type) + { + return @$" +
  • + +
  • "; + } + } +} diff --git a/DllImportGenerator/Tools/PInvokeDump/Reporting/Text.cs b/DllImportGenerator/Tools/PInvokeDump/Reporting/Text.cs new file mode 100644 index 000000000000..c4f4894e2276 --- /dev/null +++ b/DllImportGenerator/Tools/PInvokeDump/Reporting/Text.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DllImportGenerator.Tools.Reporting +{ + internal static class Text + { + public static string Generate(PInvokeDump dump) + { + var result = new StringBuilder(); + foreach ((string assemblyPath, IReadOnlyCollection importedMethods) in dump.MethodsByAssemblyPath) + { + result.AppendLine($"{assemblyPath} - total: {importedMethods.Count}"); + if (importedMethods.Count == 0) + continue; + + foreach (var method in importedMethods) + { + result.AppendLine($" {method.EnclosingTypeName}{Type.Delimiter}{method.MethodName}"); + result.AppendLine($" {method.ReturnType} ({string.Join(", ", method.ArgumentTypes.Select(t => t.ToString()))})"); + } + } + + result.AppendLine(); + result.AppendLine("All types:"); + foreach (string type in dump.AllTypeNames.OrderBy(t => t)) + result.AppendLine($" {type}"); + + return result.ToString(); + } + } +}