Table of Contents

Handle errors and recovery

CStructSharp separates expected data/layout failures from invalid method arguments and unexpected application defects. Expected failures derive from CStructException and include a stable Code. The exception types and CStructErrorCode live in the CStructSharp.Diagnostics namespace.

Use the exception type or code to decide what the application can do. Keep the message and inner exception for diagnostics; do not parse message wording as a program protocol.

Error categories

Code Exception Typical cause
InvalidLayout CStructLayoutException Unsupported syntax, duplicate or unknown type, invalid constant expression, recursive by-value storage. Never raised for a problem in the data.
InvalidPath CStructPathException Unknown root/member, bad array index, or invalid pointer accessor
ReadFailed CStructReadException Truncated bytes, malformed encoding, invalid pointer target, a decoded count or selector that cannot be evaluated, typed-mapping failure
ReadLimitExceeded CStructReadLimitException Read array, string, byte, nesting, or pointer limit reached
WriteFailed CStructWriteException Missing field, wrong shape, out-of-range value, a supplied count that cannot be evaluated, encoding, pointer, union, or physical output failure
WriteLimitExceeded CStructWriteLimitException Write array, string, byte, or nesting limit reached

Null arguments, unsupported stream capabilities, and invalid option values remain ordinary argument exceptions. Cancellation and unexpected runtime defects are not wrapped as malformed binary data.

Choose throwing or non-throwing reads

Use ReadValue<T> when an invalid value should follow the application's exception path. Use TryReadValue<T> when an expected CStructSharp failure should become false.

The first-parse example shows both a successful typed read and a truncated TryReadValue call. On the truncated input, it returns false and the output receives its default value. A stream overload also restores the position captured before the attempt.

TryReadValue<T> catches categorized CStructSharp failures only. It will not turn a null argument or an unrelated bug in application code into false, and it lets an OperationCanceledException through: cancellation through ReadOptions.CancellationToken (cancel a long operation) is the caller's decision to stop, not a property of the input.

The same rule has three more forms. The awaitable TryReadValueAsync<T> returns a ReadAttempt<T> - Succeeded, Value, and Failure - because an out parameter cannot cross an await; Failure is the exception the throwing form would have raised. A generated layout class has TryParse(input, out value) and TryParse(input, out value, out CStructException? failure) for every input kind (sequences and TryParse). And a value you already hold has TryGet<T>(path, out value, out failure) and GetOrDefault<T>(path, fallback), which tell an absent member (a CStructPathException) from an unconvertible one (a CStructReadException) without a catch.

Know what can be recovered

Operation Expected failure behavior
Layout construction No binary input has been touched. Fix or reject the layout.
TryReadValue<T> on a stream Returns false and restores the starting position.
ParseAsync and the other awaitable reads A seekable stream returns to its origin on any failure; a stream that cannot seek has been consumed up to the budget plus one byte.
WriteAsync Serializes first and writes once: a validation failure writes nothing.
UpdateAsync Updates a buffered copy and writes back only the changed runs; a failure leaves the stream unchanged.
ParseMany / Records Records before the failing one were delivered; the failure names the record by index ([3].header).
ResolveAddress / length lookup Restores position after inspection.
Serialize to a new byte[] No result array is returned.
Span / IBufferWriter serialization An initialized or advanced prefix may remain.
Write Earlier fields may already be written.
Update validation failure Content, length, and position remain unchanged.
Update physical commit failure A destination-accepted prefix may remain; position restoration is best effort.

This distinction is why the choice between owned output, direct output, and an update matters.

What is not an error

Some situations look like problems but are deliberate, documented behavior. The library stays quiet about them, and each has an opt-in where strictness makes sense:

