diff --git a/Disasmo/DisasmoSymbolInfo.cs b/Disasmo/DisasmoSymbolInfo.cs index 52527b7..d67efdf 100644 --- a/Disasmo/DisasmoSymbolInfo.cs +++ b/Disasmo/DisasmoSymbolInfo.cs @@ -2,14 +2,16 @@ public class DisasmoSymbolInfo { - public DisasmoSymbolInfo(string target, string className, string methodName) + public DisasmoSymbolInfo(string target, string className, string methodName, string genericArguments) { Target = target; ClassName = className; MethodName = methodName; + GenericArguments = genericArguments; } public string Target { get; } public string ClassName { get; } public string MethodName { get; } + public string GenericArguments { get; } } \ No newline at end of file diff --git a/Disasmo/Resources/DisasmoLoader4.cs_template b/Disasmo/Resources/DisasmoLoader4.cs_template index 320a3b7..01b8be0 100644 --- a/Disasmo/Resources/DisasmoLoader4.cs_template +++ b/Disasmo/Resources/DisasmoLoader4.cs_template @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -10,6 +13,8 @@ using System.Runtime.Loader; public class DisasmoLoader { + private static bool _shownGenericsMessage; + public static void Main(string[] args) { PrecompileAllMethodsInType(args); @@ -28,11 +33,16 @@ public class DisasmoLoader var alc = new AssemblyLoadContext("DisasmoALC", unloadable == "True"); Assembly asm = alc.LoadFromAssemblyPath(Path.Combine(Environment.CurrentDirectory, assemblyName)); + var generics = new Dictionary>(); + var genericArgs = ParseGenericsArgs(args[4], alc); Type fastType = asm.GetType(typeName); if (fastType != null) { - PrecompileMethods(fastType, methodName); - PrecompileProperties(fastType, methodName); + foreach (var instance in MakeGenericInstances(fastType, generics, genericArgs)) + { + PrecompileMethods(instance, methodName, generics, genericArgs); + PrecompileProperties(instance, methodName, generics, genericArgs); + } return; } @@ -44,47 +54,47 @@ public class DisasmoLoader // This is the easiest solution to that problem if (type.FullName?.Replace('+', '.').Contains(typeName) == true) { - PrecompileMethods(type, methodName); - PrecompileProperties(type, methodName); + foreach (var instance in MakeGenericInstances(type, generics, genericArgs)) + { + PrecompileMethods(instance, methodName, generics, genericArgs); + PrecompileProperties(instance, methodName, generics, genericArgs); + } } } } - private static void PrecompileProperties(Type type, string propertyName) + private static void PrecompileProperties(Type type, string propertyName, Dictionary> generics, Dictionary> genericArgs) { foreach (PropertyInfo propInfo in type.GetProperties((BindingFlags)60)) { if (propInfo.Name == "*" || propInfo.Name == propertyName) { if (propInfo.GetMethod != null) - RuntimeHelpers.PrepareMethod(propInfo.GetMethod.MethodHandle); + Prepare(propInfo.GetMethod, generics, genericArgs); if (propInfo.SetMethod != null) - RuntimeHelpers.PrepareMethod(propInfo.SetMethod.MethodHandle); + Prepare(propInfo.SetMethod, generics, genericArgs); } } } - private static void PrecompileMethods(Type type, string methodName) + private static void PrecompileMethods(Type type, string methodName, Dictionary> generics, Dictionary> genericArgs) { foreach (MethodBase method in type.GetMethods((BindingFlags)60).Concat( type.GetConstructors((BindingFlags)60).Select(c => (MethodBase)c))) { - if (method.IsGenericMethod) - continue; - try { if (method.DeclaringType == type || method.DeclaringType == null) { if (methodName == "*" || method.Name == methodName) { - RuntimeHelpers.PrepareMethod(method.MethodHandle); + Prepare(method, generics, genericArgs); } else if (method.Name.Contains(">g__" + methodName)) { // Special case for local functions - RuntimeHelpers.PrepareMethod(method.MethodHandle); + Prepare(method, generics, genericArgs); } } } @@ -93,4 +103,346 @@ public class DisasmoLoader } } } + + private static void Prepare(MethodBase method, Dictionary> generics, Dictionary> genericArgs) + { + if (method is MethodInfo) + { + foreach (var instance in MakeGenericInstances((MethodInfo)method, generics, genericArgs)) + { + try + { + RuntimeHelpers.PrepareMethod(instance.MethodHandle); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + } + else + { + try + { + RuntimeHelpers.PrepareMethod(method.MethodHandle); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + } + + private static Dictionary> ParseGenericsArgs(ReadOnlySpan data, AssemblyLoadContext alc) + { + var result = new Dictionary>(); + while (!data.IsEmpty) + { + int eq = data.IndexOf('='); + string name = data[..eq].ToString(); + data = data[eq..]; + List types = new List(); + while (data[0] != ';') + { + data = data[1..]; + types.Add(ParseType(ref data, alc)); + } + + if (!types.Contains(null)) + { + if (!result.ContainsKey(name)) + { + result[name] = new List() { types.ToArray() }; + } + else + { + result[name].Add(types.ToArray()); + } + } + + data = data[1..]; + } + + return result; + } + + private static Type ParseType(ref ReadOnlySpan data, AssemblyLoadContext alc) + { + int nameEnd = data.IndexOfAny("[],;"); + string name = data[..nameEnd].ToString(); + Type type = FindType(name, alc); + data = data[nameEnd..]; + + if (data[0] == ']' || data[0] == ',' || data[0] == ';') + { + return type; + } + + int argEnd = data.IndexOf(']'); + + if (argEnd == 1) + { + data = data[2..]; + return type?.MakeArrayType(); + } + + if (argEnd == 2 && data[1] == '*') + { + data = data[3..]; + return type?.MakePointerType(); + } + + if (int.TryParse(data[1..argEnd], NumberStyles.Integer, CultureInfo.InvariantCulture, out int rank)) + { + data = data[(argEnd + 1)..]; + return type?.MakeArrayType(rank); + } + + List args = new List(); + + while (data[0] != ']') + { + data = data[1..]; + args.Add(ParseType(ref data, alc)); + } + + data = data[1..]; + if (args.Contains(null)) + { + return null; + } + return type?.MakeGenericType(args.ToArray()); + } + + private static Type MakeNullableType(Type type, bool nullable, AssemblyLoadContext alc) + { + if (!type.IsValueType || !nullable) return type; + return FindType("System.Nullable`1", alc).MakeGenericType(type); + } + + private static Type FindType(string name, AssemblyLoadContext alc) + { + bool nullable = name.EndsWith("?"); + if (nullable) name = name.TrimEnd('?'); + + name = name switch + { + "bool" => "System.Boolean", + "byte" => "System.Byte", + "sbyte" => "System.SByte", + "char" => "System.Char", + "decimal" => "System.Decimal", + "double" => "System.Double", + "float" => "System.Single", + "int" => "System.Int32", + "uint" => "System.UInt32", + "nint" => "System.IntPtr", + "nuint" => "System.UIntPtr", + "long" => "System.Int64", + "ulong" => "System.UInt64", + "short" => "System.Int16", + "ushort" => "System.UInt16", + "object" => "System.Object", + "string" => "System.String", + _ => name + }; + + foreach (Assembly assembly in alc.Assemblies) + { + Type type = assembly.GetType(name); + if (type != null) + { + return MakeNullableType(type, nullable, alc); + } + } + + foreach (Assembly assembly in AssemblyLoadContext.Default.Assemblies) + { + Type type = assembly.GetType(name); + if (type != null) + { + return MakeNullableType(type, nullable, alc); + } + } + + return null; + } + + private static IEnumerable MakeGenericInstances(Type type, Dictionary> arguments, Dictionary> genericArgs) + { + if (!type.IsGenericType) + { + yield return type; + yield break; + } + + var hasVariant = false; + type = type.GetGenericTypeDefinition(); + var variants = MakeGenericVariants(type, arguments, genericArgs); + + foreach (var variant in variants) + { + if (variant.Length != type.GetGenericArguments().Length) + { + Console.WriteLine($"Type {type} has {type.GetGenericArguments().Length} generic parameters, but {variant.Length} arguments were provided."); + continue; + } + + Type instance = null; + try + { + instance = type.MakeGenericType(variant); + } + catch (ArgumentException ex) + { + // probably fails some constraint + Console.WriteLine($"Failed to apply arguments {string.Join(", ", variant.Select(t => t.Name))} to type {type}: {ex.Message}"); + continue; + } + + if (instance != null) + { + hasVariant = true; + yield return instance; + } + } + + if (!hasVariant) + { + Console.WriteLine("Unable to create instance for type " + type + "."); + GenericsInfoMessage(); + } + } + + private static IEnumerable MakeGenericInstances(MethodBase method, Dictionary> arguments, Dictionary> genericArgs) + { + if (method is not MethodInfo info || !method.IsGenericMethod) + { + yield return method; + yield break; + } + + var hasVariant = false; + info = info.GetGenericMethodDefinition(); + var variants = MakeGenericVariants(info, arguments, genericArgs); + + foreach (var variant in variants) + { + if (variant.Length != info.GetGenericArguments().Length) + { + Console.WriteLine($"Method {info} has {info.GetGenericArguments().Length} generic parameters, but {variant.Length} arguments were provided."); + continue; + } + + MethodInfo instance = null; + try + { + instance = info.MakeGenericMethod(variant); + } + catch (ArgumentException ex) + { + // probably fails some constraint + Console.WriteLine($"Failed to apply arguments {string.Join(", ", variant.Select(t => t.Name))} to type {info}: {ex.Message}"); + continue; + } + + if (instance != null) + { + hasVariant = true; + yield return instance; + } + } + + if (!hasVariant) + { + Console.WriteLine("Unable to create instance for method " + method + "."); + GenericsInfoMessage(); + } + } + + private static void GenericsInfoMessage() + { + if (_shownGenericsMessage) return; + _shownGenericsMessage = true; + + Console.WriteLine("Generic parameters for types and methods can be specified by prefixing them with a comment like this:"); + Console.WriteLine("// Disasmo-Generic: System.Int32,System.String"); + Console.WriteLine("// Disasmo-Generic: System.Byte,System.Single"); + Console.WriteLine("void Foo() { }"); + Console.WriteLine("Types can either be specified by their C# alias or FullName. Generic types take their arguments in brackets:"); + Console.WriteLine("// Disasmo-Generic: System.ValueTuple`2[System.Int32,string]"); + Console.WriteLine("A ? suffix is short for a nullable value type. Arrays are specified by appending the number of dimensions in bracket or empty brackets for SZArrays:"); + Console.WriteLine("// Disasmo-Generic: System.Int32?,int[],byte[4]"); + + } + + private static List MakeGenericVariants(Type type, Dictionary> arguments, Dictionary> genericArgs) + { + if (genericArgs.TryGetValue(type.FullName, out var result)) return result; + return MakeGenericVariants(type.GetGenericArguments(), arguments); + } + + private static List MakeGenericVariants(MethodInfo info, Dictionary> arguments, Dictionary> genericArgs) + { + if (genericArgs.TryGetValue(info.Name, out var result)) return result; + return MakeGenericVariants(info.GetGenericArguments(), arguments); + } + + private static List MakeGenericVariants(Type[] parameters, Dictionary> arguments) + { + var results = new List() { new Type[parameters.Length] }; + + arguments.TryGetValue("*", out var applyAlways); + + for (int i = 0; i < parameters.Length; i++) + { + arguments.TryGetValue(parameters[i].Name, out var applyArgument); + + var count = (applyAlways?.Count ?? 0) + (applyArgument?.Count ?? 0); + + if (count == 0) + { + Console.WriteLine("No arguments to apply for generic parameter " + parameters[i].Name); + results.Clear(); // cross product will be empty, don't return to print this warning potentially multiple times + } + else if (count == 1) + { + var argument = applyAlways?[0] ?? applyArgument?[0]; + + foreach (var variant in results) + { + variant[i] = argument; + } + } + else + { + var preResults = results; + results = new List(); + + foreach (var variant in preResults) + { + if (applyAlways != null) + { + foreach (var argument in applyAlways) + { + var copy = variant.ToArray(); + copy[i] = argument; + results.Add(copy); + } + } + if (applyArgument != null) + { + foreach (var argument in applyArgument) + { + var copy = variant.ToArray(); + copy[i] = argument; + results.Add(copy); + } + } + } + } + } + + return results; + } } diff --git a/Disasmo/Utils/SymbolUtils.cs b/Disasmo/Utils/SymbolUtils.cs index 7b197c7..50d0baa 100644 --- a/Disasmo/Utils/SymbolUtils.cs +++ b/Disasmo/Utils/SymbolUtils.cs @@ -1,14 +1,17 @@ -using Microsoft.CodeAnalysis; +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace Disasmo.Utils; -public class SymbolUtils +public static class SymbolUtils { public static DisasmoSymbolInfo FromSymbol(ISymbol symbol) { string target; string hostType; string methodName; + string genericArguments; if (symbol is IMethodSymbol ms) { @@ -16,35 +19,114 @@ public static DisasmoSymbolInfo FromSymbol(ISymbol symbol) { // hack for mangled names target = "*" + symbol.Name + "*"; - hostType = symbol.ContainingType.ToString(); + hostType = symbol.ContainingType.MetadataName; methodName = "*"; } else if (ms.MethodKind == MethodKind.Constructor) { - target = "*" + symbol.ContainingType.Name + ":.ctor"; - hostType = symbol.ContainingType.ToString(); + target = "*" + symbol.ContainingType.MetadataName + ":.ctor"; + hostType = symbol.ContainingType.MetadataName(); methodName = "*"; } else { - target = "*" + symbol.ContainingType.Name + ":" + symbol.Name; - hostType = symbol.ContainingType.ToString(); + target = "*" + symbol.ContainingType.MetadataName + ":" + symbol.Name; + hostType = symbol.ContainingType.MetadataName(); methodName = symbol.Name; } + + genericArguments = GenericsForSymbol(symbol); } else if (symbol is IPropertySymbol prop) { - target = "*" + symbol.ContainingType.Name + ":get_" + symbol.Name + " " + "*" + symbol.ContainingType.Name + ":set_" + symbol.Name; - hostType = symbol.ContainingType.ToString(); + target = "*" + symbol.ContainingType.MetadataName + ":get_" + symbol.Name + " " + "*" + symbol.ContainingType.MetadataName + ":set_" + symbol.Name; + hostType = symbol.ContainingType.MetadataName(); methodName = symbol.Name; + genericArguments = (prop.GetMethod, prop.SetMethod) switch + { + (not null, not null) => GenericsForSymbol(prop.GetMethod, false, false) + GenericsForSymbol(prop.SetMethod), + (null, not null) => GenericsForSymbol(prop.GetMethod), + (not null, null) => GenericsForSymbol(prop.SetMethod), + _ => "" + }; } else { // the whole class target = symbol.Name + ":*"; - hostType = symbol.ToString(); + hostType = symbol.MetadataName; methodName = "*"; + genericArguments = GenericsForSymbol(symbol); + } + return new DisasmoSymbolInfo(target, hostType, methodName, genericArguments); + } + + private static string MetadataName(this INamedTypeSymbol type) + { + string name = ""; + if (type.ContainingType != null) + { + name = type.ContainingType.MetadataName() + "+"; + } + else if (type.ContainingNamespace != null && !type.ContainingNamespace.IsGlobalNamespace) + { + name = type.ContainingNamespace.ToString() + "."; + } + return name + type.MetadataName; + } + + private static string GenericsForSymbol(ISymbol symbol, bool includeNested = true, bool includeContaining = true) + { + const string MARKER = "Disasmo-Generic:"; + + if (symbol is null) + { + return ""; + } + + string result = ""; + + foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) + { + foreach (var trivia in syntaxReference.GetSyntax().GetLeadingTrivia()) + { + if (!trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) && !trivia.IsKind(SyntaxKind.MultiLineCommentTrivia)) continue; + + var str = trivia.ToFullString().AsSpan(); + while (!str.IsEmpty) + { + var idxStart = str.IndexOf(MARKER.AsSpan(), StringComparison.OrdinalIgnoreCase); + if (idxStart < 0) break; + var len = str.Slice(idxStart + MARKER.Length).IndexOf('\n'); + if (len < 0) len = str.Length - idxStart - MARKER.Length; + result = result + SymbolName(symbol) + "=" + str.Slice(idxStart + MARKER.Length, len).Trim().ToString().Replace(" ", "") + ";"; + str = str.Slice(idxStart + MARKER.Length + len); + } + } } - return new DisasmoSymbolInfo(target, hostType, methodName); + + if (includeContaining) + { + result += GenericsForSymbol(symbol.ContainingType, false); + } + + if (includeNested && symbol is ITypeSymbol typeSymbol) + { + foreach (var member in typeSymbol.GetMembers()) + { + if (member is IMethodSymbol) + { + result += GenericsForSymbol(member, false, false); + } + } + } + + return result; + } + + private static string SymbolName(ISymbol symbol) + { + if (symbol is INamedTypeSymbol namedTypeSymbol) return namedTypeSymbol.MetadataName(); + return symbol.MetadataName; } } \ No newline at end of file diff --git a/src/Vsix/ViewModels/MainViewModel.cs b/src/Vsix/ViewModels/MainViewModel.cs index 0bf65d9..c24c5c4 100644 --- a/src/Vsix/ViewModels/MainViewModel.cs +++ b/src/Vsix/ViewModels/MainViewModel.cs @@ -225,7 +225,7 @@ public async Task RunFinalExe(DisasmoSymbolInfo symbolInfo) envVars["DOTNET_JitDumpFgFile"] = currentFgFile; } - string command = $"\"{LoaderAppManager.DisasmoLoaderName}.dll\" \"{fileName}.dll\" \"{symbolInfo.ClassName}\" \"{symbolInfo.MethodName}\" {SettingsVm.UseUnloadableContext}"; + string command = $"\"{LoaderAppManager.DisasmoLoaderName}.dll\" \"{fileName}.dll\" \"{symbolInfo.ClassName}\" \"{symbolInfo.MethodName}\" {SettingsVm.UseUnloadableContext} \"{symbolInfo.GenericArguments}\""; if (SettingsVm.RunAppMode) { command = $"\"{fileName}.dll\""; @@ -535,13 +535,6 @@ public async void RunOperationAsync(ISymbol symbol) clrCheckedFilesDir = dir; } - if (symbol is IMethodSymbol { IsGenericMethod: true }) - { - // TODO: ask user to specify type parameters - Output = "Generic methods are not supported yet."; - return; - } - ThrowIfCanceled(); // Find Release-x64 configuration: