Migrating from dissect.cstruct
dissect.cstruct is the Python library many forensic parsers were written
with. A definition written for it compiles in CStructSharp as it is - Windows SDK, Linux kernel, IDA, and C99 type
spellings, flag declarations, [EOF] and [] arrays, #include and #pragma pack lines, typedef struct _X {...} X, *PX; lists - and the API calls its users make have direct equivalents. This page is the checklist.
Definitions
| dissect.cstruct | CStructSharp | Notes |
|---|---|---|
DWORD, BYTE, WCHAR, __u32, u8, wchar_t, unsigned __int64, uleb128, ... |
built in | See the alias table. Aliases resolve to canonical codecs, so debug output says uint32 for a DWORD. |
long = 32 bits |
long = 64 bits (LP64) |
Use CStructCompilationOptions { CLongWidth = 32 } for the dissect reading. Windows LONG/ULONG are always 32 bits in both. |
int24 aligned to 4 |
aligned to 1 | Only aligned placement is affected. |
flag F : uint32 { A, B, C }; |
same | Reads as FlagValueResult with Names; writes accept "A\|C". |
enum { A, B }; (anonymous) |
same | Members become constants. |
enum E { A = 0x80000000 }; (no backing type) |
same | 32 bits, uint32 unless a member is written negative (the GCC rule; dissect always uses uint32); CStructCompilationOptions { DefaultEnumStorage = "uint32" } pins dissect's reading. |
E.A in an expression |
same | The bare name A stays local to the enum in CStructSharp. |
Members separated by line breaks, names like 0 |
same | The comma is optional; a member name may be all digits. Duplicate member names are an error (dissect keeps the last). |
typedef enum _E : DWORD { ... } E; |
same | Every struct typedef form has an enum/flag counterpart. |
uint32 _; repeated |
same | _ is padding: skipped on read, zeroed on write, never in the result (dissect shows the last one). |
struct gen { ... } gen;, union tag { ... }; inside a struct |
same | The tag becomes a global type; the unnamed form promotes the body. |
#define X SOMENAME, #define M (1 << 63) |
same | A non-expression value is a text constant; a 64-bit value is published exactly; both fail only when a count uses them. |
char data[EOF];, uint16 v[]; |
same | Read-to-end and zero-terminated arrays; both need a fixed element size. |
union { ... }; inside a struct |
same | Anonymous members are promoted; a promoted union writes back through the widest member supplied. |
struct { ... } timeval; |
same | The trailing name is the type. |
#define MAGIC b"CD001" |
same | Published on CStruct.Constants as a byte[]; a "text" define is a string. |
#include, #ifdef, #undef, #pragma pack |
same | #include is recorded on CStruct.Includes; #pragma pack clamps alignment (dissect ignores it); #ifdef names come from the source or CStructCompilationOptions.Defined. |
sizeof(T), offsetof(T, f) |
same | Folded at construction; T must be complete and fixed-size. |
hdr.count (a nested field) in an expression |
same | The head must be a scalar struct field; items[0].count is not accepted (use the bare name after the element is read). |
uint8 *a, b; (both pointers in dissect) |
C semantics | Only a is a pointer. |
| Big-endian bitfields allocated from the high bit | low bit first | CStructCompilationOptions { BitfieldAllocation = BitfieldAllocation.HighBitFirst } selects the dissect/RFC reading. |
char x[4] → bytes |
string of code units |
Declare uint8 x[4] for a byte array. |
int48, int128, float16, void * |
same | Int128/UInt128/Half results; void * is an opaque address. |
PWSTR, LPSTR, HANDLE, time_t |
built in | wchar */char * pointers, an opaque address, and the long family. |
flag F : USHORT { X = 0x10000 }, duplicate member names |
rejected | dissect accepts a value that does not fit the backing type and a name declared twice; both are defects in a header, and CStructSharp reports them. |
A struct-typed field registered from Python (cs.add_custom_type) |
ICustomCodec |
See a custom codec. |
API
| dissect.cstruct | CStructSharp |
|---|---|
cstruct().load(definition) |
new CStruct(definition) or CStruct.GetOrCompile(definition) |
cs.load(a); cs.load(b) |
one string, or CStructCompilationOptions { Prelude = a } with b |
cs.copy().load(more) (ELF 32/64) |
the same prelude with two bodies |
cs.endian = ">" after reading a header |
layout.WithEndianness(false) |
cs.header(fh) |
layout.Parse(stream, "header") - the stream advances |
cs.uint32(fh), cs.uint64[n] (fh), cs.char[None] (fh) |
layout.Parse(stream, "uint32"), "uint64[N]" with variables, "char[]" |
obj.dumps() |
layout.Serialize("header", obj) |
cs.header(a=1, b=2).dumps() |
layout.Serialize("header", new Dictionary<string, object?> { ... }) |
len(cs.header) |
layout.GetStructSizeInBytes("header") |
cs.add_custom_type("varint", MyType) |
CStructCompilationOptions { Codecs = [new MyVarint()] } implementing ICustomCodec |
cs.typedefs, struct.fields |
layout.Layout.Declarations |
cs.cdef() |
layout.ToDefinition() |
dumpstruct(obj) |
ParseWithDebug byte ranges |
obj.field.dereference() |
Pointer.Value (dereferenced during the read by default) |
A custom codec
The protobuf varint that dissect.target registers with add_custom_type is a short span-based class in
CStructSharp (ICustomCodec lives in CStructSharp.Codecs). Read sees the bytes from the value's start and
reports how many it used; Write fills a window and asks for a larger one with DestinationTooSmall:
sealed class Varint : ICustomCodec
{
public string Name => "varint";
public int? FixedSize => null; // variable length
public int Alignment => 1;
public OperationStatus Read(ReadOnlySpan<byte> source, out object? value, out int bytesConsumed)
{
ulong result = 0;
for (int index = 0; index < source.Length && index < 10; index++)
{
result |= (ulong)(source[index] & 0x7F) << (7 * index);
if ((source[index] & 0x80) == 0)
{
value = result;
bytesConsumed = index + 1;
return OperationStatus.Done;
}
}
value = null;
bytesConsumed = 0;
return source.Length >= 10 ? OperationStatus.InvalidData : OperationStatus.NeedMoreData;
}
public OperationStatus Write(Span<byte> destination, object value, out int bytesWritten)
{
ulong remaining = Convert.ToUInt64(value);
bytesWritten = 0;
do
{
if (bytesWritten == destination.Length) return OperationStatus.DestinationTooSmall;
byte chunk = (byte)(remaining & 0x7F);
remaining >>= 7;
destination[bytesWritten++] = remaining == 0 ? chunk : (byte)(chunk | 0x80);
}
while (remaining != 0);
return OperationStatus.Done;
}
}
var options = new CStructCompilationOptions { Codecs = [new Varint()] };
var layout = new CStruct("struct entry { varint length; uint8 data[length]; };", compilationOptions: options);
A custom codec is read through its delegate on every operation (never a span fast path), may be an array element or a pointer target, is measured by reading when it has no fixed size, and is neither bitfield storage nor an enum backing type. Keep one instance per codec: the list is compared by reference in the compiled-layout cache.
What stays different on purpose
CStructSharp prefers C where dissect deviates from it, and keeps dissect's behavior reachable through an option:
the long width, the default enum storage, bitfield bit order, and uint8 *a, b;. It does not substitute macro
text, follow #include, or evaluate #if expressions, and it reports the header defects dissect glosses over
(duplicate names, values outside a declared backing type). Dynamic unions (a union with a runtime-sized member)
are not supported; they are rare in real definitions.