Optimize some of the string operations

This commit is contained in:
LukeFZ
2023-11-30 05:13:19 +01:00
parent 9f6309fb46
commit ab841ccb2b
4 changed files with 1346 additions and 1302 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,95 +1,92 @@
/*
Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
Copyright 2020 Robert Xiao - https://robertxiao.ca
All rights reserved.
*/
using System;
using System.Collections.Generic;
namespace Il2CppInspector.Cpp
{
/// <summary>
/// A utility class for managing names in a common namespace.
/// </summary>
public class CppNamespace
{
// The central data structure that keeps track of which names have been generated
// The value for any given key K is the number of unique objects originally named K, minus 1.
// Each time we see a particular name assigned to a new, different object, we bump its rename count
// and give it a suffix. For example, if we have three different objects all named X,
// we'd name them X, X_1, and X_2, and renameCount["X"] would be 2.
private readonly Dictionary<string, int> renameCount = new Dictionary<string, int>();
// Mark a name as reserved without assigning an object to it (e.g. for keywords and built-in names)
public void ReserveName(string name) {
if (renameCount.ContainsKey(name)) {
throw new Exception($"Can't reserve {name}: already taken!");
}
renameCount[name] = 0;
}
// Try to mark a name as reserved without assigning an object to it (e.g. for keywords and built-in names)
public bool TryReserveName(string name) {
if (renameCount.ContainsKey(name))
return false;
renameCount[name] = 0;
return true;
}
// Create a Namer object which will give names to objects of type T which are unique within this namespace
public Namer<T> MakeNamer<T>(Namer<T>.KeyFunc keyFunc) {
return new Namer<T>(this, keyFunc);
}
/// <summary>
/// A class for managing objects of a common type within a namespace.
/// </summary>
/// <typeparam name="T"></typeparam>
public class Namer<T>
{
// Parent namespace
private CppNamespace ns;
// Names given out by this Namer.
private readonly Dictionary<T, string> names = new Dictionary<T, string>();
// The function which maps a T object to a suitably mangled name
// That name might be further mangled by the Namer to make the name unique within the namespace
public delegate string KeyFunc(T t);
private readonly KeyFunc keyFunc;
public Namer(CppNamespace ns, KeyFunc keyFunc) {
this.ns = ns;
this.keyFunc = keyFunc;
}
// Uniquely name an object within the parent namespace
public string GetName(T t) {
// If we've named this particular object before, just return that name
string name;
if (names.TryGetValue(t, out name))
return name;
// Obtain the mangled name for the object
name = keyFunc(t);
// Check if the mangled name has been given to another object - if it has,
// we need to give the object a new suffixed name (e.g. X_1).
// We might need to repeat this process if the new suffixed name also exists.
// Each iteration tacks on another suffix - so we normally expect this to only take
// a single iteration. (It might take multiple iterations in rare cases, e.g.
// another object had the mangled name X_1).
if (ns.renameCount.ContainsKey(name)) {
int v = ns.renameCount[name] + 1;
while (ns.renameCount.ContainsKey(name + "_" + v))
v++;
ns.renameCount[name] = v;
name = name + "_" + v;
}
ns.renameCount[name] = 0;
names[t] = name;
return name;
}
}
}
}
/*
Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
Copyright 2020 Robert Xiao - https://robertxiao.ca
All rights reserved.
*/
using System;
using System.Collections.Generic;
namespace Il2CppInspector.Cpp
{
/// <summary>
/// A utility class for managing names in a common namespace.
/// </summary>
public class CppNamespace
{
// The central data structure that keeps track of which names have been generated
// The value for any given key K is the number of unique objects originally named K, minus 1.
// Each time we see a particular name assigned to a new, different object, we bump its rename count
// and give it a suffix. For example, if we have three different objects all named X,
// we'd name them X, X_1, and X_2, and renameCount["X"] would be 2.
private readonly Dictionary<string, int> renameCount = new Dictionary<string, int>();
// Mark a name as reserved without assigning an object to it (e.g. for keywords and built-in names)
public void ReserveName(string name) {
if (!renameCount.TryAdd(name, 0)) {
throw new Exception($"Can't reserve {name}: already taken!");
}
}
// Try to mark a name as reserved without assigning an object to it (e.g. for keywords and built-in names)
public bool TryReserveName(string name)
{
return renameCount.TryAdd(name, 0);
}
// Create a Namer object which will give names to objects of type T which are unique within this namespace
public Namer<T> MakeNamer<T>(Namer<T>.KeyFunc keyFunc) {
return new Namer<T>(this, keyFunc);
}
/// <summary>
/// A class for managing objects of a common type within a namespace.
/// </summary>
/// <typeparam name="T"></typeparam>
public class Namer<T>
{
// Parent namespace
private CppNamespace ns;
// Names given out by this Namer.
private readonly Dictionary<T, string> names = new Dictionary<T, string>();
// The function which maps a T object to a suitably mangled name
// That name might be further mangled by the Namer to make the name unique within the namespace
public delegate string KeyFunc(T t);
private readonly KeyFunc keyFunc;
public Namer(CppNamespace ns, KeyFunc keyFunc) {
this.ns = ns;
this.keyFunc = keyFunc;
}
// Uniquely name an object within the parent namespace
public string GetName(T t) {
// If we've named this particular object before, just return that name
string name;
if (names.TryGetValue(t, out name))
return name;
// Obtain the mangled name for the object
name = keyFunc(t);
// Check if the mangled name has been given to another object - if it has,
// we need to give the object a new suffixed name (e.g. X_1).
// We might need to repeat this process if the new suffixed name also exists.
// Each iteration tacks on another suffix - so we normally expect this to only take
// a single iteration. (It might take multiple iterations in rare cases, e.g.
// another object had the mangled name X_1).
if (ns.renameCount.ContainsKey(name)) {
int v = ns.renameCount[name] + 1;
while (ns.renameCount.ContainsKey(name + "_" + v))
v++;
ns.renameCount[name] = v;
name = name + "_" + v;
}
ns.renameCount[name] = 0;
names[t] = name;
return name;
}
}
}
}

View File

