// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Globalization; using System.Text; using EpicGames.Core; using EpicGames.UHT.Types; namespace EpicGames.UHT.Utils { static class UhtStringBuilderExtensions { /// /// String of tabs used to generate code with proper indentation /// public static StringView TabsString = new(new string('\t', 128)); /// /// String of spaces used to generate code with proper indentation /// public static StringView SpacesString = new(new string(' ', 128)); /// /// Names of meta data entries that will not appear in shipping builds for game code /// private static readonly HashSet s_hiddenMetaDataNames = new(new string[]{ UhtNames.Comment, UhtNames.ToolTip }, StringComparer.OrdinalIgnoreCase); /// /// Append tabs to the builder /// /// Destination builder /// Number of tabs to insert /// Destination builder /// Thrown if the number of tabs is out of range public static StringBuilder AppendTabs(this StringBuilder builder, int tabs) { if (tabs < 0 || tabs > TabsString.Length) { throw new ArgumentOutOfRangeException($"Number of tabs specified must be between 0 and {TabsString.Length - 1} inclusive"); } else if (tabs > 0) { builder.Append(TabsString.Span[..tabs]); } return builder; } /// /// Append spaces to the builder /// /// Destination builder /// Number of spaces to insert /// Destination builder /// Thrown if the number of spaces is out of range public static StringBuilder AppendSpaces(this StringBuilder builder, int spaces) { if (spaces < 0 || spaces > SpacesString.Length) { throw new ArgumentOutOfRangeException($"Number of spaces specified must be between 0 and {SpacesString.Length - 1} inclusive"); } else if (spaces > 0) { builder.Append(SpacesString.Span[..spaces]); } return builder; } /// /// Append a name declaration to the builder /// /// Destination builder /// Name prefix /// Name /// Optional name suffix /// Destination builder public static StringBuilder AppendNameDecl(this StringBuilder builder, string? namePrefix, string name, string? nameSuffix) { return builder.Append(namePrefix).Append(name).Append(nameSuffix); } /// /// Append a name declaration to the builder /// /// Destination builder /// Property context used to get the name prefix /// Name /// Optional name suffix /// Destination builder public static StringBuilder AppendNameDecl(this StringBuilder builder, IUhtPropertyMemberContext context, string name, string nameSuffix) { return builder.AppendNameDecl(context.NamePrefix, name, nameSuffix); } /// /// Append a name definition to the builder /// /// Destination builder /// Optional name of the statics block which will be output in the form of "StaticsName::" /// Name prefix /// Name /// Optional name suffix /// Destination builder public static StringBuilder AppendNameDef(this StringBuilder builder, string? staticsName, string? namePrefix, string name, string? nameSuffix) { if (!String.IsNullOrEmpty(staticsName)) { builder.Append(staticsName).Append("::"); } return builder.AppendNameDecl(namePrefix, name, nameSuffix); } /// /// Append a name definition to the builder /// /// Destination builder /// Property context used to get the statics name and name prefix /// Name /// Optional name suffix /// Destination builder public static StringBuilder AppendNameDef(this StringBuilder builder, IUhtPropertyMemberContext context, string name, string nameSuffix) { return builder.AppendNameDef(context.StaticsName, context.NamePrefix, name, nameSuffix); } /// /// Append the meta data parameters. This is intended to be used as arguments to a function call. /// /// Destination builder /// Source type containing the meta data /// Optional name of the statics block which will be output in the form of "StaticsName::" /// Name prefix /// Name /// Optional name suffix /// Suffix to be added to the meta data name /// Destination builder private static StringBuilder AppendMetaDataParams(this StringBuilder builder, UhtType type, string? staticsName, string? namePrefix, string name, string? nameSuffix, string? metaNameSuffix) { if (!type.MetaData.IsEmpty()) { return builder .Append("METADATA_PARAMS(") .Append("UE_ARRAY_COUNT(") .AppendNameDef(staticsName, namePrefix, name, nameSuffix).Append(metaNameSuffix) .Append("), ") .AppendNameDef(staticsName, namePrefix, name, nameSuffix).Append(metaNameSuffix) .Append(')'); } else { return builder.Append("METADATA_PARAMS(0, nullptr)"); } } /// /// Append the meta data parameters. This is intended to be used as arguments to a function call. /// /// Destination builder /// Source type containing the meta data /// Optional name of the statics block which will be output in the form of "StaticsName::" /// Name /// Destination builder public static StringBuilder AppendMetaDataParams(this StringBuilder builder, UhtType type, string? staticsName, string name) { return builder.AppendMetaDataParams(type, staticsName, null, name, null, null); } /// /// Append the meta data parameters. This is intended to be used as arguments to a function call. /// /// Destination builder /// Source type containing the meta data /// Property context used to get the statics name and name prefix /// Name /// Optional name suffix /// Destination builder public static StringBuilder AppendMetaDataParams(this StringBuilder builder, UhtProperty property, IUhtPropertyMemberContext context, string name, string nameSuffix) { return builder.AppendMetaDataParams(property, null, context.NamePrefix, name, nameSuffix, context.MetaDataSuffix); } /// /// Append the meta data declaration. /// /// Destination builder /// Source type containing the meta data /// Name prefix /// Name /// Optional name suffix /// Optional meta data name suffix /// Number of tabs to indent /// Destination builder public static StringBuilder AppendMetaDataDecl(this StringBuilder builder, UhtType type, string? namePrefix, string name, string? nameSuffix, string? metaNameSuffix, int tabs) { if (!type.MetaData.IsEmpty()) { bool isPartOfEngine = type.Module.IsPartOfEngine; List> sortedMetaData = type.MetaData.GetSorted(); builder.AppendTabs(tabs).Append("static constexpr UECodeGen_Private::FMetaDataPairParam ").AppendNameDecl(namePrefix, name, nameSuffix).Append(metaNameSuffix).Append("[] = {\r\n"); foreach (KeyValuePair kvp in sortedMetaData) { bool restricted = !isPartOfEngine && s_hiddenMetaDataNames.Contains(kvp.Key); if (restricted) { builder.Append("#if !UE_BUILD_SHIPPING\r\n"); } builder.AppendTabs(tabs + 1).Append("{ ").AppendUTF8LiteralString(kvp.Key).Append(", ").AppendUTF8LiteralString(kvp.Value).Append(" },\r\n"); if (restricted) { builder.Append("#endif\r\n"); } } builder.AppendTabs(tabs).Append("};\r\n"); } return builder; } /// /// Append the meta data declaration /// /// Destination builder /// Source type containing the meta data /// Property context used to get the statics name and name prefix /// Name /// Optional name suffix /// Number of tabs to indent /// Destination builder public static StringBuilder AppendMetaDataDecl(this StringBuilder builder, UhtProperty property, IUhtPropertyMemberContext context, string name, string nameSuffix, int tabs) { return property.AppendMetaDataDecl(builder, context, name, nameSuffix, tabs); } /// /// Append the given text as a UTF8 encoded string /// /// Destination builder /// If false, don't encode the text but include a nullptr /// Text to include or an empty string if null. /// Destination builder public static StringBuilder AppendUTF8LiteralString(this StringBuilder builder, bool useText, string? text) { if (!useText) { builder.Append("nullptr"); } else if (text == null) { builder.Append(""); } else { builder.AppendUTF8LiteralString(text); } return builder; } /// /// Append the given text as a UTF8 encoded string /// /// Destination builder /// Text to include or an empty string if null. /// Destination builder public static StringBuilder AppendUTF8LiteralString(this StringBuilder builder, string? text) { if (text == null) { builder.Append(""); } else { builder.AppendUTF8LiteralString(new StringView(text)); } return builder; } /// /// Append the given text as a UTF8 encoded string /// /// Destination builder /// Text to be encoded /// Destination builder public static StringBuilder AppendUTF8LiteralString(this StringBuilder builder, StringView text) { builder.Append('\"'); ReadOnlySpan span = text.Span; int length = span.Length; if (length > 0) { bool trailingHex = false; int index = 0; while (true) { // Scan forward looking for anything that can just be blindly copied int startIndex = index; while (index < length) { char cskip = span[index]; if (cskip < 31 || cskip > 127 || cskip == '"' || cskip == '\\') { break; } ++index; } // If we found anything if (startIndex < index) { // We close and open the literal here in order to ensure that successive hex characters aren't appended to the hex sequence, causing a different number if (trailingHex && UhtFCString.IsHexDigit(span[startIndex])) { builder.Append("\"\""); } builder.Append(span[startIndex..index]); } // We have either reached the end of the string, break if (index == length) { break; } // This character requires special processing char c = span[index++]; switch (c) { case '\r': trailingHex = false; break; case '\n': trailingHex = false; builder.Append("\\n"); break; case '\\': trailingHex = false; builder.Append("\\\\"); break; case '\"': trailingHex = false; builder.Append("\\\""); break; default: if (c < 31) { trailingHex = true; builder.Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", (uint)c); } else { trailingHex = false; if (Char.IsHighSurrogate(c)) { if (index == length) { builder.Append('?'); break; } char clow = span[index]; if (Char.IsLowSurrogate(clow)) { ++index; builder.AppendEscapedUtf32((ulong)Char.ConvertToUtf32(c, clow)); trailingHex = true; } else { builder.Append('?'); } } else if (Char.IsLowSurrogate(c)) { builder.Append('?'); } else { builder.AppendEscapedUtf32(c); trailingHex = true; } } break; } } } builder.Append('\"'); return builder; } /// /// Encode a single UTF32 value as UTF8 characters /// /// Destination builder /// Character to encode /// Destination builder public static StringBuilder AppendEscapedUtf32(this StringBuilder builder, ulong c) { if (c < 0x80) { builder .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", c); } else if (c < 0x800) { builder .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", 0xC0 + (c >> 6)) .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", 0x80 + (c & 0x3f)); } else if (c < 0x10000) { builder .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", 0xE0 + (c >> 12)) .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", 0x80 + ((c >> 6) & 0x3f)) .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", 0x80 + (c & 0x3f)); } else { builder .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", 0xF0 + (c >> 18)) .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", 0x80 + ((c >> 12) & 0x3f)) .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", 0x80 + ((c >> 6) & 0x3f)) .Append("\\x").AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", 0x80 + (c & 0x3f)); } return builder; } /// /// Append the given name of the class but always encode interfaces as the native interface name (i.e. "I...") /// /// Destination builder /// Class to append name /// Destination builder /// Class has an unexpected class type public static StringBuilder AppendClassSourceNameOrInterfaceName(this StringBuilder builder, UhtClass classObj) { switch (classObj.ClassType) { case UhtClassType.Class: case UhtClassType.NativeInterface: return builder.Append(classObj.SourceName); case UhtClassType.Interface: return builder.Append('I').Append(classObj.SourceName[1..]); default: throw new NotImplementedException(); } } /// /// Append the given name of the class but always encode interfaces as the native interface proxy name (i.e. "I...") /// /// Destination builder /// Class to append name /// Destination builder /// Class has an unexpected class type public static StringBuilder AppendClassSourceNameOrInterfaceProxyName(this StringBuilder builder, UhtClass classObj) { switch (classObj.ClassType) { case UhtClassType.Class: return builder.Append(classObj.SourceName); case UhtClassType.NativeInterface: return builder.Append(classObj.SourceName).Append(UhtNames.VerseProxySuffix); case UhtClassType.Interface: return builder.Append('I').Append(classObj.SourceName[1..]).Append(UhtNames.VerseProxySuffix); default: throw new NotImplementedException(); } } /// /// Given a verse name, encode it so it can be represented as an FName /// /// Destination builder /// Name to encode /// Builder public static StringBuilder AppendEncodedVerseName(this StringBuilder builder, ReadOnlySpan name) { bool isFirstChar = true; while (!name.IsEmpty) { char c = name[0]; name = name[1..]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9' && !isFirstChar)) { builder.Append(c); } else if (c == '[' && !name.IsEmpty && name[0] == ']') { name = name[1..]; builder.Append("_K"); } else if (c == '-' && !name.IsEmpty && name[0] == '>') { name = name[1..]; builder.Append("_T"); } else if (c == '_') { builder.Append("__"); } else if (c == '(') { builder.Append("_L"); } else if (c == ',') { builder.Append("_M"); } else if (c == ':') { builder.Append("_N"); } else if (c == '^') { builder.Append("_P"); } else if (c == '?') { builder.Append("_Q"); } else if (c == ')') { builder.Append("_R"); } else if (c == '\'') { builder.Append("_U"); } else { builder.Append('_').AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", (uint)c); } isFirstChar = false; } return builder; } } /// /// Provides a cache of StringBuilders /// public class StringBuilderCache { /// /// Cache of StringBuilders with large initial buffer sizes /// public static readonly StringBuilderCache Big = new(256, 256 * 1024); /// /// Cache of StringBuilders with small initial buffer sizes /// public static readonly StringBuilderCache Small = new(256, 1 * 1024); /// /// Capacity of the cache /// private readonly int _capacity; /// /// Initial buffer size for new StringBuilders. Resetting StringBuilders might result /// in the initial chunk size being smaller. /// private readonly int _initialBufferSize; /// /// Stack of cached StringBuilders /// private readonly Stack _stack; /// /// Create a new StringBuilder cache /// /// Maximum number of StringBuilders to cache /// Initial buffer size for newly created StringBuilders public StringBuilderCache(int capacity, int initialBufferSize) { _capacity = capacity; _initialBufferSize = initialBufferSize; _stack = new Stack(_capacity); } /// /// Borrow a StringBuilder from the cache. /// /// public StringBuilder Borrow() { lock (_stack) { if (_stack.Count > 0) { return _stack.Pop(); } } return new StringBuilder(_initialBufferSize); } /// /// Return a StringBuilder to the cache /// /// The builder being returned public void Return(StringBuilder builder) { // Sadly, clearing the builder (sets length to 0) will reallocate chunks. builder.Clear(); lock (_stack) { if (_stack.Count < _capacity) { _stack.Push(builder); } } } } /// /// Structure to automate the borrowing and returning of a StringBuilder. /// Use some form of a "using" pattern. /// public readonly struct BorrowStringBuilder : IDisposable { /// /// Owning cache /// private StringBuilderCache Cache { get; } /// /// Borrowed string builder /// public StringBuilder StringBuilder { get; } /// /// Borrow a string builder from the given cache /// /// String builder cache public BorrowStringBuilder(StringBuilderCache cache) { Cache = cache; StringBuilder = Cache.Borrow(); } /// /// Return the string builder to the cache /// public void Dispose() { Cache.Return(StringBuilder); } } }