Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.Perforce/PerforceReflection.cs
2025-05-18 13:04:45 +08:00

677 lines
21 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Buffers.Binary;
using System.Buffers.Text;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Text.RegularExpressions;
using EpicGames.Core;
namespace EpicGames.Perforce
{
/// <summary>
/// String constants for perforce values
/// </summary>
static class StringConstants
{
public static readonly Utf8String True = new Utf8String("true");
public static readonly Utf8String False = new Utf8String("false");
public static readonly Utf8String New = new Utf8String("new");
public static readonly Utf8String None = new Utf8String("none");
public static readonly Utf8String Default = new Utf8String("default");
}
/// <summary>
/// Delegate type for creating a record instance
/// </summary>
/// <returns>New instance</returns>
public delegate object CreateRecordDelegate();
/// <summary>
/// Information about a nested record
/// </summary>
record struct NestedRecordInfo(PropertyInfo PropertyInfo, CreateRecordDelegate CreateInstance);
/// <summary>
/// Name and rank of a tagged property within a record
/// </summary>
record struct TaggedPropertyNameAndRank(Utf8String Tag, int Rank);
/// <summary>
/// Stores cached information about a property with a <see cref="PerforceTagAttribute"/> attribute.
/// </summary>
class TaggedPropertyInfo
{
/// <summary>
/// Name of the tag. Specified in the attribute or inferred from the field name.
/// </summary>
public Utf8String Tag { get; }
/// <summary>
/// Whether this tag is optional or not.
/// </summary>
public bool Optional { get; }
/// <summary>
/// List of nested records before getting to the target object
/// </summary>
public NestedRecordInfo[] ParentRecords { get; }
/// <summary>
/// The property containing the value of this data.
/// </summary>
public PropertyInfo PropertyInfo { get; }
/// <summary>
/// Writes an instance of this field from an object
/// </summary>
public Action<IMemoryWriter, object> Write { get; set; }
/// <summary>
/// Parser for this field type
/// </summary>
public Action<object, int> ReadFromInteger { get; set; }
/// <summary>
/// Parser for this field type
/// </summary>
public Action<object, Utf8String> ReadFromString { get; set; }
/// <summary>
/// Index into the bitmask of required types
/// </summary>
public ulong RequiredTagBitMask { get; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="name"></param>
/// <param name="optional"></param>
/// <param name="parentRecords"></param>
/// <param name="propertyInfo"></param>
/// <param name="requiredTagBitMask"></param>
public TaggedPropertyInfo(Utf8String name, bool optional, NestedRecordInfo[] parentRecords, PropertyInfo propertyInfo, ulong requiredTagBitMask)
{
Tag = name;
Optional = optional;
ParentRecords = parentRecords;
PropertyInfo = propertyInfo;
RequiredTagBitMask = requiredTagBitMask;
Write = (obj, writer) => throw new PerforceException($"Field {name} does not have a serializer.");
ReadFromInteger = (obj, value) => throw new PerforceException($"Field {name} was not expecting an integer value.");
ReadFromString = (obj, str) => throw new PerforceException($"Field {name} was not expecting a string value.");
}
/// <inheritdoc/>
public override string ToString()
{
StringBuilder fullName = new StringBuilder(Tag.ToString());
if (ParentRecords.Length > 0)
{
fullName.Append('0');
for (int idx = 1; idx < ParentRecords.Length; idx++)
{
fullName.Append(",0");
}
}
return fullName.ToString();
}
}
/// <summary>
/// Stores cached information about a record
/// </summary>
class CachedRecordInfo
{
/// <summary>
/// Type of the record
/// </summary>
public Type Type { get; }
/// <summary>
/// Method to construct this record
/// </summary>
public CreateRecordDelegate CreateInstance { get; }
/// <summary>
/// List of fields in the record. These should be ordered to match P4 output for maximum efficiency.
/// </summary>
public List<TaggedPropertyInfo> Properties { get; } = new List<TaggedPropertyInfo>();
/// <summary>
/// Map of name to tag info
/// </summary>
public Dictionary<TaggedPropertyNameAndRank, TaggedPropertyInfo> NameAndRankToInfo { get; set; } = new Dictionary<TaggedPropertyNameAndRank, TaggedPropertyInfo>();
/// <summary>
/// Bitmask of all the required tags. Formed by bitwise-or'ing the RequiredTagBitMask fields for each required CachedTagInfo.
/// </summary>
public ulong RequiredTagsBitMask { get; set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="type">The record type</param>
/// <param name="createInstance">Method used to create an instance of this record</param>
public CachedRecordInfo(Type type, CreateRecordDelegate createInstance)
{
Type = type;
CreateInstance = createInstance;
}
}
/// <summary>
/// Information about an enum
/// </summary>
class CachedEnumInfo
{
/// <summary>
/// The enum type
/// </summary>
public Type _enumType;
/// <summary>
/// Whether the enum has the [Flags] attribute
/// </summary>
public bool _bHasFlagsAttribute;
/// <summary>
/// Map of name to value
/// </summary>
public Dictionary<Utf8String, int> _nameToValue = new Dictionary<Utf8String, int>();
/// <summary>
/// Map of value to name
/// </summary>
public Dictionary<int, Utf8String> _valueToName = new Dictionary<int, Utf8String>();
/// <summary>
/// List of name/value pairs
/// </summary>
public List<KeyValuePair<string, int>> _nameValuePairs = new List<KeyValuePair<string, int>>();
/// <summary>
/// Constructor
/// </summary>
/// <param name="enumType">The type to construct from</param>
public CachedEnumInfo(Type enumType)
{
_enumType = enumType;
_bHasFlagsAttribute = enumType.GetCustomAttribute<FlagsAttribute>() != null;
FieldInfo[] fields = enumType.GetFields(BindingFlags.Public | BindingFlags.Static);
foreach (FieldInfo field in fields)
{
PerforceEnumAttribute? attribute = field.GetCustomAttribute<PerforceEnumAttribute>();
if (attribute != null)
{
object? value = field.GetValue(null);
if (value != null)
{
Utf8String name = new Utf8String(attribute.Name);
_nameToValue[name] = (int)value;
_valueToName[(int)value] = name;
_nameValuePairs.Add(new KeyValuePair<string, int>(attribute.Name, (int)value));
}
}
}
}
/// <summary>
/// Gets the name of a particular enum value
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public Utf8String GetName(int value) => _valueToName[value];
/// <summary>
/// Parses the given integer as an enum
/// </summary>
/// <param name="value">The value to convert to an enum</param>
/// <returns>The enum value corresponding to the given value</returns>
public object ParseInteger(int value)
{
return Enum.ToObject(_enumType, value);
}
/// <summary>
/// Parses the given text as an enum
/// </summary>
/// <param name="text">The text to parse</param>
/// <returns>The enum value corresponding to the given text</returns>
public object ParseString(Utf8String text)
{
return Enum.ToObject(_enumType, ParseToInteger(text));
}
/// <summary>
/// Parses the given text as an enum
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public int ParseToInteger(Utf8String name)
{
if (_bHasFlagsAttribute)
{
int result = 0;
for (int offset = 0; offset < name.Length;)
{
if (name.Span[offset] == (byte)' ')
{
offset++;
}
else
{
// Find the end of this name
int startOffset = ++offset;
while (offset < name.Length && name.Span[offset] != (byte)' ')
{
offset++;
}
// Take the subset
Utf8String item = name.Slice(startOffset, offset - startOffset);
// Get the value
int itemValue;
if (_nameToValue.TryGetValue(item, out itemValue))
{
result |= itemValue;
}
}
}
return result;
}
else
{
int result;
_nameToValue.TryGetValue(name, out result);
return result;
}
}
/// <summary>
/// Parses an enum value, using PerforceEnumAttribute markup for names.
/// </summary>
/// <param name="value">Value of the enum.</param>
/// <returns>Text for the enum.</returns>
public string GetEnumText(int value)
{
if (_bHasFlagsAttribute)
{
List<string> names = new List<string>();
int combinedIntegerValue = 0;
foreach (KeyValuePair<string, int> pair in _nameValuePairs)
{
if ((value & pair.Value) != 0)
{
names.Add(pair.Key);
combinedIntegerValue |= pair.Value;
}
}
if (combinedIntegerValue != value)
{
throw new ArgumentException($"Invalid enum value {value}");
}
return String.Join(" ", names);
}
else
{
string? name = null;
foreach (KeyValuePair<string, int> pair in _nameValuePairs)
{
if (value == pair.Value)
{
name = pair.Key;
break;
}
}
if (name == null)
{
throw new ArgumentException($"Invalid enum value {value}");
}
return name;
}
}
}
/// <summary>
/// Utility methods for converting to/from native types
/// </summary>
static class PerforceReflection
{
/// <summary>
/// Unix epoch; used for converting times back into C# datetime objects
/// </summary>
public static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Constant for the default changelist, where valid.
/// </summary>
public const int DefaultChange = -2;
/// <summary>
/// Cached map of enum types to a lookup mapping from p4 strings to enum values.
/// </summary>
static readonly ConcurrentDictionary<Type, CachedEnumInfo> s_enumTypeToInfo = new ConcurrentDictionary<Type, CachedEnumInfo>();
/// <summary>
/// Cached set of record
/// </summary>
static readonly ConcurrentDictionary<Type, CachedRecordInfo> s_recordTypeToInfo = new ConcurrentDictionary<Type, CachedRecordInfo>();
/// <summary>
/// Default type for info
/// </summary>
public static CachedRecordInfo InfoRecordInfo = GetCachedRecordInfo(typeof(PerforceInfo));
/// <summary>
/// Default type for errors
/// </summary>
public static CachedRecordInfo ErrorRecordInfo = GetCachedRecordInfo(typeof(PerforceError));
/// <summary>
/// Default type for errors
/// </summary>
public static CachedRecordInfo IoRecordInfo = GetCachedRecordInfo(typeof(PerforceIo));
/// <summary>
/// Serializes a sequence of objects to a stream
/// </summary>
/// <param name="obj">Object to serialize</param>
/// <param name="writer">Writer for output data</param>
public static void Serialize(object obj, IMemoryWriter writer)
{
CachedRecordInfo recordInfo = GetCachedRecordInfo(obj.GetType());
foreach (TaggedPropertyInfo tagInfo in recordInfo.Properties)
{
object? value = tagInfo.PropertyInfo.GetValue(obj);
if (value != null)
{
WriteUtf8StringWithTag(writer, tagInfo.Tag);
tagInfo.Write(writer, value!);
}
}
}
static void WriteIntegerWithTag(IMemoryWriter writer, int value)
{
Span<byte> span = writer.GetSpanAndAdvance(5);
span[0] = (byte)'i';
BinaryPrimitives.WriteInt32LittleEndian(span.Slice(1, 4), value);
}
static void WriteStringWithTag(IMemoryWriter writer, string str)
{
int length = Encoding.UTF8.GetByteCount(str);
Span<byte> span = writer.GetSpanAndAdvance(1 + length + 4);
span[0] = (byte)'s';
BinaryPrimitives.WriteInt32LittleEndian(span.Slice(1, 4), length);
Encoding.UTF8.GetBytes(str, span.Slice(5));
}
static void WriteUtf8StringWithTag(IMemoryWriter writer, Utf8String str)
{
Span<byte> span = writer.GetSpanAndAdvance(1 + str.Length + 4);
span[0] = (byte)'s';
BinaryPrimitives.WriteInt32LittleEndian(span.Slice(1, 4), str.Length);
str.Span.CopyTo(span.Slice(5));
}
/// <summary>
/// Gets a mapping of flags to enum values for the given type
/// </summary>
/// <param name="enumType">The enum type to retrieve flags for</param>
/// <returns>Map of name to enum value</returns>
static CachedEnumInfo GetCachedEnumInfo(Type enumType)
{
CachedEnumInfo? enumInfo;
if (!s_enumTypeToInfo.TryGetValue(enumType, out enumInfo))
{
enumInfo = new CachedEnumInfo(enumType);
if (!s_enumTypeToInfo.TryAdd(enumType, enumInfo))
{
enumInfo = s_enumTypeToInfo[enumType];
}
}
return enumInfo;
}
/// <summary>
/// Parses an enum value, using PerforceEnumAttribute markup for names.
/// </summary>
/// <param name="enumType">Type of the enum to parse.</param>
/// <param name="value">Value of the enum.</param>
/// <returns>Text for the enum.</returns>
public static string GetEnumText(Type enumType, object value)
{
return GetCachedEnumInfo(enumType).GetEnumText((int)value);
}
/// <summary>
/// Gets reflection data for the given record type
/// </summary>
/// <param name="recordType">The type to retrieve record info for</param>
/// <returns>The cached reflection information for the given type</returns>
public static CachedRecordInfo GetCachedRecordInfo(Type recordType)
{
CachedRecordInfo? record;
if (!s_recordTypeToInfo.TryGetValue(recordType, out record))
{
record = new CachedRecordInfo(recordType, GetCreateRecordDelegate(recordType));
// Find all the properties in the record
AddRecordProperties(recordType, record, Array.Empty<NestedRecordInfo>());
record.NameAndRankToInfo = record.Properties.ToDictionary(x => new TaggedPropertyNameAndRank(x.Tag, x.ParentRecords.Length), x => x);
// Try to save the record info, or get the version that's already in the cache
if (!s_recordTypeToInfo.TryAdd(recordType, record))
{
record = s_recordTypeToInfo[recordType];
}
}
return record;
}
static void AddRecordProperties(Type recordType, CachedRecordInfo rootRecord, NestedRecordInfo[] parentRecords)
{
// Get all the fields for this type
PropertyInfo[] properties = recordType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Build the map of all tags for this record
foreach (PropertyInfo property in properties)
{
PerforceTagAttribute? tagAttribute = property.GetCustomAttribute<PerforceTagAttribute>();
if (tagAttribute != null)
{
string tagName = tagAttribute.Name ?? property.Name;
ulong requiredTagBitMask = 0;
if (!tagAttribute.Optional && parentRecords.Length == 0)
{
requiredTagBitMask = rootRecord.RequiredTagsBitMask + 1;
if (requiredTagBitMask == 0)
{
throw new PerforceException("Too many required tags in {0}; max is {1}", recordType.Name, sizeof(ulong) * 8);
}
rootRecord.RequiredTagsBitMask |= requiredTagBitMask;
}
TaggedPropertyInfo tagInfo = new TaggedPropertyInfo(new Utf8String(tagName), tagAttribute.Optional, parentRecords, property, requiredTagBitMask);
Type fieldType = property.PropertyType;
PropertyInfo propertyCopy = property;
if (fieldType == typeof(DateTime))
{
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, new Utf8String(((long)((DateTime)value - PerforceReflection.UnixEpoch).TotalSeconds).ToString()));
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsDateTime(value));
}
else if (fieldType == typeof(bool))
{
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, ((bool)value) ? StringConstants.True : StringConstants.False);
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsBool(value));
}
else if (fieldType == typeof(Nullable<bool>))
{
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsNullableBool(value));
}
else if (fieldType == typeof(int))
{
tagInfo.Write = (writer, value) => WriteIntegerWithTag(writer, (int)value);
tagInfo.ReadFromInteger = (obj, value) => propertyCopy.SetValue(obj, value);
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsInt(value));
}
else if (fieldType == typeof(long))
{
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, new Utf8String(((long)value).ToString()));
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsLong(value));
}
else if (fieldType == typeof(string))
{
tagInfo.Write = (writer, value) => WriteStringWithTag(writer, (string)value);
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseString(value));
}
else if (fieldType == typeof(Utf8String))
{
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, (Utf8String)value);
tagInfo.ReadFromString = (obj, str) => propertyCopy.SetValue(obj, str.Clone());
}
else if (fieldType.IsEnum)
{
CachedEnumInfo enumInfo = GetCachedEnumInfo(fieldType);
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, enumInfo.GetName((int)value));
tagInfo.ReadFromInteger = (obj, value) => propertyCopy.SetValue(obj, enumInfo.ParseInteger(value));
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, enumInfo.ParseString(value));
}
else if (fieldType == typeof(DateTimeOffset?))
{
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsNullableDateTimeOffset(value));
}
else if (fieldType == typeof(List<string>))
{
tagInfo.ReadFromString = (obj, value) => ((List<string>)propertyCopy.GetValue(obj)!).Add(value.ToString());
}
else if (fieldType == typeof(ReadOnlyMemory<byte>))
{
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, value.Memory);
}
else
{
throw new PerforceException("Unsupported type of {0}.{1} for tag '{2}'", recordType.Name, fieldType.Name, tagName);
}
rootRecord.Properties.Add(tagInfo);
}
PerforceRecordListAttribute? subElementAttribute = property.GetCustomAttribute<PerforceRecordListAttribute>();
if (subElementAttribute != null)
{
Type newRecordType = property.PropertyType.GenericTypeArguments[0];
NestedRecordInfo newParentRecord = new NestedRecordInfo(property, GetCreateRecordDelegate(newRecordType));
AddRecordProperties(newRecordType, rootRecord, parentRecords.Append(newParentRecord).ToArray());
}
}
}
static CreateRecordDelegate GetCreateRecordDelegate(Type type)
{
ConstructorInfo? constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
if (constructor == null)
{
throw new PerforceException($"Unable to find default constructor for {type}");
}
DynamicMethod dynamicMethod = new DynamicMethod("_", type, null);
ILGenerator generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Newobj, constructor);
generator.Emit(OpCodes.Ret);
return (CreateRecordDelegate)dynamicMethod.CreateDelegate(typeof(CreateRecordDelegate));
}
static object ParseString(Utf8String str)
{
return str.ToString();
}
static object ParseStringAsDateTime(Utf8String str)
{
string text = str.ToString();
DateTime time;
if (DateTime.TryParse(text, out time))
{
return time;
}
else
{
return PerforceReflection.UnixEpoch + TimeSpan.FromSeconds(Int64.Parse(text));
}
}
static object ParseStringAsBool(Utf8String str)
{
return str.Length == 0 || str == StringConstants.True;
}
static object ParseStringAsNullableBool(Utf8String str)
{
return str == StringConstants.True;
}
static object ParseStringAsInt(Utf8String str)
{
int value;
int bytesConsumed;
if (Utf8Parser.TryParse(str.Span, out value, out bytesConsumed) && bytesConsumed == str.Length)
{
return value;
}
else if (str == StringConstants.New || str == StringConstants.None)
{
return -1;
}
else if (str.Length > 0 && str[0] == '#')
{
return ParseStringAsInt(str.Slice(1));
}
else if (str == StringConstants.Default)
{
return DefaultChange;
}
else
{
throw new PerforceException($"Unable to parse {str} as an integer");
}
}
static object ParseStringAsLong(Utf8String str)
{
long value;
int bytesConsumed;
if (!Utf8Parser.TryParse(str.Span, out value, out bytesConsumed) || bytesConsumed != str.Length)
{
throw new PerforceException($"Unable to parse {str} as a long value");
}
return value;
}
static object ParseStringAsNullableDateTimeOffset(Utf8String str)
{
string text = str.ToString();
return DateTimeOffset.Parse(Regex.Replace(text, "[^0-9]*$", "")); // Strip timezone name (eg. "EST")
}
}
}