@@ -1,451 +1,458 @@
/*
Copyright 2020-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
// NOTE: The types in this file should not be created directly. Always create types using the CppTypeCollection API!
namespace Il2CppInspector.Cpp
{
// Value type with fields
public enum ComplexValueType
{
Struct,
Union,
Enum
}
// A type with no fields
public class CppType
{
// The name of the type
public virtual string Name { get; set; }
// The logical group this type is part of
// This is purely for querying types in related groups and has no bearing on the code
public string Group { get; set; }
// The size of the C++ type in bits
public virtual int Size { get; set; }
// The alignment of the type
public int AlignmentBytes { get; set; }
// The size of the C++ type in bytes
public virtual int SizeBytes => (Size / 8) + (Size % 8 > 0 ? 1 : 0);
public CppType(string name = null, int size = 0, int alignmentBytes = 0) {
Name = name;
Size = size;
AlignmentBytes = alignmentBytes;
}
// Generate pointer to this type
public CppPointerType AsPointer(int WordSize) => new CppPointerType(WordSize, this);
// Generate array of this type
public CppArrayType AsArray(int Length) => new CppArrayType(this, Length);
// Generate typedef to this type
public CppAlias AsAlias(string Name) => new CppAlias(Name, this);
// Return the type as a field
public virtual string ToFieldString(string fieldName, string format = "") => Name + " " + fieldName;
public virtual string ToString(string format = "") => format == "o" ? $"/* {SizeBytes:x2} - {Name} */" : "";
public override string ToString() => ToString();
}
// A pointer type
public class CppPointerType : CppType
{
public override string Name => ElementType.Name + " *";
public CppType ElementType { get; }
public CppPointerType(int WordSize, CppType elementType) : base(null, WordSize) => ElementType = elementType;
// Return the type as a field
public override string ToFieldString(string fieldName, string format = "") => ElementType.ToFieldString("*" + fieldName, format);
public override string ToString(string format = "") => ToFieldString("");
}
// An array type
public class CppArrayType : CppType
{
public override string Name => ElementType.Name;
public int Length { get; }
public CppType ElementType { get; }
// Even an array of 1-bit bitfields must use at least 1 byte each
public override int Size => SizeBytes * 8;
public override int SizeBytes => ElementType.SizeBytes * Length;
public CppArrayType(CppType elementType, int length) : base() {
ElementType = elementType;
Length = length;
}
// Return the type as a field
public override string ToFieldString(string fieldName, string format = "")
=> ElementType.ToFieldString(fieldName, format) + "[" + Length + "]";
public override string ToString(string format = "") => ElementType + "[" + Length + "]";
}
// A function pointer type
public class CppFnPtrType : CppType
{
// Function return type
public CppType ReturnType { get; }
// Function argument names and types by position (some may have no names)
public List<(string Name, CppType Type)> Arguments { get; }
// Regex which matches a function pointer
public const string Regex = @"(\S+)\s*\(\s*\*\s*(\S+?)\s*?\)\s*\(\s*(.*)\s*\)";
public CppFnPtrType(int WordSize, CppType returnType, List<(string Name, CppType Type)> arguments) : base(null, WordSize) {
ReturnType = returnType;
Arguments = arguments;
}
// Generate a CppFnPtrType from a text signature (typedef or field)
public static CppFnPtrType FromSignature(CppTypeCollection types, string text) {
if (text.StartsWith("typedef "))
text = text.Substring(8);
if (text.EndsWith(";"))
text = text[..^1];
var typedef = System.Text.RegularExpressions.Regex.Match(text, Regex + "$");
var returnType = types.GetType(typedef.Groups[1].Captures[0].ToString());
var fnPtrName = typedef.Groups[2].Captures[0].ToString();
var argumentText = typedef.Groups[3].Captures[0].ToString() + ")";
// Look for each argument one at a time
// An argument is complete when we have zero bracket depth and either a comma or a close bracket (final argument)
var arguments = new List<string>();
while (argumentText.Length > 0) {
string argument = null;
var originalArgumentText = argumentText;
var depth = 0;
while (depth >= 0) {
var firstComma = argumentText.IndexOf(",");
var firstOpenBracket = argumentText.IndexOf("(");
var firstCloseBracket = argumentText.IndexOf(")");
if (firstOpenBracket == -1) {
argument += argumentText.Substring(0, 1 + ((firstComma != -1) ? firstComma : firstCloseBracket));
// End of argument if we get a comma or close bracket at zero depth,
// but only for the final close bracket if we are inside a function pointer signature
if (depth == 0 || firstComma == -1)
depth--;
// This condition handles function pointers followed by more arguments, ie. "), "
if (firstComma != -1 && firstCloseBracket < firstComma)
depth -= 2;
} else if (firstOpenBracket < firstCloseBracket) {
depth++;
argument += argumentText.Substring(0, firstOpenBracket + 1);
} else {
depth--;
argument += argumentText.Substring(0, firstCloseBracket + 1);
}
argumentText = originalArgumentText.Substring(argument.Length);
}
// Function with no arguments ie. (*foo)()
if (argument.Length > 1) {
arguments.Add(argument[..^1].Trim());
}
}
// Split argument names and types
var fnPtrArguments = new List<(string, CppType)>();
foreach (var argument in arguments) {
string name;
CppType type;
// Function pointer
if (argument.IndexOf("(") != -1) {
type = FromSignature(types, argument);
name = type.Name;
// Non-function pointer
} else {
name = argument.IndexOf("*") != -1? argument.Substring(argument.LastIndexOf("*") + 1).Trim() :
argument.IndexOf(" ") != -1? argument.Substring(argument.LastIndexOf(" ") + 1) : "";
type = types.GetType(argument.Substring(0, argument.Length - name.Length));
}
fnPtrArguments.Add((name, type));
}
return new CppFnPtrType(types.WordSize, returnType, fnPtrArguments) {Name = fnPtrName};
}
// Output as a named field in a type
public override string ToFieldString(string name, string format = "") => $"{ReturnType.Name} (*{name})("
+ string.Join(", ", Arguments.Select(a => a.Type is CppFnPtrType fn ? fn.ToFieldString(a.Name, format) : a.Type.Name + (a.Name.Length > 0 ? " " + a.Name : "")))
+ ")";
// Output as a typedef declaration
public override string ToString(string format = "") => "typedef " + ToFieldString(Name) + ";\n";
// Output as a function signature
public string ToSignatureString() => $"{ReturnType.Name} {Name}("
+ string.Join(", ", Arguments.Select(a => a.Type is CppFnPtrType fn? fn.ToFieldString(a.Name) : a.Type.Name + (a.Name.Length > 0? " " + a.Name : "")))
+ ")";
}
// A named alias for another type
// These are not stored in the type collection but generated on-the-fly for fields by GetType()
public class CppAlias : CppType
{
public CppType ElementType { get; }
public override int Size => ElementType.Size;
public override int SizeBytes => ElementType.SizeBytes;
public CppAlias(string name, CppType elementType) : base(name) => ElementType = elementType;
public override string ToString(string format = "") => $"typedef {ElementType.ToFieldString(Name)};";
}
// A struct, union, enum or class type (type with fields)
public class CppComplexType : CppType, IEnumerable<CppField>
{
// Various enumerators
public List<CppField> this[int byteOffset] => Fields[byteOffset * 8];
public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName);
public IEnumerator<CppField> GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// Collection which flattens all nested fields, calculating their direct bit offsets from the start of the type
// Unions can still cause some offsets to have multiple values
public class FlattenedFieldsCollection : IEnumerable<CppField>
{
public SortedDictionary<int, List<CppField>> Fields;
public FlattenedFieldsCollection(CppComplexType t) => Fields = getFlattenedFields(t);
private SortedDictionary<int, List<CppField>> getFlattenedFields(CppComplexType t) {
var flattened = new SortedDictionary<int, List<CppField>>();
foreach (var field in t.Fields.Values.SelectMany(f => f)) {
var type = field.Type;
while (type is CppAlias aliasType)
type = aliasType.ElementType;
if (type is CppComplexType ct) {
var baseOffset = field.Offset;
var fields = ct.Flattened.Fields.Select(kl => new {
Key = kl.Key + baseOffset,
Value = kl.Value.Select(f => new CppField(f.Name, f.Type, f.BitfieldSize) { Offset = f.Offset + baseOffset }).ToList()
}).ToDictionary(kv => kv.Key, kv => kv.Value);
flattened = new SortedDictionary<int, List<CppField>>(flattened.Union(fields).ToDictionary(kv => kv.Key, kv => kv.Value));
} else {
if (flattened.ContainsKey(field.Offset))
flattened[field.Offset].Add(field);
else
flattened.Add(field.Offset, new List<CppField> { field });
}
}
return flattened;
}
public List<CppField> this[int byteOffset] => Fields[byteOffset * 8];
public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName);
public IEnumerator<CppField> GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
private FlattenedFieldsCollection flattenedFields;
public FlattenedFieldsCollection Flattened {
get {
if (flattenedFields == null)
flattenedFields = new FlattenedFieldsCollection(this);
return flattenedFields;
}
}
// The compound type
public ComplexValueType ComplexValueType;
// Dictionary of byte offset in the type to each field
// Unions and bitfields can have more than one field at the same offset
public SortedDictionary<int, List<CppField>> Fields { get; internal set; } = new SortedDictionary<int, List<CppField>>();
public CppComplexType(ComplexValueType complexValueType) : base("", 0) {
ComplexValueType = complexValueType;
// An empty class shall always have sizeof() >= 1
// This will get overwritten the first time a field is added
Size = 8;
}
// Add a field to the type. Returns the offset of the field in the type
public int AddField(CppField field, int alignmentBytes = 0) {
// Unions and enums always have an offset of zero
// An empty struct has a Size (bits) of 8 so the first field must also be set to zero offset
field.Offset = ComplexValueType == ComplexValueType.Struct ? (Fields.Any()? Size : 0) : 0;
// If we just came out of a bitfield, move to the next byte if necessary
if (field.BitfieldSize == 0 && field.Offset % 8 != 0)
field.Offset = (field.Offset / 8) * 8 + 8;
// A 2, 4 or 8-byte value etc. must be aligned on an equivalent boundary
// The same goes for the first entry in a struct, union or array
// This block searches depth-first for the first field or element in any child types to find the required alignment boundary
// https://en.wikipedia.org/wiki/Data_structure_alignment
if (field.BitfieldSize == 0) {
var firstSimpleType = field.Type;
var foundType = false;
while (!foundType) {
var simpleType = firstSimpleType switch {
CppAlias alias => alias.ElementType,
CppComplexType { ComplexValueType: ComplexValueType.Struct } complex => complex.Fields.FirstOrDefault().Value?.First().Type,
CppArrayType array => array.ElementType,
_ => firstSimpleType
};
if (simpleType == firstSimpleType)
foundType = true;
firstSimpleType = simpleType;
}
// Empty classes shall always have sizeof() >= 1 and alignment doesn't matter
// Empty classes will be returned as null by the above code (complex? null conditional operator)
// https://www.stroustrup.com/bs_faq2.html#sizeof-empty
if (firstSimpleType != null)
if (field.OffsetBytes % firstSimpleType.SizeBytes != 0)
field.Offset += (firstSimpleType.SizeBytes - field.OffsetBytes % firstSimpleType.SizeBytes) * 8;
}
// Respect alignment directives
if (alignmentBytes > 0 && field.OffsetBytes % alignmentBytes != 0)
field.Offset += (alignmentBytes - field.OffsetBytes % alignmentBytes) * 8;
if (field.Type.AlignmentBytes > 0 && field.OffsetBytes % field.Type.AlignmentBytes != 0)
field.Offset += (field.Type.AlignmentBytes - field.OffsetBytes % field.Type.AlignmentBytes) * 8;
if (Fields.ContainsKey(field.Offset))
Fields[field.Offset].Add(field);
else
Fields.Add(field.Offset, new List<CppField> { field });
// Update type size. This lazy evaluation only works if there are no value type forward declarations in the type
// Union size is the size of the largest element in the union
if (ComplexValueType == ComplexValueType.Union)
if (field.Size > Size)
Size = field.Size;
// For structs we look for the last item and add the size; adding the sizes without offsets might fail because of alignment padding
if (ComplexValueType == ComplexValueType.Struct)
Size = field.Offset + field.Size;
return Size;
}
// Add a field to the type
public int AddField(string name, CppType type, int alignmentBytes = 0, int bitfield = 0, bool isConst = false)
=> AddField(new CppField(name, type, bitfield, isConst), alignmentBytes);
// Return the type as a field
public override string ToFieldString(string fieldName, string format = "")
=> (ComplexValueType == ComplexValueType.Struct ? "struct " : "union ") + Name + " " + fieldName;
// Summarize all field names and offsets
public override string ToString(string format = "") {
var sb = new StringBuilder();
sb.Append(ComplexValueType == ComplexValueType.Struct ? "struct " : "union ");
if (AlignmentBytes != 0)
sb.Append($"__declspec(align({AlignmentBytes})) ");
sb.Append(Name + (Name.Length > 0 ? " " : ""));
sb.Append("{");
foreach (var field in Fields.Values.SelectMany(f => f)) {
var fieldString = field.ToString(format);
var suffix = ";";
// C-compatible enum field
if (field.Type is CppEnumType) {
var sbEnum = new StringBuilder();
sbEnum.AppendLine("#if defined(_CPLUSPLUS_)");
sbEnum.AppendLine(fieldString + ";");
sbEnum.AppendLine("#else");
sbEnum.AppendLine(field.ToString(format + "c") + ";");
sbEnum.Append("#endif");
fieldString = sbEnum.ToString();
suffix = "";
}
sb.Append("\n " + string.Join("\n ", fieldString.Split('\n')) + suffix);
}
sb.Append($"\n}}{(format == "o"? $" /* Size: 0x{SizeBytes:x2} */" : "")};");
sb.Append("\n");
return sb.ToString();
}
}
// Enumeration type
public class CppEnumType : CppComplexType
{
// The underlying type of the enum
public CppType UnderlyingType { get; }
public override int Size => UnderlyingType.Size;
public CppEnumType(CppType underlyingType) : base(ComplexValueType.Enum) => UnderlyingType = underlyingType;
public void AddField(string name, object value) => AddField(new CppEnumField(this, name, UnderlyingType, value));
// Return the type as a field
public override string ToFieldString(string fieldName, string format = "") {
// C++
if (!format.Contains('c'))
return Name + " " + fieldName;
// For the C-compatible definition, we have an alignment problem when the enum
// does not derive from the architecture integer width.
return UnderlyingType.Name + " " + fieldName;
}
// Format specifier: 'c' = don't output C++-style enum with base type, use C-compatible code only
public override string ToString(string format = "") {
var sb = new StringBuilder();
// Don't output " : {underlyingType.Name}" because it breaks C
if (format.Contains('c'))
sb.Append($"enum {Name} {{");
else
sb.Append($"enum class {Name} : {UnderlyingType.Name} {{");
foreach (var field in Fields.Values.SelectMany(f => f))
sb.Append("\n " + string.Join("\n ", field.ToString(format).Split('\n')) + ",");
sb.AppendLine($"\n}}{(format == "o"? $" /* Size: 0x{SizeBytes:x2} */" : "")};");
return sb.ToString();
}
}
}
/*
Copyright 2020-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
// NOTE: The types in this file should not be created directly. Always create types using the CppTypeCollection API!
namespace Il2CppInspector.Cpp
{
// Value type with fields
public enum ComplexValueType
{
Struct,
Union,
Enum
}
// A type with no fields
public class CppType
{
// The name of the type
public virtual string Name { get; set; }
// The logical group this type is part of
// This is purely for querying types in related groups and has no bearing on the code
public string Group { get; set; }
// The size of the C++ type in bits
public virtual int Size { get; set; }
// The alignment of the type
public int AlignmentBytes { get; set; }
// The size of the C++ type in bytes
public virtual int SizeBytes => (Size / 8) + (Size % 8 > 0 ? 1 : 0);
public CppType(string name = null, int size = 0, int alignmentBytes = 0) {
Name = name;
Size = size;
AlignmentBytes = alignmentBytes;
}
// Generate pointer to this type
public CppPointerType AsPointer(int WordSize) => new CppPointerType(WordSize, this);
// Generate array of this type
public CppArrayType AsArray(int Length) => new CppArrayType(this, Length);
// Generate typedef to this type
public CppAlias AsAlias(string Name) => new CppAlias(Name, this);
// Return the type as a field
public virtual string ToFieldString(string fieldName, string format = "") => Name + " " + fieldName;
public virtual string ToString(string format = "") => format == "o" ? $"/* {SizeBytes:x2} - {Name} */" : "";
public override string ToString() => ToString();
}
// A pointer type
public class CppPointerType : CppType
{
public override string Name => ElementType.Name + " *";
public CppType ElementType { get; }
public CppPointerType(int WordSize, CppType elementType) : base(null, WordSize) => ElementType = elementType;
// Return the type as a field
public override string ToFieldString(string fieldName, string format = "") => ElementType.ToFieldString("*" + fieldName, format);
public override string ToString(string format = "") => ToFieldString("");
}
// An array type
public class CppArrayType : CppType
{
public override string Name => ElementType.Name;
public int Length { get; }
public CppType ElementType { get; }
// Even an array of 1-bit bitfields must use at least 1 byte each
public override int Size => SizeBytes * 8;
public override int SizeBytes => ElementType.SizeBytes * Length;
public CppArrayType(CppType elementType, int length) : base() {
ElementType = elementType;
Length = length;
}
// Return the type as a field
public override string ToFieldString(string fieldName, string format = "")
=> ElementType.ToFieldString(fieldName, format) + "[" + Length + "]";
public override string ToString(string format = "") => ElementType + "[" + Length + "]";
}
// A function pointer type
public class CppFnPtrType : CppType
{
// Function return type
public CppType ReturnType { get; }
// Function argument names and types by position (some may have no names)
public List<(string Name, CppType Type)> Arguments { get; }
// Regex which matches a function pointer
public const string Regex = @"(\S+)\s*\(\s*\*\s*(\S+?)\s*?\)\s*\(\s*(.*)\s*\)";
public CppFnPtrType(int WordSize, CppType returnType, List<(string Name, CppType Type)> arguments) : base(null, WordSize) {
ReturnType = returnType;
Arguments = arguments;
}
// Generate a CppFnPtrType from a text signature (typedef or field)
public static CppFnPtrType FromSignature(CppTypeCollection types, string text) {
if (text.StartsWith("typedef "))
text = text.Substring(8);
if (text.EndsWith(";"))
text = text[..^1];
var typedef = System.Text.RegularExpressions.Regex.Match(text, Regex + "$");
var returnType = types.GetType(typedef.Groups[1].Captures[0].ToString());
var fnPtrName = typedef.Groups[2].Captures[0].ToString();
var argumentText = typedef.Groups[3].Captures[0].ToString() + ")";
// Look for each argument one at a time
// An argument is complete when we have zero bracket depth and either a comma or a close bracket (final argument)
var arguments = new List<string>();
while (argumentText.Length > 0) {
string argument = null;
var originalArgumentText = argumentText;
var depth = 0;
while (depth >= 0) {
var firstComma = argumentText.IndexOf(",");
var firstOpenBracket = argumentText.IndexOf("(");
var firstCloseBracket = argumentText.IndexOf(")");
if (firstOpenBracket == -1) {
argument += argumentText.Substring(0, 1 + ((firstComma != -1) ? firstComma : firstCloseBracket));
// End of argument if we get a comma or close bracket at zero depth,
// but only for the final close bracket if we are inside a function pointer signature
if (depth == 0 || firstComma == -1)
depth--;
// This condition handles function pointers followed by more arguments, ie. "), "
if (firstComma != -1 && firstCloseBracket < firstComma)
depth -= 2;
} else if (firstOpenBracket < firstCloseBracket) {
depth++;
argument += argumentText.Substring(0, firstOpenBracket + 1);
} else {
depth--;
argument += argumentText.Substring(0, firstCloseBracket + 1);
}
argumentText = originalArgumentText.Substring(argument.Length);
}
// Function with no arguments ie. (*foo)()
if (argument.Length > 1) {
arguments.Add(argument[..^1].Trim());
}
}
// Split argument names and types
var fnPtrArguments = new List<(string, CppType)>();
foreach (var argument in arguments) {
string name;
CppType type;
// Function pointer
if (argument.IndexOf("(") != -1) {
type = FromSignature(types, argument);
name = type.Name;
// Non-function pointer
} else {
name = argument.IndexOf("*") != -1? argument.Substring(argument.LastIndexOf("*") + 1).Trim() :
argument.IndexOf(" ") != -1? argument.Substring(argument.LastIndexOf(" ") + 1) : "";
type = types.GetType(argument.Substring(0, argument.Length - name.Length));
}
fnPtrArguments.Add((name, type));
}
return new CppFnPtrType(types.WordSize, returnType, fnPtrArguments) {Name = fnPtrName};
}
// Output as a named field in a type
public override string ToFieldString(string name, string format = "") => $"{ReturnType.Name} (*{name})("
+ string.Join(", ", Arguments.Select(a => a.Type is CppFnPtrType fn ? fn.ToFieldString(a.Name, format) : a.Type.Name + (a.Name.Length > 0 ? " " + a.Name : "")))
+ ")";
// Output as a typedef declaration
public override string ToString(string format = "") => "typedef " + ToFieldString(Name) + ";\n";
// Output as a function signature
public string ToSignatureString() => $"{ReturnType.Name} {Name}("
+ string.Join(", ", Arguments.Select(a => a.Type is CppFnPtrType fn? fn.ToFieldString(a.Name) : a.Type.Name + (a.Name.Length > 0? " " + a.Name : "")))
+ ")";
}
// A named alias for another type
// These are not stored in the type collection but generated on-the-fly for fields by GetType()
public class CppAlias : CppType
{
public CppType ElementType { get; }
public override int Size => ElementType.Size;
public override int SizeBytes => ElementType.SizeBytes;
public CppAlias(string name, CppType elementType) : base(name) => ElementType = elementType;
public override string ToString(string format = "") => $"typedef {ElementType.ToFieldString(Name)};";
}
// A struct, union, enum or class type (type with fields)
public class CppComplexType : CppType, IEnumerable<CppField>
{
// Various enumerators
public List<CppField> this[int byteOffset] => Fields[byteOffset * 8];
public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName);
public IEnumerator<CppField> GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// Collection which flattens all nested fields, calculating their direct bit offsets from the start of the type
// Unions can still cause some offsets to have multiple values
public class FlattenedFieldsCollection : IEnumerable<CppField>
{
public SortedDictionary<int, List<CppField>> Fields;
public FlattenedFieldsCollection(CppComplexType t) => Fields = getFlattenedFields(t);
private SortedDictionary<int, List<CppField>> getFlattenedFields(CppComplexType t) {
var flattened = new SortedDictionary<int, List<CppField>>();
foreach (var field in t.Fields.Values.SelectMany(f => f)) {
var type = field.Type;
while (type is CppAlias aliasType)
type = aliasType.ElementType;
if (type is CppComplexType ct) {
var baseOffset = field.Offset;
var fields = ct.Flattened.Fields.Select(kl => new {
Key = kl.Key + baseOffset,
Value = kl.Value.Select(f => new CppField(f.Name, f.Type, f.BitfieldSize) { Offset = f.Offset + baseOffset }).ToList()
}).ToDictionary(kv => kv.Key, kv => kv.Value);
flattened = new SortedDictionary<int, List<CppField>>(flattened.Union(fields).ToDictionary(kv => kv.Key, kv => kv.Value));
} else {
if (flattened.ContainsKey(field.Offset))
flattened[field.Offset].Add(field);
else
flattened.Add(field.Offset, new List<CppField> { field });
}
}
return flattened;
}
public List<CppField> this[int byteOffset] => Fields[byteOffset * 8];
public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName);
public IEnumerator<CppField> GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
private FlattenedFieldsCollection flattenedFields;
public FlattenedFieldsCollection Flattened {
get {
if (flattenedFields == null)
flattenedFields = new FlattenedFieldsCollection(this);
return flattenedFields;
}
}
// The compound type
public ComplexValueType ComplexValueType;
// Dictionary of byte offset in the type to each field
// Unions and bitfields can have more than one field at the same offset
public SortedDictionary<int, List<CppField>> Fields { get; internal set; } = new SortedDictionary<int, List<CppField>>();
public CppComplexType(ComplexValueType complexValueType) : base("", 0) {
ComplexValueType = complexValueType;
// An empty class shall always have sizeof() >= 1
// This will get overwritten the first time a field is added
Size = 8;
}
// Add a field to the type. Returns the offset of the field in the type
public int AddField(CppField field, int alignmentBytes = 0) {
// Unions and enums always have an offset of zero
// An empty struct has a Size (bits) of 8 so the first field must also be set to zero offset
field.Offset = ComplexValueType == ComplexValueType.Struct ? (Fields.Any()? Size : 0) : 0;
// If we just came out of a bitfield, move to the next byte if necessary
if (field.BitfieldSize == 0 && field.Offset % 8 != 0)
field.Offset = (field.Offset / 8) * 8 + 8;
// A 2, 4 or 8-byte value etc. must be aligned on an equivalent boundary
// The same goes for the first entry in a struct, union or array
// This block searches depth-first for the first field or element in any child types to find the required alignment boundary
// https://en.wikipedia.org/wiki/Data_structure_alignment
if (field.BitfieldSize == 0) {
var firstSimpleType = field.Type;
var foundType = false;
while (!foundType) {
var simpleType = firstSimpleType switch {
CppAlias alias => alias.ElementType,
CppComplexType { ComplexValueType: ComplexValueType.Struct } complex => complex.Fields.FirstOrDefault().Value?.First().Type,
CppArrayType array => array.ElementType,
_ => firstSimpleType
};
if (simpleType == firstSimpleType)
foundType = true;
firstSimpleType = simpleType;
}
// Empty classes shall always have sizeof() >= 1 and alignment doesn't matter
// Empty classes will be returned as null by the above code (complex? null conditional operator)
// https://www.stroustrup.com/bs_faq2.html#sizeof-empty
if (firstSimpleType != null)
if (field.OffsetBytes % firstSimpleType.SizeBytes != 0)
field.Offset += (firstSimpleType.SizeBytes - field.OffsetBytes % firstSimpleType.SizeBytes) * 8;
}
// Respect alignment directives
if (alignmentBytes > 0 && field.OffsetBytes % alignmentBytes != 0)
field.Offset += (alignmentBytes - field.OffsetBytes % alignmentBytes) * 8;
if (field.Type.AlignmentBytes > 0 && field.OffsetBytes % field.Type.AlignmentBytes != 0)
field.Offset += (field.Type.AlignmentBytes - field.OffsetBytes % field.Type.AlignmentBytes) * 8;
if (Fields.ContainsKey(field.Offset))
Fields[field.Offset].Add(field);
else
Fields.Add(field.Offset, new List<CppField> { field });
// Update type size. This lazy evaluation only works if there are no value type forward declarations in the type
// Union size is the size of the largest element in the union
if (ComplexValueType == ComplexValueType.Union)
if (field.Size > Size)
Size = field.Size;
// For structs we look for the last item and add the size; adding the sizes without offsets might fail because of alignment padding
if (ComplexValueType == ComplexValueType.Struct)
Size = field.Offset + field.Size;
return Size;
}
// Add a field to the type
public int AddField(string name, CppType type, int alignmentBytes = 0, int bitfield = 0, bool isConst = false)
=> AddField(new CppField(name, type, bitfield, isConst), alignmentBytes);
// Return the type as a field
public override string ToFieldString(string fieldName, string format = "")
=> (ComplexValueType == ComplexValueType.Struct ? "struct " : "union ") + Name + " " + fieldName;
// Summarize all field names and offsets
public override string ToString(string format = "") {
var sb = new StringBuilder();
sb.Append(ComplexValueType == ComplexValueType.Struct ? "struct " : "union ");
if (AlignmentBytes != 0)
sb.Append($"__declspec(align({AlignmentBytes})) ");
sb.Append(Name + (Name.Length > 0 ? " " : ""));
sb.Append("{");
foreach (var field in Fields.Values.SelectMany(f => f)) {
var fieldString = field.ToString(format);
var suffix = ";";
// C-compatible enum field
if (field.Type is CppEnumType) {
var sbEnum = new StringBuilder();
sbEnum.AppendLine("#if defined(_CPLUSPLUS_)");
sbEnum.AppendLine(fieldString + ";");
sbEnum.AppendLine("#else");
sbEnum.AppendLine(field.ToString(format + "c") + ";");
sbEnum.Append("#endif");
fieldString = sbEnum.ToString();
suffix = "";
}
sb.Append("\n ");
foreach (var fieldStr in fieldString.Split('\n'))
{
sb.Append(fieldStr);
sb.Append("\n ");
}
sb.Append(suffix);
}
sb.Append($"\n}}{(format == "o"? $" /* Size: 0x{SizeBytes:x2} */" : "")};");
sb.Append("\n");
return sb.ToString();
}
}
// Enumeration type
public class CppEnumType : CppComplexType
{
// The underlying type of the enum
public CppType UnderlyingType { get; }
public override int Size => UnderlyingType.Size;
public CppEnumType(CppType underlyingType) : base(ComplexValueType.Enum) => UnderlyingType = underlyingType;
public void AddField(string name, object value) => AddField(new CppEnumField(this, name, UnderlyingType, value));
// Return the type as a field
public override string ToFieldString(string fieldName, string format = "") {
// C++
if (!format.Contains('c'))
return Name + " " + fieldName;
// For the C-compatible definition, we have an alignment problem when the enum
// does not derive from the architecture integer width.
return UnderlyingType.Name + " " + fieldName;
}
// Format specifier: 'c' = don't output C++-style enum with base type, use C-compatible code only
public override string ToString(string format = "") {
var sb = new StringBuilder();
// Don't output " : {underlyingType.Name}" because it breaks C
if (format.Contains('c'))
sb.Append($"enum {Name} {{");
else
sb.Append($"enum class {Name} : {UnderlyingType.Name} {{");
foreach (var field in Fields.Values.SelectMany(f => f))
sb.Append("\n " + string.Join("\n ", field.ToString(format).Split('\n')) + ",");
sb.AppendLine($"\n}}{(format == "o"? $" /* Size: 0x{SizeBytes:x2} */" : "")};");
return sb.ToString();
}
}
}

