From 503507422d187df12dcbf0430e702e12961a4d5c Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Sun, 28 Nov 2021 17:50:17 +0100 Subject: [PATCH] Added prototype for converting message classes into example JSON --- .../AssemblyLoaderMessageParser.cs | 133 ++++++++++++++ .../MetadataReaderMessageParser.cs | 164 ++++++++++++++++++ .../ParseTapetiMessagesPrototype.csproj | 12 ++ ParseTapetiMessagesPrototype/Program.cs | 21 +++ PettingZoo.sln | 10 +- 5 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs create mode 100644 ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs create mode 100644 ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj create mode 100644 ParseTapetiMessagesPrototype/Program.cs diff --git a/ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs b/ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs new file mode 100644 index 0000000..1bfbc66 --- /dev/null +++ b/ParseTapetiMessagesPrototype/AssemblyLoaderMessageParser.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Newtonsoft.Json.Linq; + +namespace ParseTapetiMessagesPrototype +{ + public static class AssemblyLoaderMessageParser + { + public static void ParseAssembly(string classLibraryFilename) + { + var loadContext = new AssemblyLoadContext(null, true); + try + { + var assembly = loadContext.LoadFromAssemblyPath(classLibraryFilename); + + foreach (var assemblyType in assembly.GetTypes()) + HandleType(assemblyType); + } + finally + { + loadContext.Unload(); + } + } + + + private static void HandleType(Type type) + { + if (!type.IsClass) + return; + + // For this prototype, filter out anything not ending in Message + // Might want to show a full tree in PettingZoo since this is just a convention + if (!type.Name.EndsWith("Message") || type.Name != "RelatieUpdateMessage") + return; + + Console.WriteLine($"{type.Namespace}.{type.Name}"); + + // We can't create an instance of the type to serialize easily, as most will depend on + // assemblies not included in the NuGet package, so we'll parse the Type ourselves. + // This is still slightly easier than using MetadataReader, as we can more easily check against + // standard types like Nullable. + // + // The only external dependencies should be the attributes, like [RequiredGuid]. The messaging models + // themselves should not inherit from classes outside of their assembly, or include properties + // with types from other assemblies. With that assumption, walking the class structure should be safe. + var serialized = TypeToJObject(type); + + Console.WriteLine(serialized); + Console.WriteLine(""); + } + + + private static JObject TypeToJObject(Type type) + { + var result = new JObject(); + + foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + // Note: unfortunately we can not call GetCustomAttributes here, as that would + // trigger assemblies not included in the package to be loaded + + var value = PropertyToJToken(propertyInfo.PropertyType); + result.Add(propertyInfo.Name, value); + } + + return result; + } + + + private static readonly Dictionary TypeMap = new() + { + { typeof(short), 0 }, + { typeof(ushort), 0 }, + { typeof(int), 0 }, + { typeof(uint), 0 }, + { typeof(long), 0 }, + { typeof(ulong), 0 }, + { typeof(decimal), 0.0 }, + { typeof(float), 0.0 }, + { typeof(bool), false } + }; + + + private static JToken PropertyToJToken(Type propertyType) + { + var actualType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + + + // String is also a class + if (actualType == typeof(string)) + return ""; + + + if (actualType.IsClass) + { + // IEnumerable + var enumerableInterface = actualType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + + if (enumerableInterface != null) + return new JArray(TypeToJObject(enumerableInterface.GetGenericArguments()[0])); + + + return TypeToJObject(actualType); + } + + if (actualType.IsArray) + return new JArray(TypeToJObject(actualType.GetElementType())); + + if (actualType.IsEnum) + return Enum.GetNames(actualType).FirstOrDefault(); + + + // Special cases for runtime generated values + if (actualType == typeof(DateTime)) + { + // Strip the milliseconds for a cleaner result + var now = DateTime.UtcNow; + return new DateTime(now.Ticks - now.Ticks % TimeSpan.TicksPerSecond, now.Kind); + } + + if (actualType == typeof(Guid)) + return Guid.NewGuid().ToString(); + + return TypeMap.TryGetValue(actualType, out var mappedToken) + ? mappedToken + : $"(unknown type: {actualType.Name})"; + } + } +} diff --git a/ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs b/ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs new file mode 100644 index 0000000..f3081cb --- /dev/null +++ b/ParseTapetiMessagesPrototype/MetadataReaderMessageParser.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace ParseTapetiMessagesPrototype +{ + public static class MetadataReaderMessageParser + { + public static void ParseAssembly(string classLibraryFilename) + { + try + { + using var fileStream = new FileStream(classLibraryFilename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var peReader = new PEReader(fileStream); + + var metadataReader = peReader.GetMetadataReader(); + + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var typeDefinitionHandle in metadataReader.TypeDefinitions) + { + var typeDefinition = metadataReader.GetTypeDefinition(typeDefinitionHandle); + HandleTypeDefinition(metadataReader, typeDefinition); + } + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } + + + private static void HandleTypeDefinition(MetadataReader metadataReader, TypeDefinition typeDefinition) + { + var typeNamespace = metadataReader.GetString(typeDefinition.Namespace); + var typeName = metadataReader.GetString(typeDefinition.Name); + + // For this prototype, filter out anything not ending in Message + // Might want to show a full tree in PettingZoo since this is just a convention + if (!typeName.EndsWith("Message")) + return; + + Console.WriteLine($"{typeNamespace}.{typeName}"); + + var example = new JObject(); + + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var propertyDefinitionHandle in typeDefinition.GetProperties()) + { + // TODO get properties from base class + + var propertyDefinition = metadataReader.GetPropertyDefinition(propertyDefinitionHandle); + HandlePropertyDefinition(metadataReader, propertyDefinition, example); + } + + Console.WriteLine(example.ToString()); + Console.WriteLine(); + } + + private static void HandlePropertyDefinition(MetadataReader metadataReader, PropertyDefinition propertyDefinition, JObject targetObject) + { + var fieldName = metadataReader.GetString(propertyDefinition.Name); + var signature = propertyDefinition.DecodeSignature(new JsonSignatureProvider(), null); + + targetObject.Add(fieldName, signature.ReturnType); + } + + + private class JsonSignatureProvider : ISignatureTypeProvider + { + public JToken GetPrimitiveType(PrimitiveTypeCode typeCode) + { + return typeCode switch + { + PrimitiveTypeCode.Boolean => false, + + PrimitiveTypeCode.Byte or + PrimitiveTypeCode.Int16 or + PrimitiveTypeCode.Int32 or + PrimitiveTypeCode.Int64 or + PrimitiveTypeCode.IntPtr or + PrimitiveTypeCode.SByte or + PrimitiveTypeCode.UInt16 or + PrimitiveTypeCode.UInt32 or + PrimitiveTypeCode.UInt64 or + PrimitiveTypeCode.UIntPtr => 0, + + PrimitiveTypeCode.Char or + PrimitiveTypeCode.String => "", + + PrimitiveTypeCode.Double or + PrimitiveTypeCode.Single => 0.0, + + // TODO recurse + PrimitiveTypeCode.Object => "OBJECT", + + _ => $"Unsupported primitive type code: {typeCode}" + }; + } + + public JToken GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind = 0) => "typedef"; + + public JToken GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind = 0) + { + var typeReference = reader.GetTypeReference(handle); + var typeName = reader.GetString(typeReference.Name); + + return typeName; + } + + + public JToken GetTypeFromSpecification(MetadataReader reader, object genericContext, TypeSpecificationHandle handle, byte rawTypeKind = 0) => "typespec"; + + public JToken GetSZArrayType(JToken elementType) => new JValue(elementType + "[]"); + public JToken GetPointerType(JToken elementType) => null; + public JToken GetByReferenceType(JToken elementType) => null; + public JToken GetGenericMethodParameter(object genericContext, int index) => "!!" + index; + public JToken GetGenericTypeParameter(object genericContext, int index) => "!" + index; + + public JToken GetPinnedType(JToken elementType) => elementType + " pinned"; + public JToken GetGenericInstantiation(JToken genericType, ImmutableArray typeArguments) => genericType + "<" + string.Join(",", typeArguments) + ">"; + public JToken GetModifiedType(JToken modifierType, JToken unmodifiedType, bool isRequired) => unmodifiedType + (isRequired ? " modreq(" : " modopt(") + modifierType + ")"; + + public JToken GetArrayType(JToken elementType, ArrayShape shape) + { + var builder = new StringBuilder(); + + builder.Append(elementType); + builder.Append('['); + + for (int i = 0; i < shape.Rank; i++) + { + int lowerBound = 0; + + if (i < shape.LowerBounds.Length) + { + lowerBound = shape.LowerBounds[i]; + builder.Append(lowerBound); + } + + builder.Append("..."); + + if (i < shape.Sizes.Length) + { + builder.Append(lowerBound + shape.Sizes[i] - 1); + } + + if (i < shape.Rank - 1) + { + builder.Append(','); + } + } + + builder.Append(']'); + return builder.ToString(); + } + + public JToken GetFunctionPointerType(MethodSignature signature) => "methodptr(something)"; + } + } +} diff --git a/ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj b/ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj new file mode 100644 index 0000000..b84e63f --- /dev/null +++ b/ParseTapetiMessagesPrototype/ParseTapetiMessagesPrototype.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/ParseTapetiMessagesPrototype/Program.cs b/ParseTapetiMessagesPrototype/Program.cs new file mode 100644 index 0000000..9b753ce --- /dev/null +++ b/ParseTapetiMessagesPrototype/Program.cs @@ -0,0 +1,21 @@ +namespace ParseTapetiMessagesPrototype +{ + public class Program + { + public static void Main() + { + const string classLibraryFilename = "D:\\Temp\\lib\\netstandard2.0\\Messaging.Relatie.dll"; + + // There are advantages to using the MetadataReader, for example no code is run (LoadAssemblyForReflection is no longer + // supported in .NET Core) and the assembly is not locked at all. This comes at the cost of complexity however, so + // this prototype explores both options. + // + // In the final version perhaps we can work around loading the assembly into our own process by spawning a new process + // to convert it into metadata used by the main process. + + //MetadataReaderMessageParser.ParseAssembly(classLibraryFilename); + + AssemblyLoaderMessageParser.ParseAssembly(classLibraryFilename); + } + } +} diff --git a/PettingZoo.sln b/PettingZoo.sln index 642c3a1..f4d15a5 100644 --- a/PettingZoo.sln +++ b/PettingZoo.sln @@ -10,9 +10,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParseTapetiMessagesPrototype", "ParseTapetiMessagesPrototype\ParseTapetiMessagesPrototype.csproj", "{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -32,6 +34,10 @@ Global {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Debug|Any CPU.Build.0 = Debug|Any CPU {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.ActiveCfg = Release|Any CPU {220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.Build.0 = Release|Any CPU + {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE