using EpicGames.UHT.Types; using System; using System.Collections.Generic; using System.Text; using UnrealSharpScriptGenerator.PropertyTranslators; using UnrealSharpScriptGenerator.Tooltip; using UnrealSharpScriptGenerator.Utilities; namespace UnrealSharpScriptGenerator.Exporters; public static class StructExporter { public static void ExportStruct(UhtScriptStruct structObj, bool isManualExport) { GeneratorStringBuilder stringBuilder = new(); List exportedProperties = new(); Dictionary getSetBackedProperties = new(); List inheritanceHierarchy = new(); UhtStruct? currentStruct = structObj; while (currentStruct is not null) { inheritanceHierarchy.Add(currentStruct); currentStruct = currentStruct.SuperStruct; } inheritanceHierarchy.Reverse(); foreach (UhtStruct inheritance in inheritanceHierarchy) { ScriptGeneratorUtilities.GetExportedProperties(inheritance, exportedProperties, getSetBackedProperties); } // Check there are not properties with the same name, remove otherwise List propertyNames = new(); for (int i = 0; i < exportedProperties.Count; i++) { UhtProperty property = exportedProperties[i]; string scriptName = property.GetParameterName(); if (propertyNames.Contains(scriptName)) { exportedProperties.RemoveAt(i); i--; } else { propertyNames.Add(scriptName); } } bool nullableEnabled = structObj.HasMetadata(UhtTypeUtilities.NullableEnable); bool isRecordStruct = structObj.HasMetadata("RecordStruct"); bool isReadOnly = structObj.HasMetadata("ReadOnly"); bool useProperties = structObj.HasMetadata("UseProperties"); bool isBlittable = structObj.IsStructBlittable(); bool isCopyable = structObj.IsStructNativelyCopyable(); bool isDestructible = structObj.IsStructNativelyDestructible(); bool isEquatable = structObj.IsStructEquatable(exportedProperties); string typeNameSpace = structObj.GetNamespace(); stringBuilder.GenerateTypeSkeleton(typeNameSpace, isBlittable, nullableEnabled); stringBuilder.AppendTooltip(structObj); AttributeBuilder attributeBuilder = new AttributeBuilder(structObj); if (isBlittable) { attributeBuilder.AddIsBlittableAttribute(); attributeBuilder.AddStructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential); } attributeBuilder.AddGeneratedTypeAttribute(structObj); attributeBuilder.Finish(); stringBuilder.AppendLine(attributeBuilder.ToString()); string structName = structObj.GetStructName(); List? csInterfaces = null; if (isBlittable || !isManualExport) { csInterfaces = new List { $"MarshalledStruct<{structName}>" }; if (isDestructible) { csInterfaces.Add("IDisposable"); } } if (isEquatable) { // If null create the list and add the interface (csInterfaces ??= new()).Add($"IEquatable<{structName}>"); } stringBuilder.DeclareType(structObj, isRecordStruct ? "record struct" : "struct", structName, csInterfaces: csInterfaces, modifiers: isReadOnly ? " readonly" : null); if (isCopyable) { stringBuilder.AppendLine(isDestructible ? "private NativeStructHandle NativeHandle;" : "private byte[] Allocation;"); } // For manual exports we just want to generate attributes if (!isManualExport) { List reservedNames = GetReservedNames(exportedProperties); ExportStructProperties(structObj, stringBuilder, exportedProperties, isBlittable, reservedNames, isReadOnly, useProperties); } if (isBlittable) { StaticConstructorUtilities.ExportStaticConstructor(stringBuilder, structObj, new List(), new List(), new Dictionary(), new Dictionary(), new List(), true); stringBuilder.AppendLine(); stringBuilder.AppendLine($"public static {structName} FromNative(IntPtr buffer) => BlittableMarshaller<{structName}>.FromNative(buffer, 0);"); stringBuilder.AppendLine(); stringBuilder.AppendLine($"public void ToNative(IntPtr buffer) => BlittableMarshaller<{structName}>.ToNative(buffer, 0, this);"); } else if (!isManualExport) { stringBuilder.AppendLine(); StaticConstructorUtilities.ExportStaticConstructor(stringBuilder, structObj, exportedProperties, new List(), new Dictionary(), new Dictionary(), new List()); stringBuilder.AppendLine(); ExportMirrorStructMarshalling(stringBuilder, structObj, exportedProperties); if (isDestructible) { stringBuilder.AppendLine(); stringBuilder.AppendLine("public void Dispose()"); stringBuilder.OpenBrace(); stringBuilder.AppendLine("NativeHandle?.Dispose();"); stringBuilder.CloseBrace(); } } if (isEquatable) { ExportStructEquality(structObj, structName, stringBuilder, exportedProperties); } if (structObj.CanSupportArithmetic(exportedProperties)) { ExportStructArithmetic(structObj, structName, stringBuilder, exportedProperties); } stringBuilder.CloseBrace(); if (!isBlittable && !isManualExport) { ExportStructMarshaller(stringBuilder, structObj); } FileExporter.SaveGlueToDisk(structObj, stringBuilder); } public static void ExportStructEquality(UhtStruct structObj, string structName, GeneratorStringBuilder stringBuilder, List exportedProperties) { stringBuilder.AppendLine(); stringBuilder.AppendLine($"public override bool Equals(object? obj)"); stringBuilder.OpenBrace(); stringBuilder.AppendLine($"return obj is {structName} other && Equals(other);"); stringBuilder.CloseBrace(); stringBuilder.AppendLine(); stringBuilder.AppendLine($"public bool Equals({structName} other)"); stringBuilder.OpenBrace(); if (exportedProperties.Count == 0) { stringBuilder.AppendLine("return true;"); } else { StringBuilder equalitySb = new StringBuilder(); for (int i = 0; i < exportedProperties.Count; i++) { UhtProperty property = exportedProperties[i]; string scriptName = property.GetPropertyName(); equalitySb.Append($"this.{scriptName} == other.{scriptName}"); if (i < exportedProperties.Count - 1) { equalitySb.Append(" && "); } } stringBuilder.AppendLine($"return {equalitySb};"); } stringBuilder.CloseBrace(); stringBuilder.AppendLine(); stringBuilder.AppendLine("public override int GetHashCode()"); stringBuilder.OpenBrace(); if (exportedProperties.Count == 0) { stringBuilder.AppendLine("return 0;"); } // More accurate hashcode equality else if (exportedProperties.Count <= 8) { StringBuilder hashSb = new StringBuilder(); for (int i = 0; i < exportedProperties.Count; i++) { UhtProperty property = exportedProperties[i]; string scriptName = property.GetPropertyName(); hashSb.Append($"{scriptName}"); if (i < exportedProperties.Count - 1) { hashSb.Append(", "); } } stringBuilder.AppendLine($"return HashCode.Combine({hashSb});"); } // Fallback to xor for more than 8 properties as HashCode.Combine only supports up to 8 parameters else { StringBuilder hashSb = new StringBuilder(); for (int i = 0; i < exportedProperties.Count; i++) { UhtProperty property = exportedProperties[i]; string scriptName = property.GetPropertyName(); hashSb.Append($"{scriptName}.GetHashCode()"); if (i < exportedProperties.Count - 1) { hashSb.Append(" ^ "); } } stringBuilder.AppendLine($"return {hashSb};"); } stringBuilder.CloseBrace(); stringBuilder.AppendLine(); stringBuilder.AppendLine($"public static bool operator ==({structName} left, {structName} right)"); stringBuilder.OpenBrace(); stringBuilder.AppendLine("return left.Equals(right);"); stringBuilder.CloseBrace(); stringBuilder.AppendLine(); stringBuilder.AppendLine($"public static bool operator !=({structName} left, {structName} right)"); stringBuilder.OpenBrace(); stringBuilder.AppendLine("return !(left == right);"); stringBuilder.CloseBrace(); } public static void ExportStructArithmetic(UhtStruct structObj, string structName, GeneratorStringBuilder stringBuilder, List exportedProperties) { // Addition operator stringBuilder.AppendLine(); stringBuilder.AppendLine($"public static {structName} operator +({structName} lhs, {structName} rhs)"); stringBuilder.OpenBrace(); stringBuilder.AppendLine($"return new {structName}"); stringBuilder.OpenBrace(); stringBuilder.AppendLine(); for (int i = 0; i < exportedProperties.Count; i++) { UhtProperty property = exportedProperties[i]; string scriptName = property.GetPropertyName(); PropertyTranslator translator = PropertyTranslatorManager.GetTranslator(property)!; translator.ExportPropertyArithmetic(stringBuilder, property, ArithmeticKind.Add); if (i < exportedProperties.Count - 1) { stringBuilder.Append(", "); stringBuilder.AppendLine(); } } stringBuilder.UnIndent(); stringBuilder.AppendLine("};"); stringBuilder.CloseBrace(); // Subtraction operator stringBuilder.AppendLine(); stringBuilder.AppendLine($"public static {structName} operator -({structName} lhs, {structName} rhs)"); stringBuilder.OpenBrace(); stringBuilder.AppendLine($"return new {structName}"); stringBuilder.OpenBrace(); stringBuilder.AppendLine(); for (int i = 0; i < exportedProperties.Count; i++) { UhtProperty property = exportedProperties[i]; string scriptName = property.GetPropertyName(); PropertyTranslator translator = PropertyTranslatorManager.GetTranslator(property)!; translator.ExportPropertyArithmetic(stringBuilder, property, ArithmeticKind.Subtract); if (i < exportedProperties.Count - 1) { stringBuilder.Append(", "); stringBuilder.AppendLine(); } } stringBuilder.UnIndent(); stringBuilder.AppendLine("};"); stringBuilder.CloseBrace(); // Multiplication operator stringBuilder.AppendLine(); stringBuilder.AppendLine($"public static {structName} operator *({structName} lhs, {structName} rhs)"); stringBuilder.OpenBrace(); stringBuilder.AppendLine($"return new {structName}"); stringBuilder.OpenBrace(); stringBuilder.AppendLine(); for (int i = 0; i < exportedProperties.Count; i++) { UhtProperty property = exportedProperties[i]; string scriptName = property.GetPropertyName(); PropertyTranslator translator = PropertyTranslatorManager.GetTranslator(property)!; translator.ExportPropertyArithmetic(stringBuilder, property, ArithmeticKind.Multiply); if (i < exportedProperties.Count - 1) { stringBuilder.Append(", "); stringBuilder.AppendLine(); } } stringBuilder.UnIndent(); stringBuilder.AppendLine("};"); stringBuilder.CloseBrace(); // Division operator stringBuilder.AppendLine(); stringBuilder.AppendLine($"public static {structName} operator /({structName} lhs, {structName} rhs)"); stringBuilder.OpenBrace(); stringBuilder.AppendLine($"return new {structName}"); stringBuilder.OpenBrace(); stringBuilder.AppendLine(); for (int i = 0; i < exportedProperties.Count; i++) { UhtProperty property = exportedProperties[i]; string scriptName = property.GetPropertyName(); PropertyTranslator translator = PropertyTranslatorManager.GetTranslator(property)!; translator.ExportPropertyArithmetic(stringBuilder, property, ArithmeticKind.Divide); if (i < exportedProperties.Count - 1) { stringBuilder.Append(", "); stringBuilder.AppendLine(); } } stringBuilder.UnIndent(); stringBuilder.AppendLine("};"); stringBuilder.CloseBrace(); // Modulo operator stringBuilder.AppendLine(); stringBuilder.AppendLine($"public static {structName} operator %({structName} lhs, {structName} rhs)"); stringBuilder.OpenBrace(); stringBuilder.AppendLine($"return new {structName}"); stringBuilder.OpenBrace(); stringBuilder.AppendLine(); for (int i = 0; i < exportedProperties.Count; i++) { UhtProperty property = exportedProperties[i]; string scriptName = property.GetPropertyName(); PropertyTranslator translator = PropertyTranslatorManager.GetTranslator(property)!; translator.ExportPropertyArithmetic(stringBuilder, property, ArithmeticKind.Modulo); if (i < exportedProperties.Count - 1) { stringBuilder.Append(", "); stringBuilder.AppendLine(); } } stringBuilder.UnIndent(); stringBuilder.AppendLine("};"); stringBuilder.CloseBrace(); } public static void ExportStructProperties(UhtStruct structObj, GeneratorStringBuilder stringBuilder, List exportedProperties, bool suppressOffsets, List reservedNames, bool isReadOnly, bool useProperties) { foreach (UhtProperty property in exportedProperties) { PropertyTranslator translator = PropertyTranslatorManager.GetTranslator(property)!; translator.ExportMirrorProperty(structObj, stringBuilder, property, suppressOffsets, reservedNames, isReadOnly, useProperties); } } public static List GetReservedNames(List properties) { List reservedNames = new(); foreach (UhtProperty property in properties) { if (reservedNames.Contains(property.SourceName)) { continue; } reservedNames.Add(property.SourceName); } return reservedNames; } public static void ExportStructMarshaller(GeneratorStringBuilder builder, UhtScriptStruct structObj) { string structName = structObj.GetStructName(); builder.AppendLine(); builder.AppendLine($"public static class {structName}Marshaller"); builder.OpenBrace(); builder.AppendLine($"public static {structName} FromNative(IntPtr nativeBuffer, int arrayIndex)"); builder.OpenBrace(); builder.AppendLine($"return new {structName}(nativeBuffer + (arrayIndex * GetNativeDataSize()));"); builder.CloseBrace(); builder.AppendLine(); builder.AppendLine($"public static void ToNative(IntPtr nativeBuffer, int arrayIndex, {structName} obj)"); builder.OpenBrace(); builder.AppendLine($"obj.ToNative(nativeBuffer + (arrayIndex * GetNativeDataSize()));"); builder.CloseBrace(); builder.AppendLine(); builder.AppendLine($"public static int GetNativeDataSize()"); builder.OpenBrace(); builder.AppendLine($"return {structName}.NativeDataSize;"); builder.CloseBrace(); builder.CloseBrace(); } public static void ExportMirrorStructMarshalling(GeneratorStringBuilder builder, UhtScriptStruct structObj, List properties) { string structName = structObj.GetStructName(); bool isCopyable = structObj.IsStructNativelyCopyable(); bool isDestructible = structObj.IsStructNativelyDestructible(); if (isCopyable) { builder.AppendLine(); builder.AppendLine($"public {structName}()"); builder.OpenBrace(); builder.AppendLine(isDestructible ? "NativeHandle = new NativeStructHandle(NativeClassPtr);" : "Allocation = new byte[NativeDataSize];"); builder.CloseBrace(); } builder.AppendLine(); builder.AppendLine("[System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); builder.AppendLine($"public {structName}(IntPtr InNativeStruct)"); builder.OpenBrace(); builder.BeginUnsafeBlock(); if (isCopyable) { if (isDestructible) { builder.AppendLine("NativeHandle = new NativeStructHandle(NativeClassPtr);"); builder.AppendLine("fixed (NativeStructHandleData* StructDataPointer = &NativeHandle.Data)"); builder.OpenBrace(); builder.AppendLine($"IntPtr AllocationPointer = {ExporterCallbacks.UScriptStructCallbacks}.CallGetStructLocation(StructDataPointer, NativeClassPtr);"); } else { builder.AppendLine("Allocation = new byte[NativeDataSize];"); builder.AppendLine("fixed (byte* AllocationPointer = Allocation)"); builder.OpenBrace(); } builder.AppendLine($"{ExporterCallbacks.UScriptStructCallbacks}.CallNativeCopy(NativeClassPtr, InNativeStruct, (nint) AllocationPointer);"); builder.CloseBrace(); } else { foreach (UhtProperty property in properties) { PropertyTranslator translator = PropertyTranslatorManager.GetTranslator(property)!; string scriptName = property.GetPropertyName(); string assignmentOrReturn = $"{scriptName} ="; string offsetName = $"{property.SourceName}_Offset"; builder.TryAddWithEditor(property); translator.ExportFromNative(builder, property, property.SourceName, assignmentOrReturn, "InNativeStruct", offsetName, false, false); builder.TryEndWithEditor(property); } } builder.EndUnsafeBlock(); builder.CloseBrace(); builder.AppendLine(); builder.AppendLine($"public static {structName} FromNative(IntPtr buffer) => new {structName}(buffer);"); builder.AppendLine(); builder.AppendLine("public void ToNative(IntPtr buffer)"); builder.OpenBrace(); builder.BeginUnsafeBlock(); if (structObj.IsStructNativelyCopyable()) { if (structObj.IsStructNativelyDestructible()) { builder.AppendLine("if (NativeHandle is null)"); builder.OpenBrace(); builder.AppendLine("NativeHandle = new NativeStructHandle(NativeClassPtr);"); builder.CloseBrace(); builder.AppendLine(); builder.AppendLine("fixed (NativeStructHandleData* StructDataPointer = &NativeHandle.Data)"); builder.OpenBrace(); builder.AppendLine($"IntPtr AllocationPointer = {ExporterCallbacks.UScriptStructCallbacks}.CallGetStructLocation(StructDataPointer, NativeClassPtr);"); } else { builder.AppendLine("if (Allocation is null)"); builder.OpenBrace(); builder.AppendLine("Allocation = new byte[NativeDataSize];"); builder.AppendLine(); builder.CloseBrace(); builder.AppendLine("fixed (byte* AllocationPointer = Allocation)"); builder.OpenBrace(); } builder.AppendLine($"{ExporterCallbacks.UScriptStructCallbacks}.CallNativeCopy(NativeClassPtr, (nint) AllocationPointer, buffer);"); builder.CloseBrace(); } else { foreach (UhtProperty property in properties) { PropertyTranslator translator = PropertyTranslatorManager.GetTranslator(property)!; string scriptName = property.GetPropertyName(); string offsetName = $"{property.SourceName}_Offset"; builder.TryAddWithEditor(property); translator.ExportToNative(builder, property, property.SourceName, "buffer", offsetName, scriptName); builder.TryEndWithEditor(property); } } builder.EndUnsafeBlock(); builder.CloseBrace(); } }