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)
This commit is contained in:
IChooseYou
2026-03-10 15:20:56 -06:00
committed by IChooseYou
parent 6c8b7d3d97
commit ea85b7a621
5 changed files with 1297 additions and 4 deletions

View File

@@ -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<int> allChildren = ctx.childMap.value(structId);
QVector<int> 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<int> 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<int> allChildren = ctx.childMap.value(structId);
QVector<int> 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<int> 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<NodeKind, QString>* 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<NodeKind, QString>* 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<NodeKind, QString>* typeAliases,
bool emitAsserts) {
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
ctx.output += QStringLiteral("using System.Runtime.InteropServices;\n\n");
QVector<int> 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<int> 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);
}
}

View File

@@ -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<NodeKind, QString>* 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<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCodeAll(CodeFormat fmt, const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
// ── Individual backends ──
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* 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<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderRust(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderRustTree(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderRustAll(const NodeTree& tree,
const QHash<NodeKind, QString>* 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<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCSharpTree(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCSharpAll(const NodeTree& tree,
const QHash<NodeKind, QString>* 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);

View File

@@ -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<NodeKind, QString>* 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() {

View File

@@ -16,8 +16,10 @@
#include <QLineEdit>
#include <QMap>
#include <QButtonGroup>
#include <QComboBox>
#include <QPushButton>
#include <QTimer>
#include <QToolButton>
#include <Qsci/qsciscintilla.h>
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;
};