Table of Contents

Write and serialize values

Reading turns bytes into values. Serialization does the reverse: it checks that a C# value matches a selected layout and encodes the value as bytes.

Before writing, construct the same CStruct configuration you use for reading. Byte order, alignment, pointer size, runtime variables, and the selected root must describe the destination format. A different choice can produce valid bytes for the wrong format.

Start with an owned byte array

For most application code, start with the overload that returns byte[]:

byte[] output = layout.Serialize("sample", value);

The library creates an exact-sized result and owns any staging required during the operation. This is the simplest choice when the result will be sent, saved, or passed to another API.

The executable round-trip example reads this layout and writes it in three ways:

struct sample {
    uint16 id;
    uint8 flags;
};
private static void RoundTrip()
{
    var layout = new CStruct("struct sample { uint16 id; uint8 flags; };");
    byte[] input = [0x34, 0x12, 0xA5];
    object parsed = layout.Parse(input, "sample");
    SequenceEqual(input, layout.Serialize("sample", parsed));

    Span<byte> destination = stackalloc byte[8];
    destination.Fill(0xCC);
    int written = layout.Serialize(destination, "sample", parsed);
    Equal(3, written);
    SequenceEqual(input, destination[..written].ToArray());
    Equal((byte)0xCC, destination[written]);

    var writer = new ArrayBufferWriter<byte>();
    Equal(3L, layout.Serialize(writer, "sample", parsed));
    SequenceEqual(input, writer.WrittenSpan.ToArray());
}

For id = 0x1234 and flags = 0xA5, every output form produces:

34 12 A5
└ id ┘ flags

The first two bytes are little-endian 0x1234.

Supply a value with the correct shape

A struct can be supplied as:

  • the dynamic object returned by parsing;
  • a dictionary, ExpandoObject, or parsed StructValue with matching member names; or
  • an instance of a class implementing ICStructMapped<T> (generated by [CStructMapped] or written by hand), registered with MappedTypes.Register<T>().

All required fields must be present and convertible to the declared layout type. A fixed array must have exactly the declared number of elements. A numeric value must fit its width. Null is valid only for a scalar pointer, where it encodes address zero.

Enums and unions need extra care. Keep EnumValueResult when an unknown enum value must survive a round trip. Keep an unmodified UnionValue to preserve all overlapping raw bytes, or explicitly select a member before writing a new union value.

Write into existing storage

Use Serialize(Span<byte>, ...) when you already have a writable memory region:

Span<byte> destination = stackalloc byte[8];
int written = layout.Serialize(destination, "sample", value);
ReadOnlySpan<byte> result = destination[..written];

The return value is the number of bytes initialized at the beginning of the span. Capacity after that prefix remains unchanged. If the span is too small, serialization fails.

Use Serialize(IBufferWriter<byte>, ...) for pipelines, pooled writers, or other APIs that expose IBufferWriter<byte>. CStructSharp requests writable windows, fills them, advances the writer, and returns the number of appended bytes.

These two forms use storage owned by the caller. If a later field fails after earlier output has been initialized or advanced, CStructSharp cannot roll that prefix back. Stage through the byte[] overload when all-or-nothing output is more important than avoiding an allocation.

Write to a stream

Write writes at a writable, seekable stream's current position:

using var stream = new MemoryStream();
layout.Write(stream, "sample", value);

The stream remains open and belongs to the caller. Direct stream writing is not transactional: a later conversion or physical write failure can leave earlier bytes written. To change an existing field while keeping surrounding bytes in place, use Update existing data instead.

WriteAsync is the awaitable form for a file opened for asynchronous I/O or a network stream: the value is serialized first - every validation the synchronous writer performs happens before a byte is sent, so a rejected value writes nothing - and the bytes go out in one WriteAsync. Its cancellationToken parameter is linked with WriteOptions.CancellationToken.

private static async Task WriteAsyncExample()
{
    var layout = new CStruct("struct header { uint16 kind; uint32 length; };");
    var value = new Dictionary<string, object?> { ["kind"] = (ushort)2, ["length"] = 6u, };
    using var stream = new MemoryStream();

    // Validation happens before the write: a rejected value leaves the stream empty.
    await layout.WriteAsync(stream, "header", value);
    SequenceEqual([0x02, 0x00, 0x06, 0x00, 0x00, 0x00], stream.ToArray());
    try
    {
        await layout.WriteAsync(stream, "header", new Dictionary<string, object?> { ["kind"] = 70000, ["length"] = 6u, });
        True(false, "70000 does not fit uint16");
    }
    catch (CStructWriteException)
    {
        Equal(6L, stream.Length);
    }
}

Verify output

For a new format integration:

  1. Check the returned length against the expected layout size.
  2. Compare the result with a known byte fixture, not only with another implementation using the same assumptions.
  3. Parse the output with the same layout and compare meaningful values.
  4. Remember that alignment padding may be normalized to zero. Untouched union raw storage is the explicit byte-preserving case.

Most write failures come from a missing member, the wrong collection length, a number outside its declared range, an incorrect enum or union shape, insufficient output capacity, or a configured safety limit. Inspect CStructWriteException.Path when available before changing the layout. A member the layout does not declare is not a failure by default - it is skipped - so a misspelled key such as lenght silently leaves the field to another (missing) value; set WriteOptions.UnknownMembers = UnknownMemberPolicy.Reject to have it named instead.

Use Spans and buffer writers for more ownership detail, or Enums and Unions for their lossless write models.