Table of Contents

Paths, array indices, and pointer access

A path tells CStructSharp which part of a prepared layout an operation should use. It is not a C expression or JSONPath. It starts with an exported root name and follows fields with dots:

root
root.header.kind
root.items[2].value
root.choice.large
root.pointer.address
root.pointer.value.code

Paths and layout names are case-sensitive.

Segments and indices

Each dot-separated part is a segment. A segment may contain zero or more array indices, one per repeated [...] in the field's own declaration (root.matrix[2][3], not comma-separated). Each index is zero or an unpadded positive decimal integer that fits Int32.

Path Result
root.items[0] Valid first element
root.items[12].value Valid nested field
root.items[01] InvalidPath; leading zero
root.items[-1] InvalidPath; signs are not allowed
root.items[0x1] InvalidPath; decimal only
root.items[1][2] InvalidPath; items has only one dimension
root..value InvalidPath; empty segment

The selected field must actually be an array, and each index must be below its own dimension's evaluated count. Runtime arrays need the same variable values as parsing/writing.

Fixed char[N] and wchar[N] elements can be selected as raw code units. Terminated strings are selected as whole fields and do not expose character indices. For an array such as cstring names[2], root.names[1] selects the second whole string, not its second character. Its address follows the preceding string's complete encoded terminator.

The exact syntax appears in Public path EBNF.

Multidimensional arrays

A field declared with more than one dimension (uint8 matrix[3][4]) accepts up to that many indices in one segment, outermost first, matching the declaration's own bracket order:

root.matrix          ──► every row, as a nested list of rows of columns
root.matrix[2]        ──► the third row alone, as a list of columns
root.matrix[2][3]      ──► one scalar/struct element

Supplying fewer indices than the field has dimensions selects the corresponding lower-dimensional sub-array rather than one element - root.matrix[2] is exactly the third row, not an error, and behaves like an ordinary one-dimensional array field from that point on (it can itself be indexed further, has its own GetArrayLength, and so on). Traversing into a nested field or pointer accessor still requires every dimension to be indexed first (root.grid[1].member is rejected the same way root.grid.member is for a plain array with no index at all; root.grid[1][2].member is fine). Supplying more indices than the field has dimensions is rejected. A fixed table of fixed-width strings (char names[10][32]) selects one row at a time as a whole string, exactly like a one-dimensional char[32] field, with only the outer dimension nesting.

See Arrays and strings for the declaration syntax and its one restriction: every dimension of a multidimensional declaration must currently be compile-time fixed. Runtime counts are supported for one-dimensional arrays.

Structs and unions

A struct segment follows normal sequential placement. A union member begins at the union address, so every member's first byte position is the same. If that member is a struct, its own child fields then advance within the selected view.

ReadValue reads the selected field at its resolved address. It does not round that address up again, even when a pointer reaches a union at an unaligned byte position. An array's field alignment applies to its start, not separately to each enum element. Nested structs still place their own fields according to their compiled layout.

Selecting a whole struct/union and selecting one scalar are different result shapes. Use Parse for a struct (StructValue) or ReadValue for any selection, including unions (UnionValue) and single direct values.

Pointer accessors

After a pointer field:

  • .address selects the pointer storage and reads/writes its encoded coordinate;
  • .value follows one declared level and selects the target.
root.ptr.address       ──► bytes containing the first stored pointer
root.ptr.value         ──► first target (or next Pointer for T **)
root.ptr.value.value   ──► final target for T **

The words address and value are special only immediately after a pointer. An ordinary non-pointer field may use either name normally.

Operations that accept paths

Operation What a path selects
Parse A root or nested struct
ReadValue / ReadValue<T> A root, nested object, union, scalar, or array item
ParseWithDebug / ReadValueWithDebug A value plus ranges visited while reading it
ResolveAddress The absolute stream position of the selected storage/target
GetArrayLength A fixed/runtime array or terminated string
Serialize / Write The value shape to encode
Update Existing storage to locate and replace

The feature table gives exact support and limitations for each language feature.

An invalid selector produces CStructPathException / InvalidPath. A valid path that encounters truncated data, bad text, or an invalid pointer while locating the target produces a read error instead. Traversal may also reach a configured read limit.

Positions and origins

For stream operations, the stream's position at entry is the root's starting point where documented. Address results are absolute stream positions and therefore include a nonzero starting position.

Read-only address/length inspection and update discovery restore the caller's visible position. A successful normal read advances through the selected value.

Span and memory coordinates start at zero within the region passed to the method. If the region is a slice, paths and pointers cannot see bytes before or after that slice.

Common mistakes are omitting the root, using a leading-zero index, indexing terminated text, confusing a pointer's stored number with its storage position, or using one .value for a multi-level pointer.