Situation What happens Opt in to strictness
Trailing bytes after the selected struct They are left unread; a stream stays positioned right after the struct so a following record can be read. Compare the position or GetStructSizeInBytes against the input length yourself.
A supplied value has members the layout does not declare Serialize, Write, and Update skip them. WriteOptions.UnknownMembers = UnknownMemberPolicy.Reject.
NUL padding in fixed text (char name[8], bounded utf8[N]) The padding stays in the string: "ab\0\0". ReadOptions.TrimFixedText = true, or .TrimEnd('\0').
An enum field holds a value with no declared name EnumValueResult.Name is null and Value keeps the number; flags decompose into the named bits plus the remainder. Check Name is null (or FlagValueResult.Remainder != 0) in application code; the layout cannot declare "closed" enums.
A pointer that was not dereferenced Pointer.Value is null and IsDereferenced is false; nothing was read at the target. Leave ReadOptions.DereferencePointers at its default of true.

Read a message

Every message names what failed first and then, in parentheses, every fact the library knows: the field being read or written and its layout type, the requested path, and the position at which the operation stopped. The same facts are available as properties, so an application can format them its own way:

Not enough bytes: needed 4, available 1 (field 'length' (uint32), in 'header', offset 3).
Value 70000 does not fit: uint16 accepts 0 to 65535 (field 'kind' (uint16), in 'header', offset 0).
Unknown root 'Header'. Names are case-sensitive; did you mean 'header'?
Unknown field 'nope' in 'header' (path 'header.nope', offset 6).
Array length 2147483647 exceeds MaxArrayElements (1000000) (field 'data' (uint8), in 'p', offset 4).
Property Meaning
Member, MemberType The innermost field the failure belongs to and its layout type spelling.
Path The path the operation was asked for (header, header.nope, packet.items[2]).
Offset Where the operation stopped: the absolute stream position, or the offset within the supplied region. It is at or after the failing item, not necessarily its start.
Code The stable category (ReadFailed, WriteFailed, InvalidPath, ...) for switch statements and logs.

A layout error carries Line and Column instead; see Limits and diagnostics.

Record useful diagnostic context

When reporting a failure, keep:

  • the exception type and Code;
  • its normalized Path and Offset when present;
  • the layout options and operation options;
  • runtime variables;
  • the input's starting stream position; and
  • a minimal byte sample that reproduces the problem.

Do not forward managed exception messages, inner exceptions, or DebugData unchanged to an untrusted client. They may reveal data or implementation detail. Map the stable code to an application-safe error response instead.

Read Limits and diagnostics for the complete taxonomy and unknown-enum behavior. Use Debugging contributor failures when diagnosing the library itself.

Common mistakes

Use this checklist when a new integration almost works but produces the wrong values or positions.

Mistake Why it causes trouble Better approach
Constructing CStruct for every record Repeats layout parsing and preparation Build it once per layout configuration and reuse it
Copying a C struct without checking its ABI Native widths, padding, bitfields, and pointers may differ Translate the documented file format to explicit Portable types
Relying on constructor defaults for a persisted format The intended byte order, placement, or pointer width is hidden Pass all three format choices explicitly
Reading the whole root for one early field Decodes values the caller does not need Use ReadValue with a path
Sharing one stream across concurrent calls Seeks and reads interfere Give each operation a separate stream or lock the complete call
Treating a stored pointer as process memory File coordinates are not safe native addresses Use Pointer, addressing options, and traversal limits
Discarding EnumValueResult or union raw storage Unknown numbers or overlapping bytes may be lost Keep the rich result until faithful round trip is no longer needed
Treating char[N] as terminated text Fixed capacity and scanning have different extents Choose fixed or terminated syntax from the format specification
Expecting span, writer, or stream output to roll back A late failure can leave a prefix Stage through an owned array when all-or-nothing output matters
Raising every safety limit after a failure Can hide a wrong count, offset, or byte order Verify the format and change only the justified limit
Feeding arbitrary headers to the core Include paths, function-like macros, and compiler ABI rules are not interpreted Keep the directives Portable understands and write the rest as Portable syntax

Before shipping a reader or writer, keep at least one known byte fixture and verify offsets, decoded values, output bytes, failure categories, and starting/ending stream positions. A successful round trip by itself can reproduce the same wrong assumption in both directions.