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:
.addressselects the pointer storage and reads/writes its encoded coordinate;.valuefollows 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.