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:
- Is the input in memory or in a stream?
- Do you want a whole
StructValue, one selected value, or a C# type? - 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[]:
- Start with
Parse(bytes, "root")while learning the format. - 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 generatedParse(generated code). - Use
ReadValue(bytes, "root.header.flags")when only one field is needed. - Start writes with
Serialize("root", value). - Consider spans or
IBufferWriter<byte>only after measuring allocation in the real workload.
For a large file:
- Open a readable, seekable stream.
- Set
Positionto the start of the structure. - Use a selected
ReadValuewhen later fields are irrelevant. - Use
Updateonly when the existing field's storage plan must stay in place. - Use the
*Asyncforms when the program must stay responsive while the bytes arrive (a server, a UI), with aCancellationTokenthat bounds the wait; a file of records isParseManyAsyncor the generatedRecordsAsync.
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
Parsefor a scalar path and expecting every operation to return the sameStructValuewrapper. UseReadValuefor 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
Writeto patch existing data. It writes new output from the current position;Updatefirst finds existing storage by path.
Continue with Read values and paths, Map values to C# types, or Write and serialize values.