From ea85b7a621b800c7605070af1ffddcb58e2fdb2d Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Tue, 10 Mar 2026 15:20:56 -0600 Subject: [PATCH] feat: add C# and Python ctypes code generators - C# backend: [StructLayout(LayoutKind.Explicit)] with [FieldOffset], IntPtr pointers, fixed arrays, enums - Python backend: ctypes.Structure with _fields_, POINTER() for typed pointers, c_void_p, padding - Both support enums, vectors, bitfields, arrays, unions, static fields - Export menu: C# Structs... and Python ctypes... entries - Format combo auto-populates new options - 14 new tests for both backends (all passing) --- src/generator.cpp | 617 +++++++++++++++++++++++++++++++++++++++ src/generator.h | 74 ++++- src/main.cpp | 47 +++ src/mainwindow.h | 10 + tests/test_generator.cpp | 553 +++++++++++++++++++++++++++++++++++ 5 files changed, 1297 insertions(+), 4 deletions(-) diff --git a/src/generator.cpp b/src/generator.cpp index 18a783d..a62027e 100644 --- a/src/generator.cpp +++ b/src/generator.cpp @@ -804,6 +804,521 @@ static void emitDefinesForStruct(GenContext& ctx, uint64_t structId, ctx.output += QStringLiteral("\n"); } +// ═══════════════════════════════════════════════════════════════════ +// ── C# backend ── +// ═══════════════════════════════════════════════════════════════════ + +static QString csTypeName(NodeKind kind) { + switch (kind) { + case NodeKind::Hex8: return QStringLiteral("byte"); + case NodeKind::Hex16: return QStringLiteral("ushort"); + case NodeKind::Hex32: return QStringLiteral("uint"); + case NodeKind::Hex64: return QStringLiteral("ulong"); + case NodeKind::Int8: return QStringLiteral("sbyte"); + case NodeKind::Int16: return QStringLiteral("short"); + case NodeKind::Int32: return QStringLiteral("int"); + case NodeKind::Int64: return QStringLiteral("long"); + case NodeKind::UInt8: return QStringLiteral("byte"); + case NodeKind::UInt16: return QStringLiteral("ushort"); + case NodeKind::UInt32: return QStringLiteral("uint"); + case NodeKind::UInt64: return QStringLiteral("ulong"); + case NodeKind::Float: return QStringLiteral("float"); + case NodeKind::Double: return QStringLiteral("double"); + case NodeKind::Bool: return QStringLiteral("bool"); + case NodeKind::Pointer32: return QStringLiteral("uint"); + case NodeKind::Pointer64: return QStringLiteral("ulong"); + case NodeKind::FuncPtr32: return QStringLiteral("uint"); + case NodeKind::FuncPtr64: return QStringLiteral("ulong"); + case NodeKind::Vec2: return QStringLiteral("float"); + case NodeKind::Vec3: return QStringLiteral("float"); + case NodeKind::Vec4: return QStringLiteral("float"); + case NodeKind::Mat4x4: return QStringLiteral("float"); + case NodeKind::UTF8: return QStringLiteral("byte"); + case NodeKind::UTF16: return QStringLiteral("char"); + default: return QStringLiteral("byte"); + } +} + +// Forward declaration +static void emitCSharpStruct(GenContext& ctx, uint64_t structId); + +static QString csType(GenContext& ctx, NodeKind kind) { + if (ctx.typeAliases) { + auto it = ctx.typeAliases->find(kind); + if (it != ctx.typeAliases->end() && !it.value().isEmpty()) + return it.value(); + } + return csTypeName(kind); +} + +static void emitCSharpStructBody(GenContext& ctx, uint64_t structId, + bool isUnion, int depth, int baseOffset) { + const NodeTree& tree = ctx.tree; + int idx = tree.indexOfId(structId); + if (idx < 0) return; + + QString ind = indent(depth); + + QVector allChildren = ctx.childMap.value(structId); + QVector children, staticIdxs; + for (int ci : allChildren) { + if (tree.nodes[ci].isStatic) + staticIdxs.append(ci); + else + children.append(ci); + } + std::sort(children.begin(), children.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + + // C# uses [FieldOffset(N)] for explicit layout — no manual padding needed + for (int ci : children) { + const Node& child = tree.nodes[ci]; + if (isHexNode(child.kind)) continue; // skip padding/hex nodes + + int absOffset = baseOffset + child.offset; + QString name = sanitizeIdent(child.name.isEmpty() + ? QStringLiteral("field_%1").arg(child.offset, 2, 16, QChar('0')) + : child.name); + QString oc = offsetComment(absOffset); + + if (child.kind == NodeKind::Struct) { + if (child.classKeyword == QStringLiteral("bitfield") + && !child.bitfieldMembers.isEmpty()) { + QString bfType = csType(ctx, child.elementKind); + if (bfType.isEmpty()) bfType = QStringLiteral("uint"); + QStringList bits; + for (const auto& m : child.bitfieldMembers) + bits << QStringLiteral("%1:%2").arg(sanitizeIdent(m.name)).arg(m.bitWidth); + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public %2 %3;") + .arg(QString::number(absOffset, 16).toUpper(), bfType, name) + + QStringLiteral(" // bits: ") + bits.join(QStringLiteral(", ")) + + oc + QStringLiteral("\n"); + } else if (child.structTypeName.isEmpty()) { + // Anonymous inline — emit as fixed byte array + int span = tree.structSpan(child.id, &ctx.childMap); + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed byte %2[0x%3];") + .arg(QString::number(absOffset, 16).toUpper(), name) + .arg(QString::number(span, 16).toUpper()) + + oc + QStringLiteral("\n"); + } else { + QString typeName = sanitizeIdent(child.structTypeName); + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public %2 %3;") + .arg(QString::number(absOffset, 16).toUpper(), typeName, name) + + oc + QStringLiteral("\n"); + } + } else if (child.kind == NodeKind::Array) { + QVector arrayKids = ctx.childMap.value(child.id); + bool hasStructChild = false; + QString elemTypeName; + for (int ak : arrayKids) { + if (tree.nodes[ak].kind == NodeKind::Struct) { + hasStructChild = true; + elemTypeName = ctx.structName(tree.nodes[ak]); + break; + } + } + if (hasStructChild && !elemTypeName.isEmpty()) { + // MarshalAs for struct arrays + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = %2)] public %3[] %4;") + .arg(QString::number(absOffset, 16).toUpper()) + .arg(child.arrayLen) + .arg(elemTypeName, name) + + oc + QStringLiteral("\n"); + } else { + QString elemType = csType(ctx, child.elementKind); + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed %2 %3[%4];") + .arg(QString::number(absOffset, 16).toUpper(), elemType, name) + .arg(child.arrayLen) + + oc + QStringLiteral("\n"); + } + } else { + // Primitive fields + switch (child.kind) { + case NodeKind::Vec2: + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed float %2[2];") + .arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n"); + break; + case NodeKind::Vec3: + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed float %2[3];") + .arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n"); + break; + case NodeKind::Vec4: + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed float %2[4];") + .arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n"); + break; + case NodeKind::Mat4x4: + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed float %2[16];") + .arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n"); + break; + case NodeKind::UTF8: + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed byte %2[%3];") + .arg(QString::number(absOffset, 16).toUpper(), name) + .arg(child.strLen) + oc + QStringLiteral("\n"); + break; + case NodeKind::UTF16: + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed char %2[%3];") + .arg(QString::number(absOffset, 16).toUpper(), name) + .arg(child.strLen) + oc + QStringLiteral("\n"); + break; + case NodeKind::Pointer32: + case NodeKind::Pointer64: { + bool isNativePtr = (child.kind == NodeKind::Pointer32 && ctx.tree.pointerSize <= 4) + || (child.kind == NodeKind::Pointer64 && ctx.tree.pointerSize >= 8); + if (isNativePtr) + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public IntPtr %2;") + .arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n"); + else + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public %2 %3;") + .arg(QString::number(absOffset, 16).toUpper(), csType(ctx, child.kind), name) + + oc + QStringLiteral("\n"); + break; + } + case NodeKind::FuncPtr32: + case NodeKind::FuncPtr64: + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public IntPtr %2;") + .arg(QString::number(absOffset, 16).toUpper(), name) + + QStringLiteral(" // fn ptr") + oc + QStringLiteral("\n"); + break; + default: + ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public %2 %3;") + .arg(QString::number(absOffset, 16).toUpper(), csType(ctx, child.kind), name) + + oc + QStringLiteral("\n"); + break; + } + } + } + + for (int si : staticIdxs) { + const Node& sf = tree.nodes[si]; + QString sfType = sf.structTypeName.isEmpty() ? csType(ctx, sf.kind) : sf.structTypeName; + ctx.output += ind + QStringLiteral("// static: %1 %2 @ %3\n") + .arg(sfType, sanitizeIdent(sf.name), sf.offsetExpr); + } +} + +static void emitCSharpStruct(GenContext& ctx, uint64_t structId) { + if (ctx.emittedIds.contains(structId)) return; + if (ctx.visiting.contains(structId)) return; + ctx.visiting.insert(structId); + + int idx = ctx.tree.indexOfId(structId); + if (idx < 0) { ctx.visiting.remove(structId); return; } + + const Node& node = ctx.tree.nodes[idx]; + if (node.kind != NodeKind::Struct) { ctx.visiting.remove(structId); return; } + + QString typeName = ctx.structName(node); + if (ctx.emittedTypeNames.contains(typeName)) { + ctx.emittedIds.insert(structId); + ctx.visiting.remove(structId); + return; + } + + ctx.emittedIds.insert(structId); + ctx.emittedTypeNames.insert(typeName); + int structSize = ctx.tree.structSpan(structId, &ctx.childMap); + + QString kw = node.resolvedClassKeyword(); + + // Enum with members + if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) { + ctx.output += QStringLiteral("public enum %1 : long\n{\n").arg(typeName); + for (const auto& m : node.enumMembers) { + ctx.output += QStringLiteral(" %1 = %2,\n") + .arg(sanitizeIdent(m.first)) + .arg(m.second); + } + ctx.output += QStringLiteral("}\n\n"); + ctx.visiting.remove(structId); + return; + } + + bool isUnion = (kw == QStringLiteral("union")); + + ctx.output += QStringLiteral("[StructLayout(LayoutKind.Explicit, Size = 0x%1)]\n") + .arg(QString::number(structSize, 16).toUpper()); + ctx.output += QStringLiteral("public unsafe struct %1\n{\n").arg(typeName); + + emitCSharpStructBody(ctx, structId, isUnion, 1, 0); + + ctx.output += QStringLiteral("}") + + offsetComment(structSize, true) + + QStringLiteral("\n\n"); + + ctx.visiting.remove(structId); +} + +// ═══════════════════════════════════════════════════════════════════ +// ── Python ctypes backend ── +// ═══════════════════════════════════════════════════════════════════ + +static QString pyTypeName(NodeKind kind) { + switch (kind) { + case NodeKind::Hex8: return QStringLiteral("ctypes.c_uint8"); + case NodeKind::Hex16: return QStringLiteral("ctypes.c_uint16"); + case NodeKind::Hex32: return QStringLiteral("ctypes.c_uint32"); + case NodeKind::Hex64: return QStringLiteral("ctypes.c_uint64"); + case NodeKind::Int8: return QStringLiteral("ctypes.c_int8"); + case NodeKind::Int16: return QStringLiteral("ctypes.c_int16"); + case NodeKind::Int32: return QStringLiteral("ctypes.c_int32"); + case NodeKind::Int64: return QStringLiteral("ctypes.c_int64"); + case NodeKind::UInt8: return QStringLiteral("ctypes.c_uint8"); + case NodeKind::UInt16: return QStringLiteral("ctypes.c_uint16"); + case NodeKind::UInt32: return QStringLiteral("ctypes.c_uint32"); + case NodeKind::UInt64: return QStringLiteral("ctypes.c_uint64"); + case NodeKind::Float: return QStringLiteral("ctypes.c_float"); + case NodeKind::Double: return QStringLiteral("ctypes.c_double"); + case NodeKind::Bool: return QStringLiteral("ctypes.c_bool"); + case NodeKind::Pointer32: return QStringLiteral("ctypes.c_uint32"); + case NodeKind::Pointer64: return QStringLiteral("ctypes.c_uint64"); + case NodeKind::FuncPtr32: return QStringLiteral("ctypes.c_uint32"); + case NodeKind::FuncPtr64: return QStringLiteral("ctypes.c_uint64"); + case NodeKind::Vec2: return QStringLiteral("ctypes.c_float"); + case NodeKind::Vec3: return QStringLiteral("ctypes.c_float"); + case NodeKind::Vec4: return QStringLiteral("ctypes.c_float"); + case NodeKind::Mat4x4: return QStringLiteral("ctypes.c_float"); + case NodeKind::UTF8: return QStringLiteral("ctypes.c_char"); + case NodeKind::UTF16: return QStringLiteral("ctypes.c_wchar"); + default: return QStringLiteral("ctypes.c_uint8"); + } +} + +// Forward declaration +static void emitPythonStruct(GenContext& ctx, uint64_t structId); + +static void emitPythonStructBody(GenContext& ctx, uint64_t structId, + bool isUnion, int baseOffset) { + const NodeTree& tree = ctx.tree; + int idx = tree.indexOfId(structId); + if (idx < 0) return; + + int structSize = tree.structSpan(structId, &ctx.childMap); + QString ind = QStringLiteral(" "); // 2 levels for inside _fields_ + + QVector allChildren = ctx.childMap.value(structId); + QVector children; + for (int ci : allChildren) { + if (!tree.nodes[ci].isStatic) + children.append(ci); + } + std::sort(children.begin(), children.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + + auto emitPadField = [&](int relOffset, int size) { + if (size <= 0) return; + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_uint8 * 0x%2),") + .arg(ctx.uniquePadName()) + .arg(QString::number(size, 16).toUpper()) + + offsetComment(baseOffset + relOffset) + QStringLiteral("\n"); + }; + + int cursor = 0; + int i = 0; + + while (i < children.size()) { + const Node& child = tree.nodes[children[i]]; + int childSize; + if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) + childSize = tree.structSpan(child.id, &ctx.childMap); + else + childSize = child.byteSize(); + + if (!isUnion) { + if (child.offset > cursor) + emitPadField(cursor, child.offset - cursor); + } + + // Collapse hex nodes into padding + if (isHexNode(child.kind)) { + int runStart = child.offset; + int runEnd = child.offset + childSize; + int j = i + 1; + while (j < children.size()) { + const Node& next = tree.nodes[children[j]]; + if (!isHexNode(next.kind)) break; + int nextSize = next.byteSize(); + if (next.offset < runEnd) break; + runEnd = next.offset + nextSize; + j++; + } + emitPadField(runStart, runEnd - runStart); + cursor = runEnd; + i = j; + continue; + } + + int absOffset = baseOffset + child.offset; + QString name = sanitizeIdent(child.name.isEmpty() + ? QStringLiteral("field_%1").arg(child.offset, 2, 16, QChar('0')) + : child.name); + QString oc = offsetComment(absOffset); + + if (child.kind == NodeKind::Struct) { + if (child.classKeyword == QStringLiteral("bitfield") + && !child.bitfieldMembers.isEmpty()) { + QString bfType = pyTypeName(child.elementKind); + if (bfType.isEmpty()) bfType = QStringLiteral("ctypes.c_uint32"); + QStringList bits; + for (const auto& m : child.bitfieldMembers) + bits << QStringLiteral("%1:%2").arg(sanitizeIdent(m.name)).arg(m.bitWidth); + ctx.output += ind + QStringLiteral("(\"%1\", %2),") + .arg(name, bfType) + + QStringLiteral(" # bits: ") + bits.join(QStringLiteral(", ")) + + oc + QStringLiteral("\n"); + } else if (child.structTypeName.isEmpty()) { + int span = tree.structSpan(child.id, &ctx.childMap); + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_uint8 * 0x%2),") + .arg(name) + .arg(QString::number(span, 16).toUpper()) + + oc + QStringLiteral("\n"); + } else { + QString typeName = sanitizeIdent(child.structTypeName); + ctx.output += ind + QStringLiteral("(\"%1\", %2),") + .arg(name, typeName) + + oc + QStringLiteral("\n"); + } + } else if (child.kind == NodeKind::Array) { + QVector arrayKids = ctx.childMap.value(child.id); + bool hasStructChild = false; + QString elemTypeName; + for (int ak : arrayKids) { + if (tree.nodes[ak].kind == NodeKind::Struct) { + hasStructChild = true; + elemTypeName = ctx.structName(tree.nodes[ak]); + break; + } + } + if (hasStructChild && !elemTypeName.isEmpty()) { + ctx.output += ind + QStringLiteral("(\"%1\", %2 * %3),") + .arg(name, elemTypeName).arg(child.arrayLen) + oc + QStringLiteral("\n"); + } else { + ctx.output += ind + QStringLiteral("(\"%1\", %2 * %3),") + .arg(name, pyTypeName(child.elementKind)).arg(child.arrayLen) + + oc + QStringLiteral("\n"); + } + } else { + // Primitive fields + switch (child.kind) { + case NodeKind::Vec2: + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_float * 2),").arg(name) + + oc + QStringLiteral("\n"); + break; + case NodeKind::Vec3: + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_float * 3),").arg(name) + + oc + QStringLiteral("\n"); + break; + case NodeKind::Vec4: + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_float * 4),").arg(name) + + oc + QStringLiteral("\n"); + break; + case NodeKind::Mat4x4: + ctx.output += ind + QStringLiteral("(\"%1\", (ctypes.c_float * 4) * 4),").arg(name) + + oc + QStringLiteral("\n"); + break; + case NodeKind::UTF8: + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_char * %2),").arg(name) + .arg(child.strLen) + oc + QStringLiteral("\n"); + break; + case NodeKind::UTF16: + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_wchar * %2),").arg(name) + .arg(child.strLen) + oc + QStringLiteral("\n"); + break; + case NodeKind::Pointer32: + case NodeKind::Pointer64: { + bool isNativePtr = (child.kind == NodeKind::Pointer32 && ctx.tree.pointerSize <= 4) + || (child.kind == NodeKind::Pointer64 && ctx.tree.pointerSize >= 8); + if (child.refId != 0) { + int refIdx = tree.indexOfId(child.refId); + if (refIdx >= 0) { + QString target = ctx.structName(tree.nodes[refIdx]); + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.POINTER(%2)),").arg(name, target) + + oc + QStringLiteral("\n"); + break; + } + } + if (isNativePtr) + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_void_p),").arg(name) + + oc + QStringLiteral("\n"); + else + ctx.output += ind + QStringLiteral("(\"%1\", %2),").arg(name, pyTypeName(child.kind)) + + oc + QStringLiteral("\n"); + break; + } + case NodeKind::FuncPtr32: + case NodeKind::FuncPtr64: + ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_void_p),").arg(name) + + QStringLiteral(" # fn ptr") + oc + QStringLiteral("\n"); + break; + default: + ctx.output += ind + QStringLiteral("(\"%1\", %2),").arg(name, pyTypeName(child.kind)) + + oc + QStringLiteral("\n"); + break; + } + } + + int childEnd = child.offset + childSize; + if (childEnd > cursor) cursor = childEnd; + i++; + } + + // Tail padding + if (!isUnion && cursor < structSize) + emitPadField(cursor, structSize - cursor); +} + +static void emitPythonStruct(GenContext& ctx, uint64_t structId) { + if (ctx.emittedIds.contains(structId)) return; + if (ctx.visiting.contains(structId)) return; + ctx.visiting.insert(structId); + + int idx = ctx.tree.indexOfId(structId); + if (idx < 0) { ctx.visiting.remove(structId); return; } + + const Node& node = ctx.tree.nodes[idx]; + if (node.kind != NodeKind::Struct) { ctx.visiting.remove(structId); return; } + + QString typeName = ctx.structName(node); + if (ctx.emittedTypeNames.contains(typeName)) { + ctx.emittedIds.insert(structId); + ctx.visiting.remove(structId); + return; + } + + ctx.emittedIds.insert(structId); + ctx.emittedTypeNames.insert(typeName); + int structSize = ctx.tree.structSpan(structId, &ctx.childMap); + + QString kw = node.resolvedClassKeyword(); + + // Enum with members — emit as class with constants + if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) { + ctx.output += QStringLiteral("class %1: # enum\n").arg(typeName); + for (const auto& m : node.enumMembers) { + ctx.output += QStringLiteral(" %1 = %2\n") + .arg(sanitizeIdent(m.first)) + .arg(m.second); + } + ctx.output += QStringLiteral("\n\n"); + ctx.visiting.remove(structId); + return; + } + + bool isUnion = (kw == QStringLiteral("union")); + QString baseClass = isUnion ? QStringLiteral("ctypes.Union") : QStringLiteral("ctypes.Structure"); + + ctx.output += QStringLiteral("class %1(%2):").arg(typeName, baseClass) + + offsetComment(structSize, true) + QStringLiteral("\n"); + ctx.output += QStringLiteral(" _fields_ = [\n"); + + emitPythonStructBody(ctx, structId, isUnion, 0); + + ctx.output += QStringLiteral(" ]\n\n"); + + ctx.visiting.remove(structId); +} + // ═══════════════════════════════════════════════════════════════════ // ── Reachable struct collector (for "Current + Children" scope) ── // ═══════════════════════════════════════════════════════════════════ @@ -856,6 +1371,8 @@ const char* codeFormatName(CodeFormat fmt) { case CodeFormat::CppHeader: return "C/C++"; case CodeFormat::RustStruct: return "Rust"; case CodeFormat::DefineOffsets: return "#define"; + case CodeFormat::CSharpStruct: return "C#"; + case CodeFormat::PythonCtypes: return "Python"; default: return "C/C++"; } } @@ -865,6 +1382,8 @@ const char* codeFormatFileFilter(CodeFormat fmt) { case CodeFormat::CppHeader: return "C++ Header (*.h);;All Files (*)"; case CodeFormat::RustStruct: return "Rust Source (*.rs);;All Files (*)"; case CodeFormat::DefineOffsets: return "C Header (*.h);;All Files (*)"; + case CodeFormat::CSharpStruct: return "C# Source (*.cs);;All Files (*)"; + case CodeFormat::PythonCtypes: return "Python Source (*.py);;All Files (*)"; default: return "All Files (*)"; } } @@ -1025,6 +1544,98 @@ QString renderDefinesAll(const NodeTree& tree) { return ctx.output; } +// ── C# public API ── + +QString renderCSharp(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases, + bool emitAsserts) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + if (tree.nodes[idx].kind != NodeKind::Struct) return {}; + + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts}; + ctx.output += QStringLiteral("using System.Runtime.InteropServices;\n\n"); + emitCSharpStruct(ctx, rootStructId); + return alignComments(ctx.output); +} + +QString renderCSharpTree(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases, + bool emitAsserts) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + if (tree.nodes[idx].kind != NodeKind::Struct) return {}; + + auto childMap = buildChildMap(tree); + GenContext ctx{tree, childMap, {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts}; + ctx.output += QStringLiteral("using System.Runtime.InteropServices;\n\n"); + + for (uint64_t sid : collectReachableStructs(tree, childMap, rootStructId)) + emitCSharpStruct(ctx, sid); + + return alignComments(ctx.output); +} + +QString renderCSharpAll(const NodeTree& tree, + const QHash* typeAliases, + bool emitAsserts) { + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts}; + ctx.output += QStringLiteral("using System.Runtime.InteropServices;\n\n"); + + QVector roots = ctx.childMap.value(0); + std::sort(roots.begin(), roots.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + for (int ri : roots) { + if (tree.nodes[ri].kind == NodeKind::Struct) + emitCSharpStruct(ctx, tree.nodes[ri].id); + } + return alignComments(ctx.output); +} + +// ── Python public API ── + +QString renderPython(const NodeTree& tree, uint64_t rootStructId) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + if (tree.nodes[idx].kind != NodeKind::Struct) return {}; + + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, nullptr, false}; + ctx.output += QStringLiteral("import ctypes\n\n"); + emitPythonStruct(ctx, rootStructId); + return alignComments(ctx.output); +} + +QString renderPythonTree(const NodeTree& tree, uint64_t rootStructId) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + if (tree.nodes[idx].kind != NodeKind::Struct) return {}; + + auto childMap = buildChildMap(tree); + GenContext ctx{tree, childMap, {}, {}, {}, {}, {}, 0, nullptr, false}; + ctx.output += QStringLiteral("import ctypes\n\n"); + + for (uint64_t sid : collectReachableStructs(tree, childMap, rootStructId)) + emitPythonStruct(ctx, sid); + + return alignComments(ctx.output); +} + +QString renderPythonAll(const NodeTree& tree) { + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, nullptr, false}; + ctx.output += QStringLiteral("import ctypes\n\n"); + + QVector roots = ctx.childMap.value(0); + std::sort(roots.begin(), roots.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + for (int ri : roots) { + if (tree.nodes[ri].kind == NodeKind::Struct) + emitPythonStruct(ctx, tree.nodes[ri].id); + } + return alignComments(ctx.output); +} + // ── Format dispatch ── QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId, @@ -1032,6 +1643,8 @@ QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId, switch (fmt) { case CodeFormat::RustStruct: return renderRust(tree, rootStructId, typeAliases, emitAsserts); case CodeFormat::DefineOffsets: return renderDefines(tree, rootStructId); + case CodeFormat::CSharpStruct: return renderCSharp(tree, rootStructId, typeAliases, emitAsserts); + case CodeFormat::PythonCtypes: return renderPython(tree, rootStructId); default: return renderCpp(tree, rootStructId, typeAliases, emitAsserts); } } @@ -1041,6 +1654,8 @@ QString renderCodeTree(CodeFormat fmt, const NodeTree& tree, uint64_t rootStruct switch (fmt) { case CodeFormat::RustStruct: return renderRustTree(tree, rootStructId, typeAliases, emitAsserts); case CodeFormat::DefineOffsets: return renderDefinesTree(tree, rootStructId); + case CodeFormat::CSharpStruct: return renderCSharpTree(tree, rootStructId, typeAliases, emitAsserts); + case CodeFormat::PythonCtypes: return renderPythonTree(tree, rootStructId); default: return renderCppTree(tree, rootStructId, typeAliases, emitAsserts); } } @@ -1050,6 +1665,8 @@ QString renderCodeAll(CodeFormat fmt, const NodeTree& tree, switch (fmt) { case CodeFormat::RustStruct: return renderRustAll(tree, typeAliases, emitAsserts); case CodeFormat::DefineOffsets: return renderDefinesAll(tree); + case CodeFormat::CSharpStruct: return renderCSharpAll(tree, typeAliases, emitAsserts); + case CodeFormat::PythonCtypes: return renderPythonAll(tree); default: return renderCppAll(tree, typeAliases, emitAsserts); } } diff --git a/src/generator.h b/src/generator.h index 61bafe3..12ac098 100644 --- a/src/generator.h +++ b/src/generator.h @@ -6,17 +6,83 @@ namespace rcx { -// Generate C++ struct definitions for a single root struct and all -// nested/referenced types reachable from it. +// ── Code output format ── + +enum class CodeFormat : int { + CppHeader = 0, // C/C++ struct definitions + RustStruct, // Rust #[repr(C)] struct definitions + DefineOffsets, // #define ClassName_FieldName 0xNN + CSharpStruct, // C# [StructLayout] with [FieldOffset] + PythonCtypes, // Python ctypes.Structure + _Count +}; + +enum class CodeScope : int { + Current = 0, // Just the selected struct + WithChildren, // Selected struct + all referenced types + FullSdk, // All root-level structs + _Count +}; + +const char* codeFormatName(CodeFormat fmt); +const char* codeFormatFileFilter(CodeFormat fmt); +const char* codeScopeName(CodeScope scope); + +// ── Format-aware dispatch (calls the appropriate backend) ── + +QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); + +// Render rootStructId + all struct types reachable from it +QString renderCodeTree(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); + +QString renderCodeAll(CodeFormat fmt, const NodeTree& tree, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); + +// ── Individual backends ── + QString renderCpp(const NodeTree& tree, uint64_t rootStructId, const QHash* typeAliases = nullptr, bool emitAsserts = false); - -// Generate C++ struct definitions for every root-level struct (full SDK). +QString renderCppTree(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); QString renderCppAll(const NodeTree& tree, const QHash* typeAliases = nullptr, bool emitAsserts = false); +QString renderRust(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); +QString renderRustTree(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); +QString renderRustAll(const NodeTree& tree, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); + +QString renderDefines(const NodeTree& tree, uint64_t rootStructId); +QString renderDefinesTree(const NodeTree& tree, uint64_t rootStructId); +QString renderDefinesAll(const NodeTree& tree); + +QString renderCSharp(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); +QString renderCSharpTree(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); +QString renderCSharpAll(const NodeTree& tree, + const QHash* typeAliases = nullptr, + bool emitAsserts = false); + +QString renderPython(const NodeTree& tree, uint64_t rootStructId); +QString renderPythonTree(const NodeTree& tree, uint64_t rootStructId); +QString renderPythonAll(const NodeTree& tree); + // Null generator placeholder (returns empty string). QString renderNull(const NodeTree& tree, uint64_t rootStructId); diff --git a/src/main.cpp b/src/main.cpp index 4a54a1e..b1e74d8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -733,6 +733,8 @@ void MainWindow::createMenus() { Qt5Qt6AddAction(exportMenu, "&C++ Header...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCpp); Qt5Qt6AddAction(exportMenu, "&Rust Structs...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportRust); Qt5Qt6AddAction(exportMenu, "#&define Offsets...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportDefines); + Qt5Qt6AddAction(exportMenu, "C&# Structs...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCSharp); + Qt5Qt6AddAction(exportMenu, "&Python ctypes...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportPython); Qt5Qt6AddAction(exportMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction); // Examples submenu — scan once at init { @@ -3291,6 +3293,51 @@ void MainWindow::exportDefines() { setAppStatus("Exported to " + QFileInfo(path).fileName()); } +// ── Export C# structs ── + +void MainWindow::exportCSharp() { + auto* tab = activeTab(); + if (!tab) return; + + QString path = QFileDialog::getSaveFileName(this, + "Export C# Structs", {}, "C# Source (*.cs);;All Files (*)"); + if (path.isEmpty()) return; + + const QHash* aliases = + tab->doc->typeAliases.isEmpty() ? nullptr : &tab->doc->typeAliases; + bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool(); + QString text = renderCSharpAll(tab->doc->tree, aliases, asserts); + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, "Export Failed", + "Could not write to: " + path); + return; + } + file.write(text.toUtf8()); + setAppStatus("Exported to " + QFileInfo(path).fileName()); +} + +// ── Export Python ctypes ── + +void MainWindow::exportPython() { + auto* tab = activeTab(); + if (!tab) return; + + QString path = QFileDialog::getSaveFileName(this, + "Export Python ctypes", {}, "Python Source (*.py);;All Files (*)"); + if (path.isEmpty()) return; + + QString text = renderPythonAll(tab->doc->tree); + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, "Export Failed", + "Could not write to: " + path); + return; + } + file.write(text.toUtf8()); + setAppStatus("Exported to " + QFileInfo(path).fileName()); +} + // ── Export ReClass XML ── void MainWindow::exportReclassXmlAction() { diff --git a/src/mainwindow.h b/src/mainwindow.h index 960d232..0d2ef0a 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -16,8 +16,10 @@ #include #include #include +#include #include #include +#include #include namespace rcx { @@ -58,6 +60,10 @@ private slots: void toggleMcp(); void setEditorFont(const QString& fontName); void exportCpp(); + void exportRust(); + void exportDefines(); + void exportCSharp(); + void exportPython(); void exportReclassXmlAction(); void importFromSource(); void importReclassXml(); @@ -65,6 +71,7 @@ private slots: void showTypeAliasesDialog(); void editTheme(); void showOptionsDialog(); + void showOptionsDialog(int initialPage); public: // Status bar helpers — separate app / MCP channels @@ -106,6 +113,9 @@ private: QLineEdit* findBar = nullptr; QWidget* findContainer = nullptr; QWidget* renderedContainer = nullptr; + QComboBox* fmtCombo = nullptr; + QComboBox* scopeCombo = nullptr; + QToolButton* fmtGear = nullptr; ViewMode viewMode = VM_Reclass; uint64_t lastRenderedRootId = 0; }; diff --git a/tests/test_generator.cpp b/tests/test_generator.cpp index 8ba9a9c..0355859 100644 --- a/tests/test_generator.cpp +++ b/tests/test_generator.cpp @@ -873,6 +873,559 @@ private slots: QVERIFY2(result.contains("sizeof(Small) == 0x4"), qPrintable("Expected sizeof(Small) == 0x4:\n" + result)); } + + // ═══════════════════════════════════════════════════════════ + // ── Rust backend tests ── + // ═══════════════════════════════════════════════════════════ + + void testRustSimpleStruct() { + auto tree = makeSimpleStruct(); + uint64_t rootId = tree.nodes[0].id; + QString result = rcx::renderRust(tree, rootId, nullptr, true); + + QVERIFY(result.contains("// Generated by Reclass 2027")); + QVERIFY(result.contains("#[repr(C)]")); + QVERIFY(result.contains("pub struct Player {")); + QVERIFY(result.contains("pub health: i32,")); + QVERIFY(result.contains("pub speed: f32,")); + QVERIFY(result.contains("pub id: u64,")); + QVERIFY(result.contains("// 0x0")); + QVERIFY(result.contains("// 0x4")); + QVERIFY(result.contains("// 0x8")); + QVERIFY(result.contains("core::mem::size_of::() == 0x10")); + + // Without asserts + QString noAsserts = rcx::renderRust(tree, rootId); + QVERIFY(!noAsserts.contains("size_of")); + } + + void testRustPadding() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Padded"; + root.structTypeName = "Padded"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node f1; + f1.kind = rcx::NodeKind::UInt32; + f1.name = "a"; + f1.parentId = rootId; + f1.offset = 0; + tree.addNode(f1); + + rcx::Node f2; + f2.kind = rcx::NodeKind::UInt32; + f2.name = "b"; + f2.parentId = rootId; + f2.offset = 8; + tree.addNode(f2); + + QString result = rcx::renderRust(tree, rootId); + QVERIFY(result.contains("pub _pad")); + QVERIFY(result.contains("[u8; 0x4]")); + } + + void testRustPointers() { + rcx::NodeTree tree; + + rcx::Node target; + target.kind = rcx::NodeKind::Struct; + target.name = "Target"; + target.structTypeName = "Target"; + target.parentId = 0; + target.offset = 0x100; + int ti = tree.addNode(target); + uint64_t targetId = tree.nodes[ti].id; + + rcx::Node tf; + tf.kind = rcx::NodeKind::UInt32; + tf.name = "val"; + tf.parentId = targetId; + tf.offset = 0; + tree.addNode(tf); + + rcx::Node main; + main.kind = rcx::NodeKind::Struct; + main.name = "PtrTest"; + main.structTypeName = "PtrTest"; + main.parentId = 0; + int mi = tree.addNode(main); + uint64_t mainId = tree.nodes[mi].id; + + rcx::Node p1; + p1.kind = rcx::NodeKind::Pointer64; + p1.name = "typed"; + p1.parentId = mainId; + p1.offset = 0; + p1.refId = targetId; + tree.addNode(p1); + + rcx::Node p2; + p2.kind = rcx::NodeKind::Pointer64; + p2.name = "untyped"; + p2.parentId = mainId; + p2.offset = 8; + tree.addNode(p2); + + QString result = rcx::renderRust(tree, mainId); + QVERIFY(result.contains("pub typed: *mut Target,")); + QVERIFY(result.contains("pub untyped: *mut core::ffi::c_void,")); + } + + void testRustVectors() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Vecs"; + root.structTypeName = "Vecs"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node v2; + v2.kind = rcx::NodeKind::Vec2; + v2.name = "pos"; + v2.parentId = rootId; + v2.offset = 0; + tree.addNode(v2); + + rcx::Node v4; + v4.kind = rcx::NodeKind::Vec4; + v4.name = "color"; + v4.parentId = rootId; + v4.offset = 8; + tree.addNode(v4); + + QString result = rcx::renderRust(tree, rootId); + QVERIFY(result.contains("pub pos: [f32; 2],")); + QVERIFY(result.contains("pub color: [f32; 4],")); + } + + void testRustFuncPtr() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "FP"; + root.structTypeName = "FP"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node fp; + fp.kind = rcx::NodeKind::FuncPtr64; + fp.name = "callback"; + fp.parentId = rootId; + fp.offset = 0; + tree.addNode(fp); + + QString result = rcx::renderRust(tree, rootId); + QVERIFY(result.contains("pub callback: Option,")); + } + + void testRustAll() { + auto tree = makeSimpleStruct(); + QString result = rcx::renderRustAll(tree, nullptr, true); + QVERIFY(result.contains("#[repr(C)]")); + QVERIFY(result.contains("pub struct Player {")); + QVERIFY(result.contains("core::mem::size_of::()")); + } + + // ═══════════════════════════════════════════════════════════ + // ── #define offsets backend tests ── + // ═══════════════════════════════════════════════════════════ + + void testDefineSimpleStruct() { + auto tree = makeSimpleStruct(); + uint64_t rootId = tree.nodes[0].id; + QString result = rcx::renderDefines(tree, rootId); + + QVERIFY(result.contains("#pragma once")); + QVERIFY(result.contains("// Player")); + QVERIFY(result.contains("#define Player_health 0x0")); + QVERIFY(result.contains("#define Player_speed 0x4")); + QVERIFY(result.contains("#define Player_id 0x8")); + } + + void testDefineSkipsHex() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "HexTest"; + root.structTypeName = "HexTest"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node h; + h.kind = rcx::NodeKind::Hex32; + h.name = "padding"; + h.parentId = rootId; + h.offset = 0; + tree.addNode(h); + + rcx::Node f; + f.kind = rcx::NodeKind::UInt32; + f.name = "real_field"; + f.parentId = rootId; + f.offset = 4; + tree.addNode(f); + + QString result = rcx::renderDefines(tree, rootId); + QVERIFY(!result.contains("padding")); + QVERIFY(result.contains("#define HexTest_real_field 0x4")); + } + + void testDefineAll() { + auto tree = makeSimpleStruct(); + QString result = rcx::renderDefinesAll(tree); + QVERIFY(result.contains("#pragma once")); + QVERIFY(result.contains("#define Player_health 0x0")); + } + + // ═══════════════════════════════════════════════════════════ + // ── Format dispatch tests ── + // ═══════════════════════════════════════════════════════════ + + void testCodeFormatDispatch() { + auto tree = makeSimpleStruct(); + uint64_t rootId = tree.nodes[0].id; + + QString cpp = rcx::renderCode(rcx::CodeFormat::CppHeader, tree, rootId); + QVERIFY(cpp.contains("struct Player")); + + QString rust = rcx::renderCode(rcx::CodeFormat::RustStruct, tree, rootId); + QVERIFY(rust.contains("pub struct Player")); + + QString defs = rcx::renderCode(rcx::CodeFormat::DefineOffsets, tree, rootId); + QVERIFY(defs.contains("#define Player_health")); + } + + void testCodeFormatAllDispatch() { + auto tree = makeSimpleStruct(); + + QString cpp = rcx::renderCodeAll(rcx::CodeFormat::CppHeader, tree); + QVERIFY(cpp.contains("struct Player")); + + QString rust = rcx::renderCodeAll(rcx::CodeFormat::RustStruct, tree); + QVERIFY(rust.contains("pub struct Player")); + + QString defs = rcx::renderCodeAll(rcx::CodeFormat::DefineOffsets, tree); + QVERIFY(defs.contains("#define Player_health")); + } + + // ═══════════════════════════════════════════════════════════ + // ── Scope tests (Current + Deps) ── + // ═══════════════════════════════════════════════════════════ + + void testTreeScopeIncludesReferencedTypes() { + rcx::NodeTree tree; + + // Target struct (referenced by pointer) + rcx::Node target; + target.kind = rcx::NodeKind::Struct; + target.name = "Target"; + target.structTypeName = "Target"; + target.parentId = 0; + target.offset = 0x100; + int ti = tree.addNode(target); + uint64_t targetId = tree.nodes[ti].id; + + rcx::Node tf; + tf.kind = rcx::NodeKind::UInt32; + tf.name = "val"; + tf.parentId = targetId; + tf.offset = 0; + tree.addNode(tf); + + // Main struct with a pointer to Target + rcx::Node main; + main.kind = rcx::NodeKind::Struct; + main.name = "Main"; + main.structTypeName = "Main"; + main.parentId = 0; + int mi = tree.addNode(main); + uint64_t mainId = tree.nodes[mi].id; + + rcx::Node ptr; + ptr.kind = rcx::NodeKind::Pointer64; + ptr.name = "pTarget"; + ptr.parentId = mainId; + ptr.offset = 0; + ptr.refId = targetId; + tree.addNode(ptr); + + // "Current" scope: only Main, no Target definition + QString current = rcx::renderCpp(tree, mainId); + QVERIFY(current.contains("struct Main\n{")); + QVERIFY(!current.contains("struct Target\n{")); + + // "Current + Deps" scope: Main AND Target definitions + QString withDeps = rcx::renderCppTree(tree, mainId); + QVERIFY(withDeps.contains("struct Main\n{")); + QVERIFY(withDeps.contains("struct Target\n{")); + + // Same for Rust + QString rustDeps = rcx::renderRustTree(tree, mainId); + QVERIFY(rustDeps.contains("pub struct Main {")); + QVERIFY(rustDeps.contains("pub struct Target {")); + + // Same for #define + QString defDeps = rcx::renderDefinesTree(tree, mainId); + QVERIFY(defDeps.contains("#define Main_pTarget")); + QVERIFY(defDeps.contains("#define Target_val")); + } + + void testTreeScopeDispatch() { + rcx::NodeTree tree; + + rcx::Node a; + a.kind = rcx::NodeKind::Struct; + a.name = "A"; + a.structTypeName = "A"; + a.parentId = 0; + int ai = tree.addNode(a); + uint64_t aId = tree.nodes[ai].id; + + rcx::Node af; + af.kind = rcx::NodeKind::UInt32; + af.name = "x"; + af.parentId = aId; + af.offset = 0; + tree.addNode(af); + + // renderCodeTree should work for all formats + QString cpp = rcx::renderCodeTree(rcx::CodeFormat::CppHeader, tree, aId); + QVERIFY(cpp.contains("struct A")); + + QString rust = rcx::renderCodeTree(rcx::CodeFormat::RustStruct, tree, aId); + QVERIFY(rust.contains("pub struct A")); + + QString defs = rcx::renderCodeTree(rcx::CodeFormat::DefineOffsets, tree, aId); + QVERIFY(defs.contains("#define A_x")); + + QString cs = rcx::renderCodeTree(rcx::CodeFormat::CSharpStruct, tree, aId); + QVERIFY(cs.contains("public unsafe struct A")); + + QString py = rcx::renderCodeTree(rcx::CodeFormat::PythonCtypes, tree, aId); + QVERIFY(py.contains("class A(ctypes.Structure)")); + } + + // ── C# backend ── + + void testCSharpSimpleStruct() { + auto tree = makeSimpleStruct(); + uint64_t rootId = tree.nodes[0].id; + QString result = rcx::renderCSharp(tree, rootId); + + QVERIFY(result.contains("using System.Runtime.InteropServices;")); + QVERIFY(result.contains("[StructLayout(LayoutKind.Explicit, Size = 0x10)]")); + QVERIFY(result.contains("public unsafe struct Player")); + QVERIFY(result.contains("[FieldOffset(0x0)] public int health;")); + QVERIFY(result.contains("[FieldOffset(0x4)] public float speed;")); + QVERIFY(result.contains("[FieldOffset(0x8)] public ulong id;")); + } + + void testCSharpPointers() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Foo"; + root.structTypeName = "Foo"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node p; + p.kind = rcx::NodeKind::Pointer64; + p.name = "ptr"; + p.parentId = rootId; + p.offset = 0; + tree.addNode(p); + + QString result = rcx::renderCSharp(tree, rootId); + QVERIFY(result.contains("IntPtr ptr")); + } + + void testCSharpAll() { + auto tree = makeSimpleStruct(); + QString result = rcx::renderCSharpAll(tree); + QVERIFY(result.contains("public unsafe struct Player")); + QVERIFY(result.contains("[StructLayout(")); + } + + void testCSharpEnum() { + rcx::NodeTree tree; + rcx::Node e; + e.kind = rcx::NodeKind::Struct; + e.name = "Color"; + e.structTypeName = "Color"; + e.classKeyword = "enum"; + e.parentId = 0; + e.offset = 0; + e.enumMembers = {{"Red", 0}, {"Green", 1}, {"Blue", 2}}; + tree.addNode(e); + + QString result = rcx::renderCSharpAll(tree); + QVERIFY(result.contains("public enum Color : long")); + QVERIFY(result.contains("Red = 0")); + QVERIFY(result.contains("Green = 1")); + QVERIFY(result.contains("Blue = 2")); + } + + void testCSharpVectors() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Xform"; + root.structTypeName = "Xform"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node v; + v.kind = rcx::NodeKind::Vec3; + v.name = "position"; + v.parentId = rootId; + v.offset = 0; + tree.addNode(v); + + QString result = rcx::renderCSharp(tree, rootId); + QVERIFY(result.contains("public fixed float position[3]")); + } + + // ── Python ctypes backend ── + + void testPythonSimpleStruct() { + auto tree = makeSimpleStruct(); + uint64_t rootId = tree.nodes[0].id; + QString result = rcx::renderPython(tree, rootId); + + QVERIFY(result.contains("import ctypes")); + QVERIFY(result.contains("class Player(ctypes.Structure)")); + QVERIFY(result.contains("_fields_ = [")); + QVERIFY(result.contains("(\"health\", ctypes.c_int32)")); + QVERIFY(result.contains("(\"speed\", ctypes.c_float)")); + QVERIFY(result.contains("(\"id\", ctypes.c_uint64)")); + } + + void testPythonPointers() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Bar"; + root.structTypeName = "Bar"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node p; + p.kind = rcx::NodeKind::Pointer64; + p.name = "ptr"; + p.parentId = rootId; + p.offset = 0; + tree.addNode(p); + + QString result = rcx::renderPython(tree, rootId); + QVERIFY(result.contains("(\"ptr\", ctypes.c_void_p)")); + } + + void testPythonTypedPointers() { + rcx::NodeTree tree; + rcx::Node target; + target.kind = rcx::NodeKind::Struct; + target.name = "Target"; + target.structTypeName = "Target"; + target.parentId = 0; + target.offset = 0; + int ti = tree.addNode(target); + uint64_t targetId = tree.nodes[ti].id; + + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Holder"; + root.structTypeName = "Holder"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node p; + p.kind = rcx::NodeKind::Pointer64; + p.name = "ref"; + p.parentId = rootId; + p.offset = 0; + p.refId = targetId; + tree.addNode(p); + + QString result = rcx::renderPython(tree, rootId); + QVERIFY(result.contains("ctypes.POINTER(Target)")); + } + + void testPythonAll() { + auto tree = makeSimpleStruct(); + QString result = rcx::renderPythonAll(tree); + QVERIFY(result.contains("class Player(ctypes.Structure)")); + } + + void testPythonEnum() { + rcx::NodeTree tree; + rcx::Node e; + e.kind = rcx::NodeKind::Struct; + e.name = "Status"; + e.structTypeName = "Status"; + e.classKeyword = "enum"; + e.parentId = 0; + e.offset = 0; + e.enumMembers = {{"Active", 1}, {"Inactive", 0}}; + tree.addNode(e); + + QString result = rcx::renderPythonAll(tree); + QVERIFY(result.contains("class Status:")); + QVERIFY(result.contains("Active = 1")); + QVERIFY(result.contains("Inactive = 0")); + } + + void testPythonVectors() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Pos"; + root.structTypeName = "Pos"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node v; + v.kind = rcx::NodeKind::Vec4; + v.name = "color"; + v.parentId = rootId; + v.offset = 0; + tree.addNode(v); + + QString result = rcx::renderPython(tree, rootId); + QVERIFY(result.contains("(\"color\", ctypes.c_float * 4)")); + } + + void testCSharpDispatch() { + auto tree = makeSimpleStruct(); + uint64_t rootId = tree.nodes[0].id; + QString result = rcx::renderCode(rcx::CodeFormat::CSharpStruct, tree, rootId); + QVERIFY(result.contains("[StructLayout(")); + } + + void testPythonDispatch() { + auto tree = makeSimpleStruct(); + uint64_t rootId = tree.nodes[0].id; + QString result = rcx::renderCode(rcx::CodeFormat::PythonCtypes, tree, rootId); + QVERIFY(result.contains("ctypes.Structure")); + } }; QTEST_MAIN(TestGenerator)