Model and visualize C / TwinCAT–style struct memory layout — size, alignment, and padding, byte by byte.
A small tool for modeling C / TwinCAT–style memory layout and computing struct sizes,
alignment, and padding. You compose objects from primitives, arrays, strings, and nested
objects, then render a human-readable memory map with MemoryViewer.DisplayMemoryView.
- 📐 Accurate layout — computes member offsets, struct size, and alignment the way C / TwinCAT /
StructLayoutdo. - 🧱 Composable building blocks — primitives, arrays,
STRING/WSTRING, and arbitrarily nested objects. - 🎚️ Configurable packing — set a pack value (1–8) to cap alignment, just like
#pragma pack/{attribute 'pack_mode'}. - 🔍 Readable memory map — renders data, data padding, and trailing object padding as an indented tree.
| Type | Represents | Size | Alignment |
|---|---|---|---|
Primitive(size) |
A scalar field (e.g. BYTE, INT, LREAL) |
size |
size |
CArray(length, element) |
A contiguous array | length * element.Size |
element.Alignment |
CString(length) |
STRING(length) |
length + 1 (null terminator) |
1 |
CString(length, wide: true) |
WSTRING(length) |
(length + 1) * 2 |
2 |
CObject(name, context, pack) |
A struct | sum of members + padding | largest member alignment |
Context(defaultPack) sets the maximum alignment (the pack value, 1–8). Each member is
aligned to min(member.Alignment, pack). The struct's total size is rounded up to a
multiple of its largest member alignment (trailing object padding), and gaps inserted
to align members appear as data padding.
Note on the default pack value: In a typical C or TwinCAT application the effective pack value isn't fixed — it commonly defaults to 4 on 32-bit targets and 8 on 64-bit targets, because it follows the natural word size of the OS / hardware (and the compiler / PLC runtime). The same source can therefore lay out differently across platforms unless an explicit pack is set (
#pragma pack/{attribute 'pack_mode'}). Pick the pack value here that matches your target to reproduce its layout.A
pack_mode(or member/objectpack) of0means "natural alignment" — i.e. no override, so each member uses its own natural alignment capped only by the surrounding context's default pack. In this library, passingpack: 0to a member/object falls back to theContextdefault pack for that reason.
| Project | Type | Purpose |
|---|---|---|
PackCalculator |
Class library | The reusable layout engine — reference this from your own code. |
PackCalculator.Example |
Console app | A runnable demo showing how to compose objects and print a memory map. |
Integrate by adding a reference to the PackCalculator library:
<ProjectReference Include="..\PackCalculator\PackCalculator.csproj" />var context = new Context(8); // pack = 8
var obj = new CObject("Example", context, 0);
obj.AddMember(new Primitive(1));
obj.AddMember(new Primitive(8));
MemoryViewer.DisplayMemoryView(obj);
Console.WriteLine($"Size: {obj.Size} bytes");A byte followed by an 8-byte field forces the larger field onto an 8-byte boundary.
var obj = new CObject("Simple", new Context(8), 0);
obj.AddMember(new Primitive(1));
obj.AddMember(new Primitive(8));
MemoryViewer.DisplayMemoryView(obj);Simple
Primitive(1): 1 bytes
Data Padding: 7 bytes
Primitive(8): 8 bytes
End: 16 bytes
The same members under Context(1) cap every member's alignment at 1, removing all padding.
var obj = new CObject("Packed", new Context(1), 0);
obj.AddMember(new Primitive(1));
obj.AddMember(new Primitive(8));
MemoryViewer.DisplayMemoryView(obj);Packed
Primitive(1): 1 bytes
Primitive(8): 8 bytes
End: 9 bytes
STRING is byte-aligned; WSTRING is word-aligned (2 bytes/char), so it can introduce a
padding byte after an odd-sized field. Both reserve space for a null terminator.
var obj = new CObject("Message", new Context(8), 0);
obj.AddMember(new Primitive(1)); // byte flag
obj.AddMember(new CString(5, wide: true)); // WSTRING(5)
MemoryViewer.DisplayMemoryView(obj);Message
Primitive(1): 1 bytes
Data Padding: 1 bytes
WSTRING(5): 12 bytes
End: 14 bytes
Arrays are laid out contiguously, and nested objects align by their own largest member.
var inner = new CObject("Inner", new Context(8), 0);
inner.AddMember(new Primitive(3));
var obj = new CObject("Test", new Context(8), 0);
obj.AddMember(new Primitive(1));
obj.AddMember(new Primitive(2));
obj.AddMember(new Primitive(8));
obj.AddMember(new CArray(3, new Primitive(1)));
obj.AddMember(new CString(10));
obj.AddMember(new CString(10, wide: true));
obj.AddMember(inner);
MemoryViewer.DisplayMemoryView(obj);Test
Primitive(1): 1 bytes
Data Padding: 1 bytes
Primitive(2): 2 bytes
Data Padding: 4 bytes
Primitive(8): 8 bytes
Array[3] of Primitive(1)
Primitive(1): 1 bytes
Primitive(1): 1 bytes
Primitive(1): 1 bytes
End: 3 bytes
STRING(10): 11 bytes
WSTRING(10): 22 bytes
Data Padding: 2 bytes
Inner
Primitive(3): 3 bytes
End: 3 bytes
Object Padding: 7 bytes
End: 64 bytes
Versioning is handled automatically by MinVer from Git
tags (tag prefix v, e.g. tag v1.0.0 → package version 1.0.0). To cut and publish a release:
git tag v1.0.0
git push origin v1.0.0
dotnet pack PackCalculator\PackCalculator.csproj -c Release
dotnet nuget push PackCalculator\bin\Release\PackCalculator.1.0.0.nupkg --api-key <KEY> --source https://api.nuget.org/v3/index.jsonReplace
v1.0.0/1.0.0with the version you are releasing, and<KEY>with your nuget.org API key. Without a tag, builds produce a0.0.0-alpha.0.<height>pre-release version.