Configure variables, options, and limits
Some layout choices are fixed when you construct CStruct. Others change for each piece of data. CStructSharp keeps
those two groups separate:
- constructor and compilation options describe the format and bound layout preparation;
- operation variables provide integer values such as an externally known array count; and
- read, write, or update options limit work performed on one input.
Supply a runtime variable
This layout cannot know the payload count until the caller supplies COUNT:
struct packet {
uint8 kind;
uint8 payload[COUNT];
};
Pass a read-only integer dictionary to every operation that needs the count:
private static void RuntimePayload()
{
var layout = new CStruct("struct packet { uint8 kind; uint8 payload[COUNT]; };");
var variables = new Dictionary<string, int> { ["COUNT"] = 3 };
byte[] bytes = [0x7F, 0x10, 0x20, 0x30];
StructValue packet = layout.Parse(bytes, "packet", variables);
Equal((byte)0x7F, packet.Get<byte>("kind"));
Equal(3, packet.Get<byte[]>("payload").Length);
object? secondPayload = layout.ReadValue(bytes, "packet.payload[1]", variables);
Equal((byte)0x20, (byte)secondPayload!);
using var stream = new MemoryStream(bytes);
stream.Position = 1;
Equal(3, layout.GetArrayLength(stream, "packet.payload", variables));
Equal(1L, stream.Position);
}
The operation copies the entries before it evaluates the layout. A caller value overrides a layout #define with
the same name for that operation, but CStructSharp does not change the dictionary.
Use the same variables for related read, address, length, write, and update calls. Omitting or changing COUNT can
make a later path refer to a different byte position than the initial parse.
Choose the right option type
| Option type | When it applies | Examples of work it limits |
|---|---|---|
CStructCompilationOptions |
Constructing CStruct |
Source length, layout/expression depth, expression work |
ReadOptions |
Parse, read, debug, address, length, pointer traversal | Arrays, strings, total bytes, nesting, pointers |
WriteOptions |
Serialize and direct stream writes | Arrays, strings, total output, nesting, pointer encoding |
UpdateOptions |
Locating and replacing existing storage | Separate traversal-read limits plus inherited write limits |
Properties are init-only, so configure a complete object with an initializer:
var options = new ReadOptions
{
MaxArrayElements = 10_000,
MaxStringBytes = 1_024 * 1_024,
DereferencePointers = false,
};
The public operation reads the supplied values at its outer entry. Reuse an initialized options object when several calls use the same policy; create another object for a different policy.
The four option types are C# records: a with expression makes a copy that changes only the members you name,
and two option objects with the same members are equal. That is how a shared policy gets one variation without
repeating every setting:
private static void OptionsWith()
{
var layout = new CStruct("struct sample { uint8 count; uint16 values[count]; char name[4]; };");
byte[] bytes = [1, 0x34, 0x12, (byte)'a', (byte)'b', 0, 0];
// One shared policy, and a variation that changes a single member.
var strict = new ReadOptions { MaxArrayElements = 8, MaxStringBytes = 64, };
ReadOptions trimmed = strict with { TrimFixedText = true, };
Equal(8, trimmed.MaxArrayElements);
Equal("ab\0\0", layout.Parse(bytes, "sample", options: strict).Get<string>("name"));
Equal("ab", layout.Parse(bytes, "sample", options: trimmed).Get<string>("name"));
// Records compare by their members, so an equal policy is the same policy.
True(strict == new ReadOptions { MaxArrayElements = 8, MaxStringBytes = 64, }, "equal members, equal options");
True(strict != trimmed, "one member differs");
}
The Codecs and Defined members of CStructCompilationOptions hold collections, which compare by reference: two
compilation options that carry different list instances are not equal even when the lists have the same contents.
Understand the defaults
Default limits are intentionally finite:
- layout source: 128 KiB;
- layout or expression/dependency depth: 256;
- expression work: 100,000 steps/nodes;
- one read or write array: 1,000,000 elements;
- one encoded read or write string: 16 MiB;
- total bytes read or written: 64 MiB;
- read pointer depth: 64; and
- read or write nesting depth: 256.
MaxTotalBytesRead counts every byte read from the stream, including rereads. It is not a limit on the input's
file size. Debug parsing spends the same budget as a plain parse: a packed header with a uint16 and a uint32
needs a budget of 6 either way, because debug records carry byte ranges rather than copies. Layouts with unions,
pointers, or selected reads may reread bytes for traversal or overlapping fields, so the input size is a lower
bound, not the exact cost.
UpdateOptions has separate MaxTraversal* values for bytes read while finding the destination. After the target is
found, its inherited write limits apply to the replacement.
These are safety ceilings, not a promise that every value below them is appropriate for your application. For a network message expected to contain at most 100 items, set a limit near 100 rather than relying on one million.
Choose a policy for the quiet cases
Two options change behavior rather than a limit. Both default to the permissive choice:
ReadOptions.TrimFixedText(defaultfalse): fixed-capacity text such aschar name[8],wchar[N], or a boundedutf8 name[N]buffer keeps its NUL padding when read, so61 62 00 00is"ab\0\0". Set the option to read"ab"; only trailing NULs are removed and writing still zero-pads to the declared capacity.WriteOptions.UnknownMembers(defaultIgnore): a supplied value may carry members the struct does not declare and they are skipped.UnknownMemberPolicy.Rejectfails the write before any byte is written -'bogus' is not a member of 'root' (WriteOptions.UnknownMembers is Reject). The layout declares: kind, tail.- for dictionaries,StructValues, and .NET objects alike, nested structs included. It also applies toUpdateOptions.
The what is not an error list explains the other quiet cases.
Handle limit failures
Exceeding a read limit throws CStructReadLimitException with code ReadLimitExceeded. Exceeding a write limit
throws CStructWriteLimitException with code WriteLimitExceeded. Invalid non-positive option values are argument
errors and are rejected before work begins.
Do not respond to a limit failure by raising every limit globally. Confirm the real format maximum, distinguish trusted from untrusted data, and change only the relevant policy. A count that is unexpectedly huge may indicate wrong byte order, a wrong starting position, or an incorrect runtime variable rather than a legitimate large value.
Cancel a long operation
Limits bound how much work an operation may do; they do not bound how long a caller is willing to wait. The
three option records carry a CancellationToken for that. A token is a small handle that another part of the
program - a timeout, a request that was abandoned, a user pressing Stop - can switch to "cancelled"; code that
holds the token checks it at sensible points and stops. The library checks it where a read or write reaches a
boundary: when it enters a struct or union, when it follows a pointer, between the 64 KiB blocks of a numeric
array, between the elements of an array of structs, and between the 256-byte chunks of a terminated string. It
never checks per primitive, so a small read costs nothing for it.
private static void Cancellation()
{
var layout = new CStruct("struct point { uint16 x; uint16 y; }; struct path { uint8 count; point points[count]; };");
byte[] bytes = [2, 1, 0, 2, 0, 3, 0, 4, 0];
// A token that is already cancelled ends the read at its first boundary, before any byte is decoded.
using var cancelled = new CancellationTokenSource();
cancelled.Cancel();
var options = new ReadOptions { CancellationToken = cancelled.Token, };
Throws<OperationCanceledException>(() => layout.Parse(bytes, "path", options: options));
// The same options with a live token read normally; a timeout is the usual source of one.
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
StructValue path = layout.Parse(bytes, "path", options: options with { CancellationToken = timeout.Token, });
Equal(2, path.Get<StructValue[]>("points").Length);
}
Cancellation is not a read failure: the operation ends with the runtime's OperationCanceledException, which
TryReadValue<T> lets through (it restores the stream position first) rather than turning into false. An update
stages its bytes before it commits, so a cancelled update leaves the destination unchanged; a direct stream write
may have written a prefix, as any late failure may. Generated classes observe the same token through their
ReadOptions/WriteOptions arguments.
Continue with Handle errors and recovery, or use the exact language limits when defining an input policy.