Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get P/Invoke information from assembly metadata #146

Merged
merged 1 commit into from
Oct 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions DllImportGenerator/DllImportGenerator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
Expand Down
187 changes: 187 additions & 0 deletions DllImportGenerator/Tools/PInvokeDump/PInvokeDump.TypeProviders.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Information about return/argument type in a P/Invoke
/// </summary>
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 { }

/// <summary>
/// Simple type provider for decoding a method signature
/// </summary>
private class TypeProvider : ISignatureTypeProvider<ParameterInfo, UnusedGenericContext>
{
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<ParameterInfo> signature)
{
return new ParameterInfo(
ParameterInfo.IndirectKind.FunctionPointer,
$"method {signature.ReturnType} ({string.Join(", ", signature.ParameterTypes.Select(t => t.ToString()))})");
}

public ParameterInfo GetGenericInstantiation(ParameterInfo genericType, ImmutableArray<ParameterInfo> 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)}");
}
}
}
}
165 changes: 165 additions & 0 deletions DllImportGenerator/Tools/PInvokeDump/PInvokeDump.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// P/Invoke method information from assembly metadata
/// </summary>
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<ParameterInfo> 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;
}
}

/// <summary>
/// Class for processing assemblies to retrieve information about their P/Invokes
/// </summary>
public sealed partial class PInvokeDump
{
private readonly TypeProvider typeProvider = new TypeProvider();
private readonly Dictionary<string, IReadOnlyCollection<PInvokeMethod>> methodsByAssemblyPath = new Dictionary<string, IReadOnlyCollection<PInvokeMethod>>();
private readonly HashSet<string> allTypeNames = new HashSet<string>();

public IReadOnlySet<string> AllTypeNames => allTypeNames;
public IReadOnlyDictionary<string, IReadOnlyCollection<PInvokeMethod>> MethodsByAssemblyPath => methodsByAssemblyPath;
public int Count { get; private set; }

/// <summary>
/// Process an assembly
/// </summary>
/// <param name="assemblyFile">Assembly to process</param>
/// <returns>
/// hasMetadata: True if the assembly has metadata.
/// count: Number of P/Invoke methods found in the assembly.
/// </returns>
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<PInvokeMethod> pinvokeMethods = new List<PInvokeMethod>();
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<ParameterInfo> 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<string>() { 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;
}
}
}
13 changes: 13 additions & 0 deletions DllImportGenerator/Tools/PInvokeDump/PInvokeDump.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>DllImportGenerator.Tools</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.CommandLine" Version="[2.0.*-*, )" />
</ItemGroup>

</Project>
Loading