View File

@@ -1,176 +1,185 @@
/*
Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Il2CppInspector.Reflection
{
public static class Extensions
{
// Convert a list of CustomAttributeData objects into C#-friendly attribute usages
public static string ToString(this IEnumerable<CustomAttributeData> attributes, Scope scope = null,
string linePrefix = "", string attributePrefix = "", bool inline = false, bool emitPointer = false, bool mustCompile = false) {
var sb = new StringBuilder();
foreach (var cad in attributes) {
// Find a constructor that either has no parameters, or all optional parameters
var parameterlessConstructor = cad.AttributeType.DeclaredConstructors.Any(c => !c.IsStatic && c.IsPublic && c.DeclaredParameters.All(p => p.IsOptional));
// IL2CPP doesn't retain attribute arguments so we have to comment out those with non-optional arguments if we want the output to compile
var commentStart = mustCompile && !parameterlessConstructor? inline? "/* " : "// " : "";
var commentEnd = commentStart.Length > 0 && inline? " */" : "";
var arguments = "";
// Set AttributeUsage(AttributeTargets.All) if making output that compiles to mitigate CS0592
if (mustCompile && cad.AttributeType.FullName == "System.AttributeUsageAttribute") {
commentStart = "";
commentEnd = "";
arguments = "(AttributeTargets.All)";
}
var name = cad.AttributeType.GetScopedCSharpName(scope);
var suffix = name.LastIndexOf("Attribute", StringComparison.Ordinal);
if (suffix != -1)
name = name[..suffix];
sb.Append($"{linePrefix}{commentStart}[{attributePrefix}{name}{arguments}]{commentEnd}");
if (emitPointer)
sb.Append($" {(inline? "/*" : "//")} {cad.VirtualAddress.ToAddressString()}{(inline? " */" : "")}");
sb.Append(inline? " ":"\n");
}
return sb.ToString();
}
// Output a ulong as a 32 or 64-bit hexadecimal address
public static string ToAddressString(this ulong address) => address <= 0xffff_ffff
? string.Format($"0x{(uint)address:X8}")
: string.Format($"0x{address:X16}");
public static string ToAddressString(this long address) => ((ulong) address).ToAddressString();
public static string ToAddressString(this (ulong start, ulong end)? address) => ToAddressString(address?.start ?? 0) + "-" + ToAddressString(address?.end ?? 0);
public static string ToAddressString(this (ulong start, ulong end) address) => ToAddressString(address.start) + "-" + ToAddressString(address.end);
// C# string literal escape characters
// Taken from: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/strings/#regular-and-verbatim-string-literals
private static Dictionary<char, string> escapeChars = new Dictionary<char, string> {
['\''] = @"\'",
['"'] = @"\""",
['\\'] = @"\\",
['\0'] = @"\0",
['\a'] = @"\a",
['\b'] = @"\b",
['\f'] = @"\f",
['\n'] = @"\n",
['\r'] = @"\r",
['\t'] = @"\t",
['\v'] = @"\v"
};
// Output a string in Python-friendly syntax
public static string ToEscapedString(this string str) {
// Replace standard escape characters
var s = new StringBuilder();
for (var i = 0; i < str.Length; i++)
// Standard escape characters
s.Append(escapeChars.ContainsKey(str[i]) ? escapeChars[str[i]]
// Replace everything else with UTF-16 Unicode
: str[i] < 32 || str[i] > 126 ? @"\u" + $"{(int) str[i]:X4}"
: str[i].ToString());
return s.ToString();
}
public static string ToCIdentifier(this string str, bool allowScopeQualifiers = false) {
// replace * with Ptr
str = str.Replace("*", "Ptr");
// escape non-ASCII characters
var s = new StringBuilder();
for (var i = 0; i < str.Length; i++)
if (str[i] < 32 || str[i] > 126)
s.Append($"u{(int) str[i]:X4}");
else
s.Append(str[i]);
str = s.ToString();
// replace illegal characters
str = Regex.Replace(str, allowScopeQualifiers? @"[^a-zA-Z0-9_\.:]" : "[^a-zA-Z0-9_]", "_");
// ensure identifier starts with a letter or _ (and is non-empty)
if (!Regex.IsMatch(str, "^[a-zA-Z_]"))
str = "_" + str;
return str;
}
// Output a value in C#-friendly syntax
public static string ToCSharpValue(this object value, TypeInfo type, Scope usingScope = null) {
if (value is bool)
return (bool) value ? "true" : "false";
if (value is float f)
return value switch {
float.PositiveInfinity => "1F / 0F",
float.NegativeInfinity => "-1F / 0F",
float.NaN => "0F / 0F",
_ => f.ToString(CultureInfo.InvariantCulture) + "f"
};
if (value is double d)
return value switch {
double.PositiveInfinity => "1D / 0D",
double.NegativeInfinity => "-1D / 0D",
double.NaN => "0D / 0D",
_ => d.ToString(CultureInfo.InvariantCulture)
};
if (value is string str) {
return $"\"{str.ToEscapedString()}\"";
}
if (value is char) {
var cValue = (int) (char) value;
if (cValue < 32 || cValue > 126)
return $"'\\x{cValue:x4}'";
return $"'{value}'";
}
if (type.IsEnum) {
var flags = type.GetCustomAttributes("System.FlagsAttribute").Any();
var values = type.GetEnumNames().Zip(type.GetEnumValues().OfType<object>(), (k, v) => new {k, v}).ToDictionary(x => x.k, x => x.v);
var typeName = type.GetScopedCSharpName(usingScope);
// We don't know what type the enumeration or value is, so we use Object.Equals() to do content-based equality testing
if (!flags) {
// Defined enum name
if (values.FirstOrDefault(v => v.Value.Equals(value)).Key is string enumValue)
return typeName + "." + enumValue;
// Undefined enum value (return a cast)
return "(" + typeName + ") " + value;
}
// Logical OR a series of flags together
// Values like 0x8000_0000_0000_0000 can't be cast to Int64
// but values like 0xffff_ffff can't be cast to UInt64 (due to sign extension)
// so we're just going to have to try to find a type that doesn't make it explode
if (value is byte || value is ushort || value is uint || value is ulong) {
var flagValue = Convert.ToUInt64(value);
var setFlags = values.Where(x => (Convert.ToUInt64(x.Value) & flagValue) == Convert.ToUInt64(x.Value)).Select(x => typeName + "." + x.Key);
return string.Join(" | ", setFlags);
}
else if (value is sbyte || value is short || value is int || value is long) {
var flagValue = Convert.ToInt64(value);
var setFlags = values.Where(x => (Convert.ToInt64(x.Value) & flagValue) == Convert.ToInt64(x.Value)).Select(x => typeName + "." + x.Key);
return string.Join(" | ", setFlags);
} else {
throw new ArgumentException("Unsupported enum underlying type");
}
}
// Structs and generic type parameters must use 'default' rather than 'null'
return value?.ToString() ?? (type.IsValueType || type.IsGenericParameter? "default" : "null");
}
}
}
/*
Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Il2CppInspector.Reflection
{
public static class Extensions
{
// Convert a list of CustomAttributeData objects into C#-friendly attribute usages
public static string ToString(this IEnumerable<CustomAttributeData> attributes, Scope scope = null,
string linePrefix = "", string attributePrefix = "", bool inline = false, bool emitPointer = false, bool mustCompile = false) {
var sb = new StringBuilder();
foreach (var cad in attributes) {
// Find a constructor that either has no parameters, or all optional parameters
var parameterlessConstructor = cad.AttributeType.DeclaredConstructors.Any(c => !c.IsStatic && c.IsPublic && c.DeclaredParameters.All(p => p.IsOptional));
// IL2CPP doesn't retain attribute arguments so we have to comment out those with non-optional arguments if we want the output to compile
var commentStart = mustCompile && !parameterlessConstructor? inline? "/* " : "// " : "";
var commentEnd = commentStart.Length > 0 && inline? " */" : "";
var arguments = "";
// Set AttributeUsage(AttributeTargets.All) if making output that compiles to mitigate CS0592
if (mustCompile && cad.AttributeType.FullName == "System.AttributeUsageAttribute") {
commentStart = "";
commentEnd = "";
arguments = "(AttributeTargets.All)";
}
var name = cad.AttributeType.GetScopedCSharpName(scope);
var suffix = name.LastIndexOf("Attribute", StringComparison.Ordinal);
if (suffix != -1)
name = name[..suffix];
sb.Append($"{linePrefix}{commentStart}[{attributePrefix}{name}{arguments}]{commentEnd}");
if (emitPointer)
sb.Append($" {(inline? "/*" : "//")} {cad.VirtualAddress.ToAddressString()}{(inline? " */" : "")}");
sb.Append(inline? " ":"\n");
}
return sb.ToString();
}
// Output a ulong as a 32 or 64-bit hexadecimal address
public static string ToAddressString(this ulong address) => address <= 0xffff_ffff
? string.Format($"0x{(uint)address:X8}")
: string.Format($"0x{address:X16}");
public static string ToAddressString(this long address) => ((ulong) address).ToAddressString();
public static string ToAddressString(this (ulong start, ulong end)? address) => ToAddressString(address?.start ?? 0) + "-" + ToAddressString(address?.end ?? 0);
public static string ToAddressString(this (ulong start, ulong end) address) => ToAddressString(address.start) + "-" + ToAddressString(address.end);
// C# string literal escape characters
// Taken from: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/strings/#regular-and-verbatim-string-literals
private static Dictionary<char, string> escapeChars = new Dictionary<char, string> {
['\''] = @"\'",
['"'] = @"\""",
['\\'] = @"\\",
['\0'] = @"\0",
['\a'] = @"\a",
['\b'] = @"\b",
['\f'] = @"\f",
['\n'] = @"\n",
['\r'] = @"\r",
['\t'] = @"\t",
['\v'] = @"\v"
};
// Output a string in Python-friendly syntax
public static string ToEscapedString(this string str) {
// Replace standard escape characters
var s = new StringBuilder();
foreach (var chr in str)
{
if (escapeChars.TryGetValue(chr, out var escaped))
s.Append(escaped);
else if (chr < 32 || chr > 126)
{
s.Append("\\u");
s.Append($"{(int) chr:X4}");
}
else
s.Append(chr);
}
return s.ToString();
}
public static string ToCIdentifier(this string str, bool allowScopeQualifiers = false) {
// replace * with Ptr
str = str.Replace("*", "Ptr");
// escape non-ASCII characters
var s = new StringBuilder();
for (var i = 0; i < str.Length; i++)
if (str[i] < 32 || str[i] > 126)
s.Append($"u{(int) str[i]:X4}");
else
s.Append(str[i]);
str = s.ToString();
// replace illegal characters
str = Regex.Replace(str, allowScopeQualifiers? @"[^a-zA-Z0-9_\.:]" : "[^a-zA-Z0-9_]", "_");
// ensure identifier starts with a letter or _ (and is non-empty)
if (!Regex.IsMatch(str, "^[a-zA-Z_]"))
str = "_" + str;
return str;
}
// Output a value in C#-friendly syntax
public static string ToCSharpValue(this object value, TypeInfo type, Scope usingScope = null) {
if (value is bool)
return (bool) value ? "true" : "false";
if (value is float f)
return value switch {
float.PositiveInfinity => "1F / 0F",
float.NegativeInfinity => "-1F / 0F",
float.NaN => "0F / 0F",
_ => f.ToString(CultureInfo.InvariantCulture) + "f"
};
if (value is double d)
return value switch {
double.PositiveInfinity => "1D / 0D",
double.NegativeInfinity => "-1D / 0D",
double.NaN => "0D / 0D",
_ => d.ToString(CultureInfo.InvariantCulture)
};
if (value is string str) {
return $"\"{str.ToEscapedString()}\"";
}
if (value is char) {
var cValue = (int) (char) value;
if (cValue < 32 || cValue > 126)
return $"'\\x{cValue:x4}'";
return $"'{value}'";
}
if (type.IsEnum) {
var flags = type.GetCustomAttributes("System.FlagsAttribute").Any();
var values = type.GetEnumNames().Zip(type.GetEnumValues().OfType<object>(), (k, v) => new {k, v}).ToDictionary(x => x.k, x => x.v);
var typeName = type.GetScopedCSharpName(usingScope);
// We don't know what type the enumeration or value is, so we use Object.Equals() to do content-based equality testing
if (!flags) {
// Defined enum name
if (values.FirstOrDefault(v => v.Value.Equals(value)).Key is string enumValue)
return typeName + "." + enumValue;
// Undefined enum value (return a cast)
return "(" + typeName + ") " + value;
}
// Logical OR a series of flags together
// Values like 0x8000_0000_0000_0000 can't be cast to Int64
// but values like 0xffff_ffff can't be cast to UInt64 (due to sign extension)
// so we're just going to have to try to find a type that doesn't make it explode
if (value is byte || value is ushort || value is uint || value is ulong) {
var flagValue = Convert.ToUInt64(value);
var setFlags = values.Where(x => (Convert.ToUInt64(x.Value) & flagValue) == Convert.ToUInt64(x.Value)).Select(x => typeName + "." + x.Key);
return string.Join(" | ", setFlags);
}
else if (value is sbyte || value is short || value is int || value is long) {
var flagValue = Convert.ToInt64(value);
var setFlags = values.Where(x => (Convert.ToInt64(x.Value) & flagValue) == Convert.ToInt64(x.Value)).Select(x => typeName + "." + x.Key);
return string.Join(" | ", setFlags);
} else {
throw new ArgumentException("Unsupported enum underlying type");
}
}
// Structs and generic type parameters must use 'default' rather than 'null'
return value?.ToString() ?? (type.IsValueType || type.IsGenericParameter? "default" : "null");
}
}
}