Table of Contents

Choose an API

CStructSharp offers several entry points because binary data arrives in different forms and applications need different results. Make the choice in three parts:

  1. Is the input in memory or in a stream?
  2. Do you want a whole StructValue, one selected value, or a C# type?
  3. Are you creating new bytes, writing to a destination, or changing bytes that already exist?

Start with the simplest method that matches the job. A byte[] result is often easier to use than a custom buffer until measurements show that allocation matters.

Choose an input

Your data is in Use What happens
byte[], ReadOnlySpan<byte>, or ReadOnlyMemory<byte> Parse or ReadValue The call is synchronous and does not retain the input.
A readable, seekable Stream Parse or ReadValue Reading begins at the stream's current position.
A ReadOnlySequence<byte> (a PipeReader's buffer, a chain of pooled segments) Parse or ReadValue One segment is read in place; several are copied into a pooled buffer bounded by MaxTotalBytesRead.
A stream whose bytes arrive while the program runs (a file opened for asynchronous I/O, a socket, a request body) ParseAsync or ReadValueAsync The bytes are read with ReadAsync into a pooled buffer and decoded by the same reader; the thread is free while they arrive (async guide).
One record after another with nothing between them ParseMany / ParseManyAsync Each record is parsed on the step of the loop that reaches it, and trailing bytes shorter than a record fail.

A span is a short-lived view over a section of memory. ReadOnlyMemory<byte> is a storable memory object, but the CStructSharp operation still finishes synchronously and does not keep it. Use a stream for files or data sources that already expose seeking. Use memory APIs when the bytes are already available as an array or memory region.

Every operation has the same shape for every input kind: the input comes first, then the path, then optional variables and options. A byte array named bytes can be passed directly (layout.Parse(bytes, "header")) or as bytes.AsSpan(); both read without copying. The path is optional for reads: layout.Parse(bytes) selects the first declared struct, which layout.DefaultRoot names.

Pointer coordinates in memory APIs start at zero within the region you pass. If you pass a slice, a pointer cannot refer to bytes before that slice.

Choose a read result

Result you need Method Use it when
A whole struct with runtime field names Parse Exploring a format, building tools, or handling layouts that vary at runtime
One field, nested value, union, or array ReadValue You don't need the rest of the object, or the selection is not a struct
A known C# type ReadValue<T> Application code benefits from typed properties and checked conversion; T is a scalar, an array, or a class implementing ICStructMapped<T>
A known C# type with an expected failure path TryReadValue<T> (TryReadValueAsync<T> returns a ReadAttempt<T>) Truncated or malformed input is an ordinary outcome
One member of a value you already parsed, with an expected failure path TryGet<T> / GetOrDefault<T> on StructValue An absent conditional member or an unconvertible value is an ordinary outcome
A generated class or an allocation-free view Wire.Parse / Wire.TryParse / new Wire.HeaderView(bytes) on a [CStructLayout] class The layout is in your source; see runtime or generated?
A sequence of records, typed or untyped Wire.Records / Wire.HeaderView.Enumerate / ParseMany A file of entries, a message body of frames (sequences and TryParse)
Values plus byte ranges ParseWithDebug (struct) / ReadValueWithDebug (anything) A hex viewer or diagnostic tool must show where values came from
Only a field's stream position ResolveAddress You need a coordinate without materializing the value
An array or terminated string length GetArrayLength The count depends on variables or scanned input

Parse returns a StructValue and throws a CStructPathException when the path selects anything else. ReadValue handles every selection: structs, unions (UnionValue), scalars, array elements, enum values, pointer parts, and other selected fields.

The untyped ReadValue result uses the library's direct C# representation. For example, uint16 becomes ushort, a struct becomes a StructValue (readable through dynamic members or as an IDictionary<string, object?>), an enum becomes EnumValueResult, and a union becomes UnionValue. ReadValue<T> performs an additional checked mapping to your requested type.

Choose a write operation

What you want to do Method Ownership and failure behavior
Create a new byte[] Serialize returning byte[] The library allocates and returns an exact-sized array.
Fill an existing Span<byte> Serialize(Span<byte>, ...) Returns the number of initialized bytes; unused capacity is unchanged.
Append to a pipeline or pooled writer Serialize(IBufferWriter<byte>, ...) Appends directly and returns the byte count.
Write at a stream's current position Write Writes directly to a writable, seekable stream.
Replace a value already present in a stream Update Locates the path and validates the replacement before committing it.
Write or update without blocking a thread WriteAsync / UpdateAsync WriteAsync serializes first and writes once (a failure writes nothing); UpdateAsync needs a seekable stream and writes back only the bytes that changed.

The byte[] overload is the easiest choice for most new code. Span and buffer-writer output avoid the final owned array, but they cannot undo a prefix that was already initialized or advanced if a later write fails. Write can likewise leave earlier fields written after a later error.

Update is different: it is for fixed coordinates in existing data. It will not extend the stream or move following fields. Errors CStructSharp can detect are found before it writes to the destination, although a physical stream failure during the final commit may still leave a written prefix.

A practical decision path

For a small file already loaded into a byte[]:

  1. Start with Parse(bytes, "root") while learning the format.
  2. Change to ReadValue<MyType>(bytes, "root") when the C# shape is stable - or, when the layout itself is part of the program, put it on a [CStructLayout] class and call its generated Parse (generated code).
  3. Use ReadValue(bytes, "root.header.flags") when only one field is needed.
  4. Start writes with Serialize("root", value).
  5. Consider spans or IBufferWriter<byte> only after measuring allocation in the real workload.

For a large file:

  1. Open a readable, seekable stream.
  2. Set Position to the start of the structure.
  3. Use a selected ReadValue when later fields are irrelevant.
  4. Use Update only when the existing field's storage plan must stay in place.
  5. Use the *Async forms when the program must stay responsive while the bytes arrive (a server, a UI), with a CancellationToken that bounds the wait; a file of records is ParseManyAsync or the generated RecordsAsync.

For bytes that arrive in pieces (a socket, a PipeReader): parse the whole records each chunk holds with ParseMany over the ReadOnlySequence<byte>, or check a count-prefixed header with TryReadValue before the rest has arrived, and let the pipe keep the partial tail (async guide).

Common mistakes

  • Calling Parse for a scalar path and expecting every operation to return the same StructValue wrapper. Use ReadValue for one scalar.
  • Omitting the root name in a layout with helper declarations. Pass the case-sensitive root explicitly.
  • Sharing one stream between concurrent calls. The compiled layout is reusable; the stream is mutable and needs exclusive use for the complete operation.
  • Choosing span output only because it sounds faster. It requires capacity planning and has weaker rollback behavior than staging a byte[].
  • Using Write to patch existing data. It writes new output from the current position; Update first finds existing storage by path.

Continue with Read values and paths, Map values to C# types, or Write and serialize values.